refactor(battle-node): add frame-handler contract, context, and empty registry shim

This commit is contained in:
gamer147
2026-06-03 13:59:02 -04:00
parent 57d91236a0
commit 73d2c4e1b8
3 changed files with 67 additions and 0 deletions

View File

@@ -30,6 +30,18 @@ public sealed class BattleSession
public IBattleParticipant B { get; }
public BattleSessionPhase Phase => _state.SessionPhase;
// Per-URI handlers consulted before the legacy switch. Populated incrementally (Tasks 5-14);
// each registered URI is served by its handler and its legacy switch arm goes dead.
private static readonly IReadOnlyDictionary<NetworkBattleUri, IFrameHandler> Handlers =
new Dictionary<NetworkBattleUri, IFrameHandler>();
private FrameDispatchContext BuildContext(IBattleParticipant from, MsgEnvelope env) =>
new()
{
A = A, B = B, From = from, Other = ReferenceEquals(from, A) ? B : A,
Env = env, Type = Type, BattleId = BattleId, State = _state,
};
public BattleSession(string battleId, BattleType type, IBattleParticipant a, IBattleParticipant b,
ILogger<BattleSession> log)
{
@@ -132,6 +144,10 @@ public sealed class BattleSession
internal IReadOnlyList<DispatchRoute> ComputeFrames(
IBattleParticipant from, MsgEnvelope env)
{
if (Handlers.TryGetValue(env.Uri, out var handler))
return handler.Handle(BuildContext(from, env));
// --- legacy switch (shrinking; deleted in the final task) ---
var result = new List<DispatchRoute>();
var other = ReferenceEquals(from, A) ? B : A;
var phaseFrom = from as IHasHandshakePhase;

View File

@@ -0,0 +1,41 @@
using SVSim.BattleNode.Lifecycle;
using SVSim.BattleNode.Protocol;
using SVSim.BattleNode.Sessions.Participants; // IHasHandshakePhase
namespace SVSim.BattleNode.Sessions.Dispatch;
/// <summary>Everything a handler reads or mutates for one inbound frame. <see cref="A"/>/<see cref="B"/>
/// are the session's positional participants (preserved so handlers that iterate participants in a
/// stable order — e.g. the mulligan barrier — match the legacy switch byte-for-byte). <see cref="From"/>
/// is the sender; <see cref="Other"/> is the non-sender.</summary>
internal sealed class FrameDispatchContext
{
internal required IBattleParticipant A { get; init; }
internal required IBattleParticipant B { get; init; }
internal required IBattleParticipant From { get; init; }
internal required IBattleParticipant Other { get; init; }
internal required MsgEnvelope Env { get; init; }
internal required BattleType Type { get; init; }
internal required string BattleId { get; init; }
internal required BattleSessionState State { get; init; }
/// <summary>Sender's per-side handshake phase (null for a non-IHasHandshakePhase participant,
/// e.g. NoOpBot or the dispatch-test scripted-bot stub). Setting it advances the sender.</summary>
internal BattleSessionPhase? SenderPhase
{
get => (From as IHasHandshakePhase)?.Phase;
set { if (From is IHasHandshakePhase p && value is { } v) p.Phase = v; }
}
/// <summary>Both participants have completed the handshake. Reads A/B (not From/Other) so the
/// result is identical regardless of which side sent the frame — matches legacy BothAfterReady.</summary>
internal bool BothAfterReady() =>
(A as IHasHandshakePhase)?.Phase == BattleSessionPhase.AfterReady &&
(B as IHasHandshakePhase)?.Phase == BattleSessionPhase.AfterReady;
/// <summary>True for any participant carrying the synthetic opponent viewer id — i.e. a
/// <c>ScriptedBotParticipant</c> OR a <c>NoOpBotParticipant</c>. Callers that must exclude Bot
/// mode rely on a preceding <c>Type == BattleType.Bot</c> guard. Mirrors the legacy
/// IsRealForwardableFromScripted guard.</summary>
internal bool IsScriptedBot(IBattleParticipant p) => p.ViewerId == ScriptedLifecycle.FakeOpponentViewerId;
}

View File

@@ -0,0 +1,10 @@
namespace SVSim.BattleNode.Sessions.Dispatch;
/// <summary>Handles one (or more) inbound URI(s). Pure: returns the routes to dispatch and may
/// mutate <see cref="FrameDispatchContext.State"/> / advance <see cref="FrameDispatchContext.SenderPhase"/>,
/// but does not touch the wire. Stateless singletons live in BattleSession's registry; a single
/// handler may be registered under multiple URIs (e.g. Retire/Kill).</summary>
internal interface IFrameHandler
{
IReadOnlyList<DispatchRoute> Handle(FrameDispatchContext ctx);
}