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:
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string,object?></c> / <c>List<object?></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;
|
||||
}
|
||||
|
||||
50
SVSim.BattleNode/Sessions/Dispatch/WireKeys.cs
Normal file
50
SVSim.BattleNode/Sessions/Dispatch/WireKeys.cs
Normal 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";
|
||||
}
|
||||
Reference in New Issue
Block a user