feat(battle-node): cross-side gift + Echo-frame token mining
Close the two generated-token gaps that desynced PvP live test #3 (the Forestcraft Fairy), both sourced from the 2026-06-03 decomp-validation table. - MineAddOps now returns (idx, cardId, isSelf) and no longer drops isSelf:0. isSelf is the sender's perspective tag on CardObj.IsPlayer (RegisterToken.cs:22) and a card has one CardObj.Index, so an isSelf:0 add is the opponent's card. - New shared BattleSessionState.RecordTokensFrom routes isSelf:1 -> sender, isSelf:0 -> opponent (the gift lives in the recipient's map, consulted when they play it). PlayActionsHandler delegates to it. - EchoHandler now mines via the same helper but still returns no routes. An Echo's orderList carries the same add-op shape as a send (MakeEchoData -> MakeCommonSendAndEchoCardData), so MineAddOps applies verbatim; mining != relaying. Choice/copy/private-group adds stay skipped (no concrete cardId). Full solution 963/963 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,22 @@
|
||||
using SVSim.BattleNode.Protocol;
|
||||
|
||||
namespace SVSim.BattleNode.Sessions.Dispatch.Handlers;
|
||||
|
||||
/// <summary>Echo is the receiver's per-frame ack; the client has no inbound Echo handler, so the
|
||||
/// node consumes it (bullet-2 audit). Relaying would risk an echo->echo storm.</summary>
|
||||
/// node never relays it (bullet-2 audit — relaying would risk an echo->echo storm). It IS mined,
|
||||
/// though: an Echo's orderList carries the same add-op shape as PlayActions
|
||||
/// (SendCardDataMaker.MakeEchoData -> MakeCommonSendAndEchoCardData), so it can hold a token's real
|
||||
/// identity — notably the receiver's own (isSelf:1) view of a cross-side gift. We mine it into the
|
||||
/// right side's idx->cardId map and still return no routes (mining != relaying).</summary>
|
||||
internal sealed class EchoHandler : IFrameHandler
|
||||
{
|
||||
public IReadOnlyList<DispatchRoute> Handle(FrameDispatchContext ctx) => Array.Empty<DispatchRoute>();
|
||||
public IReadOnlyList<DispatchRoute> Handle(FrameDispatchContext ctx)
|
||||
{
|
||||
if (ctx.Type == BattleType.Pvp && ctx.BothAfterReady())
|
||||
{
|
||||
var orderList = (ctx.Env.Body as RawBody)?.Entries.GetValueOrDefault("orderList");
|
||||
ctx.State.RecordTokensFrom(ctx.From, ctx.Other, orderList);
|
||||
}
|
||||
return Array.Empty<DispatchRoute>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,10 @@ internal sealed class PlayActionsHandler : IFrameHandler
|
||||
|
||||
var orderList = entries.GetValueOrDefault("orderList");
|
||||
|
||||
// Mine generated-token identities from this frame's add ops into the sender's idx->cardId
|
||||
// map, so a token played in a LATER frame resolves its cardId (bullet-3 audit F1).
|
||||
foreach (var (idx, cardId) in KnownListBuilder.MineAddOps(orderList))
|
||||
ctx.State.RecordToken(ctx.From, idx, cardId);
|
||||
// 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
|
||||
// frame resolves its cardId — by whichever side ends up playing it (bullet-3 audit F1).
|
||||
ctx.State.RecordTokensFrom(ctx.From, ctx.Other, orderList);
|
||||
|
||||
var deckMap = ctx.State.GetOrSeedDeckMap(ctx.From);
|
||||
var played = KnownListBuilder.BuildPlayedCard(deckMap, playIdx, orderList);
|
||||
|
||||
Reference in New Issue
Block a user