fix(battlenode): translate live isSelf target frames to engine vid shape on ingest (live PvP fidelity)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -305,28 +305,35 @@ internal sealed class NodeNativeBattleHarness : IDisposable
|
||||
/// (NetworkBattleReceiver.cs:1093).</summary>
|
||||
public const int AttackOpcode = 10;
|
||||
|
||||
/// <summary>The engine's "self" viewer id (== <c>Certification.viewer_id</c> seeded by EngineGlobalInit).
|
||||
/// The IsRecovery target parse derives a target's owner from <c>vid != PlayerStaticData.UserViewerID</c>
|
||||
/// (== this value) — NOT from the <c>isSelf</c> key (that key is only read on the live, non-recovery
|
||||
/// parse). So a target vid == this resolves on BattlePlayer (engine seat A); vid != this on BattleEnemy
|
||||
/// (seat B).</summary>
|
||||
private const long SelfSeatVid = EngineGlobalInit.ThisViewerId;
|
||||
// NOTE (live-fidelity migration): the target-builders below emit the REAL client wire shape —
|
||||
// a sender-relative <c>isSelf</c> flag on each targetList entry — NOT the engine's internal
|
||||
// <c>vid</c> stamp. Real client-sent attack/evolve/targeted-play frames carry
|
||||
// <c>{targetIdx, isSelf, selectSkillIndex}</c> (verified in the client-send captures, e.g.
|
||||
// data_dumps/captures/battle_test/battle-traffic_cl1.ndjson); the previous <c>vid</c> shape was a
|
||||
// harness workaround that masked a missing ingest translation. SessionBattleEngine.Receive now
|
||||
// translates isSelf → the engine vid on the engine's OWN dict copy (the engine's IsRecovery target
|
||||
// parse derives owner from <c>vid != PlayerStaticData.UserViewerID</c>, NetworkBattleReceiver.cs:2186),
|
||||
// so the harness drives the live contract end-to-end.
|
||||
//
|
||||
// isSelf is relative to the FRAME's SENDER: <c>isSelf:1</c> = the target sits on the sender's own
|
||||
// seat; <c>isSelf:0</c> = it sits on the OTHER seat. The builders take <paramref name="targetOnEnemySeat"/>
|
||||
// (stable signature) and map it to <c>isSelf:0</c> (true) / <c>isSelf:1</c> (false), since every
|
||||
// builder is driven by seat A attacking/targeting seat B's card (targetOnEnemySeat:true) or its own
|
||||
// (false).
|
||||
private static long IsSelfFlag(bool targetOnEnemySeat) => targetOnEnemySeat ? 0 : 1;
|
||||
|
||||
/// <summary>A viewer id distinct from <see cref="SelfSeatVid"/>, stamped when the target sits on the
|
||||
/// engine's ENEMY seat (so the recovery parse marks it isSelf=true → BattleEnemy).</summary>
|
||||
private const long EnemySeatVid = EngineGlobalInit.ThisViewerId + 1;
|
||||
|
||||
/// <summary>Build a PlayActions ATTACK frame. <paramref name="attackerIdx"/> is the attacker's in-play
|
||||
/// engine <c>Index</c> (the wire <c>playIdx</c>); the target is described in <c>targetList</c> as
|
||||
/// <c>{targetIdx, vid, selectSkillIndex}</c>.
|
||||
/// <summary>Build a PlayActions ATTACK frame in the REAL client wire shape. <paramref name="attackerIdx"/>
|
||||
/// is the attacker's in-play engine <c>Index</c> (the wire <c>playIdx</c>); the target is described in
|
||||
/// <c>targetList</c> as <c>{targetIdx, isSelf, selectSkillIndex}</c> — the sender-relative <c>isSelf</c>
|
||||
/// flag a live client actually sends (see <see cref="IsSelfFlag"/>).
|
||||
/// <para>The dispatch reads <c>(_isPlayer ? PlayerTargetDataList : OpponentTargetDataList)</c>
|
||||
/// (WatchOperationCollection.InPlayActionOperation), and the <c>targetList</c> key populates the seat's
|
||||
/// list matching the ingest's <c>isPlayer</c> — so a seat-A (<c>isPlayer:true</c>) attack correctly fills
|
||||
/// <c>PlayerTargetDataList</c>. The target's OWNER is then resolved by
|
||||
/// <c>PlayerTargetDataList</c>. The target's OWNER is resolved by
|
||||
/// <c>NetworkBattleGenericTool.LookForActionDataToTargetCard</c> with fixed-seat semantics:
|
||||
/// <c>isSelf == false</c> → <c>BattlePlayer</c> (engine seat A); <c>isSelf == true</c> → <c>BattleEnemy</c>
|
||||
/// (engine seat B). Under IsRecovery, <c>isSelf</c> is computed from <c>vid</c> (see
|
||||
/// <see cref="EnemySeatVid"/>), so <paramref name="targetOnEnemySeat"/> selects the vid stamp.</para>
|
||||
/// the engine's IsRecovery parse derives owner from a <c>vid</c> stamp, which
|
||||
/// <c>SessionBattleEngine.TranslateTargetOwners</c> writes on ingest from this <c>isSelf</c> flag —
|
||||
/// so <paramref name="targetOnEnemySeat"/> drives the absolute target seat through the live contract.</para>
|
||||
/// <para>For a seat-A attack on seat B's leader: <c>targetIdx = 0</c> (the leader/Class card is Index 0)
|
||||
/// and <c>targetOnEnemySeat = true</c>.</para></summary>
|
||||
public static Dictionary<string, object?> AttackBody(int attackerIdx, int targetIdx, bool targetOnEnemySeat) => new()
|
||||
@@ -338,7 +345,7 @@ internal sealed class NodeNativeBattleHarness : IDisposable
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["targetIdx"] = (long)targetIdx,
|
||||
["vid"] = targetOnEnemySeat ? EnemySeatVid : SelfSeatVid,
|
||||
["isSelf"] = IsSelfFlag(targetOnEnemySeat),
|
||||
["selectSkillIndex"] = new List<object?>(),
|
||||
},
|
||||
},
|
||||
@@ -355,13 +362,14 @@ internal sealed class NodeNativeBattleHarness : IDisposable
|
||||
|
||||
/// <summary>Build a PlayActions PLAY_HAND_SELECT (targeted hand-play) frame. <paramref name="playIdx"/>
|
||||
/// is the played hand card's engine <c>Index</c> (the wire <c>playIdx</c>); the single target is
|
||||
/// described in <c>targetList</c> in the SAME <c>{targetIdx, vid, selectSkillIndex}</c> shape as
|
||||
/// described in <c>targetList</c> in the SAME real <c>{targetIdx, isSelf, selectSkillIndex}</c> shape as
|
||||
/// <see cref="AttackBody"/>/<see cref="EvolveSelectBody"/> (the receive parse reads it identically —
|
||||
/// <c>CreateTargetList</c> in NetworkBattleReceiver.cs:2164 — into the seat's TargetDataList, and under
|
||||
/// IsRecovery resolves the target's owner from <c>vid</c>, not an isSelf key).
|
||||
/// IsRecovery resolves the target's owner from the <c>vid</c> that
|
||||
/// <c>SessionBattleEngine.TranslateTargetOwners</c> derives from this <c>isSelf</c> flag on ingest).
|
||||
/// <para>For a seat-A spell targeting an enemy follower: <paramref name="targetIdx"/> = the enemy
|
||||
/// follower's in-play engine Index and <paramref name="targetOnEnemySeat"/> = <c>true</c> (vid stamped
|
||||
/// <see cref="EnemySeatVid"/> → isSelf=true → <c>LookForActionDataToTargetCard</c> resolves it on
|
||||
/// follower's in-play engine Index and <paramref name="targetOnEnemySeat"/> = <c>true</c> (<c>isSelf:0</c>
|
||||
/// → translated to the seat-B vid → <c>LookForActionDataToTargetCard</c> resolves it on
|
||||
/// <c>BattleEnemy.ClassAndInPlayCardList</c>).</para></summary>
|
||||
public static Dictionary<string, object?> TargetedPlayBody(int playIdx, int targetIdx, bool targetOnEnemySeat) => new()
|
||||
{
|
||||
@@ -372,7 +380,7 @@ internal sealed class NodeNativeBattleHarness : IDisposable
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["targetIdx"] = (long)targetIdx,
|
||||
["vid"] = targetOnEnemySeat ? EnemySeatVid : SelfSeatVid,
|
||||
["isSelf"] = IsSelfFlag(targetOnEnemySeat),
|
||||
["selectSkillIndex"] = new List<object?>(),
|
||||
},
|
||||
},
|
||||
@@ -434,9 +442,10 @@ internal sealed class NodeNativeBattleHarness : IDisposable
|
||||
|
||||
/// <summary>Build a PlayActions EVOLUTION_SELECT frame: the follower at engine <c>Index</c>
|
||||
/// <paramref name="cardIdx"/> evolves and targets the card at <paramref name="targetIdx"/>. The target is
|
||||
/// described in the SAME <c>{targetIdx, vid, selectSkillIndex}</c> shape as <see cref="AttackBody"/>
|
||||
/// (the dispatch resolves the target's owner from <c>vid</c> under IsRecovery, not from an isSelf key);
|
||||
/// <paramref name="targetOnEnemySeat"/> selects the vid stamp.</summary>
|
||||
/// described in the SAME real <c>{targetIdx, isSelf, selectSkillIndex}</c> shape as <see cref="AttackBody"/>
|
||||
/// (the dispatch resolves the target's owner from the <c>vid</c> that
|
||||
/// <c>SessionBattleEngine.TranslateTargetOwners</c> derives from this <c>isSelf</c> on ingest);
|
||||
/// <paramref name="targetOnEnemySeat"/> selects the isSelf flag.</summary>
|
||||
public static Dictionary<string, object?> EvolveSelectBody(int cardIdx, int targetIdx, bool targetOnEnemySeat) => new()
|
||||
{
|
||||
["playIdx"] = cardIdx,
|
||||
@@ -446,7 +455,7 @@ internal sealed class NodeNativeBattleHarness : IDisposable
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["targetIdx"] = (long)targetIdx,
|
||||
["vid"] = targetOnEnemySeat ? EnemySeatVid : SelfSeatVid,
|
||||
["isSelf"] = IsSelfFlag(targetOnEnemySeat),
|
||||
["selectSkillIndex"] = new List<object?>(),
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user