using MikuSB.Data; using MikuSB.Data.Excel; using MikuSB.Database; using MikuSB.Database.Inventory; using MikuSB.Proto; using Newtonsoft.Json.Linq; using System.Text.Json; using System.Text.Json.Serialization; namespace MikuSB.GameServer.Server.CallGS.Handlers.Weapon; [CallGSApi("Weapon_Upgrade")] public class Weapon_Upgrade : ICallGSHandler { public async Task Handle(Connection connection, string param, ushort seqNo) { var player = connection.Player!; var req = JsonSerializer.Deserialize(param); if (req == null || req.Id <= 0 || req.TbMat == null || req.TbMat.Count == 0) { await CallGSRouter.SendScript(connection, "Weapon_Upgrade", "{\"sErr\":\"tip.material_not_enough\"}"); return; } var targetWeapon = player.InventoryManager.GetWeaponItem((uint)req.Id); if (targetWeapon == null) { await CallGSRouter.SendScript(connection, "Weapon_Upgrade", "{\"sErr\":\"tip.material_not_enough\"}"); return; } var config = WeaponUpgradeConfig.Load(); if (!config.TryGetWeaponTemplate(targetWeapon.TemplateId, out var targetTemplate)) { await CallGSRouter.SendScript(connection, "Weapon_Upgrade", "{\"sErr\":\"error.BadParam\"}"); return; } var requestedMaterials = new Dictionary(); foreach (var row in req.TbMat) { if (row == null || row.Count < 2) continue; var itemId = (uint)Math.Max(0, row[0]); var count = (uint)Math.Max(0, row[1]); if (itemId == 0 || count == 0) continue; requestedMaterials[itemId] = requestedMaterials.GetValueOrDefault(itemId) + count; } if (requestedMaterials.Count == 0) { await CallGSRouter.SendScript(connection, "Weapon_Upgrade", "{\"sErr\":\"tip.material_not_enough\"}"); return; } ulong totalExp = 0; var syncItems = new List(); var equippedWeaponIds = player.CharacterManager.CharacterData.Characters .Select(x => x.WeaponUniqueId) .Where(x => x != 0) .ToHashSet(); foreach (var (itemId, count) in requestedMaterials) { if (itemId == targetWeapon.UniqueId) { await CallGSRouter.SendScript(connection, "Weapon_Upgrade", "{\"sErr\":\"tip.material_not_enough\"}"); return; } var material = FindInventoryItem(player.InventoryManager.InventoryData, itemId); if (material == null || material.ItemCount < count) { await CallGSRouter.SendScript(connection, "Weapon_Upgrade", "{\"sErr\":\"tip.material_not_enough\"}"); return; } if (material is GameWeaponInfo materialWeapon && (materialWeapon.EquipAvatarId != 0 || equippedWeaponIds.Contains(materialWeapon.UniqueId))) { await CallGSRouter.SendScript(connection, "Weapon_Upgrade", "{\"sErr\":\"tip.material_not_enough\"}"); return; } if (!TryGetMaterialGain(config, material, out var gainExp)) { await CallGSRouter.SendScript(connection, "Weapon_Upgrade", "{\"sErr\":\"error.BadParam\"}"); return; } totalExp += gainExp * count; } foreach (var (itemId, count) in requestedMaterials) { var material = FindInventoryItem(player.InventoryManager.InventoryData, itemId)!; material.ItemCount -= count; if (material.ItemCount == 0) { RemoveInventoryItem(player.InventoryManager.InventoryData, itemId); syncItems.Add(BuildRemovedProto(material)); } else { syncItems.Add(material.ToProto()); } } var maxLevel = config.GetWeaponMaxLevel(targetTemplate.BreakLimitId, targetWeapon.Break); var oldLevel = targetWeapon.Level == 0 ? 1u : targetWeapon.Level; targetWeapon.Level = oldLevel; var (newLevel, newExp) = config.ApplyWeaponExp(targetWeapon.Level, targetWeapon.Exp, totalExp, targetTemplate.Color, maxLevel); targetWeapon.Level = newLevel; targetWeapon.Exp = newExp; syncItems.Add(targetWeapon.ToProto()); DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData); var sync = new NtfSyncPlayer(); sync.Items.AddRange(syncItems); var bMaxUnlock = maxLevel > 0 && targetWeapon.Level >= maxLevel; var arg = $"{{\"bMaxUnLock\":{(bMaxUnlock ? "true" : "false")}}}"; await CallGSRouter.SendScript(connection, "Weapon_Upgrade", arg, sync); } private static BaseGameItemInfo? FindInventoryItem(InventoryData inventory, uint itemId) { if (inventory.Weapons.TryGetValue(itemId, out var weapon)) { return weapon; } if (inventory.Skins.TryGetValue(itemId, out var skin)) { return skin; } if (inventory.Items.TryGetValue(itemId, out var item)) { return item; } return null; } private static void RemoveInventoryItem(InventoryData inventory, uint itemId) { inventory.Weapons.Remove(itemId); inventory.Skins.Remove(itemId); inventory.Items.Remove(itemId); } private static Item BuildRemovedProto(BaseGameItemInfo item) { var proto = item.ToProto(); proto.Count = 0; return proto; } private static bool TryGetMaterialGain(WeaponUpgradeConfig config, BaseGameItemInfo item, out ulong exp) { exp = 0; if (config.TryGetWeaponTemplate(item.TemplateId, out var weaponTemplate)) { exp = weaponTemplate.ProvideExp; if (item is GameWeaponInfo weapon && weapon.Level > 1) { exp += config.GetWeaponRecycleExp(weaponTemplate, weapon.Level); } return true; } if (config.TryGetSuppliesTemplate(item.TemplateId, out var suppliesTemplate)) { exp = suppliesTemplate.ProvideExp; return true; } return false; } } internal sealed class WeaponUpgradeParam { [JsonPropertyName("Id")] public int Id { get; set; } [JsonPropertyName("tbMat")] public List> TbMat { get; set; } = []; } internal sealed class WeaponUpgradeConfig { private readonly Dictionary _weaponNeedExpNormal; private readonly Dictionary _weaponNeedExpSsr; private readonly Dictionary _breakLimits; private readonly Dictionary _recycleById; private readonly Dictionary _weaponTemplates; private readonly Dictionary _suppliesTemplates; private readonly Dictionary _weaponRecycleExpNormal = []; private readonly Dictionary _weaponRecycleExpSsr = []; public WeaponUpgradeConfig( Dictionary weaponNeedExpNormal, Dictionary weaponNeedExpSsr, Dictionary breakLimits, Dictionary recycleById, Dictionary weaponTemplates, Dictionary suppliesTemplates) { _weaponNeedExpNormal = weaponNeedExpNormal; _weaponNeedExpSsr = weaponNeedExpSsr; _breakLimits = breakLimits; _recycleById = recycleById; _weaponTemplates = weaponTemplates; _suppliesTemplates = suppliesTemplates; BuildRecycleExpTable(_weaponNeedExpNormal, _weaponRecycleExpNormal); BuildRecycleExpTable(_weaponNeedExpSsr, _weaponRecycleExpSsr); } public static WeaponUpgradeConfig Load() { var normalExp = new Dictionary(); var ssrExp = new Dictionary(); foreach (var row in GameData.UpgradeExpData.Values) { normalExp[row.Lv] = row.WeaponNeedExp; ssrExp[row.Lv] = row.SSRWeaponNeedExp; } var breakLimits = new Dictionary(); foreach (var row in GameData.BreakLevelLimitData.Values) { breakLimits[row.ID] = [ row.Break0, row.Break1, row.Break2, row.Break3, row.Break4, row.Break5, row.Break6 ]; } var recycleById = new Dictionary(); foreach (var row in GameData.RecycleData.Values) { recycleById[row.ID] = new RecycleEntry( GetUIntFlexible(row.RecycleBase), GetDecimalFlexible(row.RecycleRatio)); } var weaponTemplates = GameData.WeaponData.Values.ToDictionary( x => GameResourceTemplateId.FromGdpl(x.Genre, x.Detail, x.Particular, x.Level), x => new MaterialTemplate(x.Color, x.ProvideExp, x.ConsumeGold, x.RecycleID, x.BreakLimitID)); var suppliesTemplates = GameData.SuppliesData.Values.ToDictionary( x => GameResourceTemplateId.FromGdpl(x.Genre, x.Detail, x.Particular, x.Level), x => new MaterialTemplate(x.Color, x.ProvideExp, x.ConsumeGold, 0, 0)); return new WeaponUpgradeConfig(normalExp, ssrExp, breakLimits, recycleById, weaponTemplates, suppliesTemplates); } public bool TryGetWeaponTemplate(ulong templateId, out MaterialTemplate template) => _weaponTemplates.TryGetValue(templateId, out template!); public bool TryGetSuppliesTemplate(ulong templateId, out MaterialTemplate template) => _suppliesTemplates.TryGetValue(templateId, out template!); public ulong GetWeaponRecycleExp(MaterialTemplate template, uint level) { if (template.RecycleId <= 0 || !_recycleById.TryGetValue(template.RecycleId, out var recycle)) { return 0; } var levelExp = template.Color == 5 ? _weaponRecycleExpSsr : _weaponRecycleExpNormal; var baseExp = levelExp.GetValueOrDefault((int)level); return (ulong)Math.Floor((recycle.RecycleBase + baseExp) * recycle.RecycleRatio); } public uint GetWeaponMaxLevel(int breakLimitId, uint currentBreak) { if (!_breakLimits.TryGetValue(breakLimitId, out var limits) || limits.Length == 0) { return 0; } var index = (int)Math.Min(currentBreak, (uint)(limits.Length - 1)); return limits[index]; } public (uint Level, uint Exp) ApplyWeaponExp(uint level, uint exp, ulong addExp, int color, uint maxLevel) { if (addExp == 0) { return (level, exp); } if (maxLevel > 0 && level >= maxLevel) { return (maxLevel, checked((uint)(exp + addExp))); } var destLevel = level; var destExp = exp + addExp; var needExp = GetWeaponNeedExp(color, destLevel); while (needExp > 0 && destExp >= needExp) { destExp -= needExp; destLevel++; if (maxLevel > 0 && destLevel >= maxLevel) { return (maxLevel, checked((uint)destExp)); } needExp = GetWeaponNeedExp(color, destLevel); if (needExp == 0) { return (destLevel, checked((uint)destExp)); } } return (destLevel, checked((uint)destExp)); } private uint GetWeaponNeedExp(int color, uint level) { return color == 5 ? _weaponNeedExpSsr.GetValueOrDefault((int)level) : _weaponNeedExpNormal.GetValueOrDefault((int)level); } private static void BuildRecycleExpTable(Dictionary needExp, Dictionary recycleExp) { ulong current = 0; foreach (var level in needExp.Keys.OrderBy(x => x)) { recycleExp[level] = current; current += needExp[level]; } } private static uint GetUIntFlexible(JToken? token) { if (token == null) { return 0; } return token.Type switch { JTokenType.Integer => token.Value(), JTokenType.Float => (uint)Math.Max(0, token.Value()), JTokenType.String when uint.TryParse(token.Value(), out var result) => result, _ => 0 }; } private static decimal GetDecimalFlexible(JToken? token) { if (token == null) { return 0m; } return token.Type switch { JTokenType.Integer => token.Value(), JTokenType.Float => token.Value(), JTokenType.String when decimal.TryParse(token.Value(), out var result) => result, _ => 0m }; } } internal readonly record struct MaterialTemplate(int Color, uint ProvideExp, uint ConsumeGold, int RecycleId, int BreakLimitId); internal readonly record struct RecycleEntry(uint RecycleBase, decimal RecycleRatio);