From ac78473a3e9321b6f57b591ec7b16001608f7293 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Mon, 1 Jun 2026 21:25:11 -0400 Subject: [PATCH] feat(battle-node): add RealParticipant.Phase for per-side handshake state Internal setter; defaults to AwaitingInitNetwork. PvP needs A and B to progress through the handshake states independently, which the session-level BattleSession.Phase can't model. Session migration to read realFrom.Phase is the next task. --- .../Sessions/Participants/RealParticipant.cs | 7 ++++ .../Participants/RealParticipantTests.cs | 37 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/SVSim.BattleNode/Sessions/Participants/RealParticipant.cs b/SVSim.BattleNode/Sessions/Participants/RealParticipant.cs index d2ec4d9..3a0caed 100644 --- a/SVSim.BattleNode/Sessions/Participants/RealParticipant.cs +++ b/SVSim.BattleNode/Sessions/Participants/RealParticipant.cs @@ -28,6 +28,13 @@ public sealed class RealParticipant : IBattleParticipant public InboundTracker Inbound { get; } = new(); public OutboundSequencer Outbound { get; } = new(); + /// Per-side handshake progression. Session reads this when gating + /// handshake-phase synthesis (Matched / BattleStart / Deal / Swap response / + /// Ready). Session transitions via the setter after dispatch. Defaults to + /// AwaitingInitNetwork; only RealParticipant tracks this — bots have no phase + /// because they never send the gating URIs. + internal BattleSessionPhase Phase { get; set; } = BattleSessionPhase.AwaitingInitNetwork; + public event Func? FrameEmitted; public RealParticipant(WebSocket ws, long viewerId, MatchContext context, diff --git a/SVSim.UnitTests/BattleNode/Sessions/Participants/RealParticipantTests.cs b/SVSim.UnitTests/BattleNode/Sessions/Participants/RealParticipantTests.cs index 2613657..79d4049 100644 --- a/SVSim.UnitTests/BattleNode/Sessions/Participants/RealParticipantTests.cs +++ b/SVSim.UnitTests/BattleNode/Sessions/Participants/RealParticipantTests.cs @@ -90,6 +90,43 @@ public class RealParticipantTests Assert.That(result, Is.EqualTo(int.MinValue)); } + [Test] + public void Phase_defaults_to_AwaitingInitNetwork() + { + var ws = new TestWebSocket(); + var p = new RealParticipant(ws, viewerId: 1, FixtureCtx(), + NullLogger.Instance); + + Assert.That(p.Phase, Is.EqualTo(SVSim.BattleNode.Sessions.BattleSessionPhase.AwaitingInitNetwork)); + } + + [Test] + public void Phase_setter_is_visible_to_same_assembly() + { + var ws = new TestWebSocket(); + var p = new RealParticipant(ws, viewerId: 1, FixtureCtx(), + NullLogger.Instance); + + // Setter is `internal`; SVSim.UnitTests has InternalsVisibleTo on SVSim.BattleNode. + p.Phase = SVSim.BattleNode.Sessions.BattleSessionPhase.AfterReady; + + Assert.That(p.Phase, Is.EqualTo(SVSim.BattleNode.Sessions.BattleSessionPhase.AfterReady)); + } + + [Test] + public void Phase_is_per_instance_not_shared() + { + var wsA = new TestWebSocket(); + var wsB = new TestWebSocket(); + var a = new RealParticipant(wsA, viewerId: 1, FixtureCtx(), NullLogger.Instance); + var b = new RealParticipant(wsB, viewerId: 2, FixtureCtx(), NullLogger.Instance); + + a.Phase = SVSim.BattleNode.Sessions.BattleSessionPhase.AfterReady; + + Assert.That(b.Phase, Is.EqualTo(SVSim.BattleNode.Sessions.BattleSessionPhase.AwaitingInitNetwork), + "B's Phase must not change when A's Phase is set."); + } + private static MatchContext FixtureCtx() => new( SelfDeckCardIds: Enumerable.Range(1, 30).Select(_ => 100_011_010L).ToList(), ClassId: "1", CharaId: "1", CardMasterName: "card_master_node_10015",