From 6063a3a0cd680f372b331653018e668244c4c287 Mon Sep 17 00:00:00 2001 From: Kei-Luna Date: Mon, 25 May 2026 13:45:14 +0900 Subject: [PATCH] DreamCard_LevelSettlement DreamCard_UpdateData --- .../Chapter/Chapter_DealLevelSettlement.cs | 8 + .../DreamCard/DreamCard_LevelSettlement.cs | 281 ++++++++++++++++++ .../DreamCard/DreamCard_UpdateData.cs | 59 ++++ 3 files changed, 348 insertions(+) create mode 100644 GameServer/Server/CallGS/Handlers/DreamCard/DreamCard_LevelSettlement.cs create mode 100644 GameServer/Server/CallGS/Handlers/DreamCard/DreamCard_UpdateData.cs diff --git a/GameServer/Server/CallGS/Handlers/Chapter/Chapter_DealLevelSettlement.cs b/GameServer/Server/CallGS/Handlers/Chapter/Chapter_DealLevelSettlement.cs index 19e3b2c..4bc720d 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.DreamCard; using MikuSB.GameServer.Server.CallGS.Handlers.Tower; using MikuSB.GameServer.Server.CallGS.Handlers.VirCapture; @@ -80,6 +81,13 @@ public class Chapter_DealLevelSettlement : ICallGSHandler return response; } + if (string.Equals(sCmd, "DreamCard_LevelSettlement", StringComparison.Ordinal)) + { + var (response, sync) = DreamCard_LevelSettlement.HandleSettlement(connection.Player!, tbParam); + extraSync = sync; + return response; + } + return tbParam?.DeepClone() ?? new JsonObject(); } diff --git a/GameServer/Server/CallGS/Handlers/DreamCard/DreamCard_LevelSettlement.cs b/GameServer/Server/CallGS/Handlers/DreamCard/DreamCard_LevelSettlement.cs new file mode 100644 index 0000000..b407d39 --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/DreamCard/DreamCard_LevelSettlement.cs @@ -0,0 +1,281 @@ +using MikuSB.Database; +using MikuSB.Database.Player; +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.DreamCard; + +[CallGSApi("DreamCard_LevelSettlement")] +public class DreamCard_LevelSettlement : ICallGSHandler +{ + private const uint LevelGroupId = 152; + private const uint LevelSubNum = 10; + private const int OrdinaryType = 1; + private const int ChallengeType = 2; + private const int EndlessType = 3; + + private static readonly Lazy SettlementIndex = new(LoadIndex); + + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var (response, sync) = HandleSettlement(connection.Player!, JsonNode.Parse(param)); + await CallGSRouter.SendScript(connection, "DreamCard_LevelSettlement", response.ToJsonString(), sync); + } + + public static (JsonObject Response, NtfSyncPlayer Sync) HandleSettlement(PlayerInstance player, JsonNode? tbParam) + { + var req = tbParam?.Deserialize(); + if (req == null || req.LevelId <= 0 || req.Diff <= 0 || req.Type is < OrdinaryType or > EndlessType) + return (new JsonObject { ["sErr"] = "error.BadParam" }, new NtfSyncPlayer()); + + var sync = new NtfSyncPlayer(); + var response = new JsonObject + { + ["nID"] = req.LevelId, + ["nDiff"] = req.Diff, + ["nType"] = req.Type + }; + + switch (req.Type) + { + case OrdinaryType: + HandleOrdinary(player, sync, response, req); + break; + case ChallengeType: + HandleChallenge(player, sync, response, req); + break; + case EndlessType: + HandleEndless(response, req); + break; + } + + DatabaseHelper.SaveDatabaseType(player.Data); + return (response, sync); + } + + private static void HandleOrdinary(PlayerInstance player, NtfSyncPlayer sync, JsonObject response, DreamCardLevelSettlementParam req) + { + var baseSid = (uint)(LevelSubNum * req.LevelId); + + var passAttr = GetOrCreateAttr(player.Data, LevelGroupId, baseSid + 1); + passAttr.Val += 1; + SyncAttr(sync, player, passAttr); + + var diffAttr = GetOrCreateAttr(player.Data, LevelGroupId, baseSid + 2); + diffAttr.Val = Math.Max(diffAttr.Val, (uint)req.Diff); + SyncAttr(sync, player, diffAttr); + + var starAttr = GetOrCreateAttr(player.Data, LevelGroupId, baseSid + 3); + starAttr.Val = MergeDifficultyBits(starAttr.Val, req.Diff, req.StarValue); + SyncAttr(sync, player, starAttr); + + if (TryGetOrdinaryRewardId((uint)req.LevelId, (uint)req.Diff, out var rewardId) && rewardId > 0) + response["nRewardID"] = rewardId; + } + + private static void HandleChallenge(PlayerInstance player, NtfSyncPlayer sync, JsonObject response, DreamCardLevelSettlementParam req) + { + var baseSid = (uint)(LevelSubNum * req.LevelId); + var scoreSid = baseSid + (uint)req.Diff + 4; + + var currentScore = (uint)Math.Max(0, req.Score); + var scoreAttr = GetOrCreateAttr(player.Data, LevelGroupId, scoreSid); + var newRecord = currentScore > scoreAttr.Val; + scoreAttr.Val = Math.Max(scoreAttr.Val, currentScore); + SyncAttr(sync, player, scoreAttr); + + var challengePeriodId = ResolveCurrentChallengePeriodId(DateTime.Now); + if (challengePeriodId > 0) + { + var periodAttr = GetOrCreateAttr(player.Data, LevelGroupId, 0); + periodAttr.Val = challengePeriodId; + SyncAttr(sync, player, periodAttr); + } + + response["NewRecord"] = newRecord; + } + + private static void HandleEndless(JsonObject response, DreamCardLevelSettlementParam req) + { + response["NewRecord"] = false; + } + + private static uint MergeDifficultyBits(uint currentValue, int diff, int starMask) + { + var bitStart = Math.Max(0, diff - 1) * 3; + var result = currentValue; + for (var i = 0; i < 3; i++) + { + if (((starMask >> i) & 1) == 0) + continue; + + result |= 1u << (bitStart + i); + } + + return result; + } + + private static bool TryGetOrdinaryRewardId(uint levelId, uint diff, out uint rewardId) + { + rewardId = 0; + var index = SettlementIndex.Value; + if (index == null) + return false; + + return index.TryGetOrdinaryRewardId(levelId, diff, out rewardId); + } + + private static uint ResolveCurrentChallengePeriodId(DateTime now) + { + var index = SettlementIndex.Value; + return index?.ResolveCurrentChallengePeriodId(now) ?? 0; + } + + private static DreamCardSettlementIndex? LoadIndex() + { + try + { + var root = Path.Combine(MikuSB.Util.ConfigManager.Config.Path.ResourcePath, "dlc", "DreamCard"); + var ordinaryLevels = LoadJson>(Path.Combine(root, "levellist.json")) ?? []; + var challengeTimes = LoadJson>(Path.Combine(root, "chall_time.json")) ?? []; + return new DreamCardSettlementIndex(ordinaryLevels, challengeTimes); + } + catch + { + return null; + } + } + + private static T? LoadJson(string path) + { + if (!File.Exists(path)) + return default; + + return JsonSerializer.Deserialize(File.ReadAllText(path)); + } + + 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 DreamCardLevelSettlementParam +{ + [JsonPropertyName("nID")] + public int LevelId { get; set; } + + [JsonPropertyName("nDiff")] + public int Diff { get; set; } + + [JsonPropertyName("nType")] + public int Type { get; set; } + + [JsonPropertyName("nStarValue")] + public int StarValue { get; set; } + + [JsonPropertyName("nScore")] + public int Score { get; set; } +} + +internal sealed class DreamCardSettlementIndex +{ + private readonly Dictionary<(uint LevelId, uint Diff), uint> ordinaryRewardIds; + private readonly List challengeTimes; + + public DreamCardSettlementIndex( + IEnumerable ordinaryLevels, + IEnumerable challengeTimes) + { + ordinaryRewardIds = ordinaryLevels + .Where(x => x.LevelListId > 0 && x.HardStage > 0) + .GroupBy(x => (x.LevelListId, x.HardStage)) + .ToDictionary(x => x.Key, x => x.First().RewardId); + + this.challengeTimes = challengeTimes.ToList(); + } + + public bool TryGetOrdinaryRewardId(uint levelId, uint diff, out uint rewardId) + { + return ordinaryRewardIds.TryGetValue((levelId, diff), out rewardId); + } + + public uint ResolveCurrentChallengePeriodId(DateTime now) + { + foreach (var entry in challengeTimes.OrderBy(x => x.ChallTimeId)) + { + var start = ParseConfigTime(entry.StartTime); + var end = ParseConfigTime(entry.EndTime); + if (!start.HasValue || !end.HasValue) + continue; + + if (start.Value <= now && now < end.Value) + return entry.ChallTimeId; + } + + return 0; + } + + 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; + } +} + +internal sealed class DreamCardOrdinarySettlementEntry +{ + [JsonPropertyName("LevelListID")] + public uint LevelListId { get; set; } + + [JsonPropertyName("HardStage")] + public uint HardStage { get; set; } + + [JsonPropertyName("RewardID")] + public uint RewardId { get; set; } +} + +internal sealed class DreamCardChallengeTimeEntry +{ + [JsonPropertyName("ChallTimeID")] + public uint ChallTimeId { get; set; } + + [JsonPropertyName("StartTime")] + public string StartTime { get; set; } = ""; + + [JsonPropertyName("EndTime")] + public string EndTime { get; set; } = ""; +} diff --git a/GameServer/Server/CallGS/Handlers/DreamCard/DreamCard_UpdateData.cs b/GameServer/Server/CallGS/Handlers/DreamCard/DreamCard_UpdateData.cs new file mode 100644 index 0000000..7498fdb --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/DreamCard/DreamCard_UpdateData.cs @@ -0,0 +1,59 @@ +using MikuSB.Database; +using MikuSB.Proto; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.DreamCard; + +[CallGSApi("DreamCard_UpdateData")] +public class DreamCard_UpdateData : ICallGSHandler +{ + private const uint DataGroupId = 62; + + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var player = connection.Player!; + var sync = new NtfSyncPlayer(); + var dirty = false; + + try + { + var entries = JsonSerializer.Deserialize>(param) ?? []; + foreach (var entry in entries) + { + if (entry.Id <= 0) + continue; + + var value = NormalizeJson(entry.Data); + player.SetStrAttr(DataGroupId, (uint)entry.Id, value); + sync.CustomStr[player.ToShiftedAttrKey(DataGroupId, (uint)entry.Id)] = value; + dirty = true; + } + } + catch + { + // Ignore malformed payloads so the client-side save queue can continue. + } + + if (dirty) + DatabaseHelper.SaveDatabaseType(player.Data); + + await CallGSRouter.SendScript(connection, "DreamCard_UpdateData", "{}", sync); + } + + private static string NormalizeJson(JsonElement data) + { + return data.ValueKind == JsonValueKind.Undefined + ? "null" + : data.GetRawText(); + } +} + +internal sealed class DreamCardUpdateDataEntry +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("data")] + public JsonElement Data { get; set; } +}