refactor(battlenode): close §A boolean-blindness items (MinedToken, Stock, KeyActionType)

Behavior-preserving; 231 BattleNode tests green.

- MinedToken record struct replaces the transpose-prone (int Idx, long CardId,
  CardOwner IsSelf) tuple returned by KnownListBuilder.Mine*. Positional deconstruct
  keeps the Record*From call sites unchanged.
- enum Stock { Normal, Bypass } replaces the negative `bool noStock` on
  IBattleParticipant.PushAsync and DispatchRoute, threaded through both participants,
  BattleSession, and all handler construction sites.
- enum KeyActionType mirrors the client's SendKeyActionDataManager.KeyActionType;
  the StripKeyActionForOpponent guard compares named values, KeyActionEntry.Type is
  the enum (wire-identical via JsonNumberEnumConverter).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-04 22:53:32 -04:00
parent a3e445cf2f
commit e70f32db79
30 changed files with 124 additions and 67 deletions

View File

@@ -20,7 +20,7 @@ public sealed class BattleNodeOptions
/// When true, <see cref="Sessions.Participants.RealParticipant"/> emits per-frame
/// diagnostic logs at Information level: <c>[sio-in]</c> on every inbound msg/alive/hand
/// envelope (URI, pubSeq, ackId, dispatch decision, ack-sent flag, ack arg, inbound
/// watermark); <c>[sio-out]</c> on every outbound push (URI, pubSeq, playSeq, noStock);
/// watermark); <c>[sio-out]</c> on every outbound push (URI, pubSeq, playSeq, stock);
/// <c>[ws-rx-text]</c> / <c>[ws-rx-bin]</c> on every WS frame received at the transport
/// layer; <c>[ws-recv-exit]</c> / <c>[ws-loop-exit]</c> on read-loop termination
/// (with WebSocket state + exception type when applicable). Default false — keeps

View File

@@ -24,7 +24,8 @@ public sealed record PlayActionsBroadcastBody(
/// until the chosen card is played — and passed through for a visible (open:1) board choice (§6,
/// provisional pending live confirmation).</summary>
public sealed record KeyActionEntry(
[property: JsonPropertyName("type")] int Type,
[property: JsonPropertyName("type")]
[property: JsonConverter(typeof(JsonNumberEnumConverter<KeyActionType>))] KeyActionType Type,
[property: JsonPropertyName("cardId")] long CardId,
[property: JsonPropertyName("selectCard")] SelectCardEntry? SelectCard);

View File

@@ -0,0 +1,23 @@
namespace SVSim.BattleNode.Protocol;
/// <summary>
/// Wire value of <c>type</c> on a keyAction entry — what kind of card-generating choice the play
/// is. Mirrors the client's <c>SendKeyActionDataManager.KeyActionType</c> exactly (same ordinals);
/// the client reads it back via <c>ConvertToInt(...)</c>, so it serializes as the underlying int
/// via <see cref="System.Text.Json.Serialization.JsonNumberEnumConverter{T}"/>. The node currently
/// relays only <see cref="Choice"/> and <see cref="HaveBeforeSkillChoice"/>
/// (<see cref="Bodies.KeyActionEntry"/> / <c>KnownListBuilder.StripKeyActionForOpponent</c>); the
/// rest are defined so the guard compares against named values instead of bare ints.
/// </summary>
public enum KeyActionType
{
None = 0,
Choice = 1,
Accelerated = 2,
Crystallize = 3,
Fusion = 4,
HaveBeforeSkillChoice = 5,
BurialRate = 6,
ChoiceEvolution = 7,
ChoiceBrave = 8,
}

View File

@@ -10,7 +10,7 @@ namespace SVSim.BattleNode.Sessions;
/// <summary>
/// v2 broker session. Holds two participants and brokers between them. Subscribes
/// to each participant's <see cref="IBattleParticipant.FrameEmitted"/>; on each frame,
/// runs <see cref="ComputeFrames"/> to determine the routing (target + frame + noStock
/// runs <see cref="ComputeFrames"/> to determine the routing (target + frame + <see cref="Stock"/>
/// flag) and dispatches via <see cref="IBattleParticipant.PushAsync"/>.
/// </summary>
/// <remarks>
@@ -112,7 +112,7 @@ public sealed class BattleSession
try
{
await survivor.PushAsync(
BattleFrames.BuildBattleFinish(BattleResult.DisconnectWin), noStock: true, cancellation)
BattleFrames.BuildBattleFinish(BattleResult.DisconnectWin), Stock.Bypass, cancellation)
.ConfigureAwait(false);
}
catch (Exception ex)
@@ -176,9 +176,9 @@ public sealed class BattleSession
try
{
var routes = ComputeFrames(from, env);
foreach (var (target, frame, noStock) in routes)
foreach (var (target, frame, stock) in routes)
{
await target.PushAsync(frame, noStock, ct);
await target.PushAsync(frame, stock, ct);
}
}
catch (Exception ex)
@@ -193,7 +193,7 @@ public sealed class BattleSession
/// <summary>
/// Pure-logic dispatch: given an inbound frame from one participant, return the list
/// of (target, frame, noStock) tuples the session should dispatch. Transitions
/// of (target, frame, stock) routes the session should dispatch. Transitions
/// <see cref="Phase"/>. Extracted so unit tests can drive the dispatch without
/// standing up real participants.
/// </summary>

View File

@@ -3,6 +3,7 @@ using SVSim.BattleNode.Protocol;
namespace SVSim.BattleNode.Sessions.Dispatch;
/// <summary>One routing decision: deliver <paramref name="Frame"/> to <paramref name="Target"/>.
/// Named form of the tuple <c>ComputeFrames</c> historically returned. <paramref name="NoStock"/>
/// true for control frames (BattleFinish, ack) — bypasses playSeq assignment + archive.</summary>
internal readonly record struct DispatchRoute(IBattleParticipant Target, MsgEnvelope Frame, bool NoStock);
/// Named form of the tuple <c>ComputeFrames</c> historically returned. <paramref name="Stock"/>
/// is <see cref="Sessions.Stock.Bypass"/> for control frames (BattleFinish, ack) — bypasses
/// playSeq assignment + archive — and <see cref="Sessions.Stock.Normal"/> for gameplay frames.</summary>
internal readonly record struct DispatchRoute(IBattleParticipant Target, MsgEnvelope Frame, Stock Stock);

View File

@@ -7,7 +7,7 @@ internal sealed class ForwardWhenBothReadyHandler : IFrameHandler
public IReadOnlyList<DispatchRoute> Handle(FrameDispatchContext ctx)
{
if (ctx.BothAfterReady())
return new[] { new DispatchRoute(ctx.Other, ctx.Env, false) };
return new[] { new DispatchRoute(ctx.Other, ctx.Env, Stock.Normal) };
return Array.Empty<DispatchRoute>();
}
}

View File

@@ -12,7 +12,7 @@ internal sealed class InitBattleHandler : IFrameHandler
{
var r = new List<DispatchRoute>
{
new(ctx.From, BattleFrames.BuildAck(NetworkBattleUri.InitBattle), true),
new(ctx.From, BattleFrames.BuildAck(NetworkBattleUri.InitBattle), Stock.Bypass),
};
ctx.SenderPhase = BattleSessionPhase.AwaitingLoaded;
return r;
@@ -26,7 +26,7 @@ internal sealed class InitBattleHandler : IFrameHandler
new(ctx.From, ServerBattleFrames.BuildMatched(
ctx.From.Context, ctx.Other.Context, ctx.From.ViewerId, ctx.Other.ViewerId,
ctx.BattleId, BattleSeeds.Stable(ctx.State.MasterSeed),
ctx.State.GetShuffledDeck(ctx.From)), false),
ctx.State.GetShuffledDeck(ctx.From)), Stock.Normal),
};
ctx.SenderPhase = BattleSessionPhase.AwaitingLoaded;
return r;

View File

@@ -12,7 +12,7 @@ internal sealed class InitNetworkHandler : IFrameHandler
var routes = new List<DispatchRoute>
{
new(ctx.From, BattleFrames.BuildAck(NetworkBattleUri.InitNetwork), true),
new(ctx.From, BattleFrames.BuildAck(NetworkBattleUri.InitNetwork), Stock.Bypass),
};
ctx.SenderPhase = BattleSessionPhase.AwaitingInitBattle;
return routes;

View File

@@ -18,7 +18,7 @@ internal sealed class JudgeHandler : IFrameHandler
if (ctx.Type == BattleType.Pvp && ctx.BothAfterReady())
{
var frame = ctx.Env with { Body = new JudgeBody(Spin: BattleFrameDefaults.DeterministicTurnSpin) };
return new[] { new DispatchRoute(ctx.From, frame, false) };
return new[] { new DispatchRoute(ctx.From, frame, Stock.Normal) };
}
return Array.Empty<DispatchRoute>();

View File

@@ -22,8 +22,8 @@ internal sealed class LoadedHandler : IFrameHandler
var r = new List<DispatchRoute>
{
new(ctx.From, ServerBattleFrames.BuildBattleStart(
ctx.From.Context, ctx.Other.Context, ctx.From.ViewerId, turnState), false),
new(ctx.From, ServerBattleFrames.BuildDeal(), false),
ctx.From.Context, ctx.Other.Context, ctx.From.ViewerId, turnState), Stock.Normal),
new(ctx.From, ServerBattleFrames.BuildDeal(), Stock.Normal),
};
ctx.SenderPhase = BattleSessionPhase.AwaitingSwap;
return r;

View File

@@ -59,6 +59,6 @@ internal sealed class PlayActionsHandler : IFrameHandler
KeyAction: KnownListBuilder.StripKeyActionForOpponent(keyAction));
var frame = ctx.Env with { Body = body };
return new[] { new DispatchRoute(ctx.Other, frame, false) };
return new[] { new DispatchRoute(ctx.Other, frame, Stock.Normal) };
}
}

View File

@@ -9,8 +9,8 @@ internal sealed class RetireKillHandler : IFrameHandler
ctx.State.SessionPhase = BattleSessionPhase.Terminal;
return new[]
{
new DispatchRoute(ctx.From, BattleFrames.BuildBattleFinish(BattleResult.RetireLose), true),
new DispatchRoute(ctx.Other, BattleFrames.BuildBattleFinish(BattleResult.RetireWin), true),
new DispatchRoute(ctx.From, BattleFrames.BuildBattleFinish(BattleResult.RetireLose), Stock.Bypass),
new DispatchRoute(ctx.Other, BattleFrames.BuildBattleFinish(BattleResult.RetireWin), Stock.Bypass),
};
}
}

View File

@@ -15,7 +15,7 @@ internal sealed class SwapHandler : IFrameHandler
var hand = ServerBattleFrames.ComputeHandAfterSwap(BattleFrames.ExtractIdxList(ctx.Env));
// SwapResponse is always immediate — completes the sender's own mulligan UI.
routes.Add(new DispatchRoute(ctx.From, ServerBattleFrames.BuildSwapResponse(hand), false));
routes.Add(new DispatchRoute(ctx.From, ServerBattleFrames.BuildSwapResponse(hand), Stock.Normal));
ctx.State.PostSwapHands[ctx.From] = hand;
ctx.SenderPhase = BattleSessionPhase.AfterReady;
@@ -32,7 +32,7 @@ internal sealed class SwapHandler : IFrameHandler
&& ctx.State.PostSwapHands.TryGetValue(opponent, out var oppoHand)
? ServerBattleFrames.BuildReady(ctx.State.PostSwapHands[p], oppoHand, idxSeed)
: ServerBattleFrames.BuildReady(ctx.State.PostSwapHands[p], idxSeed);
routes.Add(new DispatchRoute(p, ready, false));
routes.Add(new DispatchRoute(p, ready, Stock.Normal));
}
}
return routes;

View File

@@ -12,7 +12,7 @@ internal sealed class TurnEndActionsHandler : IFrameHandler
if (ctx.Type == BattleType.Pvp && ctx.BothAfterReady())
{
var frame = ctx.Env with { Body = new RawBody(new Dictionary<string, object?>()) };
return new[] { new DispatchRoute(ctx.Other, frame, false) };
return new[] { new DispatchRoute(ctx.Other, frame, Stock.Normal) };
}
return Array.Empty<DispatchRoute>();
}

View File

@@ -8,7 +8,7 @@ internal sealed class TurnEndFinalHandler : IFrameHandler
{
// case 4: Bot — Judge to sender only.
if (ctx.Type == BattleType.Bot && ctx.SenderPhase == BattleSessionPhase.AfterReady)
return new[] { new DispatchRoute(ctx.From, BattleFrames.BuildJudgeBroadcast(), false) };
return new[] { new DispatchRoute(ctx.From, BattleFrames.BuildJudgeBroadcast(), Stock.Normal) };
// case 9: general — forward the envelope to other + paired BattleFinish + Terminal.
if (ctx.SenderPhase == BattleSessionPhase.AfterReady)
@@ -16,9 +16,9 @@ internal sealed class TurnEndFinalHandler : IFrameHandler
ctx.State.SessionPhase = BattleSessionPhase.Terminal;
return new[]
{
new DispatchRoute(ctx.Other, ctx.Env, false),
new DispatchRoute(ctx.From, BattleFrames.BuildBattleFinish(BattleResult.LifeWin), true),
new DispatchRoute(ctx.Other, BattleFrames.BuildBattleFinish(BattleResult.LifeLose), true),
new DispatchRoute(ctx.Other, ctx.Env, Stock.Normal),
new DispatchRoute(ctx.From, BattleFrames.BuildBattleFinish(BattleResult.LifeWin), Stock.Bypass),
new DispatchRoute(ctx.Other, BattleFrames.BuildBattleFinish(BattleResult.LifeLose), Stock.Bypass),
};
}

View File

@@ -9,7 +9,7 @@ internal sealed class TurnEndHandler : IFrameHandler
{
// case 4: Bot — Judge to sender only (no real opponent; client flips back to its local AI).
if (ctx.Type == BattleType.Bot && ctx.SenderPhase == BattleSessionPhase.AfterReady)
return new[] { new DispatchRoute(ctx.From, BattleFrames.BuildJudgeBroadcast(), false) };
return new[] { new DispatchRoute(ctx.From, BattleFrames.BuildJudgeBroadcast(), Stock.Normal) };
// case 8: general AfterReady arm — PvP forwards a {turnState} TurnEnd to the opponent
// (handover gate). Any non-Pvp non-Bot type that reaches AfterReady consumes the frame.
@@ -21,7 +21,7 @@ internal sealed class TurnEndHandler : IFrameHandler
// the opponent (the turn taker-over) then sends a Judge, which JudgeHandler reflects
// back to it to start its turn. battleCode/actionSeq/cemetery are dropped.
var te = ctx.Env with { Body = new TurnEndBody(TurnState: TurnState.First) };
return new[] { new DispatchRoute(ctx.Other, te, false) };
return new[] { new DispatchRoute(ctx.Other, te, Stock.Normal) };
}
return Array.Empty<DispatchRoute>(); // Pvp-not-both-ready → drop (Bot already returned above)
}

View File

@@ -13,7 +13,7 @@ internal sealed class TurnStartHandler : IFrameHandler
if (ctx.Type == BattleType.Pvp && ctx.BothAfterReady())
{
var frame = ctx.Env with { Body = new OpponentTurnStartBody(Spin: BattleFrameDefaults.DeterministicTurnSpin) };
return new[] { new DispatchRoute(ctx.Other, frame, false) };
return new[] { new DispatchRoute(ctx.Other, frame, Stock.Normal) };
}
return Array.Empty<DispatchRoute>();

View File

@@ -55,7 +55,7 @@ internal static class KnownListBuilder
/// <c>idx</c>-is-list guards. This is the only place a freshly-generated card's identity exists on
/// the wire (bullet-3 audit F1; producing code <c>RegisterToken</c>/<c>RegisterActionBase</c>) —
/// the played-card op itself never carries a <c>cardId</c>.</summary>
public static IEnumerable<(int Idx, long CardId, CardOwner IsSelf)> MineAddOps(object? orderList)
public static IEnumerable<MinedToken> MineAddOps(object? orderList)
{
if (orderList is not IEnumerable<object?> ops) yield break;
foreach (var op in ops)
@@ -72,7 +72,7 @@ internal static class KnownListBuilder
if (!add.TryGetValue("idx", out var idxRaw) || idxRaw is not IEnumerable<object?> idxList) continue;
foreach (var i in idxList)
yield return ((int)AsLong(i), cardId, isSelf);
yield return new MinedToken((int)AsLong(i), cardId, isSelf);
}
}
@@ -87,7 +87,7 @@ internal static class KnownListBuilder
/// only gates the strip (<see cref="StripKeyActionForOpponent"/>), not the recording. An add whose
/// candidates contain none of the picks is skipped (defensive — no record, no desync); Echo (no
/// keyAction) yields nothing, leaving it mining-only via <see cref="MineAddOps"/>.</summary>
public static IEnumerable<(int Idx, long CardId, CardOwner IsSelf)> MineChoicePicks(object? orderList, object? keyAction)
public static IEnumerable<MinedToken> MineChoicePicks(object? orderList, object? keyAction)
{
if (orderList is not IEnumerable<object?> ops) yield break;
@@ -127,7 +127,7 @@ internal static class KnownListBuilder
if (!add.TryGetValue("idx", out var idxRaw) || idxRaw is not IEnumerable<object?> idxList) continue;
foreach (var i in idxList)
yield return ((int)AsLong(i), chosen.Value, isSelf);
yield return new MinedToken((int)AsLong(i), chosen.Value, isSelf);
}
}
@@ -144,7 +144,7 @@ internal static class KnownListBuilder
/// <c>candidates</c> (→ MineChoicePicks), a <c>string</c> <c>baseIdx</c> (private-group copy,
/// <c>RegisterCopyToken.cs:19-22</c>), and a <c>baseIdx</c> absent from the chosen map (unknown source
/// → degrade, no desync). <c>isPremium</c> (IsFoil) is cosmetic and ignored.</summary>
public static IEnumerable<(int Idx, long CardId, CardOwner IsSelf)> MineCopyTokens(
public static IEnumerable<MinedToken> MineCopyTokens(
object? orderList,
IReadOnlyDictionary<int, long> selfMap,
IReadOnlyDictionary<int, long> otherMap)
@@ -168,7 +168,7 @@ internal static class KnownListBuilder
if (!add.TryGetValue("idx", out var idxRaw) || idxRaw is not IEnumerable<object?> idxList) continue;
foreach (var i in idxList)
yield return ((int)AsLong(i), cardId, isSelf);
yield return new MinedToken((int)AsLong(i), cardId, isSelf);
}
}
@@ -186,8 +186,8 @@ internal static class KnownListBuilder
{
if (e is not IDictionary<string, object?> d) continue;
d.TryGetValue("type", out var typeRaw);
var type = (int)AsLong(typeRaw);
if (type is not (1 or 5)) continue; // only Choice / HaveBeforeSkillChoice handled
var type = (KeyActionType)(int)AsLong(typeRaw);
if (type is not (KeyActionType.Choice or KeyActionType.HaveBeforeSkillChoice)) continue;
d.TryGetValue("cardId", out var cardIdRaw);
var cardId = AsLong(cardIdRaw);

View File

@@ -0,0 +1,15 @@
using SVSim.BattleNode.Protocol;
namespace SVSim.BattleNode.Sessions.Dispatch;
/// <summary>One generated-token identity mined from a sender's <c>orderList</c> <c>add</c> op:
/// the token's <paramref name="Idx"/> in a side's index space, its resolved
/// <paramref name="CardId"/>, and <paramref name="IsSelf"/> — whose map it belongs to (the
/// sender's own token vs a cross-side gift living in the opponent's index space; routed by
/// <see cref="BattleSessionState.RecordTokensFrom"/>). Replaces the transpose-prone
/// <c>(int Idx, long CardId, CardOwner IsSelf)</c> tuple the <c>Mine*</c> methods returned:
/// <c>Idx</c> and <c>CardId</c> are both numeric, so <c>(cardId, idx, …)</c> silently compiled
/// and corrupted the reveal map. As a positional record struct it keeps the named members and
/// positional deconstruct (call sites stay <c>foreach (var (idx, cardId, isSelf) in …)</c>)
/// while the compiler rejects a transposed construction.</summary>
internal readonly record struct MinedToken(int Idx, long CardId, CardOwner IsSelf);

View File

@@ -24,9 +24,9 @@ public interface IBattleParticipant : IAsyncDisposable
/// <summary>Session calls this to deliver a frame from the OTHER participant
/// (or a server-synthesized broadcast). Real impl: encode + WS-send.
/// NoOp: swallow.</summary>
/// <param name="noStock">True for control frames (BattleFinish, JudgeResult, ack);
/// bypasses playSeq assignment + archive.</param>
Task PushAsync(MsgEnvelope envelope, bool noStock, CancellationToken ct);
/// <param name="stock"><see cref="Stock.Bypass"/> for control frames (BattleFinish, JudgeResult,
/// ack) — bypasses playSeq assignment + archive; <see cref="Stock.Normal"/> for gameplay frames.</param>
Task PushAsync(MsgEnvelope envelope, Stock stock, CancellationToken ct);
/// <summary>Participant fires this when it has a frame to send TO the session
/// (its own gameplay action). Real impl: fires on WS recv. NoOp: never fires.</summary>

View File

@@ -31,7 +31,7 @@ public sealed class NoOpBotParticipant : IBattleParticipant
public event Func<MsgEnvelope, CancellationToken, Task>? FrameEmitted;
#pragma warning restore CS0067
public Task PushAsync(MsgEnvelope envelope, bool noStock, CancellationToken ct) => Task.CompletedTask;
public Task PushAsync(MsgEnvelope envelope, Stock stock, CancellationToken ct) => Task.CompletedTask;
public Task RunAsync(CancellationToken ct) => Task.CompletedTask;
public Task TerminateAsync(BattleFinishReason reason) => Task.CompletedTask;
public ValueTask DisposeAsync() => ValueTask.CompletedTask;

View File

@@ -211,14 +211,14 @@ public sealed class RealParticipant : IBattleParticipant, IHasHandshakePhase
}
}
public async Task PushAsync(MsgEnvelope envelope, bool noStock, CancellationToken ct)
public async Task PushAsync(MsgEnvelope envelope, Stock stock, CancellationToken ct)
{
var stamped = noStock ? Outbound.WrapNoStock(envelope) : Outbound.AssignAndArchive(envelope);
var stamped = stock == Stock.Bypass ? Outbound.WrapNoStock(envelope) : Outbound.AssignAndArchive(envelope);
if (_diagnosticLogging)
{
_log.LogInformation(
"[sio-out] viewer={Vid} uri={Uri} pubSeq={Pseq} playSeq={Plseq} noStock={NoStock}",
ViewerId, stamped.Uri, stamped.PubSeq, stamped.PlaySeq, noStock);
"[sio-out] viewer={Vid} uri={Uri} pubSeq={Pseq} playSeq={Plseq} stock={Stock}",
ViewerId, stamped.Uri, stamped.PubSeq, stamped.PlaySeq, stock);
}
await EncodeAndSendAsync(stamped, WireConstants.SynchronizeEvent, ct);
}

View File

@@ -0,0 +1,17 @@
namespace SVSim.BattleNode.Sessions;
/// <summary>
/// How a pushed frame interacts with the per-participant <c>OutboundSequencer</c>: whether it
/// gets a <c>playSeq</c> and is archived for ordered replay, or bypasses both. Replaces a bare
/// (and negatively-named) <c>bool noStock</c> threaded through <see cref="IBattleParticipant.PushAsync"/>
/// and <see cref="Dispatch.DispatchRoute"/> — the literal <c>true</c>/<c>false</c> at call sites gave
/// no hint which sense was which, and was trivial to invert.
/// </summary>
public enum Stock
{
/// <summary>Gameplay frame: assign the next <c>playSeq</c> and archive it for ordered replay.</summary>
Normal = 0,
/// <summary>Control frame (BattleFinish, JudgeResult, ack): bypass <c>playSeq</c> assignment + archive.</summary>
Bypass = 1,
}

View File

@@ -414,7 +414,7 @@ public class CaptureConformanceTests
.MineCopyTokens(orderList, new Dictionary<int, long>(), otherMap)
.ToList();
Assert.That(mined, Is.EquivalentTo(new[] { (49, 123_456_789L, CardOwner.Opponent) }));
Assert.That(mined, Is.EquivalentTo(new[] { new SVSim.BattleNode.Sessions.Dispatch.MinedToken(49, 123_456_789L, CardOwner.Opponent) }));
}
[Test]

View File

@@ -116,7 +116,7 @@ public class BattleSessionDispatchConcurrencyTests
public Task RaiseAsync(MsgEnvelope env) =>
FrameEmitted?.Invoke(env, CancellationToken.None) ?? Task.CompletedTask;
public Task PushAsync(MsgEnvelope env, bool noStock, CancellationToken ct) => _detector.EnterAsync();
public Task PushAsync(MsgEnvelope env, Stock stock, CancellationToken ct) => _detector.EnterAsync();
public Task RunAsync(CancellationToken ct) => Task.CompletedTask;
public Task TerminateAsync(BattleFinishReason reason) => Task.CompletedTask;
public ValueTask DisposeAsync() => ValueTask.CompletedTask;

View File

@@ -653,7 +653,7 @@ public class BattleSessionDispatchTests
Assert.That(pb.KnownList!.Single().CardId, Is.EqualTo(100_011_010L), "generating deck card revealed");
// keyAction forwarded as {type,cardId}; selectCard stripped for the hidden choice.
Assert.That(pb.KeyAction, Is.Not.Null);
Assert.That(pb.KeyAction!.Single().Type, Is.EqualTo(1));
Assert.That(pb.KeyAction!.Single().Type, Is.EqualTo(KeyActionType.Choice));
Assert.That(pb.KeyAction.Single().CardId, Is.EqualTo(100_011_010L));
Assert.That(pb.KeyAction.Single().SelectCard, Is.Null, "the pick stays hidden for open:0");
}
@@ -864,8 +864,8 @@ public class BattleSessionDispatchTests
Assert.That(((BattleFinishBody)aRoute.Frame.Body).Result, Is.EqualTo(BattleResult.RetireLose));
Assert.That(bRoute.Frame.Uri, Is.EqualTo(NetworkBattleUri.BattleFinish));
Assert.That(((BattleFinishBody)bRoute.Frame.Body).Result, Is.EqualTo(BattleResult.RetireWin));
Assert.That(aRoute.NoStock, Is.True);
Assert.That(bRoute.NoStock, Is.True);
Assert.That(aRoute.Stock, Is.EqualTo(Stock.Bypass));
Assert.That(bRoute.Stock, Is.EqualTo(Stock.Bypass));
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.Terminal));
}
@@ -1099,7 +1099,7 @@ public class BattleSessionDispatchTests
public MatchContext Context { get; }
public event Func<MsgEnvelope, CancellationToken, Task>? FrameEmitted;
public FakeParticipant(long viewerId, MatchContext context) { ViewerId = viewerId; Context = context; }
public Task PushAsync(MsgEnvelope env, bool noStock, CancellationToken ct) => Task.CompletedTask;
public Task PushAsync(MsgEnvelope env, Stock stock, CancellationToken ct) => Task.CompletedTask;
public Task RunAsync(CancellationToken ct) => Task.CompletedTask;
public Task TerminateAsync(BattleFinishReason reason) => Task.CompletedTask;
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
@@ -1117,7 +1117,7 @@ public class BattleSessionDispatchTests
public BattleSessionPhase Phase { get; set; } = BattleSessionPhase.AwaitingInitNetwork;
public event Func<MsgEnvelope, CancellationToken, Task>? FrameEmitted;
public FakeRealParticipant(long viewerId, MatchContext context) { ViewerId = viewerId; Context = context; }
public Task PushAsync(MsgEnvelope env, bool noStock, CancellationToken ct) => Task.CompletedTask;
public Task PushAsync(MsgEnvelope env, Stock stock, CancellationToken ct) => Task.CompletedTask;
public Task RunAsync(CancellationToken ct) => Task.CompletedTask;
public Task TerminateAsync(BattleFinishReason reason) => Task.CompletedTask;
public ValueTask DisposeAsync() => ValueTask.CompletedTask;

View File

@@ -14,7 +14,7 @@ public class BattleSessionStateTests
public MatchContext Context { get; }
public event Func<SVSim.BattleNode.Protocol.MsgEnvelope, CancellationToken, Task>? FrameEmitted;
public StubParticipant(long id, MatchContext ctx) { ViewerId = id; Context = ctx; }
public Task PushAsync(SVSim.BattleNode.Protocol.MsgEnvelope e, bool n, CancellationToken c) => Task.CompletedTask;
public Task PushAsync(SVSim.BattleNode.Protocol.MsgEnvelope e, Stock n, CancellationToken c) => Task.CompletedTask;
public Task RunAsync(CancellationToken c) => Task.CompletedTask;
public Task TerminateAsync(BattleFinishReason r) => Task.CompletedTask;
public ValueTask DisposeAsync() => ValueTask.CompletedTask;

View File

@@ -131,7 +131,7 @@ public class KnownListBuilderTests
var orderList = new List<object?> { AddOp(new[] { 31L, 32L }, 900111010L) };
var mined = KnownListBuilder.MineAddOps(orderList).ToList();
Assert.That(mined, Is.EquivalentTo(new[] { (31, 900111010L, CardOwner.Self), (32, 900111010L, CardOwner.Self) }));
Assert.That(mined, Is.EquivalentTo(new[] { new MinedToken(31, 900111010L, CardOwner.Self), new MinedToken(32, 900111010L, CardOwner.Self) }));
}
[Test]
@@ -142,7 +142,7 @@ public class KnownListBuilderTests
// it; the caller routes it into the OTHER side's map.
var orderList = new List<object?> { AddOp(new[] { 31L }, 900111010L, isSelf: 0) };
Assert.That(KnownListBuilder.MineAddOps(orderList),
Is.EquivalentTo(new[] { (31, 900111010L, CardOwner.Opponent) }));
Is.EquivalentTo(new[] { new MinedToken(31, 900111010L, CardOwner.Opponent) }));
}
[Test]
@@ -205,7 +205,7 @@ public class KnownListBuilderTests
AddOp(new[] { 32L }, 900811090L),
};
var mined = KnownListBuilder.MineAddOps(orderList).ToList();
Assert.That(mined, Is.EquivalentTo(new[] { (31, 900111010L, CardOwner.Self), (32, 900811090L, CardOwner.Self) }));
Assert.That(mined, Is.EquivalentTo(new[] { new MinedToken(31, 900111010L, CardOwner.Self), new MinedToken(32, 900811090L, CardOwner.Self) }));
}
// A choice/Discover add op as it arrives in a RawBody: candidates-only (no concrete cardId —
@@ -248,7 +248,7 @@ public class KnownListBuilderTests
var keyAction = KeyActionChoice(generatingCardId: 810014030L, chosen: new[] { 810041260L }, open: 0);
Assert.That(KnownListBuilder.MineChoicePicks(orderList, keyAction),
Is.EquivalentTo(new[] { (46, 810041260L, CardOwner.Self) }));
Is.EquivalentTo(new[] { new MinedToken(46, 810041260L, CardOwner.Self) }));
}
[Test]
@@ -260,7 +260,7 @@ public class KnownListBuilderTests
var keyAction = KeyActionChoice(810014030L, new[] { 101041020L }, open: 0);
Assert.That(KnownListBuilder.MineChoicePicks(orderList, keyAction),
Is.EquivalentTo(new[] { (46, 101041020L, CardOwner.Opponent) }));
Is.EquivalentTo(new[] { new MinedToken(46, 101041020L, CardOwner.Opponent) }));
}
[Test]
@@ -303,7 +303,7 @@ public class KnownListBuilderTests
Assert.That(stripped, Is.Not.Null);
Assert.That(stripped!.Count, Is.EqualTo(1));
Assert.That(stripped[0].Type, Is.EqualTo(1));
Assert.That(stripped[0].Type, Is.EqualTo(KeyActionType.Choice));
Assert.That(stripped[0].CardId, Is.EqualTo(810014030L));
Assert.That(stripped[0].SelectCard, Is.Null);
}
@@ -357,7 +357,7 @@ public class KnownListBuilderTests
var selfMap = new Dictionary<int, long> { [5] = 100_011_010L };
var otherMap = new Dictionary<int, long>();
var mined = KnownListBuilder.MineCopyTokens(orderList, selfMap, otherMap).ToList();
Assert.That(mined, Is.EquivalentTo(new[] { (31, 100_011_010L, CardOwner.Self) }));
Assert.That(mined, Is.EquivalentTo(new[] { new MinedToken(31, 100_011_010L, CardOwner.Self) }));
}
[Test]
@@ -369,7 +369,7 @@ public class KnownListBuilderTests
var selfMap = new Dictionary<int, long>();
var otherMap = new Dictionary<int, long> { [21] = 900_841_330L };
var mined = KnownListBuilder.MineCopyTokens(orderList, selfMap, otherMap).ToList();
Assert.That(mined, Is.EquivalentTo(new[] { (49, 900_841_330L, CardOwner.Opponent) }));
Assert.That(mined, Is.EquivalentTo(new[] { new MinedToken(49, 900_841_330L, CardOwner.Opponent) }));
}
[Test]
@@ -422,7 +422,7 @@ public class KnownListBuilderTests
var orderList = new List<object?> { CopyOp(new[] { 31L, 32L }, baseIdx: 5L, isSelf: 1) };
var selfMap = new Dictionary<int, long> { [5] = 700L };
var mined = KnownListBuilder.MineCopyTokens(orderList, selfMap, new Dictionary<int, long>()).ToList();
Assert.That(mined, Is.EquivalentTo(new[] { (31, 700L, CardOwner.Self), (32, 700L, CardOwner.Self) }));
Assert.That(mined, Is.EquivalentTo(new[] { new MinedToken(31, 700L, CardOwner.Self), new MinedToken(32, 700L, CardOwner.Self) }));
}
// A uList entry as it arrives in a RawBody. Minimal = the 5 always-present fields

View File

@@ -22,7 +22,7 @@ public class NoOpBotParticipantTests
Cat: EmitCategory.Battle, PubSeq: null, PlaySeq: null,
Body: new ResultCodeOnlyBody());
Assert.DoesNotThrowAsync(() => p.PushAsync(env, noStock: false, CancellationToken.None));
Assert.DoesNotThrowAsync(() => p.PushAsync(env, Stock.Normal, CancellationToken.None));
Assert.That(fired, Is.EqualTo(0));
}

View File

@@ -22,8 +22,8 @@ public class RealParticipantTests
// First ordered push gets playSeq = 1; second = 2; etc.
// Inspect the participant's outbound sequencer state via its public Archive.
var env = NewEnvelope(NetworkBattleUri.Matched);
p.PushAsync(env, noStock: false, CancellationToken.None).Wait();
p.PushAsync(env, noStock: false, CancellationToken.None).Wait();
p.PushAsync(env, Stock.Normal, CancellationToken.None).Wait();
p.PushAsync(env, Stock.Normal, CancellationToken.None).Wait();
Assert.That(p.Outbound.Archive.Count, Is.EqualTo(2));
Assert.That(p.Outbound.Archive[1].PlaySeq, Is.EqualTo(1));
@@ -37,7 +37,7 @@ public class RealParticipantTests
var p = new RealParticipant(ws, viewerId: 1, FixtureCtx(),
NullLogger<RealParticipant>.Instance);
p.PushAsync(NewEnvelope(NetworkBattleUri.BattleFinish), noStock: true, CancellationToken.None).Wait();
p.PushAsync(NewEnvelope(NetworkBattleUri.BattleFinish), Stock.Bypass, CancellationToken.None).Wait();
// No playSeq archive entry for no-stock pushes.
Assert.That(p.Outbound.Archive.Count, Is.EqualTo(0));