diff --git a/SVSim.BattleNode/Sessions/BattleSession.cs b/SVSim.BattleNode/Sessions/BattleSession.cs index ff8b33a..9fb66bc 100644 --- a/SVSim.BattleNode/Sessions/BattleSession.cs +++ b/SVSim.BattleNode/Sessions/BattleSession.cs @@ -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 Handlers = + new Dictionary(); + + 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 log) { @@ -132,6 +144,10 @@ public sealed class BattleSession internal IReadOnlyList 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(); var other = ReferenceEquals(from, A) ? B : A; var phaseFrom = from as IHasHandshakePhase; diff --git a/SVSim.BattleNode/Sessions/Dispatch/FrameDispatchContext.cs b/SVSim.BattleNode/Sessions/Dispatch/FrameDispatchContext.cs new file mode 100644 index 0000000..a81050d --- /dev/null +++ b/SVSim.BattleNode/Sessions/Dispatch/FrameDispatchContext.cs @@ -0,0 +1,41 @@ +using SVSim.BattleNode.Lifecycle; +using SVSim.BattleNode.Protocol; +using SVSim.BattleNode.Sessions.Participants; // IHasHandshakePhase + +namespace SVSim.BattleNode.Sessions.Dispatch; + +/// Everything a handler reads or mutates for one inbound frame. / +/// 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). +/// is the sender; is the non-sender. +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; } + + /// 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. + internal BattleSessionPhase? SenderPhase + { + get => (From as IHasHandshakePhase)?.Phase; + set { if (From is IHasHandshakePhase p && value is { } v) p.Phase = v; } + } + + /// 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. + internal bool BothAfterReady() => + (A as IHasHandshakePhase)?.Phase == BattleSessionPhase.AfterReady && + (B as IHasHandshakePhase)?.Phase == BattleSessionPhase.AfterReady; + + /// True for any participant carrying the synthetic opponent viewer id — i.e. a + /// ScriptedBotParticipant OR a NoOpBotParticipant. Callers that must exclude Bot + /// mode rely on a preceding Type == BattleType.Bot guard. Mirrors the legacy + /// IsRealForwardableFromScripted guard. + internal bool IsScriptedBot(IBattleParticipant p) => p.ViewerId == ScriptedLifecycle.FakeOpponentViewerId; +} diff --git a/SVSim.BattleNode/Sessions/Dispatch/IFrameHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/IFrameHandler.cs new file mode 100644 index 0000000..45397b6 --- /dev/null +++ b/SVSim.BattleNode/Sessions/Dispatch/IFrameHandler.cs @@ -0,0 +1,10 @@ +namespace SVSim.BattleNode.Sessions.Dispatch; + +/// Handles one (or more) inbound URI(s). Pure: returns the routes to dispatch and may +/// mutate / advance , +/// 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). +internal interface IFrameHandler +{ + IReadOnlyList Handle(FrameDispatchContext ctx); +}