From 196b03718cffc8652b9ba68174ee5bc77c51428b Mon Sep 17 00:00:00 2001 From: Kei-Luna Date: Sat, 23 May 2026 20:00:15 +0900 Subject: [PATCH] TowerLevel_LevelSettlement ClimbTowerLogic_RecordProgres --- Common/Data/Excel/ClimbTowerTimeExcel.cs | 39 ++++ .../Chapter/Chapter_DealLevelSettlement.cs | 8 + .../Tower/ClimbTowerLogic_RecordProgres.cs | 219 ++++++++++++++++++ .../Tower/TowerLevel_LevelSettlement.cs | 178 ++++++++++++++ 4 files changed, 444 insertions(+) create mode 100644 GameServer/Server/CallGS/Handlers/Tower/ClimbTowerLogic_RecordProgres.cs create mode 100644 GameServer/Server/CallGS/Handlers/Tower/TowerLevel_LevelSettlement.cs diff --git a/Common/Data/Excel/ClimbTowerTimeExcel.cs b/Common/Data/Excel/ClimbTowerTimeExcel.cs index 32a9d17..da1aed0 100644 --- a/Common/Data/Excel/ClimbTowerTimeExcel.cs +++ b/Common/Data/Excel/ClimbTowerTimeExcel.cs @@ -1,4 +1,6 @@ using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.Globalization; namespace MikuSB.Data.Excel; @@ -8,6 +10,8 @@ public class ClimbTowerTimeExcel : ExcelResource [JsonProperty("ID")] public uint ID { get; set; } [JsonProperty("StartTime")] public string StartTime { get; set; } = ""; [JsonProperty("EndTime")] public string EndTime { get; set; } = ""; + [JsonProperty("Level1")] public List> Level1 { get; set; } = []; + [JsonProperty("Level2")] public JToken? Level2Raw { get; set; } public override uint GetId() => ID; @@ -15,4 +19,39 @@ public class ClimbTowerTimeExcel : ExcelResource { GameData.ClimbTowerTimeData[ID] = this; } + + public IReadOnlyList> GetLevelGroups(int type) + { + if (type == 1) + return Level1; + + if (Level2Raw == null) + return []; + + if (Level2Raw.Type == JTokenType.Array) + { + return Level2Raw + .Children() + .OfType() + .Select(x => (IReadOnlyList)x.Values().ToList()) + .ToList(); + } + + if (Level2Raw.Type == JTokenType.Object) + { + return Level2Raw + .Children() + .Select(x => new + { + Key = uint.TryParse(x.Name, CultureInfo.InvariantCulture, out var key) ? key : 0u, + Value = x.Value.Type == JTokenType.Integer ? x.Value.Value() : 0u + }) + .Where(x => x.Key > 0 && x.Value > 0) + .OrderBy(x => x.Key) + .Select(x => (IReadOnlyList)new List { x.Key, x.Value }) + .ToList(); + } + + return []; + } } diff --git a/GameServer/Server/CallGS/Handlers/Chapter/Chapter_DealLevelSettlement.cs b/GameServer/Server/CallGS/Handlers/Chapter/Chapter_DealLevelSettlement.cs index 64f579c..45b1a8a 100644 --- a/GameServer/Server/CallGS/Handlers/Chapter/Chapter_DealLevelSettlement.cs +++ b/GameServer/Server/CallGS/Handlers/Chapter/Chapter_DealLevelSettlement.cs @@ -3,6 +3,7 @@ using System.Text.Json.Nodes; using System.Text.Json.Serialization; using MikuSB.GameServer.Game.BossPvp; using MikuSB.Proto; +using MikuSB.GameServer.Server.CallGS.Handlers.Tower; namespace MikuSB.GameServer.Server.CallGS.Handlers.Chapter; @@ -56,6 +57,13 @@ public class Chapter_DealLevelSettlement : ICallGSHandler return response; } + if (string.Equals(sCmd, "TowerLevel_LevelSettlement", StringComparison.Ordinal)) + { + var (response, sync) = TowerLevel_LevelSettlement.HandleSettlement(connection.Player!, tbParam); + extraSync = sync; + return response; + } + return tbParam?.DeepClone() ?? new JsonObject(); } } diff --git a/GameServer/Server/CallGS/Handlers/Tower/ClimbTowerLogic_RecordProgres.cs b/GameServer/Server/CallGS/Handlers/Tower/ClimbTowerLogic_RecordProgres.cs new file mode 100644 index 0000000..fba93a1 --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/Tower/ClimbTowerLogic_RecordProgres.cs @@ -0,0 +1,219 @@ +using MikuSB.Data; +using MikuSB.Data.Excel; +using MikuSB.Database; +using MikuSB.Database.Player; +using MikuSB.GameServer.Game.Player; +using MikuSB.Proto; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.Tower; + +[CallGSApi("ClimbTowerLogic_RecordProgres")] +public class ClimbTowerLogic_RecordProgres : ICallGSHandler +{ + private const uint TowerGroupId = 3; + private const uint BasicProgressSid = 2; + private const uint AdvancedProgressSid = 3; + private const uint LevelStateSidBase = 10000; + + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var player = connection.Player!; + var req = JsonSerializer.Deserialize(param); + if (req == null || req.LevelId == 0 || req.Area <= 0) + { + await CallGSRouter.SendScript(connection, "ClimbTowerLogic_RecordProgres", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + var cycle = ResolveCurrentCycle(GameData.ClimbTowerTimeData.Values, DateTime.Now); + if (cycle == null) + { + await CallGSRouter.SendScript(connection, "ClimbTowerLogic_RecordProgres", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + var towerType = ResolveTowerType(cycle, (uint)req.LevelId); + if (towerType == 0) + { + await CallGSRouter.SendScript(connection, "ClimbTowerLogic_RecordProgres", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + var sync = new NtfSyncPlayer(); + + var levelStateSid = LevelStateSidBase + (uint)req.LevelId; + var levelState = GetOrCreateAttr(player.Data, TowerGroupId, levelStateSid); + levelState.Val = MergeAreaStars(levelState.Val, req.Area, req.StarMask); + SyncAttr(sync, player, levelState); + + var progressSid = towerType == 1 ? BasicProgressSid : AdvancedProgressSid; + var progressAttr = GetOrCreateAttr(player.Data, TowerGroupId, progressSid); + progressAttr.Val = req.Area >= 3 ? 0u : PackProgress((uint)req.LevelId, (uint)(req.Area + 1)); + SyncAttr(sync, player, progressAttr); + + if (req.RoleHP.Count > 0 || req.TeamEnergy.HasValue) + { + SaveRoleState(player, sync, towerType, req.RoleHP, req.TeamEnergy.GetValueOrDefault()); + } + + DatabaseHelper.SaveDatabaseType(player.Data); + await CallGSRouter.SendScript(connection, "ClimbTowerLogic_RecordProgres", "{}", sync); + } + + private static void SaveRoleState( + PlayerInstance player, + NtfSyncPlayer sync, + int towerType, + List> roleHp, + int teamEnergy) + { + var slotStart = towerType == 2 ? 4u : 1u; + + for (var slot = slotStart; slot < slotStart + 3; slot++) + { + var templateAttr = GetOrCreateAttr(player.Data, TowerGroupId, slot * 10); + var hpAttr = GetOrCreateAttr(player.Data, TowerGroupId, slot * 10 + 1); + templateAttr.Val = 0; + hpAttr.Val = 0; + SyncAttr(sync, player, templateAttr); + SyncAttr(sync, player, hpAttr); + } + + for (var i = 0; i < Math.Min(roleHp.Count, 3); i++) + { + var row = roleHp[i]; + if (row == null || row.Count < 2) + continue; + + var slot = slotStart + (uint)i; + var templateAttr = GetOrCreateAttr(player.Data, TowerGroupId, slot * 10); + var hpAttr = GetOrCreateAttr(player.Data, TowerGroupId, slot * 10 + 1); + templateAttr.Val = (uint)Math.Max(0, row[0]); + hpAttr.Val = (uint)Math.Max(0, row[1]); + SyncAttr(sync, player, templateAttr); + SyncAttr(sync, player, hpAttr); + } + + var energyAttr = GetOrCreateAttr(player.Data, TowerGroupId, slotStart * 10 + 2); + energyAttr.Val = (uint)Math.Max(0, teamEnergy); + SyncAttr(sync, player, energyAttr); + } + + private static uint MergeAreaStars(uint currentValue, int area, int starMask) + { + var areaIndex = Math.Clamp(area, 1, 3) - 1; + var result = currentValue; + for (var i = 0; i < 3; i++) + { + if (((starMask >> i) & 1) == 0) + continue; + + var bitIndex = areaIndex * 3 + i; + result |= 1u << bitIndex; + } + + return result; + } + + private static uint PackProgress(uint levelId, uint area) => (area << 24) | (levelId & 0x00FF_FFFF); + + private static int ResolveTowerType(ClimbTowerTimeExcel cycle, uint levelId) + { + if (ContainsLevel(cycle.GetLevelGroups(1), levelId)) + return 1; + + if (ContainsLevel(cycle.GetLevelGroups(2), levelId)) + return 2; + + return 0; + } + + private static bool ContainsLevel(IEnumerable> groups, uint levelId) + { + return groups.Any(group => group.Any(id => id == levelId)); + } + + 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", + System.Globalization.CultureInfo.InvariantCulture, + System.Globalization.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 ClimbTowerRecordProgressParam +{ + [JsonPropertyName("nID")] + public int LevelId { get; set; } + + [JsonPropertyName("nArea")] + public int Area { get; set; } + + [JsonPropertyName("nStar")] + public int StarMask { get; set; } + + [JsonPropertyName("tbRoleHP")] + public List> RoleHP { get; set; } = []; + + [JsonPropertyName("nTeamEnergy")] + public int? TeamEnergy { get; set; } +} diff --git a/GameServer/Server/CallGS/Handlers/Tower/TowerLevel_LevelSettlement.cs b/GameServer/Server/CallGS/Handlers/Tower/TowerLevel_LevelSettlement.cs new file mode 100644 index 0000000..0fdb5b6 --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/Tower/TowerLevel_LevelSettlement.cs @@ -0,0 +1,178 @@ +using MikuSB.Data; +using MikuSB.Data.Excel; +using MikuSB.Database; +using MikuSB.Database.Player; +using MikuSB.GameServer.Game.Player; +using MikuSB.Proto; +using MikuSB.Util; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.Tower; + +[CallGSApi("TowerLevel_LevelSettlement")] +public class TowerLevel_LevelSettlement : ICallGSHandler +{ + private static readonly Logger Logger = new("Tower"); + private const uint TowerGroupId = 3; + private const uint LaunchPassGroupId = 22; + private const uint BasicProgressSid = 2; + private const uint AdvancedProgressSid = 3; + private const uint LevelStateSidBase = 10000; + private const int FinalArea = 3; + + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var (response, sync) = HandleSettlement(connection.Player!, JsonNode.Parse(param)); + await CallGSRouter.SendScript(connection, "TowerLevel_LevelSettlement", response.ToJsonString(), sync); + } + + public static (JsonNode Response, NtfSyncPlayer Sync) HandleSettlement(PlayerInstance player, JsonNode? tbParam) + { + var req = tbParam?.Deserialize(); + if (req == null || req.TowerId == 0 || req.LevelId == 0) + { + Logger.Error($"Invalid tower settlement payload: {tbParam?.ToJsonString() ?? "null"}"); + return (new JsonObject { ["sErr"] = "error.BadParam" }, new NtfSyncPlayer()); + } + + var cycle = ResolveCurrentCycle(GameData.ClimbTowerTimeData.Values, DateTime.Now); + if (cycle == null) + return (new JsonObject { ["sErr"] = "error.BadParam" }, new NtfSyncPlayer()); + + var towerType = ResolveTowerType(cycle, (uint)req.TowerId); + if (towerType == 0) + return (new JsonObject { ["sErr"] = "error.BadParam" }, new NtfSyncPlayer()); + + var sync = new NtfSyncPlayer(); + var levelStateSid = LevelStateSidBase + (uint)req.TowerId; + var levelState = GetOrCreateAttr(player.Data, TowerGroupId, levelStateSid); + levelState.Val = MergeAreaStars(levelState.Val, FinalArea, req.StarMask); + SyncAttr(sync, player, levelState); + + var progressSid = towerType == 1 ? BasicProgressSid : AdvancedProgressSid; + var progressAttr = GetOrCreateAttr(player.Data, TowerGroupId, progressSid); + progressAttr.Val = 0; + SyncAttr(sync, player, progressAttr); + + var passAttr = GetOrCreateAttr(player.Data, LaunchPassGroupId, (uint)req.LevelId); + passAttr.Val = Math.Max(1u, passAttr.Val + 1); + SyncAttr(sync, player, passAttr); + + Logger.Info( + $"Tower settlement saved. uid={player.Uid} towerId={req.TowerId} levelId={req.LevelId} starMask={req.StarMask} " + + $"towerStateSid={levelStateSid} towerStateVal={levelState.Val} progressSid={progressSid} passVal={passAttr.Val}"); + + DatabaseHelper.SaveDatabaseType(player.Data); + return (new JsonObject(), sync); + } + + private static uint MergeAreaStars(uint currentValue, int area, int starMask) + { + var areaIndex = Math.Clamp(area, 1, 3) - 1; + var result = currentValue; + for (var i = 0; i < 3; i++) + { + if (((starMask >> i) & 1) == 0) + continue; + + var bitIndex = areaIndex * 3 + i; + result |= 1u << bitIndex; + } + + return result; + } + + private static int ResolveTowerType(ClimbTowerTimeExcel cycle, uint levelId) + { + if (ContainsLevel(cycle.GetLevelGroups(1), levelId)) + return 1; + + if (ContainsLevel(cycle.GetLevelGroups(2), levelId)) + return 2; + + return 0; + } + + private static bool ContainsLevel(IEnumerable> groups, uint levelId) + { + return groups.Any(group => group.Any(id => id == levelId)); + } + + 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", + System.Globalization.CultureInfo.InvariantCulture, + System.Globalization.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(MikuSB.Proto.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 TowerLevelSettlementParam +{ + [JsonPropertyName("nID")] + public int LevelId { get; set; } + + [JsonPropertyName("nTowerID")] + public int TowerId { get; set; } + + [JsonPropertyName("nStar")] + public int StarMask { get; set; } +}