diff --git a/SVSim.BattleNode/Sessions/BattleSession.cs b/SVSim.BattleNode/Sessions/BattleSession.cs index 1c3d086..ad7c263 100644 --- a/SVSim.BattleNode/Sessions/BattleSession.cs +++ b/SVSim.BattleNode/Sessions/BattleSession.cs @@ -183,13 +183,28 @@ public sealed class BattleSession var result = new List<(MsgEnvelope Envelope, bool NoStock)>(); switch (env.Uri) { + // The real handshake sequence (MatchingNetworkConnectChecker + Matching.cs): + // 1. WS opens. + // 2. Client emits InitNetwork (cat=general). + // 3. Server replies with InitNetwork ack → _initNetworkSuccess = true. + // 4. MatchingInitBattle() runs: status=Connect, emits InitBattle, THEN subscribes + // the OnReceivedEvent matching handler. + // 5. Server replies with Matched → handler is subscribed, status=Connect → + // transitions to StartLoad and StartBattleLoad() loads decks/scene. + // 6. Asset load completes → client emits Loaded. + // 7. Server replies with BattleStart + Deal → status=Prepared, GotoBattle(). + // Pushing Matched in response to InitNetwork (instead of InitBattle) drops it + // before the handler is subscribed; the state machine never advances. case NetworkBattleUri.InitNetwork when Phase == BattleSessionPhase.AwaitingInitNetwork: result.Add((BuildAckedEnvelope(NetworkBattleUri.InitNetwork), NoStock: true)); + Phase = BattleSessionPhase.AwaitingInitBattle; + break; + case NetworkBattleUri.InitBattle when Phase == BattleSessionPhase.AwaitingInitBattle: result.Add((ScriptedLifecycle.BuildMatched(ViewerId, ScriptedLifecycle.FakeOpponentViewerId, BattleId), NoStock: false)); - result.Add((ScriptedLifecycle.BuildBattleStart(ViewerId), NoStock: false)); Phase = BattleSessionPhase.AwaitingLoaded; break; case NetworkBattleUri.Loaded when Phase == BattleSessionPhase.AwaitingLoaded: + result.Add((ScriptedLifecycle.BuildBattleStart(ViewerId), NoStock: false)); result.Add((ScriptedLifecycle.BuildDeal(), NoStock: false)); Phase = BattleSessionPhase.AwaitingSwap; break; diff --git a/SVSim.BattleNode/Sessions/BattleSessionPhase.cs b/SVSim.BattleNode/Sessions/BattleSessionPhase.cs index 8e4abe1..3e33b6d 100644 --- a/SVSim.BattleNode/Sessions/BattleSessionPhase.cs +++ b/SVSim.BattleNode/Sessions/BattleSessionPhase.cs @@ -7,6 +7,7 @@ namespace SVSim.BattleNode.Sessions; public enum BattleSessionPhase { AwaitingInitNetwork, + AwaitingInitBattle, AwaitingLoaded, AwaitingSwap, AfterReady, diff --git a/SVSim.UnitTests/BattleNode/Integration/BattleNodeFlowTests.cs b/SVSim.UnitTests/BattleNode/Integration/BattleNodeFlowTests.cs index 2d58e42..df99569 100644 --- a/SVSim.UnitTests/BattleNode/Integration/BattleNodeFlowTests.cs +++ b/SVSim.UnitTests/BattleNode/Integration/BattleNodeFlowTests.cs @@ -40,21 +40,21 @@ public class BattleNodeFlowTests await using var client = new RawSocketIoTestClient(ws); await client.ConsumeHandshakeAsync(ct); - // 1. InitNetwork → expect InitNetwork ack push, then Matched, then BattleStart. + // 1. InitNetwork → expect InitNetwork ack push only. await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.InitNetwork, pubSeq: 1), key, ct); - var f1 = await client.ReceiveSynchronizeAsync(ct); - var f2 = await client.ReceiveSynchronizeAsync(ct); - var f3 = await client.ReceiveSynchronizeAsync(ct); - Assert.That(f1.Uri, Is.EqualTo(NetworkBattleUri.InitNetwork)); - Assert.That(f2.Uri, Is.EqualTo(NetworkBattleUri.Matched)); - Assert.That(f3.Uri, Is.EqualTo(NetworkBattleUri.BattleStart)); + Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.InitNetwork)); - // 2. Loaded → expect Deal. - await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.Loaded, pubSeq: 2), key, ct); + // 2. InitBattle → expect Matched (handler is now subscribed on the client side). + await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.InitBattle, pubSeq: 2), key, ct); + Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Matched)); + + // 3. Loaded → expect BattleStart + Deal. + await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.Loaded, pubSeq: 3), key, ct); + Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.BattleStart)); Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Deal)); - // 3. Swap with empty idxList → expect Swap response + Ready. - await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.Swap, pubSeq: 3, + // 4. Swap with empty idxList → expect Swap response + Ready. + await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.Swap, pubSeq: 4, body: new Dictionary { ["idxList"] = new List() }), key, ct); Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Swap)); Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Ready)); @@ -62,7 +62,10 @@ public class BattleNodeFlowTests private static MsgEnvelope MakeEnvelope(NetworkBattleUri uri, long pubSeq, Dictionary? body = null) => new(uri, ViewerId: 906243102, Uuid: "udid-test", Bid: null, Try: 0, - Cat: uri == NetworkBattleUri.InitNetwork ? EmitCategory.General : EmitCategory.Battle, + // EmitMsgPack: InitNetwork → general(99); other matching URIs → matching(2); else battle(1). + Cat: uri == NetworkBattleUri.InitNetwork ? EmitCategory.General + : uri == NetworkBattleUri.InitBattle ? EmitCategory.Matching + : EmitCategory.Battle, PubSeq: pubSeq, PlaySeq: null, Body: body ?? new Dictionary()); private static string MakeKey() diff --git a/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs b/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs index 1297d95..8d4292c 100644 --- a/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs +++ b/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs @@ -20,22 +20,34 @@ public class BattleSessionDispatchTests PubSeq: null, PlaySeq: null, Body: new Dictionary()); [Test] - public void InitNetwork_PushesAckThenMatchedThenBattleStart() + public void InitNetwork_PushesAckOnly_TransitionsToAwaitingInitBattle() { var s = NewSession(); var responses = s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitNetwork)); Assert.That(responses.Select(r => r.Envelope.Uri), - Is.EqualTo(new[] { NetworkBattleUri.InitNetwork, NetworkBattleUri.Matched, NetworkBattleUri.BattleStart })); + Is.EqualTo(new[] { NetworkBattleUri.InitNetwork })); + Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.AwaitingInitBattle)); + } + + [Test] + public void InitBattle_PushesMatched_TransitionsToAwaitingLoaded() + { + var s = NewSession(); + s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitNetwork)); + var responses = s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitBattle)); + Assert.That(responses.Single().Envelope.Uri, Is.EqualTo(NetworkBattleUri.Matched)); Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.AwaitingLoaded)); } [Test] - public void Loaded_PushesDeal_TransitionsToAwaitingSwap() + public void Loaded_PushesBattleStartThenDeal_TransitionsToAwaitingSwap() { var s = NewSession(); - s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitNetwork)); // advance phase + s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitNetwork)); + s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitBattle)); var responses = s.ComputeResponses(NewEnvelope(NetworkBattleUri.Loaded)); - Assert.That(responses.Single().Envelope.Uri, Is.EqualTo(NetworkBattleUri.Deal)); + Assert.That(responses.Select(r => r.Envelope.Uri), + Is.EqualTo(new[] { NetworkBattleUri.BattleStart, NetworkBattleUri.Deal })); Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.AwaitingSwap)); } @@ -44,6 +56,7 @@ public class BattleSessionDispatchTests { var s = NewSession(); s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitNetwork)); + s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitBattle)); s.ComputeResponses(NewEnvelope(NetworkBattleUri.Loaded)); var responses = s.ComputeResponses(NewEnvelope(NetworkBattleUri.Swap)); Assert.That(responses.Select(r => r.Envelope.Uri),