refactor(battle-node): name ComputeFrames routes as DispatchRoute

This commit is contained in:
gamer147
2026-06-03 13:43:39 -04:00
parent afe2984075
commit 95554cee04
2 changed files with 31 additions and 22 deletions

View File

@@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging;
using SVSim.BattleNode.Lifecycle;
using SVSim.BattleNode.Protocol;
using SVSim.BattleNode.Protocol.Bodies;
using SVSim.BattleNode.Sessions.Dispatch;
using SVSim.BattleNode.Sessions.Participants;
namespace SVSim.BattleNode.Sessions;
@@ -134,10 +135,10 @@ public sealed class BattleSession
/// <see cref="Phase"/>. Extracted so unit tests can drive the dispatch without
/// standing up real participants.
/// </summary>
internal IReadOnlyList<(IBattleParticipant Target, MsgEnvelope Frame, bool NoStock)> ComputeFrames(
internal IReadOnlyList<DispatchRoute> ComputeFrames(
IBattleParticipant from, MsgEnvelope env)
{
var result = new List<(IBattleParticipant, MsgEnvelope, bool)>();
var result = new List<DispatchRoute>();
var other = ReferenceEquals(from, A) ? B : A;
var phaseFrom = from as IHasHandshakePhase;
@@ -148,7 +149,7 @@ public sealed class BattleSession
switch (env.Uri)
{
case NetworkBattleUri.InitNetwork when phaseFrom?.Phase == BattleSessionPhase.AwaitingInitNetwork:
result.Add((from, BuildAck(NetworkBattleUri.InitNetwork), true));
result.Add(new DispatchRoute(from, BuildAck(NetworkBattleUri.InitNetwork), true));
phaseFrom!.Phase = BattleSessionPhase.AwaitingInitBattle;
break;
@@ -180,7 +181,7 @@ public sealed class BattleSession
case NetworkBattleUri.InitBattle
when Type == BattleType.Bot && phaseFrom?.Phase == BattleSessionPhase.AwaitingInitBattle:
// Ack only — NO Matched push.
result.Add((from, BuildAck(NetworkBattleUri.InitBattle), true));
result.Add(new DispatchRoute(from, BuildAck(NetworkBattleUri.InitBattle), true));
phaseFrom!.Phase = BattleSessionPhase.AwaitingLoaded;
break;
@@ -199,7 +200,7 @@ public sealed class BattleSession
// Judge to sender ONLY (not broadcast — there's no real other side).
// The client's JudgeOperation → ControlTurnStartPlayer flips back to
// the local AI's turn after this Judge arrives.
result.Add((from, BuildJudgeBroadcast(), false));
result.Add(new DispatchRoute(from, BuildJudgeBroadcast(), false));
break;
case NetworkBattleUri.InitBattle when phaseFrom?.Phase == BattleSessionPhase.AwaitingInitBattle:
@@ -207,7 +208,7 @@ public sealed class BattleSession
// selfInfo from from.Context and oppoInfo from other.Context (the scripted
// bot's Context fixture preserves the prod-captured cosmetics that previously
// lived in ScriptedProfiles).
result.Add((from, ScriptedLifecycle.BuildMatched(
result.Add(new DispatchRoute(from, ScriptedLifecycle.BuildMatched(
from.Context, other.Context,
from.ViewerId, other.ViewerId,
BattleId, ScriptedProfiles.BattleSeed), false));
@@ -222,9 +223,9 @@ public sealed class BattleSession
// arm (its silent Loaded arm above wins the match). A per-battle coin-flip is a
// follow-up (see plan § Out of scope).
var turnState = ReferenceEquals(from, A) ? 0 : 1;
result.Add((from, ScriptedLifecycle.BuildBattleStart(
result.Add(new DispatchRoute(from, ScriptedLifecycle.BuildBattleStart(
from.Context, other.Context, from.ViewerId, turnState), false));
result.Add((from, ScriptedLifecycle.BuildDeal(), false));
result.Add(new DispatchRoute(from, ScriptedLifecycle.BuildDeal(), false));
phaseFrom!.Phase = BattleSessionPhase.AwaitingSwap;
break;
}
@@ -233,7 +234,7 @@ public sealed class BattleSession
{
var hand = ScriptedLifecycle.ComputeHandAfterSwap(ExtractIdxList(env));
// SwapResponse is always immediate — it completes the sender's own mulligan UI.
result.Add((from, ScriptedLifecycle.BuildSwapResponse(hand), false));
result.Add(new DispatchRoute(from, ScriptedLifecycle.BuildSwapResponse(hand), false));
_postSwapHands[from] = hand;
phaseFrom!.Phase = BattleSessionPhase.AfterReady;
@@ -251,7 +252,7 @@ public sealed class BattleSession
var ready = opponent is IHasHandshakePhase && _postSwapHands.TryGetValue(opponent, out var oppoHand)
? ScriptedLifecycle.BuildReady(_postSwapHands[p], oppoHand) // both hands known
: ScriptedLifecycle.BuildReady(_postSwapHands[p]); // non-interactive opponent
result.Add((p, ready, false));
result.Add(new DispatchRoute(p, ready, false));
}
}
break;
@@ -264,14 +265,14 @@ public sealed class BattleSession
{
var turnEndBroadcast = BuildTurnEndBroadcast();
var judgeBroadcast = BuildJudgeBroadcast();
result.Add((from, turnEndBroadcast, false));
result.Add((other, turnEndBroadcast, false));
result.Add((from, judgeBroadcast, false));
result.Add((other, judgeBroadcast, false));
result.Add(new DispatchRoute(from, turnEndBroadcast, false));
result.Add(new DispatchRoute(other, turnEndBroadcast, false));
result.Add(new DispatchRoute(from, judgeBroadcast, false));
result.Add(new DispatchRoute(other, judgeBroadcast, false));
}
else if (Type == BattleType.Scripted)
{
result.Add((other, env, false));
result.Add(new DispatchRoute(other, env, false));
}
// Bot type: no-op (NoOpBot swallows; client handles its own turn end).
break;
@@ -286,9 +287,9 @@ public sealed class BattleSession
// this dispatch arm owns it. NoOpBotParticipant swallows. Phase → Terminal
// so the RunAsync cascade doesn't synthesize a follow-up BattleFinish.
case NetworkBattleUri.TurnEndFinal when phaseFrom?.Phase == BattleSessionPhase.AfterReady:
result.Add((other, env, false));
result.Add((from, BuildBattleFinish(BattleResult.LifeWin), true));
result.Add((other, BuildBattleFinish(BattleResult.LifeLose), true));
result.Add(new DispatchRoute(other, env, false));
result.Add(new DispatchRoute(from, BuildBattleFinish(BattleResult.LifeWin), true));
result.Add(new DispatchRoute(other, BuildBattleFinish(BattleResult.LifeLose), true));
Phase = BattleSessionPhase.Terminal;
break;
@@ -297,8 +298,8 @@ public sealed class BattleSession
// proper retire codes. Bots swallow their push (no real-opponent state).
case NetworkBattleUri.Retire:
case NetworkBattleUri.Kill:
result.Add((from, BuildBattleFinish(BattleResult.RetireLose), true));
result.Add((other, BuildBattleFinish(BattleResult.RetireWin), true));
result.Add(new DispatchRoute(from, BuildBattleFinish(BattleResult.RetireLose), true));
result.Add(new DispatchRoute(other, BuildBattleFinish(BattleResult.RetireWin), true));
Phase = BattleSessionPhase.Terminal;
break;
@@ -317,7 +318,7 @@ public sealed class BattleSession
case NetworkBattleUri.Judge when IsRealForwardableFromScripted(from, env):
// Generic forwarder for scripted-bot emissions. The Scripted bot's TurnStart,
// TurnEnd, and Judge are intended for the real participant.
result.Add((other, env, false));
result.Add(new DispatchRoute(other, env, false));
break;
// Gameplay-frame forwarding (post-AfterReady). Unified across types:
@@ -331,7 +332,7 @@ public sealed class BattleSession
case NetworkBattleUri.Echo when BothAfterReady():
case NetworkBattleUri.TurnEndActions when BothAfterReady():
case NetworkBattleUri.JudgeResult when BothAfterReady():
result.Add((other, env, false));
result.Add(new DispatchRoute(other, env, false));
break;
default:

View File

@@ -0,0 +1,8 @@
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);