From 7e167b1cef4ce5062ecb241619e04f4532548746 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Fri, 5 Jun 2026 07:30:02 -0400 Subject: [PATCH] =?UTF-8?q?refactor(battlenode):=20centralize=20inbound=20?= =?UTF-8?q?wire-key=20literals=20in=20WireKeys=20(=C2=A7C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Behavior-preserving; 231 BattleNode tests green (capture-conformance suite drives real prod frames, so a wrong constant would fail). New Sessions/Dispatch/WireKeys.cs holds the 28 inbound-body read keys (orderList / keyAction / targetList / uList field names). KnownListBuilder, PlayActionsHandler, EchoHandler, and BattleFrames.ExtractIdxList now read through it instead of repeated inline strings, so a parse-side typo ("isSelf" vs "IsSelf") can no longer silently degrade token resolution. Outbound [JsonPropertyName] attributes left as-is (already single-source per DTO). Co-Authored-By: Claude Opus 4.8 --- .../Sessions/Dispatch/BattleFrames.cs | 2 +- .../Sessions/Dispatch/Handlers/EchoHandler.cs | 2 +- .../Dispatch/Handlers/PlayActionsHandler.cs | 12 +-- .../Sessions/Dispatch/KnownListBuilder.cs | 84 +++++++++---------- .../Sessions/Dispatch/WireKeys.cs | 50 +++++++++++ 5 files changed, 100 insertions(+), 50 deletions(-) create mode 100644 SVSim.BattleNode/Sessions/Dispatch/WireKeys.cs diff --git a/SVSim.BattleNode/Sessions/Dispatch/BattleFrames.cs b/SVSim.BattleNode/Sessions/Dispatch/BattleFrames.cs index 843b870..207845b 100644 --- a/SVSim.BattleNode/Sessions/Dispatch/BattleFrames.cs +++ b/SVSim.BattleNode/Sessions/Dispatch/BattleFrames.cs @@ -55,7 +55,7 @@ internal static class BattleFrames internal static IReadOnlyList ExtractIdxList(MsgEnvelope env) { if (env.Body is not RawBody rawBody) return Array.Empty(); - if (rawBody.Entries.TryGetValue("idxList", out var raw) && raw is System.Collections.IEnumerable seq && raw is not string) + if (rawBody.Entries.TryGetValue(WireKeys.IdxList, out var raw) && raw is System.Collections.IEnumerable seq && raw is not string) { var result = new List(); foreach (var item in seq) diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/EchoHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/EchoHandler.cs index 5005a04..8efbb50 100644 --- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/EchoHandler.cs +++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/EchoHandler.cs @@ -15,7 +15,7 @@ internal sealed class EchoHandler : IFrameHandler { if (ctx.Type == BattleType.Pvp && ctx.BothAfterReady()) { - var orderList = (ctx.Env.Body as RawBody)?.Entries.GetValueOrDefault("orderList"); + var orderList = (ctx.Env.Body as RawBody)?.Entries.GetValueOrDefault(WireKeys.OrderList); ctx.State.RecordTokensFrom(ctx.From, ctx.Other, orderList); // Copy tokens ride Echo too (same add-op shape); resolve baseIdx against the side's map. ctx.State.RecordCopyTokensFrom(ctx.From, ctx.Other, orderList); diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/PlayActionsHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/PlayActionsHandler.cs index 9ccfe97..62180f1 100644 --- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/PlayActionsHandler.cs +++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/PlayActionsHandler.cs @@ -18,11 +18,11 @@ internal sealed class PlayActionsHandler : IFrameHandler return Array.Empty(); var entries = (ctx.Env.Body as RawBody)?.Entries ?? new Dictionary(); - var playIdx = (int)KnownListBuilder.AsLong(entries.GetValueOrDefault("playIdx")); - var type = (int)KnownListBuilder.AsLong(entries.GetValueOrDefault("type")); + var playIdx = (int)KnownListBuilder.AsLong(entries.GetValueOrDefault(WireKeys.PlayIdx)); + var type = (int)KnownListBuilder.AsLong(entries.GetValueOrDefault(WireKeys.Type)); - var orderList = entries.GetValueOrDefault("orderList"); - var keyAction = entries.GetValueOrDefault("keyAction"); + var orderList = entries.GetValueOrDefault(WireKeys.OrderList); + var keyAction = entries.GetValueOrDefault(WireKeys.KeyAction); // Mine generated-token identities from this frame's add ops into the right side's idx->cardId // map (isSelf:1 → sender; isSelf:0 → opponent, a cross-side gift), so a token played in a LATER @@ -40,13 +40,13 @@ internal sealed class PlayActionsHandler : IFrameHandler var deckMap = ctx.State.GetOrSeedDeckMap(ctx.From); var played = KnownListBuilder.BuildPlayedCard(deckMap, playIdx, orderList); - var oppoTargets = KnownListBuilder.RenameTargets(entries.GetValueOrDefault("targetList")); + var oppoTargets = KnownListBuilder.RenameTargets(entries.GetValueOrDefault(WireKeys.TargetList)); // Deck-sourced movements (fetch / search / summon-from-deck) ride the uList — a verbatim, // separate receive slot the node forwards unchanged (bullet-3 audit F1). The node makes no // reveal decision; cardId presence is the sender's call. Coexists with the synthesized // knownList in the same frame (capture line 75). - var uList = KnownListBuilder.RelayUList(entries.GetValueOrDefault("uList")); + var uList = KnownListBuilder.RelayUList(entries.GetValueOrDefault(WireKeys.UList)); var body = new PlayActionsBroadcastBody( PlayIdx: playIdx, diff --git a/SVSim.BattleNode/Sessions/Dispatch/KnownListBuilder.cs b/SVSim.BattleNode/Sessions/Dispatch/KnownListBuilder.cs index 17187cb..4d4a2f7 100644 --- a/SVSim.BattleNode/Sessions/Dispatch/KnownListBuilder.cs +++ b/SVSim.BattleNode/Sessions/Dispatch/KnownListBuilder.cs @@ -6,7 +6,7 @@ namespace SVSim.BattleNode.Sessions.Dispatch; /// Pure transforms from the active player's RawBody sub-structures to the opponent-facing /// shapes. No session state, no wire I/O — unit-testable in isolation. RawBody nested values arrive /// as Dictionary<string,object?> / List<object?> with numeric leaves boxed -/// as long/int/double (see MsgEnvelope.FromJson). +/// as long/int/double (see MsgEnvelope.FromJson). Inbound wire keys come from . internal static class KnownListBuilder { /// The played card's knownList entry, or null when its identity can't be synthesized @@ -32,11 +32,11 @@ internal static class KnownListBuilder foreach (var op in ops) { if (op is not IDictionary opDict) continue; - if (!opDict.TryGetValue("move", out var moveRaw) || moveRaw is not IDictionary move) continue; - if (move.TryGetValue("idx", out var idxRaw) && idxRaw is IEnumerable idxList) + if (!opDict.TryGetValue(WireKeys.Move, out var moveRaw) || moveRaw is not IDictionary move) continue; + if (move.TryGetValue(WireKeys.Idx, out var idxRaw) && idxRaw is IEnumerable idxList) { foreach (var i in idxList) - if (AsLong(i) == playIdx && move.TryGetValue("to", out var toRaw)) + if (AsLong(i) == playIdx && move.TryGetValue(WireKeys.To, out var toRaw)) return (int)AsLong(toRaw); } } @@ -61,16 +61,16 @@ internal static class KnownListBuilder foreach (var op in ops) { if (op is not IDictionary opDict) continue; - if (!opDict.TryGetValue("add", out var addRaw) || addRaw is not IDictionary add) continue; + if (!opDict.TryGetValue(WireKeys.Add, out var addRaw) || addRaw is not IDictionary add) continue; - add.TryGetValue("isSelf", out var isSelfRaw); + add.TryGetValue(WireKeys.IsSelf, out var isSelfRaw); var isSelf = (CardOwner)(int)AsLong(isSelfRaw); - if (!add.TryGetValue("card", out var cardRaw) || cardRaw is not IDictionary card) continue; - if (!card.TryGetValue("cardId", out var cardIdRaw)) continue; // candidates/isChoice → no identity yet + if (!add.TryGetValue(WireKeys.Card, out var cardRaw) || cardRaw is not IDictionary card) continue; + if (!card.TryGetValue(WireKeys.CardId, out var cardIdRaw)) continue; // candidates/isChoice → no identity yet var cardId = AsLong(cardIdRaw); - if (!add.TryGetValue("idx", out var idxRaw) || idxRaw is not IEnumerable idxList) continue; + if (!add.TryGetValue(WireKeys.Idx, out var idxRaw) || idxRaw is not IEnumerable idxList) continue; foreach (var i in idxList) yield return new MinedToken((int)AsLong(i), cardId, isSelf); } @@ -98,8 +98,8 @@ internal static class KnownListBuilder foreach (var ka in kaEntries) { if (ka is not IDictionary kaDict) continue; - if (!kaDict.TryGetValue("selectCard", out var scRaw) || scRaw is not IDictionary sc) continue; - if (!sc.TryGetValue("cardId", out var idsRaw) || idsRaw is not IEnumerable ids) continue; + if (!kaDict.TryGetValue(WireKeys.SelectCard, out var scRaw) || scRaw is not IDictionary sc) continue; + if (!sc.TryGetValue(WireKeys.CardId, out var idsRaw) || idsRaw is not IEnumerable ids) continue; foreach (var id in ids) picks.Add(AsLong(id)); } } @@ -108,10 +108,10 @@ internal static class KnownListBuilder foreach (var op in ops) { if (op is not IDictionary opDict) continue; - if (!opDict.TryGetValue("add", out var addRaw) || addRaw is not IDictionary add) continue; - if (!add.ContainsKey("isChoice")) continue; - if (!add.TryGetValue("card", out var cardRaw) || cardRaw is not IDictionary card) continue; - if (!card.TryGetValue("candidates", out var candRaw) || candRaw is not IEnumerable candidates) continue; + if (!opDict.TryGetValue(WireKeys.Add, out var addRaw) || addRaw is not IDictionary add) continue; + if (!add.ContainsKey(WireKeys.IsChoice)) continue; + if (!add.TryGetValue(WireKeys.Card, out var cardRaw) || cardRaw is not IDictionary card) continue; + if (!card.TryGetValue(WireKeys.Candidates, out var candRaw) || candRaw is not IEnumerable candidates) continue; // The chosen cardId is the candidate that the active player picked (∈ picks). One per op. long? chosen = null; @@ -122,10 +122,10 @@ internal static class KnownListBuilder } if (chosen is null) continue; // no pick in this op's pool — skip (no desync, just no record) - add.TryGetValue("isSelf", out var isSelfRaw); + add.TryGetValue(WireKeys.IsSelf, out var isSelfRaw); var isSelf = (CardOwner)(int)AsLong(isSelfRaw); - if (!add.TryGetValue("idx", out var idxRaw) || idxRaw is not IEnumerable idxList) continue; + if (!add.TryGetValue(WireKeys.Idx, out var idxRaw) || idxRaw is not IEnumerable idxList) continue; foreach (var i in idxList) yield return new MinedToken((int)AsLong(i), chosen.Value, isSelf); } @@ -153,20 +153,20 @@ internal static class KnownListBuilder foreach (var op in ops) { if (op is not IDictionary opDict) continue; - if (!opDict.TryGetValue("add", out var addRaw) || addRaw is not IDictionary add) continue; + if (!opDict.TryGetValue(WireKeys.Add, out var addRaw) || addRaw is not IDictionary add) continue; - if (!add.TryGetValue("card", out var cardRaw) || cardRaw is not IDictionary card) continue; - if (card.ContainsKey("cardId")) continue; // concrete token → MineAddOps - if (!card.TryGetValue("baseIdx", out var baseRaw)) continue; // not a copy (candidates → MineChoicePicks) + if (!add.TryGetValue(WireKeys.Card, out var cardRaw) || cardRaw is not IDictionary card) continue; + if (card.ContainsKey(WireKeys.CardId)) continue; // concrete token → MineAddOps + if (!card.TryGetValue(WireKeys.BaseIdx, out var baseRaw)) continue; // not a copy (candidates → MineChoicePicks) if (baseRaw is string) continue; // private-group copy → string baseIdx, skip var baseIdx = (int)AsLong(baseRaw); - add.TryGetValue("isSelf", out var isSelfRaw); + add.TryGetValue(WireKeys.IsSelf, out var isSelfRaw); var isSelf = (CardOwner)(int)AsLong(isSelfRaw); var map = isSelf == CardOwner.Self ? selfMap : otherMap; if (!map.TryGetValue(baseIdx, out var cardId)) continue; // unknown source → degrade - if (!add.TryGetValue("idx", out var idxRaw) || idxRaw is not IEnumerable idxList) continue; + if (!add.TryGetValue(WireKeys.Idx, out var idxRaw) || idxRaw is not IEnumerable idxList) continue; foreach (var i in idxList) yield return new MinedToken((int)AsLong(i), cardId, isSelf); } @@ -185,19 +185,19 @@ internal static class KnownListBuilder foreach (var e in entries) { if (e is not IDictionary d) continue; - d.TryGetValue("type", out var typeRaw); + d.TryGetValue(WireKeys.Type, out var typeRaw); var type = (KeyActionType)(int)AsLong(typeRaw); if (type is not (KeyActionType.Choice or KeyActionType.HaveBeforeSkillChoice)) continue; - d.TryGetValue("cardId", out var cardIdRaw); + d.TryGetValue(WireKeys.CardId, out var cardIdRaw); var cardId = AsLong(cardIdRaw); SelectCardEntry? selectCard = null; - if (d.TryGetValue("selectCard", out var scRaw) && scRaw is IDictionary sc) + if (d.TryGetValue(WireKeys.SelectCard, out var scRaw) && scRaw is IDictionary sc) { - sc.TryGetValue("open", out var openRaw); + sc.TryGetValue(WireKeys.Open, out var openRaw); var open = (ChoiceVisibility)(int)AsLong(openRaw); - if (open != ChoiceVisibility.Hidden && sc.TryGetValue("cardId", out var idsRaw) && idsRaw is IEnumerable ids) + if (open != ChoiceVisibility.Hidden && sc.TryGetValue(WireKeys.CardId, out var idsRaw) && idsRaw is IEnumerable ids) selectCard = new SelectCardEntry(ids.Select(AsLong).ToList(), open); } result.Add(new KeyActionEntry(type, cardId, selectCard)); @@ -214,8 +214,8 @@ internal static class KnownListBuilder foreach (var e in entries) { if (e is not IDictionary d) continue; - d.TryGetValue("targetIdx", out var targetIdxRaw); - d.TryGetValue("isSelf", out var isSelfRaw); + d.TryGetValue(WireKeys.TargetIdx, out var targetIdxRaw); + d.TryGetValue(WireKeys.IsSelf, out var isSelfRaw); result.Add(new OppoTargetEntry( TargetIdx: (int)AsLong(targetIdxRaw), IsSelf: (CardOwner)(int)AsLong(isSelfRaw))); @@ -238,11 +238,11 @@ internal static class KnownListBuilder { if (e is not IDictionary d) continue; - d.TryGetValue("idxList", out var idxRaw); - d.TryGetValue("from", out var fromRaw); - d.TryGetValue("to", out var toRaw); - d.TryGetValue("isSelf", out var isSelfRaw); - d.TryGetValue("skill", out var skillRaw); + d.TryGetValue(WireKeys.IdxList, out var idxRaw); + d.TryGetValue(WireKeys.From, out var fromRaw); + d.TryGetValue(WireKeys.To, out var toRaw); + d.TryGetValue(WireKeys.IsSelf, out var isSelfRaw); + d.TryGetValue(WireKeys.Skill, out var skillRaw); result.Add(new UnapprovedCardEntry( IdxList: AsIntList(idxRaw) ?? new List(), @@ -250,13 +250,13 @@ internal static class KnownListBuilder To: (int)AsLong(toRaw), IsSelf: (CardOwner)(int)AsLong(isSelfRaw), Skill: skillRaw as string ?? "", - CardId: d.TryGetValue("cardId", out var c) ? AsLong(c) : null, - Clan: d.TryGetValue("clan", out var cl) ? (int)AsLong(cl) : null, - Cost: d.TryGetValue("cost", out var co) ? (int)AsLong(co) : null, - SkillKeyCardIdx: AsIntList(d.TryGetValue("skillKeyCardIdx", out var sk) ? sk : null), - RandomTargetIdx: AsIntList(d.TryGetValue("randomTargetIdx", out var rt) ? rt : null), - IsInvoke: d.TryGetValue("isInvoke", out var iv) ? AsLong(iv) != 0 : null, - AttachTarget: d.TryGetValue("attachTarget", out var at) ? at as string : null)); + CardId: d.TryGetValue(WireKeys.CardId, out var c) ? AsLong(c) : null, + Clan: d.TryGetValue(WireKeys.Clan, out var cl) ? (int)AsLong(cl) : null, + Cost: d.TryGetValue(WireKeys.Cost, out var co) ? (int)AsLong(co) : null, + SkillKeyCardIdx: AsIntList(d.TryGetValue(WireKeys.SkillKeyCardIdx, out var sk) ? sk : null), + RandomTargetIdx: AsIntList(d.TryGetValue(WireKeys.RandomTargetIdx, out var rt) ? rt : null), + IsInvoke: d.TryGetValue(WireKeys.IsInvoke, out var iv) ? AsLong(iv) != 0 : null, + AttachTarget: d.TryGetValue(WireKeys.AttachTarget, out var at) ? at as string : null)); } return result.Count == 0 ? null : result; } diff --git a/SVSim.BattleNode/Sessions/Dispatch/WireKeys.cs b/SVSim.BattleNode/Sessions/Dispatch/WireKeys.cs new file mode 100644 index 0000000..97b0b3f --- /dev/null +++ b/SVSim.BattleNode/Sessions/Dispatch/WireKeys.cs @@ -0,0 +1,50 @@ +namespace SVSim.BattleNode.Sessions.Dispatch; + +/// +/// Single source of truth for the inbound-body (RawBody / orderList) wire-key strings the dispatch +/// path reads off the client's frames. These are the SENDER's JSON keys (mirroring the client's +/// SendCardDataMaker / CardObj serialization); a one-character typo at a read site +/// ("isSelf" vs "IsSelf") silently degrades token resolution with no error, so every +/// read goes through a constant here instead of a repeated literal. Outbound keys stay on the +/// per-DTO [JsonPropertyName] attributes (already single-sourced there). +/// +internal static class WireKeys +{ + // Top-level inbound body keys + public const string OrderList = "orderList"; + public const string KeyAction = "keyAction"; + public const string PlayIdx = "playIdx"; + public const string Type = "type"; + public const string TargetList = "targetList"; + public const string UList = "uList"; + + // orderList op keys + public const string Move = "move"; + public const string Add = "add"; + public const string Idx = "idx"; + public const string To = "to"; + public const string IsSelf = "isSelf"; + public const string Card = "card"; + public const string CardId = "cardId"; + public const string Candidates = "candidates"; + public const string IsChoice = "isChoice"; + public const string BaseIdx = "baseIdx"; + + // keyAction.selectCard keys + public const string SelectCard = "selectCard"; + public const string Open = "open"; + + // targetList entry keys + public const string TargetIdx = "targetIdx"; + + // uList entry keys + public const string IdxList = "idxList"; + public const string From = "from"; + public const string Skill = "skill"; + public const string Clan = "clan"; + public const string Cost = "cost"; + public const string SkillKeyCardIdx = "skillKeyCardIdx"; + public const string RandomTargetIdx = "randomTargetIdx"; + public const string IsInvoke = "isInvoke"; + public const string AttachTarget = "attachTarget"; +}