diff --git a/SVSim.BattleNode/Bridge/BattleNodeOptions.cs b/SVSim.BattleNode/Bridge/BattleNodeOptions.cs
index 6b190a1..36e8c25 100644
--- a/SVSim.BattleNode/Bridge/BattleNodeOptions.cs
+++ b/SVSim.BattleNode/Bridge/BattleNodeOptions.cs
@@ -20,7 +20,7 @@ public sealed class BattleNodeOptions
/// When true, emits per-frame
/// diagnostic logs at Information level: [sio-in] on every inbound msg/alive/hand
/// envelope (URI, pubSeq, ackId, dispatch decision, ack-sent flag, ack arg, inbound
- /// watermark); [sio-out] on every outbound push (URI, pubSeq, playSeq, noStock);
+ /// watermark); [sio-out] on every outbound push (URI, pubSeq, playSeq, stock);
/// [ws-rx-text] / [ws-rx-bin] on every WS frame received at the transport
/// layer; [ws-recv-exit] / [ws-loop-exit] on read-loop termination
/// (with WebSocket state + exception type when applicable). Default false — keeps
diff --git a/SVSim.BattleNode/Protocol/Bodies/PlayActionsBroadcastBody.cs b/SVSim.BattleNode/Protocol/Bodies/PlayActionsBroadcastBody.cs
index d1165ad..ac3463e 100644
--- a/SVSim.BattleNode/Protocol/Bodies/PlayActionsBroadcastBody.cs
+++ b/SVSim.BattleNode/Protocol/Bodies/PlayActionsBroadcastBody.cs
@@ -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).
public sealed record KeyActionEntry(
- [property: JsonPropertyName("type")] int Type,
+ [property: JsonPropertyName("type")]
+ [property: JsonConverter(typeof(JsonNumberEnumConverter))] KeyActionType Type,
[property: JsonPropertyName("cardId")] long CardId,
[property: JsonPropertyName("selectCard")] SelectCardEntry? SelectCard);
diff --git a/SVSim.BattleNode/Protocol/KeyActionType.cs b/SVSim.BattleNode/Protocol/KeyActionType.cs
new file mode 100644
index 0000000..94ad045
--- /dev/null
+++ b/SVSim.BattleNode/Protocol/KeyActionType.cs
@@ -0,0 +1,23 @@
+namespace SVSim.BattleNode.Protocol;
+
+///
+/// Wire value of type on a keyAction entry — what kind of card-generating choice the play
+/// is. Mirrors the client's SendKeyActionDataManager.KeyActionType exactly (same ordinals);
+/// the client reads it back via ConvertToInt(...), so it serializes as the underlying int
+/// via . The node currently
+/// relays only and
+/// ( / KnownListBuilder.StripKeyActionForOpponent); the
+/// rest are defined so the guard compares against named values instead of bare ints.
+///
+public enum KeyActionType
+{
+ None = 0,
+ Choice = 1,
+ Accelerated = 2,
+ Crystallize = 3,
+ Fusion = 4,
+ HaveBeforeSkillChoice = 5,
+ BurialRate = 6,
+ ChoiceEvolution = 7,
+ ChoiceBrave = 8,
+}
diff --git a/SVSim.BattleNode/Sessions/BattleSession.cs b/SVSim.BattleNode/Sessions/BattleSession.cs
index a6b5dd6..bb54819 100644
--- a/SVSim.BattleNode/Sessions/BattleSession.cs
+++ b/SVSim.BattleNode/Sessions/BattleSession.cs
@@ -10,7 +10,7 @@ namespace SVSim.BattleNode.Sessions;
///
/// v2 broker session. Holds two participants and brokers between them. Subscribes
/// to each participant's ; on each frame,
-/// runs to determine the routing (target + frame + noStock
+/// runs to determine the routing (target + frame +
/// flag) and dispatches via .
///
///
@@ -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
///
/// 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
/// . Extracted so unit tests can drive the dispatch without
/// standing up real participants.
///
diff --git a/SVSim.BattleNode/Sessions/Dispatch/DispatchRoute.cs b/SVSim.BattleNode/Sessions/Dispatch/DispatchRoute.cs
index d3dea72..0d5f5ba 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/DispatchRoute.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/DispatchRoute.cs
@@ -3,6 +3,7 @@ using SVSim.BattleNode.Protocol;
namespace SVSim.BattleNode.Sessions.Dispatch;
/// One routing decision: deliver to .
-/// Named form of the tuple ComputeFrames historically returned.
-/// true for control frames (BattleFinish, ack) — bypasses playSeq assignment + archive.
-internal readonly record struct DispatchRoute(IBattleParticipant Target, MsgEnvelope Frame, bool NoStock);
+/// Named form of the tuple ComputeFrames historically returned.
+/// is for control frames (BattleFinish, ack) — bypasses
+/// playSeq assignment + archive — and for gameplay frames.
+internal readonly record struct DispatchRoute(IBattleParticipant Target, MsgEnvelope Frame, Stock Stock);
diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/ForwardWhenBothReadyHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/ForwardWhenBothReadyHandler.cs
index 8ef8f47..761132e 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/ForwardWhenBothReadyHandler.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/ForwardWhenBothReadyHandler.cs
@@ -7,7 +7,7 @@ internal sealed class ForwardWhenBothReadyHandler : IFrameHandler
public IReadOnlyList 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();
}
}
diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/InitBattleHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/InitBattleHandler.cs
index ae40040..f27cc65 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/InitBattleHandler.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/InitBattleHandler.cs
@@ -12,7 +12,7 @@ internal sealed class InitBattleHandler : IFrameHandler
{
var r = new List
{
- 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;
diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/InitNetworkHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/InitNetworkHandler.cs
index f3d8661..628d200 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/InitNetworkHandler.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/InitNetworkHandler.cs
@@ -12,7 +12,7 @@ internal sealed class InitNetworkHandler : IFrameHandler
var routes = new List
{
- new(ctx.From, BattleFrames.BuildAck(NetworkBattleUri.InitNetwork), true),
+ new(ctx.From, BattleFrames.BuildAck(NetworkBattleUri.InitNetwork), Stock.Bypass),
};
ctx.SenderPhase = BattleSessionPhase.AwaitingInitBattle;
return routes;
diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/JudgeHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/JudgeHandler.cs
index 2b03e9a..2ec216d 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/JudgeHandler.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/JudgeHandler.cs
@@ -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();
diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/LoadedHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/LoadedHandler.cs
index e615fe9..2999f0a 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/LoadedHandler.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/LoadedHandler.cs
@@ -22,8 +22,8 @@ internal sealed class LoadedHandler : IFrameHandler
var r = new List
{
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;
diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/PlayActionsHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/PlayActionsHandler.cs
index 346e09c..9ccfe97 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/PlayActionsHandler.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/PlayActionsHandler.cs
@@ -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) };
}
}
diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/RetireKillHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/RetireKillHandler.cs
index b056d88..cf8a72f 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/RetireKillHandler.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/RetireKillHandler.cs
@@ -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),
};
}
}
diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/SwapHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/SwapHandler.cs
index 989f96e..3e06276 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/SwapHandler.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/SwapHandler.cs
@@ -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;
diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndActionsHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndActionsHandler.cs
index fbd4db9..1ff09f7 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndActionsHandler.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndActionsHandler.cs
@@ -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()) };
- return new[] { new DispatchRoute(ctx.Other, frame, false) };
+ return new[] { new DispatchRoute(ctx.Other, frame, Stock.Normal) };
}
return Array.Empty();
}
diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndFinalHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndFinalHandler.cs
index 2a13d41..3981a6c 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndFinalHandler.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndFinalHandler.cs
@@ -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),
};
}
diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndHandler.cs
index 8dbc55c..c74bb83 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndHandler.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndHandler.cs
@@ -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(); // Pvp-not-both-ready → drop (Bot already returned above)
}
diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnStartHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnStartHandler.cs
index 89d265a..798d157 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnStartHandler.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnStartHandler.cs
@@ -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();
diff --git a/SVSim.BattleNode/Sessions/Dispatch/KnownListBuilder.cs b/SVSim.BattleNode/Sessions/Dispatch/KnownListBuilder.cs
index 632c7cc..17187cb 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/KnownListBuilder.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/KnownListBuilder.cs
@@ -55,7 +55,7 @@ internal static class KnownListBuilder
/// 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, CardOwner IsSelf)> MineAddOps(object? orderList)
+ public static IEnumerable MineAddOps(object? orderList)
{
if (orderList is not IEnumerable