refactor(battlenode): centralize inbound wire-key literals in WireKeys (§C)

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 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-05 07:30:02 -04:00
parent 3e8901eec3
commit 7e167b1cef
5 changed files with 100 additions and 50 deletions

View File

@@ -55,7 +55,7 @@ internal static class BattleFrames
internal static IReadOnlyList<long> ExtractIdxList(MsgEnvelope env)
{
if (env.Body is not RawBody rawBody) return Array.Empty<long>();
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<long>();
foreach (var item in seq)

View File

@@ -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);

View File

@@ -18,11 +18,11 @@ internal sealed class PlayActionsHandler : IFrameHandler
return Array.Empty<DispatchRoute>();
var entries = (ctx.Env.Body as RawBody)?.Entries ?? new Dictionary<string, object?>();
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,

View File

@@ -6,7 +6,7 @@ namespace SVSim.BattleNode.Sessions.Dispatch;
/// <summary>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 <c>Dictionary&lt;string,object?&gt;</c> / <c>List&lt;object?&gt;</c> with numeric leaves boxed
/// as long/int/double (see MsgEnvelope.FromJson).</summary>
/// as long/int/double (see MsgEnvelope.FromJson). Inbound wire keys come from <see cref="WireKeys"/>.</summary>
internal static class KnownListBuilder
{
/// <summary>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<string, object?> opDict) continue;
if (!opDict.TryGetValue("move", out var moveRaw) || moveRaw is not IDictionary<string, object?> move) continue;
if (move.TryGetValue("idx", out var idxRaw) && idxRaw is IEnumerable<object?> idxList)
if (!opDict.TryGetValue(WireKeys.Move, out var moveRaw) || moveRaw is not IDictionary<string, object?> move) continue;
if (move.TryGetValue(WireKeys.Idx, out var idxRaw) && idxRaw is IEnumerable<object?> 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<string, object?> opDict) continue;
if (!opDict.TryGetValue("add", out var addRaw) || addRaw is not IDictionary<string, object?> add) continue;
if (!opDict.TryGetValue(WireKeys.Add, out var addRaw) || addRaw is not IDictionary<string, object?> 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<string, object?> 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<string, object?> 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<object?> idxList) continue;
if (!add.TryGetValue(WireKeys.Idx, out var idxRaw) || idxRaw is not IEnumerable<object?> 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<string, object?> kaDict) continue;
if (!kaDict.TryGetValue("selectCard", out var scRaw) || scRaw is not IDictionary<string, object?> sc) continue;
if (!sc.TryGetValue("cardId", out var idsRaw) || idsRaw is not IEnumerable<object?> ids) continue;
if (!kaDict.TryGetValue(WireKeys.SelectCard, out var scRaw) || scRaw is not IDictionary<string, object?> sc) continue;
if (!sc.TryGetValue(WireKeys.CardId, out var idsRaw) || idsRaw is not IEnumerable<object?> 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<string, object?> opDict) continue;
if (!opDict.TryGetValue("add", out var addRaw) || addRaw is not IDictionary<string, object?> add) continue;
if (!add.ContainsKey("isChoice")) continue;
if (!add.TryGetValue("card", out var cardRaw) || cardRaw is not IDictionary<string, object?> card) continue;
if (!card.TryGetValue("candidates", out var candRaw) || candRaw is not IEnumerable<object?> candidates) continue;
if (!opDict.TryGetValue(WireKeys.Add, out var addRaw) || addRaw is not IDictionary<string, object?> add) continue;
if (!add.ContainsKey(WireKeys.IsChoice)) continue;
if (!add.TryGetValue(WireKeys.Card, out var cardRaw) || cardRaw is not IDictionary<string, object?> card) continue;
if (!card.TryGetValue(WireKeys.Candidates, out var candRaw) || candRaw is not IEnumerable<object?> 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<object?> idxList) continue;
if (!add.TryGetValue(WireKeys.Idx, out var idxRaw) || idxRaw is not IEnumerable<object?> 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<string, object?> opDict) continue;
if (!opDict.TryGetValue("add", out var addRaw) || addRaw is not IDictionary<string, object?> add) continue;
if (!opDict.TryGetValue(WireKeys.Add, out var addRaw) || addRaw is not IDictionary<string, object?> add) continue;
if (!add.TryGetValue("card", out var cardRaw) || cardRaw is not IDictionary<string, object?> 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<string, object?> 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<object?> idxList) continue;
if (!add.TryGetValue(WireKeys.Idx, out var idxRaw) || idxRaw is not IEnumerable<object?> 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<string, object?> 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<string, object?> sc)
if (d.TryGetValue(WireKeys.SelectCard, out var scRaw) && scRaw is IDictionary<string, object?> 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<object?> ids)
if (open != ChoiceVisibility.Hidden && sc.TryGetValue(WireKeys.CardId, out var idsRaw) && idsRaw is IEnumerable<object?> 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<string, object?> 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<string, object?> 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<int>(),
@@ -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;
}

View File

@@ -0,0 +1,50 @@
namespace SVSim.BattleNode.Sessions.Dispatch;
/// <summary>
/// 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
/// <c>SendCardDataMaker</c> / <c>CardObj</c> serialization); a one-character typo at a read site
/// (<c>"isSelf"</c> vs <c>"IsSelf"</c>) 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 <c>[JsonPropertyName]</c> attributes (already single-sourced there).
/// </summary>
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";
}