From 75d840974bfc16eb3aa06403d89dfe7ec32eb19b Mon Sep 17 00:00:00 2001 From: Kei-Luna Date: Sun, 31 May 2026 05:02:04 +0900 Subject: [PATCH] Item_Recycle --- Common/Data/Excel/RecycleExcel.cs | 1 + Common/Data/Excel/SupportCardExcel.cs | 1 + .../CallGS/Handlers/Inventory/Item_Recycle.cs | 316 ++++++++++++++++++ 3 files changed, 318 insertions(+) create mode 100644 GameServer/Server/CallGS/Handlers/Inventory/Item_Recycle.cs diff --git a/Common/Data/Excel/RecycleExcel.cs b/Common/Data/Excel/RecycleExcel.cs index 32eb4bb..5c54852 100644 --- a/Common/Data/Excel/RecycleExcel.cs +++ b/Common/Data/Excel/RecycleExcel.cs @@ -6,6 +6,7 @@ namespace MikuSB.Data.Excel; public class RecycleExcel : ExcelResource { public int ID { get; set; } + public JToken? RecycleReward { get; set; } public JToken? RecycleBase { get; set; } public JToken? RecycleRatio { get; set; } diff --git a/Common/Data/Excel/SupportCardExcel.cs b/Common/Data/Excel/SupportCardExcel.cs index 91daf3a..b7e342e 100644 --- a/Common/Data/Excel/SupportCardExcel.cs +++ b/Common/Data/Excel/SupportCardExcel.cs @@ -13,6 +13,7 @@ public class SupportCardExcel : ExcelResource public uint Icon { get; set; } public uint ProvideExp { get; set; } public uint Color { get; set; } + [JsonProperty("RecycleID")] public int RecycleID { get; set; } [JsonProperty("LevelLimitID")] public int LevelLimitId { get; set; } [JsonProperty("AffixPool")] public List AffixPool { get; set; } = []; [JsonProperty("AffixCost")] public JToken? AffixCostRaw { get; set; } diff --git a/GameServer/Server/CallGS/Handlers/Inventory/Item_Recycle.cs b/GameServer/Server/CallGS/Handlers/Inventory/Item_Recycle.cs new file mode 100644 index 0000000..e50a89b --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/Inventory/Item_Recycle.cs @@ -0,0 +1,316 @@ +using MikuSB.Data; +using MikuSB.Data.Excel; +using MikuSB.Database; +using MikuSB.Database.Inventory; +using MikuSB.Enums.Item; +using MikuSB.Proto; +using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.Inventory; + +[CallGSApi("Item_Recycle")] +public class Item_Recycle : ICallGSHandler +{ + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var player = connection.Player!; + var req = JsonSerializer.Deserialize(param); + if (req?.TbItems == null || req.TbItems.Count == 0) + { + await CallGSRouter.SendScript(connection, "Item_Recycle", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + var config = RecycleConfig.Load(); + + var itemsToRecycle = new List<(BaseGameItemInfo Item, int RecycleId)>(); + foreach (var uniqueId in req.TbItems) + { + BaseGameItemInfo? item = player.InventoryManager.GetWeaponItem((uint)uniqueId) + ?? (BaseGameItemInfo?)player.InventoryManager.GetSupportCardItem((uint)uniqueId); + + if (item == null) + { + await CallGSRouter.SendScript(connection, "Item_Recycle", "{\"sErr\":\"error.Recycle.ItemNotExists\"}"); + return; + } + + var recycleId = GetRecycleId(item); + if (recycleId <= 0 || !config.HasConfig(recycleId)) + { + await CallGSRouter.SendScript(connection, "Item_Recycle", "{\"sErr\":\"error.Recycle.ItemCanNotRecycle\"}"); + return; + } + + itemsToRecycle.Add((item, recycleId)); + } + + var sync = new NtfSyncPlayer(); + + foreach (var (item, recycleId) in itemsToRecycle) + { + var rewards = config.CalcRewards(item, recycleId); + foreach (var reward in rewards) + await GrantRewardAsync(player, sync, reward); + + RemoveItem(player.InventoryManager.InventoryData, item, sync); + } + + DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData); + + await CallGSRouter.SendScript(connection, "Item_Recycle", "{}", sync); + } + + private static int GetRecycleId(BaseGameItemInfo item) + { + if (item.ItemType == ItemTypeEnum.TYPE_WEAPON) + { + var t = GameData.WeaponData.Values.FirstOrDefault(x => + GameResourceTemplateId.FromGdpl(x.Genre, x.Detail, x.Particular, x.Level) == item.TemplateId); + return t?.RecycleID ?? 0; + } + if (item.ItemType == ItemTypeEnum.TYPE_SUPPORT) + { + var t = GameData.SupportCardData.FirstOrDefault(x => x.TemplateId == item.TemplateId); + return t?.RecycleID ?? 0; + } + return 0; + } + + private static void RemoveItem(InventoryData inventory, BaseGameItemInfo item, NtfSyncPlayer sync) + { + var removed = item.ToProto(); + removed.Count = 0; + sync.Items.Add(removed); + + if (item.ItemType == ItemTypeEnum.TYPE_WEAPON) + inventory.Weapons.Remove(item.UniqueId); + else + inventory.SupportCards.Remove(item.UniqueId); + } + + private static async Task GrantRewardAsync(GameServer.Game.Player.PlayerInstance player, NtfSyncPlayer sync, IReadOnlyList reward) + { + if (reward.Count < 5) return; + + var itemType = (ItemTypeEnum)reward[0]; + var detail = reward[1]; + var particular = reward[2]; + var level = reward[3]; + var count = Math.Max(1u, reward[4]); + + switch (itemType) + { + case ItemTypeEnum.TYPE_SUPPLIES: + { + var templateId = (uint)GameResourceTemplateId.FromGdpl(reward[0], detail, particular, level); + if (!GameData.SuppliesData.TryGetValue(templateId, out var supplies)) break; + var item = await player.InventoryManager.AddSuppliesItem(supplies, count, sendPacket: false); + if (item != null) sync.Items.Add(item.ToProto()); + break; + } + } + } +} + +internal sealed class RecycleConfig +{ + private readonly Dictionary _entries; + private readonly List _weaponSupplies; + private readonly List _supportSupplies; + private readonly Dictionary _weaponLevelExp; + private readonly Dictionary _supportLevelExp; + + private readonly Dictionary _weaponLevelExpSsr; + private readonly Dictionary _supportLevelExpSsr; + + private RecycleConfig( + Dictionary entries, + List weaponSupplies, + List supportSupplies, + Dictionary weaponLevelExp, + Dictionary weaponLevelExpSsr, + Dictionary supportLevelExp, + Dictionary supportLevelExpSsr) + { + _entries = entries; + _weaponSupplies = weaponSupplies; + _supportSupplies = supportSupplies; + _weaponLevelExp = weaponLevelExp; + _weaponLevelExpSsr = weaponLevelExpSsr; + _supportLevelExp = supportLevelExp; + _supportLevelExpSsr = supportLevelExpSsr; + } + + public static RecycleConfig Load() + { + var entries = new Dictionary(); + foreach (var row in GameData.RecycleData.Values) + { + var fixedRewards = ParseRewards(row.RecycleReward); + var recycleBase = GetUInt(row.RecycleBase); + var recycleRatio = GetDecimal(row.RecycleRatio); + entries[row.ID] = new RecycleEntry(fixedRewards, recycleBase, recycleRatio); + } + + var weaponSupplies = new List(); + var supportSupplies = new List(); + foreach (var s in GameData.AllSuppliesData) + { + if (s.ProvideExp == 0) continue; + if (s.Genre == 5 && s.Detail == 2) + weaponSupplies.Add(new SupplyTemplate(s.Genre, s.Detail, s.Particular, s.Level, s.ProvideExp)); + else if (s.Genre == 5 && s.Detail == 3) + supportSupplies.Add(new SupplyTemplate(s.Genre, s.Detail, s.Particular, s.Level, s.ProvideExp)); + } + weaponSupplies.Sort((a, b) => b.ProvideExp.CompareTo(a.ProvideExp)); + supportSupplies.Sort((a, b) => b.ProvideExp.CompareTo(a.ProvideExp)); + + var weaponLevelExp = BuildLevelExpTable(GameData.UpgradeExpData.Values.Select(x => (x.Lv, x.WeaponNeedExp))); + var weaponLevelExpSsr = BuildLevelExpTable(GameData.UpgradeExpData.Values.Select(x => (x.Lv, x.SSRWeaponNeedExp))); + var supportLevelExp = BuildLevelExpTable(GameData.UpgradeExpData.Values.Select(x => (x.Lv, x.SusNeedExp))); + var supportLevelExpSsr = BuildLevelExpTable(GameData.UpgradeExpData.Values.Select(x => (x.Lv, x.SSRSusNeedExp))); + + return new RecycleConfig(entries, weaponSupplies, supportSupplies, weaponLevelExp, weaponLevelExpSsr, supportLevelExp, supportLevelExpSsr); + } + + public bool HasConfig(int recycleId) => _entries.ContainsKey(recycleId); + + public List> CalcRewards(BaseGameItemInfo item, int recycleId) + { + if (!_entries.TryGetValue(recycleId, out var entry)) + return []; + + var rewards = new List>(entry.FixedRewards); + + var expRewards = CalcExpRewards(item, entry); + rewards.AddRange(expRewards); + + return rewards; + } + + private List> CalcExpRewards(BaseGameItemInfo item, RecycleEntry entry) + { + if (entry.RecycleRatio == 0) return []; + + List supplies; + Dictionary levelExp; + + if (item.ItemType == ItemTypeEnum.TYPE_WEAPON) + { + supplies = _weaponSupplies; + var color = GetItemColor(item); + levelExp = color == 5 ? _weaponLevelExpSsr : _weaponLevelExp; + } + else if (item.ItemType == ItemTypeEnum.TYPE_SUPPORT) + { + supplies = _supportSupplies; + var color = GetItemColor(item); + levelExp = color == 5 ? _supportLevelExpSsr : _supportLevelExp; + } + else + { + return []; + } + + var baseExp = (ulong)entry.RecycleBase; + var levelAccum = levelExp.GetValueOrDefault((int)item.Level); + var totalExp = (ulong)Math.Floor((baseExp + levelAccum + item.Exp) * (double)entry.RecycleRatio); + + if (totalExp == 0 || supplies.Count == 0) return []; + + var rewards = new List>(); + var remaining = totalExp; + foreach (var supply in supplies) + { + if (remaining == 0) break; + var count = remaining / supply.ProvideExp; + if (count == 0) continue; + remaining -= count * supply.ProvideExp; + rewards.Add([supply.Genre, supply.Detail, supply.Particular, supply.Level, (uint)Math.Min(count, 99999)]); + } + return rewards; + } + + private static List> ParseRewards(JToken? token) + { + if (token == null) return []; + + if (token is JArray outerArray) + { + var rewards = new List>(); + foreach (var element in outerArray) + { + if (element is JArray inner && inner.Count >= 4) + { + var reward = inner.Select(x => x.Value()).ToArray(); + if (reward.Length < 5) + reward = [.. reward, 1]; + rewards.Add(reward); + } + } + return rewards; + } + + return []; + } + + private static int GetItemColor(BaseGameItemInfo item) + { + if (item.ItemType == ItemTypeEnum.TYPE_WEAPON) + { + var t = GameData.WeaponData.Values.FirstOrDefault(x => + GameResourceTemplateId.FromGdpl(x.Genre, x.Detail, x.Particular, x.Level) == item.TemplateId); + return t?.Color ?? 0; + } + if (item.ItemType == ItemTypeEnum.TYPE_SUPPORT) + { + var t = GameData.SupportCardData.FirstOrDefault(x => x.TemplateId == item.TemplateId); + return (int)(t?.Color ?? 0); + } + return 0; + } + + private static Dictionary BuildLevelExpTable(IEnumerable<(int Lv, uint NeedExp)> source) + { + var table = new Dictionary(); + ulong accumulated = 0; + foreach (var (lv, needExp) in source.OrderBy(x => x.Lv)) + { + table[lv] = accumulated; + accumulated += needExp; + } + return table; + } + + private static uint GetUInt(JToken? token) => token?.Type switch + { + JTokenType.Integer => token.Value(), + JTokenType.Float => (uint)Math.Max(0, token.Value()), + JTokenType.String when uint.TryParse(token.Value(), out var r) => r, + _ => 0 + }; + + private static decimal GetDecimal(JToken? token) => token?.Type switch + { + JTokenType.Integer => token.Value(), + JTokenType.Float => token.Value(), + JTokenType.String when decimal.TryParse(token.Value(), out var r) => r, + _ => 0m + }; +} + +internal readonly record struct RecycleEntry( + List> FixedRewards, + uint RecycleBase, + decimal RecycleRatio); + +internal readonly record struct SupplyTemplate(uint Genre, uint Detail, uint Particular, uint Level, uint ProvideExp); + +internal sealed class ItemRecycleParam +{ + [JsonPropertyName("tbItems")] + public List TbItems { get; set; } = []; +}