From 55bfadbd3eead2351d60d2dae477c95f0769fbf4 Mon Sep 17 00:00:00 2001 From: Kei-Luna Date: Sun, 24 May 2026 05:40:26 +0900 Subject: [PATCH] ClimbTowerLogic_GetReward --- Common/Data/Excel/ClimbTowerAwardExcel.cs | 56 +++ .../Data/Excel/ClimbTowerLevelOrderExcel.cs | 17 + Common/Data/Excel/OtherItemExcel.cs | 38 ++ Common/Data/GameData.cs | 3 + .../Tower/ClimbTowerLogic_GetReward.cs | 449 ++++++++++++++++++ 5 files changed, 563 insertions(+) create mode 100644 Common/Data/Excel/ClimbTowerAwardExcel.cs create mode 100644 Common/Data/Excel/ClimbTowerLevelOrderExcel.cs create mode 100644 Common/Data/Excel/OtherItemExcel.cs create mode 100644 GameServer/Server/CallGS/Handlers/Tower/ClimbTowerLogic_GetReward.cs diff --git a/Common/Data/Excel/ClimbTowerAwardExcel.cs b/Common/Data/Excel/ClimbTowerAwardExcel.cs new file mode 100644 index 0000000..c33013a --- /dev/null +++ b/Common/Data/Excel/ClimbTowerAwardExcel.cs @@ -0,0 +1,56 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace MikuSB.Data.Excel; + +[ResourceEntity("challenge/climbtower/climb_tower_award.json")] +public class ClimbTowerAwardExcel : ExcelResource +{ + [JsonProperty("ID")] public uint ID { get; set; } + [JsonProperty("Diff")] public JToken? DiffRaw { get; set; } + [JsonProperty("FirstAward")] public List> FirstAward { get; set; } = []; + [JsonProperty("StarCount1")] public int StarCount1 { get; set; } + [JsonProperty("StarAward1")] public List> StarAward1 { get; set; } = []; + [JsonProperty("StarCount2")] public int StarCount2 { get; set; } + [JsonProperty("StarAward2")] public List> StarAward2 { get; set; } = []; + [JsonProperty("StarCount3")] public int StarCount3 { get; set; } + [JsonProperty("StarAward3")] public List> StarAward3 { get; set; } = []; + + [JsonIgnore] + public int Diff => DiffRaw?.Type switch + { + JTokenType.Integer => Math.Max(1, DiffRaw.Value()), + JTokenType.String when int.TryParse(DiffRaw.Value(), out var value) => Math.Max(1, value), + _ => 1 + }; + + public override uint GetId() => (ID * 10u) + (uint)Diff; + + public override void Loaded() + { + if (!GameData.ClimbTowerAwardData.TryGetValue(ID, out var diffMap)) + { + diffMap = []; + GameData.ClimbTowerAwardData[ID] = diffMap; + } + + diffMap[Diff] = this; + } + + public int GetStarCount(int group) => group switch + { + 1 => StarCount1, + 2 => StarCount2, + 3 => StarCount3, + _ => 0 + }; + + public IReadOnlyList> GetRewards(int group) => group switch + { + 0 => FirstAward, + 1 => StarAward1, + 2 => StarAward2, + 3 => StarAward3, + _ => [] + }; +} diff --git a/Common/Data/Excel/ClimbTowerLevelOrderExcel.cs b/Common/Data/Excel/ClimbTowerLevelOrderExcel.cs new file mode 100644 index 0000000..9e4fea1 --- /dev/null +++ b/Common/Data/Excel/ClimbTowerLevelOrderExcel.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; + +namespace MikuSB.Data.Excel; + +[ResourceEntity("challenge/climbtower/climb_tower_levelorder.json")] +public class ClimbTowerLevelOrderExcel : ExcelResource +{ + [JsonProperty("ID")] public uint ID { get; set; } + [JsonProperty("LevelID")] public uint LevelID { get; set; } + + public override uint GetId() => ID; + + public override void Loaded() + { + GameData.ClimbTowerLevelOrderData[ID] = this; + } +} diff --git a/Common/Data/Excel/OtherItemExcel.cs b/Common/Data/Excel/OtherItemExcel.cs new file mode 100644 index 0000000..685c0a1 --- /dev/null +++ b/Common/Data/Excel/OtherItemExcel.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace MikuSB.Data.Excel; + +[ResourceEntity("item/templates/others.json")] +public class OtherItemExcel : ExcelResource +{ + public uint Genre { get; set; } + public uint Detail { get; set; } + public uint Particular { get; set; } + public uint Level { get; set; } + [JsonProperty("GMnum")] public JToken? GMnumRaw { get; set; } + + [JsonIgnore] + public uint GMnum => ReadUInt(GMnumRaw); + + public override uint GetId() => (uint)GameResourceTemplateId.FromGdpl(Genre, Detail, Particular, Level); + + public override void Loaded() + { + GameData.OtherItemData[GetId()] = this; + } + + private static uint ReadUInt(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 value) => value, + _ => 0 + }; + } +} diff --git a/Common/Data/GameData.cs b/Common/Data/GameData.cs index 81aa083..96c5bdb 100644 --- a/Common/Data/GameData.cs +++ b/Common/Data/GameData.cs @@ -34,7 +34,10 @@ public static class GameData public static Dictionary BossPvpBossData { get; private set; } = []; public static Dictionary BossPvpNumData { get; private set; } = []; public static Dictionary ClimbTowerTimeData { get; private set; } = []; + public static Dictionary> ClimbTowerAwardData { get; private set; } = []; + public static Dictionary ClimbTowerLevelOrderData { get; private set; } = []; public static Dictionary TowerLevelData { get; private set; } = []; + public static Dictionary OtherItemData { get; private set; } = []; public static Dictionary ProfileData { get; private set; } = []; public static Dictionary CardSkinPartsData { get; private set; } = []; public static Dictionary CallItemData { get; private set; } = []; diff --git a/GameServer/Server/CallGS/Handlers/Tower/ClimbTowerLogic_GetReward.cs b/GameServer/Server/CallGS/Handlers/Tower/ClimbTowerLogic_GetReward.cs new file mode 100644 index 0000000..eb8372c --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/Tower/ClimbTowerLogic_GetReward.cs @@ -0,0 +1,449 @@ +using MikuSB.Data; +using MikuSB.Data.Excel; +using MikuSB.Database; +using MikuSB.Database.Inventory; +using MikuSB.Database.Player; +using MikuSB.Enums.Item; +using MikuSB.GameServer.Game.Player; +using MikuSB.Proto; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.Tower; + +[CallGSApi("ClimbTowerLogic_GetReward")] +public class ClimbTowerLogic_GetReward : ICallGSHandler +{ + private const uint TowerGroupId = 3; + private const uint RewardStateSidBase = 100; + private const uint TowerLevelStateSidBase = 10000; + private const uint LaunchPassGroupId = 22; + private const uint AdvancedDiffSid = 4; + + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var player = connection.Player!; + var req = JsonSerializer.Deserialize(param); + if (req == null || req.Layer <= 0) + { + await CallGSRouter.SendScript(connection, "ClimbTowerLogic_GetReward", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + var cycle = ResolveCurrentCycle(GameData.ClimbTowerTimeData.Values, DateTime.Now); + if (cycle == null) + { + await CallGSRouter.SendScript(connection, "ClimbTowerLogic_GetReward", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + if (!TryResolveLayer(cycle, req.Layer, player.Data, out var towerIds, out var diff)) + { + await CallGSRouter.SendScript(connection, "ClimbTowerLogic_GetReward", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + if (!GameData.ClimbTowerAwardData.TryGetValue((uint)req.Layer, out var diffMap) || + !diffMap.TryGetValue(diff, out var rewardCfg)) + { + await CallGSRouter.SendScript(connection, "ClimbTowerLogic_GetReward", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + var groups = ResolveRequestedGroups(req.Group); + if (groups.Count == 0) + { + await CallGSRouter.SendScript(connection, "ClimbTowerLogic_GetReward", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + var claimableGroups = groups + .Where(group => CanClaimGroup(player.Data, rewardCfg, towerIds, req.Layer, group)) + .Distinct() + .ToList(); + + if (claimableGroups.Count == 0) + { + await CallGSRouter.SendScript(connection, "ClimbTowerLogic_GetReward", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + var sync = new NtfSyncPlayer(); + var rewardStateAttr = GetOrCreateAttr(player.Data, TowerGroupId, RewardStateSidBase + (uint)req.Layer); + var responseRewards = new JsonArray(); + + foreach (var group in claimableGroups) + { + rewardStateAttr.Val |= 1u << GetFlagBitOffset(group); + + foreach (var reward in rewardCfg.GetRewards(group)) + { + if (reward.Count < 5) + continue; + + await GrantRewardAsync(player, sync, reward); + responseRewards.Add(new JsonArray( + (int)reward[0], + (int)reward[1], + (int)reward[2], + (int)reward[3], + (int)reward[4])); + } + } + + SyncAttr(sync, player, rewardStateAttr); + DatabaseHelper.SaveDatabaseType(player.Data); + DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData); + DatabaseHelper.SaveDatabaseType(player.CharacterManager.CharacterData); + + var rsp = new JsonObject + { + ["tbRewards"] = responseRewards + }; + + await CallGSRouter.SendScript(connection, "ClimbTowerLogic_GetReward", rsp.ToJsonString(), sync); + } + + private static async Task GrantRewardAsync(PlayerInstance player, NtfSyncPlayer sync, IReadOnlyList reward) + { + 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_CARD: + for (var i = 0u; i < count; i++) + { + var character = await player.CharacterManager.AddCharacter(itemType, detail, particular, level, sendPacket: false); + if (character != null) + sync.Items.Add(character.ToProto()); + } + break; + case ItemTypeEnum.TYPE_WEAPON: + for (var i = 0u; i < count; i++) + { + var weapon = await player.InventoryManager.AddWeaponItem(itemType, detail, particular, level, sendPacket: false); + if (weapon != null) + sync.Items.Add(weapon.ToProto()); + } + break; + case ItemTypeEnum.TYPE_SUPPORT: + for (var i = 0u; i < count; i++) + { + var support = await player.InventoryManager.AddSupportCardItem(detail, particular, level, sendPacket: false); + if (support != null) + sync.Items.Add(support.ToProto()); + } + break; + case ItemTypeEnum.TYPE_SUPPLIES: + { + var templateId = (uint)GameResourceTemplateId.FromGdpl(reward[0], detail, particular, level); + if (GameData.SuppliesData.TryGetValue(templateId, out var supplies)) + { + var item = await player.InventoryManager.AddSuppliesItem(supplies, count, sendPacket: false); + if (item != null) + sync.Items.Add(item.ToProto()); + } + break; + } + case ItemTypeEnum.TYPE_USEABLE: + { + var item = AddOtherItem(player.InventoryManager.InventoryData, reward[0], detail, particular, level, count); + if (item != null) + sync.Items.Add(item.ToProto()); + break; + } + case ItemTypeEnum.TYPE_WEAPON_PART: + for (var i = 0u; i < count; i++) + { + var item = await player.InventoryManager.AddWeaponPartItem(itemType, detail, particular, level, sendPacket: false); + if (item != null) + sync.Items.Add(item.ToProto()); + } + break; + case ItemTypeEnum.TYPE_CARD_SKIN: + for (var i = 0u; i < count; i++) + { + var item = await player.InventoryManager.AddSkinItem(itemType, detail, particular, level, sendPacket: false); + if (item != null) + sync.Items.Add(item.ToProto()); + } + break; + case ItemTypeEnum.TYPE_HOUSE: + for (var i = 0u; i < count; i++) + { + var item = await player.InventoryManager.AddHouseFurnitureItem(itemType, detail, particular, level, sendPacket: false); + if (item != null) + sync.Items.Add(item.ToProto()); + } + break; + case ItemTypeEnum.TYPE_PROFILE: + case ItemTypeEnum.TYPE_FRAME: + case ItemTypeEnum.TYPE_BADGE: + case ItemTypeEnum.TYPE_COVER: + case ItemTypeEnum.TYPE_NAMECARD: + case ItemTypeEnum.TYPE_EXPRESSION: + case ItemTypeEnum.TYPE_BUBBLE: + case ItemTypeEnum.TYPE_ANALYST: + for (var i = 0u; i < count; i++) + { + var item = await player.InventoryManager.AddProfileItem(itemType, detail, particular, level, sendPacket: false); + if (item != null) + sync.Items.Add(item.ToProto()); + } + break; + case ItemTypeEnum.TYPE_WEAPON_SKIN: + for (var i = 0u; i < count; i++) + { + var item = await player.InventoryManager.AddWeaponSkinItem(itemType, detail, particular, level, sendPacket: false); + if (item != null) + sync.Items.Add(item.ToProto()); + } + break; + case ItemTypeEnum.TYPE_MANIFESTATION: + for (var i = 0u; i < count; i++) + { + var item = await player.InventoryManager.AddManifestationItem(itemType, detail, particular, level, sendPacket: false); + if (item != null) + sync.Items.Add(item.ToProto()); + } + break; + case ItemTypeEnum.TYPE_CARD_SKIN_PART: + for (var i = 0u; i < count; i++) + { + var item = await player.InventoryManager.AddSkinPartItem(itemType, detail, particular, level, sendPacket: false); + if (item != null) + sync.Items.Add(item.ToProto()); + } + break; + case ItemTypeEnum.TYPE_AR: + for (var i = 0u; i < count; i++) + { + var item = await player.InventoryManager.AddArItem(itemType, detail, particular, level, sendPacket: false); + if (item != null) + sync.Items.Add(item.ToProto()); + } + break; + case ItemTypeEnum.TYPE_CALL: + for (var i = 0u; i < count; i++) + { + var item = await player.InventoryManager.AddCallItem(itemType, detail, particular, level, sendPacket: false); + if (item != null) + sync.Items.Add(item.ToProto()); + } + break; + } + } + + private static BaseGameItemInfo? AddOtherItem(InventoryData inventory, uint genre, uint detail, uint particular, uint level, uint count) + { + var templateId = (uint)GameResourceTemplateId.FromGdpl(genre, detail, particular, level); + if (!GameData.OtherItemData.TryGetValue(templateId, out var otherItem)) + return null; + + var maxCount = otherItem.GMnum > 0 ? otherItem.GMnum : 99999u; + var existing = inventory.Items.Values.FirstOrDefault(x => x.TemplateId == templateId); + if (existing != null) + { + existing.ItemCount = Math.Min(existing.ItemCount + count, maxCount); + return existing; + } + + var item = new BaseGameItemInfo + { + TemplateId = templateId, + UniqueId = inventory.NextUniqueUid++, + ItemType = ItemTypeEnum.TYPE_USEABLE, + ItemCount = Math.Min(count, maxCount) + }; + inventory.Items[item.UniqueId] = item; + return item; + } + + private static bool CanClaimGroup( + PlayerGameData data, + ClimbTowerAwardExcel rewardCfg, + IReadOnlyList towerIds, + int layer, + int group) + { + if (group is < 0 or > 3 || IsRewardClaimed(data, layer, group)) + return false; + + if (group == 0) + return IsLayerPass(data, towerIds); + + var requiredStar = rewardCfg.GetStarCount(group); + return requiredStar > 0 && GetLayerStar(data, towerIds) >= requiredStar; + } + + private static bool IsLayerPass(PlayerGameData data, IReadOnlyList towerIds) + { + foreach (var towerId in towerIds) + { + if (!GameData.ClimbTowerLevelOrderData.TryGetValue(towerId, out var orderCfg)) + return false; + + var passAttr = data.Attrs.FirstOrDefault(x => x.Gid == LaunchPassGroupId && x.Sid == orderCfg.LevelID); + if (passAttr == null || passAttr.Val == 0) + return false; + } + + return true; + } + + private static int GetLayerStar(PlayerGameData data, IReadOnlyList towerIds) + { + var total = 0; + foreach (var towerId in towerIds) + { + var attr = data.Attrs.FirstOrDefault(x => x.Gid == TowerGroupId && x.Sid == TowerLevelStateSidBase + towerId); + var value = attr?.Val ?? 0; + for (var i = 0; i < 9; i++) + { + if (((value >> i) & 1u) != 0) + total++; + } + } + + return total; + } + + private static bool IsRewardClaimed(PlayerGameData data, int layer, int group) + { + var attr = data.Attrs.FirstOrDefault(x => x.Gid == TowerGroupId && x.Sid == RewardStateSidBase + (uint)layer); + if (attr == null) + return false; + + var offset = GetFlagBitOffset(group); + return ((attr.Val >> offset) & 0xFu) > 0; + } + + private static int GetFlagBitOffset(int group) => group switch + { + 0 => 0, + 1 => 4, + 2 => 8, + 3 => 12, + _ => 0 + }; + + private static List ResolveRequestedGroups(int? group) + { + if (!group.HasValue) + return [0, 1, 2, 3]; + + return group.Value is >= 0 and <= 3 ? [group.Value] : []; + } + + private static bool TryResolveLayer( + ClimbTowerTimeExcel cycle, + int layer, + PlayerGameData data, + out IReadOnlyList towerIds, + out int diff) + { + var basicGroups = cycle.GetLevelGroups(1); + if (layer <= basicGroups.Count) + { + towerIds = basicGroups[layer - 1]; + diff = 1; + return towerIds.Count > 0; + } + + var advancedIndex = layer - basicGroups.Count; + var advancedGroups = cycle.GetLevelGroups(2); + if (advancedIndex <= 0 || advancedIndex > advancedGroups.Count) + { + towerIds = []; + diff = 0; + return false; + } + + var diffAttr = data.Attrs.FirstOrDefault(x => x.Gid == TowerGroupId && x.Sid == AdvancedDiffSid); + diff = (int)(diffAttr?.Val ?? 0); + towerIds = advancedGroups[advancedIndex - 1]; + return diff > 0 && towerIds.Count > 0; + } + + private static ClimbTowerTimeExcel? ResolveCurrentCycle(IEnumerable configs, DateTime now) + { + var parsed = configs + .Select(x => new + { + Config = x, + Start = ParseConfigTime(x.StartTime), + End = ParseConfigTime(x.EndTime) + }) + .Where(x => x.Start.HasValue && x.End.HasValue) + .OrderBy(x => x.Start) + .ToList(); + + var current = parsed.FirstOrDefault(x => x.Start <= now && now < x.End); + if (current != null) + return current.Config; + + var latestStarted = parsed.LastOrDefault(x => x.Start <= now); + if (latestStarted != null) + return latestStarted.Config; + + return parsed.FirstOrDefault()?.Config; + } + + private static DateTime? ParseConfigTime(string? raw) + { + if (string.IsNullOrWhiteSpace(raw)) + return null; + + var normalized = raw.Trim().Trim('[', ']'); + if (normalized.Length != 12) + return null; + + return DateTime.TryParseExact( + normalized, + "yyyyMMddHHmm", + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out var value) + ? value + : null; + } + + private static PlayerAttr GetOrCreateAttr(PlayerGameData data, uint gid, uint sid) + { + var attr = data.Attrs.FirstOrDefault(x => x.Gid == gid && x.Sid == sid); + if (attr != null) + return attr; + + attr = new PlayerAttr + { + Gid = gid, + Sid = sid + }; + data.Attrs.Add(attr); + return attr; + } + + private static void SyncAttr(NtfSyncPlayer sync, PlayerInstance player, PlayerAttr attr) + { + sync.Custom[player.ToPackedAttrKey(attr.Gid, attr.Sid)] = attr.Val; + sync.Custom[player.ToShiftedAttrKey(attr.Gid, attr.Sid)] = attr.Val; + } +} + +internal sealed class ClimbTowerGetRewardParam +{ + [JsonPropertyName("nType")] + public int? Type { get; set; } + + [JsonPropertyName("nLayer")] + public int Layer { get; set; } + + [JsonPropertyName("nGroup")] + public int? Group { get; set; } +}