feat(battle-node): UnapprovedCardEntry + RelayUList pure transform
Verbatim uList relay shape + transform (deck-sourced summons/fetches), mirroring RenameTargets. Not yet wired into the handler. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -47,3 +47,23 @@ public sealed record KnownCardEntry(
|
||||
public sealed record OppoTargetEntry(
|
||||
[property: JsonPropertyName("targetIdx")] int TargetIdx,
|
||||
[property: JsonPropertyName("isSelf")] int IsSelf);
|
||||
|
||||
/// <summary>One entry in a relayed <c>uList</c> (the unapproved-movement list) — a skill-driven
|
||||
/// card movement (fetch / search / summon-from-deck / discard-reveal) the node forwards VERBATIM
|
||||
/// (bullet-3 audit F1; the node makes no reveal decision — <c>cardId</c> presence is the sender's
|
||||
/// call). The first five fields are always emitted; the rest are conditional in
|
||||
/// <c>SendCardDataMaker.MakeUList</c> (cardId when revealed, clan/cost when set, etc.) and omit when
|
||||
/// null. <c>isSelf</c> is actor-relative and passes through unchanged (F2).</summary>
|
||||
public sealed record UnapprovedCardEntry(
|
||||
[property: JsonPropertyName("idxList")] IReadOnlyList<int> IdxList,
|
||||
[property: JsonPropertyName("from")] int From,
|
||||
[property: JsonPropertyName("to")] int To,
|
||||
[property: JsonPropertyName("isSelf")] int IsSelf,
|
||||
[property: JsonPropertyName("skill")] string Skill,
|
||||
[property: JsonPropertyName("cardId")] long? CardId = null,
|
||||
[property: JsonPropertyName("clan")] int? Clan = null,
|
||||
[property: JsonPropertyName("cost")] int? Cost = null,
|
||||
[property: JsonPropertyName("skillKeyCardIdx")] IReadOnlyList<int>? SkillKeyCardIdx = null,
|
||||
[property: JsonPropertyName("randomTargetIdx")] IReadOnlyList<int>? RandomTargetIdx = null,
|
||||
[property: JsonPropertyName("isInvoke")] int? IsInvoke = null,
|
||||
[property: JsonPropertyName("attachTarget")] string? AttachTarget = null);
|
||||
|
||||
@@ -222,6 +222,49 @@ internal static class KnownListBuilder
|
||||
return result.Count == 0 ? null : result;
|
||||
}
|
||||
|
||||
/// <summary>Map the sender's <c>uList</c> (unapproved-movement list) to the opponent-facing
|
||||
/// <see cref="UnapprovedCardEntry"/> list, VERBATIM — the node makes no reveal decision; it forwards
|
||||
/// whatever the sender emitted (cardId present = the sender chose to reveal). The five always-present
|
||||
/// fields (idxList/from/to/isSelf/skill) map directly; the conditionals map only when their key is
|
||||
/// present (mirroring the emitter, <c>SendCardDataMaker.MakeUList:188-244</c>). Null for an
|
||||
/// absent/empty list (mirrors <see cref="RenameTargets"/>). isSelf/place-states pass through unchanged
|
||||
/// (F2; same verbatim assumption already shipped for the synthesized knownList).</summary>
|
||||
public static IReadOnlyList<UnapprovedCardEntry>? RelayUList(object? uList)
|
||||
{
|
||||
if (uList is not IEnumerable<object?> entries) return null;
|
||||
var result = new List<UnapprovedCardEntry>();
|
||||
foreach (var e in entries)
|
||||
{
|
||||
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);
|
||||
|
||||
result.Add(new UnapprovedCardEntry(
|
||||
IdxList: AsIntList(idxRaw) ?? new List<int>(),
|
||||
From: (int)AsLong(fromRaw),
|
||||
To: (int)AsLong(toRaw),
|
||||
IsSelf: (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) ? (int)AsLong(iv) : null,
|
||||
AttachTarget: d.TryGetValue("attachTarget", out var at) ? at as string : null));
|
||||
}
|
||||
return result.Count == 0 ? null : result;
|
||||
}
|
||||
|
||||
/// <summary>Coerce a boxed RawBody list leaf to <c>List<int></c> (each element via
|
||||
/// <see cref="AsLong"/>); null when the value isn't a list.</summary>
|
||||
private static IReadOnlyList<int>? AsIntList(object? value) =>
|
||||
value is IEnumerable<object?> items ? items.Select(i => (int)AsLong(i)).ToList() : null;
|
||||
|
||||
/// <summary>Coerce a boxed RawBody numeric leaf (long/int/double/decimal/string) to long; 0 for
|
||||
/// null/unparseable.</summary>
|
||||
public static long AsLong(object? value) => value switch
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using NUnit.Framework;
|
||||
using SVSim.BattleNode.Protocol.Bodies;
|
||||
using SVSim.BattleNode.Sessions.Dispatch;
|
||||
|
||||
namespace SVSim.UnitTests.BattleNode.Sessions;
|
||||
@@ -422,4 +423,86 @@ public class KnownListBuilderTests
|
||||
var mined = KnownListBuilder.MineCopyTokens(orderList, selfMap, new Dictionary<int, long>()).ToList();
|
||||
Assert.That(mined, Is.EquivalentTo(new[] { (31, 700L, 1), (32, 700L, 1) }));
|
||||
}
|
||||
|
||||
// A uList entry as it arrives in a RawBody. Minimal = the 5 always-present fields
|
||||
// (capture battle-traffic_tk2_regular.ndjson:75). Optional fields added per-test.
|
||||
private static Dictionary<string, object?> UListEntry(
|
||||
long[] idxList, int from, int to, int isSelf, string skill) => new()
|
||||
{
|
||||
["idxList"] = idxList.Select(i => (object?)i).ToList(),
|
||||
["from"] = (long)from, ["to"] = (long)to, ["isSelf"] = (long)isSelf, ["skill"] = skill,
|
||||
};
|
||||
|
||||
[Test]
|
||||
public void RelayUList_maps_the_minimal_capture_entry_shape()
|
||||
{
|
||||
// battle-traffic_tk2_regular.ndjson:75 — a hidden deck-fetch (no cardId), the only uList shape
|
||||
// in any capture. The 5 always-present fields map; conditionals stay null.
|
||||
var uList = new List<object?> { UListEntry(new[] { 16L, 22L }, from: 0, to: 10, isSelf: 1, skill: "37|36|0") };
|
||||
var relayed = KnownListBuilder.RelayUList(uList);
|
||||
|
||||
Assert.That(relayed, Is.Not.Null);
|
||||
Assert.That(relayed!.Count, Is.EqualTo(1));
|
||||
var e = relayed[0];
|
||||
Assert.That(e.IdxList, Is.EqualTo(new[] { 16, 22 }));
|
||||
Assert.That(e.From, Is.EqualTo(0));
|
||||
Assert.That(e.To, Is.EqualTo(10));
|
||||
Assert.That(e.IsSelf, Is.EqualTo(1));
|
||||
Assert.That(e.Skill, Is.EqualTo("37|36|0"));
|
||||
Assert.That(e.CardId, Is.Null);
|
||||
Assert.That(e.Clan, Is.Null);
|
||||
Assert.That(e.Cost, Is.Null);
|
||||
Assert.That(e.SkillKeyCardIdx, Is.Null);
|
||||
Assert.That(e.RandomTargetIdx, Is.Null);
|
||||
Assert.That(e.IsInvoke, Is.Null);
|
||||
Assert.That(e.AttachTarget, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RelayUList_maps_a_revealed_summon_with_all_conditional_fields()
|
||||
{
|
||||
// Decomp-grounded (no capture): a revealed summon-to-field carries cardId + clan + cost etc.
|
||||
var entry = UListEntry(new[] { 40L }, from: 0, to: 20, isSelf: 1, skill: "5|3|0");
|
||||
entry["cardId"] = 900111010L;
|
||||
entry["clan"] = 8L;
|
||||
entry["cost"] = 2L;
|
||||
entry["skillKeyCardIdx"] = new List<object?> { 7L };
|
||||
entry["randomTargetIdx"] = new List<object?> { 2L, 3L };
|
||||
entry["isInvoke"] = 1L;
|
||||
entry["attachTarget"] = "12,13";
|
||||
var relayed = KnownListBuilder.RelayUList(new List<object?> { entry });
|
||||
|
||||
var e = relayed![0];
|
||||
Assert.That(e.To, Is.EqualTo(20));
|
||||
Assert.That(e.CardId, Is.EqualTo(900111010L));
|
||||
Assert.That(e.Clan, Is.EqualTo(8));
|
||||
Assert.That(e.Cost, Is.EqualTo(2));
|
||||
Assert.That(e.SkillKeyCardIdx, Is.EqualTo(new[] { 7 }));
|
||||
Assert.That(e.RandomTargetIdx, Is.EqualTo(new[] { 2, 3 }));
|
||||
Assert.That(e.IsInvoke, Is.EqualTo(1));
|
||||
Assert.That(e.AttachTarget, Is.EqualTo("12,13"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RelayUList_preserves_multiple_entries_in_order()
|
||||
{
|
||||
var uList = new List<object?>
|
||||
{
|
||||
UListEntry(new[] { 16L }, 0, 10, 1, "a"),
|
||||
UListEntry(new[] { 22L }, 0, 20, 0, "b"),
|
||||
};
|
||||
var relayed = KnownListBuilder.RelayUList(uList);
|
||||
|
||||
Assert.That(relayed!.Count, Is.EqualTo(2));
|
||||
Assert.That(relayed[0].Skill, Is.EqualTo("a"));
|
||||
Assert.That(relayed[1].Skill, Is.EqualTo("b"));
|
||||
Assert.That(relayed[1].IsSelf, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RelayUList_returns_null_for_missing_or_empty()
|
||||
{
|
||||
Assert.That(KnownListBuilder.RelayUList(null), Is.Null);
|
||||
Assert.That(KnownListBuilder.RelayUList(new List<object?>()), Is.Null);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user