From 6bc9090c896940b10c494625f02f060a633e5643 Mon Sep 17 00:00:00 2001 From: Kei-Luna Date: Mon, 25 May 2026 13:07:34 +0900 Subject: [PATCH] VirCaptureTower_EnterLevel --- Common/Data/Excel/VirCaptureTowerExcel.cs | 44 +++++++++ Common/Data/GameData.cs | 1 + .../Chapter/Chapter_DealLevelSettlement.cs | 8 ++ .../VirCapture/VirCaptureTower_EnterLevel.cs | 79 ++++++++++++++++ .../VirCaptureTower_LevelSettlement.cs | 94 +++++++++++++++++++ 5 files changed, 226 insertions(+) create mode 100644 Common/Data/Excel/VirCaptureTowerExcel.cs create mode 100644 GameServer/Server/CallGS/Handlers/VirCapture/VirCaptureTower_EnterLevel.cs create mode 100644 GameServer/Server/CallGS/Handlers/VirCapture/VirCaptureTower_LevelSettlement.cs diff --git a/Common/Data/Excel/VirCaptureTowerExcel.cs b/Common/Data/Excel/VirCaptureTowerExcel.cs new file mode 100644 index 0000000..c7ee1d8 --- /dev/null +++ b/Common/Data/Excel/VirCaptureTowerExcel.cs @@ -0,0 +1,44 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace MikuSB.Data.Excel; + +[ResourceEntity("dlc/vircapture/tower.json")] +public class VirCaptureTowerExcel : ExcelResource +{ + [JsonProperty("ID")] public uint Id { get; set; } + [JsonProperty("Condition")] public JToken? ConditionRaw { get; set; } + [JsonProperty("MapID")] public uint MapId { get; set; } + [JsonProperty("TrialCard")] public List TrialCard { get; set; } = []; + [JsonProperty("TaskPath")] public string TaskPath { get; set; } = ""; + + [JsonIgnore] + public Dictionary Condition { get; } = []; + + public override uint GetId() => Id; + + public override void Loaded() + { + Condition.Clear(); + if (ConditionRaw is JObject obj) + { + foreach (var property in obj.Properties()) + { + if (!int.TryParse(property.Name, out var key)) + continue; + + uint value = 0; + if (property.Value.Type == JTokenType.Integer) + value = property.Value.Value(); + else if (property.Value.Type == JTokenType.String && + uint.TryParse(property.Value.Value(), out var parsed)) + value = parsed; + + if (value > 0) + Condition[key] = value; + } + } + + GameData.VirCaptureTowerData[Id] = this; + } +} diff --git a/Common/Data/GameData.cs b/Common/Data/GameData.cs index 196d768..84b219b 100644 --- a/Common/Data/GameData.cs +++ b/Common/Data/GameData.cs @@ -57,6 +57,7 @@ public static class GameData public static Dictionary VirCaptureLevelListData { get; private set; } = []; public static Dictionary MonsterCardData { get; private set; } = []; public static Dictionary FishingFoodData { get; private set; } = []; + public static Dictionary VirCaptureTowerData { get; private set; } = []; } public static class GameResourceTemplateId diff --git a/GameServer/Server/CallGS/Handlers/Chapter/Chapter_DealLevelSettlement.cs b/GameServer/Server/CallGS/Handlers/Chapter/Chapter_DealLevelSettlement.cs index 50be564..19e3b2c 100644 --- a/GameServer/Server/CallGS/Handlers/Chapter/Chapter_DealLevelSettlement.cs +++ b/GameServer/Server/CallGS/Handlers/Chapter/Chapter_DealLevelSettlement.cs @@ -4,6 +4,7 @@ using System.Text.Json.Serialization; using MikuSB.GameServer.Game.BossPvp; using MikuSB.Proto; using MikuSB.GameServer.Server.CallGS.Handlers.Tower; +using MikuSB.GameServer.Server.CallGS.Handlers.VirCapture; namespace MikuSB.GameServer.Server.CallGS.Handlers.Chapter; @@ -72,6 +73,13 @@ public class Chapter_DealLevelSettlement : ICallGSHandler return response; } + if (string.Equals(sCmd, "VirCaptureTower_LevelSettlement", StringComparison.Ordinal)) + { + var (response, sync) = VirCaptureTower_LevelSettlement.HandleSettlement(connection.Player!, tbParam); + extraSync = sync; + return response; + } + return tbParam?.DeepClone() ?? new JsonObject(); } diff --git a/GameServer/Server/CallGS/Handlers/VirCapture/VirCaptureTower_EnterLevel.cs b/GameServer/Server/CallGS/Handlers/VirCapture/VirCaptureTower_EnterLevel.cs new file mode 100644 index 0000000..7e4289b --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/VirCapture/VirCaptureTower_EnterLevel.cs @@ -0,0 +1,79 @@ +using MikuSB.Data; +using MikuSB.Database.Player; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.VirCapture; + +[CallGSApi("VirCaptureTower_EnterLevel")] +public class VirCaptureTower_EnterLevel : ICallGSHandler +{ + private const uint LaunchPassGroupId = 22; + private const uint VirCaptureGroupId = 128; + private const uint VirCaptureLevelSid = 3; + private static readonly Random Random = new(); + + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var req = JsonSerializer.Deserialize(param); + if (req == null || req.LevelId <= 0 || req.TeamId <= 0) + { + await CallGSRouter.SendScript(connection, "VirCaptureTower_EnterLevel", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + if (!GameData.VirCaptureTowerData.TryGetValue((uint)req.LevelId, out var levelCfg)) + { + await CallGSRouter.SendScript(connection, "VirCaptureTower_EnterLevel", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + var player = connection.Player!; + if (!CheckConditions(player.Data, levelCfg.Condition)) + { + await CallGSRouter.SendScript(connection, "VirCaptureTower_EnterLevel", "{\"sErr\":\"tip.LevelLocked\"}"); + return; + } + + await CallGSRouter.SendScript(connection, "VirCaptureTower_EnterLevel", $"{{\"nSeed\":{Random.Next(1, 1_000_000_000)}}}"); + } + + private static bool CheckConditions(PlayerGameData data, IReadOnlyDictionary conditions) + { + foreach (var (key, value) in conditions) + { + switch (key) + { + case 1: + if (data.Level < value) + return false; + break; + case 2: + { + var pass = data.Attrs.FirstOrDefault(x => x.Gid == LaunchPassGroupId && x.Sid == value)?.Val ?? 0; + if (pass == 0) + return false; + break; + } + case 20: + { + var virLevel = data.Attrs.FirstOrDefault(x => x.Gid == VirCaptureGroupId && x.Sid == VirCaptureLevelSid)?.Val ?? 0; + if (virLevel < value) + return false; + break; + } + } + } + + return true; + } +} + +internal sealed class VirCaptureTowerEnterLevelParam +{ + [JsonPropertyName("nID")] + public int LevelId { get; set; } + + [JsonPropertyName("nTeamID")] + public int TeamId { get; set; } +} diff --git a/GameServer/Server/CallGS/Handlers/VirCapture/VirCaptureTower_LevelSettlement.cs b/GameServer/Server/CallGS/Handlers/VirCapture/VirCaptureTower_LevelSettlement.cs new file mode 100644 index 0000000..1f5981a --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/VirCapture/VirCaptureTower_LevelSettlement.cs @@ -0,0 +1,94 @@ +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.VirCapture; + +[CallGSApi("VirCaptureTower_LevelSettlement")] +public class VirCaptureTower_LevelSettlement : ICallGSHandler +{ + private const uint LaunchLevelStateGroupId = 21; + private const uint LaunchPassGroupId = 22; + private const uint PassedFlagBit = 1u << 8; + private static readonly Logger Logger = new("VirCaptureTower"); + + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var (response, sync) = HandleSettlement(connection.Player!, JsonNode.Parse(param)); + await CallGSRouter.SendScript(connection, "VirCaptureTower_LevelSettlement", response.ToJsonString(), sync); + } + + public static (JsonNode Response, NtfSyncPlayer Sync) HandleSettlement(PlayerInstance player, JsonNode? tbParam) + { + var req = tbParam?.Deserialize(); + if (req == null || req.LevelId == 0) + { + Logger.Error($"Invalid vircapture tower settlement payload: {tbParam?.ToJsonString() ?? "null"}"); + return (new JsonObject { ["sErr"] = "error.BadParam" }, new NtfSyncPlayer()); + } + + var sync = new NtfSyncPlayer(); + + var levelStateAttr = GetOrCreateAttr(player.Data, LaunchLevelStateGroupId, (uint)req.LevelId); + levelStateAttr.Val |= MergeStarMask(req.StarMask) | PassedFlagBit; + SyncAttr(sync, player, levelStateAttr); + + var passAttr = GetOrCreateAttr(player.Data, LaunchPassGroupId, (uint)req.LevelId); + passAttr.Val = Math.Max(1u, passAttr.Val + 1); + SyncAttr(sync, player, passAttr); + + Logger.Info( + $"VirCaptureTower settlement saved. uid={player.Uid} levelId={req.LevelId} starMask={req.StarMask} " + + $"levelStateVal={levelStateAttr.Val} passVal={passAttr.Val}"); + + DatabaseHelper.SaveDatabaseType(player.Data); + return (new JsonObject(), sync); + } + + private static uint MergeStarMask(int starMask) + { + uint result = 0; + for (var i = 0; i < 3; i++) + { + if (((starMask >> i) & 1) != 0) + result |= 1u << i; + } + + return result; + } + + 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 VirCaptureTowerSettlementParam +{ + [JsonPropertyName("nID")] + public int LevelId { get; set; } + + [JsonPropertyName("nStar")] + public int StarMask { get; set; } +}