forked from Raphael/SCHALE.GameServer
commit
3d4ba39bf4
|
@ -759,20 +759,20 @@ namespace SCHALE.Common.Database
|
||||||
[Key]
|
[Key]
|
||||||
public long ServerId { get; set; }
|
public long ServerId { get; set; }
|
||||||
public long UniqueId { get; set; }
|
public long UniqueId { get; set; }
|
||||||
public int StarGrade { get; set; }
|
public int StarGrade { get; set; } = 1;
|
||||||
public int Level { get; set; }
|
public int Level { get; set; } = 1;
|
||||||
public long Exp { get; set; }
|
public long Exp { get; set; } = 0;
|
||||||
public int FavorRank { get; set; }
|
public int FavorRank { get; set; } = 1;
|
||||||
public long FavorExp { get; set; }
|
public long FavorExp { get; set; } = 0;
|
||||||
public int PublicSkillLevel { get; set; }
|
public int PublicSkillLevel { get; set; } = 1;
|
||||||
public int ExSkillLevel { get; set; }
|
public int ExSkillLevel { get; set; } = 1;
|
||||||
public int PassiveSkillLevel { get; set; }
|
public int PassiveSkillLevel { get; set; } = 1;
|
||||||
public int ExtraPassiveSkillLevel { get; set; }
|
public int ExtraPassiveSkillLevel { get; set; } = 1;
|
||||||
public int LeaderSkillLevel { get; set; }
|
public int LeaderSkillLevel { get; set; } = 1;
|
||||||
|
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public bool IsNew { get; set; }
|
public bool IsNew { get; set; } = true;
|
||||||
public bool IsLocked { get; set; }
|
public bool IsLocked { get; set; } = true;
|
||||||
public bool IsFavorite { get; set; }
|
public bool IsFavorite { get; set; }
|
||||||
public List<long> EquipmentServerIds { get; set; } = [];
|
public List<long> EquipmentServerIds { get; set; } = [];
|
||||||
public Dictionary<int, int> PotentialStats { get; set; } = [];
|
public Dictionary<int, int> PotentialStats { get; set; } = [];
|
||||||
|
|
|
@ -9025,8 +9025,8 @@ namespace SCHALE.Common.NetworkProtocol
|
||||||
public class GachaResult
|
public class GachaResult
|
||||||
{
|
{
|
||||||
public long CharacterId { get; set; }
|
public long CharacterId { get; set; }
|
||||||
public CharacterDB Character { get; set; }
|
public CharacterDB? Character { get; set; }
|
||||||
public ItemDB Stone { get; set; }
|
public ItemDB? Stone { get; set; }
|
||||||
public GachaResult(long id)
|
public GachaResult(long id)
|
||||||
{
|
{
|
||||||
this.CharacterId = id;
|
this.CharacterId = id;
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace SCHALE.Common.Utils
|
||||||
|
{
|
||||||
|
public class AccDict<T> : Dictionary<T, int> where T : notnull
|
||||||
|
{
|
||||||
|
public new int this[T key]
|
||||||
|
{
|
||||||
|
get => TryGetValue(key, out var value) ? value : 0;
|
||||||
|
set => base[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using SCHALE.Common.FlatData;
|
||||||
|
|
||||||
|
namespace SCHALE.Common.Utils
|
||||||
|
{
|
||||||
|
public enum StudentType
|
||||||
|
{
|
||||||
|
Normal,
|
||||||
|
Unique,
|
||||||
|
Event,
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CharacterExcelTExt
|
||||||
|
{
|
||||||
|
public static StudentType GetStudentType(this CharacterExcelT ch)
|
||||||
|
{
|
||||||
|
if (!ch.CollectionVisibleEndDate.StartsWith("2099"))
|
||||||
|
{
|
||||||
|
return StudentType.Unique;
|
||||||
|
}
|
||||||
|
else if (ch.CombineRecipeId == 0)
|
||||||
|
{
|
||||||
|
return StudentType.Event;
|
||||||
|
}
|
||||||
|
|
||||||
|
return StudentType.Normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +1,29 @@
|
||||||
using SCHALE.Common.Database;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SCHALE.Common.Database;
|
||||||
|
using SCHALE.Common.Database.ModelExtensions;
|
||||||
using SCHALE.Common.NetworkProtocol;
|
using SCHALE.Common.NetworkProtocol;
|
||||||
|
using SCHALE.Common.Utils;
|
||||||
using SCHALE.GameServer.Services;
|
using SCHALE.GameServer.Services;
|
||||||
|
using SCHALE.GameServer.Utils;
|
||||||
|
|
||||||
namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
|
namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
|
||||||
{
|
{
|
||||||
public class Shop : ProtocolHandlerBase
|
public class Shop : ProtocolHandlerBase
|
||||||
{
|
{
|
||||||
private readonly ISessionKeyService sessionKeyService;
|
private readonly ISessionKeyService _sessionKeyService;
|
||||||
private readonly SCHALEContext context;
|
private readonly SCHALEContext _context;
|
||||||
|
private readonly SharedDataCacheService _sharedData;
|
||||||
|
private readonly ILogger<Shop> _logger;
|
||||||
|
|
||||||
// TODO: temp storage until gacha management
|
// TODO: temp storage until gacha management
|
||||||
public List<long> SavedGachaResults { get; set; } = [];
|
public List<long> SavedGachaResults { get; set; } = [];
|
||||||
|
|
||||||
public Shop(IProtocolHandlerFactory protocolHandlerFactory, ISessionKeyService _sessionKeyService, SCHALEContext _context) : base(protocolHandlerFactory)
|
public Shop(IProtocolHandlerFactory protocolHandlerFactory, ISessionKeyService sessionKeyService, SCHALEContext context, SharedDataCacheService sharedData, ILogger<Shop> logger) : base(protocolHandlerFactory)
|
||||||
{
|
{
|
||||||
sessionKeyService = _sessionKeyService;
|
_sessionKeyService = sessionKeyService;
|
||||||
context = _context;
|
_context = context;
|
||||||
|
_sharedData = sharedData;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
[ProtocolHandler(Protocol.Shop_BeforehandGachaGet)]
|
[ProtocolHandler(Protocol.Shop_BeforehandGachaGet)]
|
||||||
|
@ -57,7 +65,7 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
|
||||||
[ProtocolHandler(Protocol.Shop_BeforehandGachaPick)]
|
[ProtocolHandler(Protocol.Shop_BeforehandGachaPick)]
|
||||||
public ResponsePacket BeforehandGachaPickHandler(ShopBeforehandGachaPickRequest req)
|
public ResponsePacket BeforehandGachaPickHandler(ShopBeforehandGachaPickRequest req)
|
||||||
{
|
{
|
||||||
var account = sessionKeyService.GetAccount(req.SessionKey);
|
var account = _sessionKeyService.GetAccount(req.SessionKey);
|
||||||
var GachaResults = new List<GachaResult>();
|
var GachaResults = new List<GachaResult>();
|
||||||
|
|
||||||
foreach (var charId in SavedGachaResults)
|
foreach (var charId in SavedGachaResults)
|
||||||
|
@ -92,39 +100,214 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
|
||||||
[ProtocolHandler(Protocol.Shop_BuyGacha3)]
|
[ProtocolHandler(Protocol.Shop_BuyGacha3)]
|
||||||
public ResponsePacket ShopBuyGacha3ResponseHandler(ShopBuyGacha3Request req)
|
public ResponsePacket ShopBuyGacha3ResponseHandler(ShopBuyGacha3Request req)
|
||||||
{
|
{
|
||||||
var gachaResults = new List<GachaResult>();
|
var account = _sessionKeyService.GetAccount(req.SessionKey);
|
||||||
|
var accountChSet = account.Characters.Select(x => x.UniqueId).ToHashSet();
|
||||||
|
|
||||||
for (int i = 0; i < 10; i++)
|
// TODO: Implement FES Gacha
|
||||||
{
|
// TODO: Check Gacha currency
|
||||||
long id = 10000 + new Random().Next(0, 94);
|
// TODO: SR pickup
|
||||||
|
// TODO: pickup stone count
|
||||||
|
// TODO: even more...
|
||||||
|
// Type Rate Acc.R
|
||||||
|
// -------------------------
|
||||||
|
// Current SSR 0.7% 0.7%
|
||||||
|
// Other SSR 2.3% 3.0%ServerId
|
||||||
|
// SR 18.5% 21.5%
|
||||||
|
// R 78.5% 100.%
|
||||||
|
|
||||||
gachaResults.Add(new(id)
|
const int gpStoneID = 90070086;
|
||||||
|
const int chUniStoneID = 23;
|
||||||
|
|
||||||
|
var rateUpChId = 10094; // 10094, 10095
|
||||||
|
var rateUpIsNormalStudent = false;
|
||||||
|
var gachaList = new List<GachaResult>(10);
|
||||||
|
var itemDict = new AccDict<long>();
|
||||||
|
bool shouldDoGuaranteedSR = true;
|
||||||
|
// itemDict[gpStoneID] = 10;
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; ++i)
|
||||||
{
|
{
|
||||||
Character = new() // hardcoded util proper db
|
var randomNumber = Random.Shared.NextInt64(1000);
|
||||||
|
if (randomNumber < 7)
|
||||||
{
|
{
|
||||||
ServerId = req.AccountId,
|
// always 3 star
|
||||||
UniqueId = id,
|
shouldDoGuaranteedSR = false;
|
||||||
|
var isNew = accountChSet.Add(rateUpChId);
|
||||||
|
gachaList.Add(new(rateUpChId)
|
||||||
|
{
|
||||||
|
Character = !isNew ? null : new()
|
||||||
|
{
|
||||||
|
AccountServerId = account.ServerId,
|
||||||
|
UniqueId = rateUpChId,
|
||||||
StarGrade = 3,
|
StarGrade = 3,
|
||||||
Level = 1,
|
},
|
||||||
FavorRank = 1,
|
Stone = isNew ? null : new()
|
||||||
PublicSkillLevel = 1,
|
{
|
||||||
ExSkillLevel = 1,
|
UniqueId = chUniStoneID,
|
||||||
PassiveSkillLevel = 1,
|
StackCount = 50,
|
||||||
ExtraPassiveSkillLevel = 1,
|
|
||||||
LeaderSkillLevel = 1,
|
|
||||||
IsNew = true,
|
|
||||||
IsLocked = true
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (!isNew)
|
||||||
|
{
|
||||||
|
itemDict[chUniStoneID] += 50;
|
||||||
|
itemDict[rateUpChId] += 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (randomNumber < 30)
|
||||||
|
{
|
||||||
|
shouldDoGuaranteedSR = false;
|
||||||
|
var normalSSRList = _sharedData.CharaListSSRNormal;
|
||||||
|
var poolSize = normalSSRList.Count;
|
||||||
|
if (rateUpIsNormalStudent) poolSize--;
|
||||||
|
|
||||||
|
var randomPoolIdx = (int)Random.Shared.NextInt64(poolSize);
|
||||||
|
if (normalSSRList[randomPoolIdx].Id == rateUpChId) randomPoolIdx++;
|
||||||
|
|
||||||
|
var chId = normalSSRList[randomPoolIdx].Id;
|
||||||
|
var isNew = accountChSet.Add(chId);
|
||||||
|
gachaList.Add(new(chId)
|
||||||
|
{
|
||||||
|
Character = !isNew ? null : new()
|
||||||
|
{
|
||||||
|
AccountServerId = account.ServerId,
|
||||||
|
UniqueId = chId,
|
||||||
|
StarGrade = 3,
|
||||||
|
},
|
||||||
|
Stone = isNew ? null : new()
|
||||||
|
{
|
||||||
|
UniqueId = chUniStoneID,
|
||||||
|
StackCount = 50,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!isNew)
|
||||||
|
{
|
||||||
|
itemDict[chUniStoneID] += 50;
|
||||||
|
itemDict[chId] += 30;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (randomNumber < 215 || (i == 9 && shouldDoGuaranteedSR))
|
||||||
|
{
|
||||||
|
shouldDoGuaranteedSR = false;
|
||||||
|
var normalSRList = _sharedData.CharaListSRNormal;
|
||||||
|
var randomPoolIdx = (int)Random.Shared.NextInt64(normalSRList.Count);
|
||||||
|
var chId = normalSRList[randomPoolIdx].Id;
|
||||||
|
var isNew = accountChSet.Add(chId);
|
||||||
|
|
||||||
|
gachaList.Add(new(chId)
|
||||||
|
{
|
||||||
|
Character = !isNew ? null : new()
|
||||||
|
{
|
||||||
|
AccountServerId = account.ServerId,
|
||||||
|
UniqueId = chId,
|
||||||
|
StarGrade = 2,
|
||||||
|
},
|
||||||
|
Stone = isNew ? null : new()
|
||||||
|
{
|
||||||
|
UniqueId = chUniStoneID,
|
||||||
|
StackCount = 10,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!isNew)
|
||||||
|
{
|
||||||
|
itemDict[chUniStoneID] += 10;
|
||||||
|
itemDict[chId] += 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var normalRList = _sharedData.CharaListRNormal;
|
||||||
|
var randomPoolIdx = (int)Random.Shared.NextInt64(normalRList.Count);
|
||||||
|
var chId = normalRList[randomPoolIdx].Id;
|
||||||
|
var isNew = accountChSet.Add(chId);
|
||||||
|
|
||||||
|
gachaList.Add(new(chId)
|
||||||
|
{
|
||||||
|
Character = !isNew ? null : new()
|
||||||
|
{
|
||||||
|
AccountServerId = account.ServerId,
|
||||||
|
UniqueId = chId,
|
||||||
|
StarGrade = 1,
|
||||||
|
},
|
||||||
|
Stone = isNew ? null : new()
|
||||||
|
{
|
||||||
|
UniqueId = chUniStoneID,
|
||||||
|
StackCount = 1,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!isNew)
|
||||||
|
{
|
||||||
|
itemDict[chUniStoneID] += 1;
|
||||||
|
itemDict[chId] += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using var transaction = _context.Database.BeginTransaction();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// add characters
|
||||||
|
_context.Characters.AddRange(
|
||||||
|
gachaList.Where(x => x.Character != null)
|
||||||
|
.Select(x => x.Character)!);
|
||||||
|
|
||||||
|
// create if item does not exist
|
||||||
|
foreach (var id in itemDict.Keys)
|
||||||
|
{
|
||||||
|
var itemExists = _context.Items
|
||||||
|
.Any(x => x.AccountServerId == account.ServerId && x.UniqueId == id);
|
||||||
|
if (!itemExists)
|
||||||
|
{
|
||||||
|
_context.Items.Add(new ItemDB()
|
||||||
|
{
|
||||||
|
IsNew = true,
|
||||||
|
UniqueId = id,
|
||||||
|
StackCount = 0,
|
||||||
|
AccountServerId = account.ServerId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_context.SaveChanges();
|
||||||
|
|
||||||
|
// perform item count update
|
||||||
|
foreach (var (id, count) in itemDict)
|
||||||
|
{
|
||||||
|
_context.Items
|
||||||
|
.Where(x => x.AccountServerId == account.ServerId && x.UniqueId == id)
|
||||||
|
.ExecuteUpdate(setters => setters.SetProperty(
|
||||||
|
item => item.StackCount, item => item.StackCount + count));
|
||||||
|
}
|
||||||
|
|
||||||
|
_context.SaveChanges();
|
||||||
|
|
||||||
|
transaction.Commit();
|
||||||
|
|
||||||
|
_context.Entry(account).Collection(x => x.Items).Reload();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError("Transaction failed: {Message}", ex.Message);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemDbList = itemDict.Keys
|
||||||
|
.Select(id => _context.Items.AsNoTracking().First(x => x.AccountServerId == account.ServerId && x.UniqueId == id))
|
||||||
|
.ToList();
|
||||||
|
foreach (var gacha in gachaList)
|
||||||
|
{
|
||||||
|
if (gacha.Stone != null)
|
||||||
|
{
|
||||||
|
gacha.Stone.ServerId = itemDbList.First(x => x.UniqueId == gacha.Stone.UniqueId).ServerId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ShopBuyGacha3Response()
|
return new ShopBuyGacha3Response()
|
||||||
{
|
{
|
||||||
GachaResults = gachaResults,
|
GachaResults = gachaList,
|
||||||
UpdateTime = DateTime.UtcNow,
|
UpdateTime = DateTime.UtcNow,
|
||||||
GemBonusRemain = long.MaxValue,
|
GemBonusRemain = int.MaxValue,
|
||||||
ConsumedItems = [],
|
ConsumedItems = [],
|
||||||
AcquiredItems = [],
|
AcquiredItems = itemDbList,
|
||||||
MissionProgressDBs = [],
|
MissionProgressDBs = [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,6 +87,7 @@ namespace SCHALE.GameServer
|
||||||
builder.Services.AddMemorySessionKeyService();
|
builder.Services.AddMemorySessionKeyService();
|
||||||
builder.Services.AddExcelTableService();
|
builder.Services.AddExcelTableService();
|
||||||
builder.Services.AddIrcService();
|
builder.Services.AddIrcService();
|
||||||
|
builder.Services.AddSharedDataCache();
|
||||||
|
|
||||||
// Add all Handler Groups
|
// Add all Handler Groups
|
||||||
var handlerGroups = Assembly
|
var handlerGroups = Assembly
|
||||||
|
|
|
@ -17,11 +17,13 @@ namespace SCHALE.GameServer.Services
|
||||||
private readonly ILogger<ExcelTableService> logger = _logger;
|
private readonly ILogger<ExcelTableService> logger = _logger;
|
||||||
private readonly Dictionary<Type, object> caches = [];
|
private readonly Dictionary<Type, object> caches = [];
|
||||||
|
|
||||||
public static async Task LoadExcels()
|
public static async Task LoadExcels(string excelDirectory = "")
|
||||||
{
|
{
|
||||||
var excelZipUrl = $"https://prod-clientpatch.bluearchiveyostar.com/{Config.Instance.VersionId}/TableBundles/Excel.zip";
|
var excelZipUrl = $"https://prod-clientpatch.bluearchiveyostar.com/{Config.Instance.VersionId}/TableBundles/Excel.zip";
|
||||||
|
|
||||||
var excelDir = $"{Path.GetDirectoryName(AppContext.BaseDirectory)}/Resources/excel/";
|
var excelDir = string.IsNullOrWhiteSpace(excelDirectory)
|
||||||
|
? Path.Join(Path.GetDirectoryName(AppContext.BaseDirectory), "Resources/excel")
|
||||||
|
: excelDirectory;
|
||||||
var excelZipPath = Path.Combine(excelDir, "Excel.zip");
|
var excelZipPath = Path.Combine(excelDir, "Excel.zip");
|
||||||
|
|
||||||
if (Directory.Exists(excelDir))
|
if (Directory.Exists(excelDir))
|
||||||
|
@ -53,14 +55,17 @@ namespace SCHALE.GameServer.Services
|
||||||
/// <typeparam name="T"></typeparam>
|
/// <typeparam name="T"></typeparam>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
/// <exception cref="FileNotFoundException"></exception>
|
/// <exception cref="FileNotFoundException"></exception>
|
||||||
public T GetTable<T>() where T : IFlatbufferObject
|
public T GetTable<T>(bool bypassCache = false, string excelDirectory = "") where T : IFlatbufferObject
|
||||||
{
|
{
|
||||||
var type = typeof(T);
|
var type = typeof(T);
|
||||||
|
|
||||||
if (caches.TryGetValue(type, out var cache))
|
if (!bypassCache && caches.TryGetValue(type, out var cache))
|
||||||
return (T)cache;
|
return (T)cache;
|
||||||
|
|
||||||
var bytesFilePath = Path.Join(Path.GetDirectoryName(AppContext.BaseDirectory), "Resources/excel/", $"{type.Name.ToLower()}.bytes");
|
var excelDir = string.IsNullOrWhiteSpace(excelDirectory)
|
||||||
|
? Path.Join(Path.GetDirectoryName(AppContext.BaseDirectory), "Resources/excel")
|
||||||
|
: excelDirectory;
|
||||||
|
var bytesFilePath = Path.Join(excelDir, $"{type.Name.ToLower()}.bytes");
|
||||||
if (!File.Exists(bytesFilePath))
|
if (!File.Exists(bytesFilePath))
|
||||||
{
|
{
|
||||||
throw new FileNotFoundException($"bytes files for {type.Name} not found");
|
throw new FileNotFoundException($"bytes files for {type.Name} not found");
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
using SCHALE.Common.FlatData;
|
||||||
|
using SCHALE.Common.Utils;
|
||||||
|
|
||||||
|
namespace SCHALE.GameServer.Services
|
||||||
|
{
|
||||||
|
public class SharedDataCacheService
|
||||||
|
{
|
||||||
|
private readonly ILogger<SharedDataCacheService> _logger;
|
||||||
|
private readonly ExcelTableService _excelTable;
|
||||||
|
private readonly List<CharacterExcelT> _charaList;
|
||||||
|
private readonly List<CharacterExcelT> _charaListR;
|
||||||
|
private readonly List<CharacterExcelT> _charaListSR;
|
||||||
|
private readonly List<CharacterExcelT> _charaListSSR;
|
||||||
|
private readonly List<CharacterExcelT> _charaListRNormal;
|
||||||
|
private readonly List<CharacterExcelT> _charaListSRNormal;
|
||||||
|
private readonly List<CharacterExcelT> _charaListSSRNormal;
|
||||||
|
private readonly List<CharacterExcelT> _charaListUnique;
|
||||||
|
private readonly List<CharacterExcelT> _charaListEvent;
|
||||||
|
|
||||||
|
public IReadOnlyList<CharacterExcelT> CharaList => _charaList;
|
||||||
|
public IReadOnlyList<CharacterExcelT> CharaListR => _charaListR;
|
||||||
|
public IReadOnlyList<CharacterExcelT> CharaListSR => _charaListSR;
|
||||||
|
public IReadOnlyList<CharacterExcelT> CharaListSSR => _charaListSSR;
|
||||||
|
public IReadOnlyList<CharacterExcelT> CharaListRNormal=> _charaListRNormal;
|
||||||
|
public IReadOnlyList<CharacterExcelT> CharaListSRNormal=> _charaListSRNormal;
|
||||||
|
public IReadOnlyList<CharacterExcelT> CharaListSSRNormal=> _charaListSSRNormal;
|
||||||
|
public IReadOnlyList<CharacterExcelT> CharaListUnique => _charaListUnique;
|
||||||
|
public IReadOnlyList<CharacterExcelT> CharaListEvent => _charaListEvent;
|
||||||
|
|
||||||
|
public SharedDataCacheService(ILogger<SharedDataCacheService> logger, ExcelTableService excelTable)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_excelTable = excelTable;
|
||||||
|
|
||||||
|
_charaList = excelTable
|
||||||
|
.GetTable<CharacterExcelTable>()
|
||||||
|
.UnPack()
|
||||||
|
.DataList!
|
||||||
|
.Where(x => x is
|
||||||
|
{
|
||||||
|
IsPlayable: true,
|
||||||
|
IsPlayableCharacter: true,
|
||||||
|
IsDummy: false,
|
||||||
|
IsNPC: false,
|
||||||
|
ProductionStep: ProductionStep.Release,
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
_charaListR = _charaList.Where(x => x.Rarity == Rarity.R).ToList();
|
||||||
|
_charaListSR = _charaList.Where(x => x.Rarity == Rarity.SR).ToList();
|
||||||
|
_charaListSSR = _charaList.Where(x => x.Rarity == Rarity.SSR).ToList();
|
||||||
|
_charaListRNormal = _charaListR.Where(x => x.GetStudentType() == StudentType.Normal).ToList();
|
||||||
|
_charaListSRNormal = _charaListSR.Where(x => x.GetStudentType() == StudentType.Normal).ToList();
|
||||||
|
_charaListSSRNormal = _charaListSSR.Where(x => x.GetStudentType() == StudentType.Normal).ToList();
|
||||||
|
_charaListUnique = _charaListR.Where(x => x.GetStudentType() == StudentType.Unique).ToList();
|
||||||
|
_charaListEvent = _charaListR.Where(x => x.GetStudentType() == StudentType.Event).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class DataCacheServiceExtensions
|
||||||
|
{
|
||||||
|
public static void AddSharedDataCache(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton<SharedDataCacheService>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||||
|
|
||||||
|
namespace SCHALE.GameServer.Utils;
|
||||||
|
|
||||||
|
public static class CollectionEntryExt
|
||||||
|
{
|
||||||
|
public static void Reload(this CollectionEntry source)
|
||||||
|
{
|
||||||
|
if (source.CurrentValue != null)
|
||||||
|
{
|
||||||
|
foreach (var item in source.CurrentValue)
|
||||||
|
{
|
||||||
|
source.EntityEntry.Context.Entry(item).State = Microsoft.EntityFrameworkCore.EntityState.Detached;
|
||||||
|
}
|
||||||
|
source.CurrentValue = null;
|
||||||
|
source.IsLoaded = false;
|
||||||
|
source.Load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,10 +17,15 @@ namespace SCHALE.Common.Utils
|
||||||
var context = connection.Context;
|
var context = connection.Context;
|
||||||
|
|
||||||
var characterExcel = connection.ExcelTableService.GetTable<CharacterExcelTable>().UnPack().DataList;
|
var characterExcel = connection.ExcelTableService.GetTable<CharacterExcelTable>().UnPack().DataList;
|
||||||
var allCharacters = characterExcel.Where(x => x.IsPlayable && x.IsPlayableCharacter && x.CollectionVisible && !account.Characters.Any(c => c.UniqueId == x.Id)).Select(x =>
|
var allCharacters = characterExcel.Where(x =>
|
||||||
|
x is
|
||||||
{
|
{
|
||||||
return CreateMaxCharacterFromId(x.Id);
|
IsPlayable: true,
|
||||||
}).ToList();
|
IsPlayableCharacter: true,
|
||||||
|
IsNPC: false,
|
||||||
|
ProductionStep: ProductionStep.Release,
|
||||||
|
}
|
||||||
|
).Select(x => CreateMaxCharacterFromId(x.Id)).ToList();
|
||||||
|
|
||||||
account.AddCharacters(context, [.. allCharacters]);
|
account.AddCharacters(context, [.. allCharacters]);
|
||||||
context.SaveChanges();
|
context.SaveChanges();
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Redirect server via Frida
|
||||||
|
|
||||||
|
## Install Frida's CLI tools
|
||||||
|
|
||||||
|
Make sure [Python](https://python.org/) is installed before you start.
|
||||||
|
|
||||||
|
Install Frida's CLI tools via Pypi.
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install frida-tools
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run Frida server on device/emulator
|
||||||
|
|
||||||
|
Download Frida server [here](https://github.com/frida/frida/releases/).
|
||||||
|
|
||||||
|
Make sure adb is enabled and Android is rooted.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```
|
||||||
|
adb push frida-server /data/local/tmp
|
||||||
|
adb shell
|
||||||
|
# in adb shell
|
||||||
|
su
|
||||||
|
chmod 755 /data/local/tmp/frida-server
|
||||||
|
/data/local/tmp/frida-server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hook client with frida
|
||||||
|
|
||||||
|
Set your server address in `ba.js`.
|
||||||
|
|
||||||
|
Launch the client, then immediately run the following command on host:
|
||||||
|
|
||||||
|
```
|
||||||
|
frida -U "ブルアカ" -l ba.js --realm=emulated
|
||||||
|
```
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Redirect server via mitmproxy
|
||||||
|
|
||||||
|
## Install mitmproxy
|
||||||
|
|
||||||
|
Download the installer from [mitmproxy.org](https://mitmproxy.org/)
|
||||||
|
|
||||||
|
## Install CA certificate
|
||||||
|
|
||||||
|
Follow the instructions from [System CA on Android Emulator](https://docs.mitmproxy.org/stable/howto-install-system-trusted-ca-android/)
|
||||||
|
|
||||||
|
## Hook client with mitmproxy
|
||||||
|
|
||||||
|
Set your server address and port in `redirect_server.py`
|
||||||
|
|
||||||
|
Install [WireGuard](https://wireguard.com/install/#android-play-store-f-droid) on client, then run mitmproxy:
|
||||||
|
|
||||||
|
```
|
||||||
|
mitmweb -m wireguard --no-http2 -s redirect_server.py --set termlog_verbosity=warn
|
||||||
|
```
|
||||||
|
|
||||||
|
It also works as a packet dumper. You can save the flow file for further works.
|
|
@ -0,0 +1,47 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
import argparse
|
||||||
|
import gzip
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from mitmproxy import io
|
||||||
|
from mitmproxy.http import HTTPFlow
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser('Flow dumper')
|
||||||
|
parser.add_argument('file', type=str, help='mitmproxy flow file')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
os.makedirs(f'{args.file}.dumps', exist_ok=True)
|
||||||
|
|
||||||
|
f = open(args.file, 'rb')
|
||||||
|
r = io.FlowReader(f)
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
for flow in r.stream():
|
||||||
|
if not isinstance(flow, HTTPFlow):
|
||||||
|
continue
|
||||||
|
if not flow.request.url.endswith('/api/gateway'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
req = flow.request.raw_content
|
||||||
|
res = json.loads(flow.response.text)
|
||||||
|
protocol = res['protocol']
|
||||||
|
|
||||||
|
mx_end = req.rfind(b'\r\n', 0, len(req) - 1)
|
||||||
|
mx_start = req.rfind(b'\r\n\r\n')
|
||||||
|
req_mx = req[mx_start + 4:mx_end]
|
||||||
|
req_bytes = req_mx[12:]
|
||||||
|
req_bytes = bytearray([x ^ 0xD9 for x in req_bytes])
|
||||||
|
req_bytes = gzip.decompress(req_bytes)
|
||||||
|
|
||||||
|
packet = json.loads(req_bytes)
|
||||||
|
with open(f'{args.file}.dumps/{i}_req_{protocol}.json', 'w') as f_req:
|
||||||
|
json.dump(packet, f_req, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
packet = json.loads(res['packet'])
|
||||||
|
with open(f'{args.file}.dumps/{i}_resp_{protocol}.json', 'w', encoding='utf8') as f_res:
|
||||||
|
json.dump(packet, f_res, indent=2, ensure_ascii=False)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
f.close()
|
|
@ -0,0 +1,45 @@
|
||||||
|
import gzip
|
||||||
|
import json
|
||||||
|
from mitmproxy import http
|
||||||
|
|
||||||
|
SERVER_HOST = 'YOUR_SERVER_HERE'
|
||||||
|
SERVER_PORT = 80
|
||||||
|
|
||||||
|
REWRITE_HOST_LIST = [
|
||||||
|
'ba-jp-sdk.bluearchive.jp',
|
||||||
|
'prod-gateway.bluearchiveyostar.com',
|
||||||
|
'prod-game.bluearchiveyostar.com',
|
||||||
|
# 'prod-notice.bluearchiveyostar.com',
|
||||||
|
# 'prod-logcollector.bluearchiveyostar.com',
|
||||||
|
]
|
||||||
|
|
||||||
|
def request(flow: http.HTTPFlow) -> None:
|
||||||
|
if flow.request.pretty_host.endswith('log.aliyuncs.com'):
|
||||||
|
flow.kill()
|
||||||
|
return
|
||||||
|
if flow.request.pretty_host in REWRITE_HOST_LIST:
|
||||||
|
flow.request.scheme = 'http'
|
||||||
|
flow.request.host = SERVER_HOST
|
||||||
|
flow.request.port = SERVER_PORT
|
||||||
|
return
|
||||||
|
|
||||||
|
def response(flow: http.HTTPFlow) -> None:
|
||||||
|
if flow.request.url.endswith('/api/gateway'):
|
||||||
|
try:
|
||||||
|
req = flow.request.raw_content
|
||||||
|
res = json.loads(flow.response.text)
|
||||||
|
protocol = res['protocol']
|
||||||
|
|
||||||
|
mx_end = req.rfind(b'\r\n', 0, len(req) - 1)
|
||||||
|
mx_start = req.rfind(b'\r\n\r\n')
|
||||||
|
req_mx = req[mx_start + 4:mx_end]
|
||||||
|
req_bytes = req_mx[12:]
|
||||||
|
req_bytes = bytearray([x ^ 0xD9 for x in req_bytes])
|
||||||
|
req_bytes = gzip.decompress(req_bytes)
|
||||||
|
print(f'Protocol: {protocol}')
|
||||||
|
print(f'[OUT]->{json.loads(req_bytes)}')
|
||||||
|
print(f'[IN]<--{json.loads(res["packet"])}')
|
||||||
|
print('')
|
||||||
|
except Exception as e:
|
||||||
|
print('Failed to dump packet', e)
|
||||||
|
return
|
Loading…
Reference in New Issue