using SVSim.BattleNode.Protocol.Bodies; 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). internal static class KnownListBuilder { /// The played card's knownList entry, or null when its identity can't be synthesized /// (token idx not in the deck map, or no matching move op). spellboost/attachTarget default to /// 0/"" for the vanilla slice; cost/clan/tribe are deferred (receiver re-derives from cardId). public static KnownCardEntry? BuildPlayedCard( IReadOnlyDictionary deckMap, int playIdx, object? orderList) { if (!deckMap.TryGetValue(playIdx, out var cardId)) return null; var to = ExtractMoveTo(orderList, playIdx); if (to is null) return null; return new KnownCardEntry(Idx: playIdx, CardId: cardId, To: to.Value, Spellboost: 0, AttachTarget: ""); } /// The to place-state of the FIRST move op whose idx list contains /// (the played card's own move; later add/alter ops are the deferred /// token slice), or null if absent. NOTE: the sender-side to is passed through verbatim — /// for the vanilla slice we assume send-side and recv-side place-state codes match, pending /// recv-capture confirmation. public static int? ExtractMoveTo(object? orderList, int playIdx) { if (orderList is not IEnumerable ops) return null; 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) { foreach (var i in idxList) if (AsLong(i) == playIdx && move.TryGetValue("to", out var toRaw)) return (int)AsLong(toRaw); } } return null; } /// Mine generated-token identities from the sender's add ops: yields /// (idx, cardId) for every idx in each {add:{idx:[...], isSelf:1, card:{cardId}}} /// op. Skips isSelf:0 adds (cross-side gifts — belong in the other side's map, deferred) /// and any add whose card has no concrete cardId — choice tokens /// (card:{candidates}, RegisterChoiceAdd), copy tokens (card:{baseIdx}, /// RegisterCopyToken), and private-group adds (string idx) — all deferred and all /// caught by the cardId-key / idx-is-list guards. This is the only place a /// freshly-generated card's identity exists on the wire (bullet-3 audit F1; producing code /// RegisterToken/RegisterActionBase) — the played-card op itself never carries a /// cardId. public static IEnumerable<(int Idx, long CardId)> MineAddOps(object? orderList) { if (orderList is not IEnumerable ops) yield break; 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; add.TryGetValue("isSelf", out var isSelfRaw); if (AsLong(isSelfRaw) != 1) continue; // own tokens only; cross-side gifts deferred 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 var cardId = AsLong(cardIdRaw); if (!add.TryGetValue("idx", out var idxRaw) || idxRaw is not IEnumerable idxList) continue; foreach (var i in idxList) yield return ((int)AsLong(i), cardId); } } /// Rename targetList -> oppoTargetList; isSelf is actor-relative /// and passes through unchanged (F2). Null for a missing/empty list. public static IReadOnlyList? RenameTargets(object? targetList) { if (targetList is not IEnumerable entries) return null; var result = new List(); foreach (var e in entries) { if (e is not IDictionary d) continue; d.TryGetValue("targetIdx", out var targetIdxRaw); d.TryGetValue("isSelf", out var isSelfRaw); result.Add(new OppoTargetEntry( TargetIdx: (int)AsLong(targetIdxRaw), IsSelf: (int)AsLong(isSelfRaw))); } return result.Count == 0 ? null : result; } /// Coerce a boxed RawBody numeric leaf (long/int/double/decimal/string) to long; 0 for /// null/unparseable. public static long AsLong(object? value) => value switch { long l => l, int i => i, double d => (long)d, decimal m => (long)m, string s when long.TryParse(s, out var p) => p, _ => 0, }; }