From 91472df6fc379d74e9319b16be5e6c63752454f2 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Mon, 1 Jun 2026 20:07:45 -0400 Subject: [PATCH] refactor(battle-node): cut handler over to BattleSessionV2 + participants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Production WS path now constructs RealParticipant + ScriptedBotParticipant and hands them to BattleSessionV2 instead of the old single-WS BattleSession. Wire behaviour preserved end-to-end (BattleNodeFlowTests still pass). Also fixes a RunAsync bug uncovered by the cutover: WhenAny would terminate the session as soon as the scripted bot's no-op RunAsync resolved, killing the live WS read loop before any traffic arrived. Phase 1 semantics are simpler — wait for ALL participants. Phase 2's Pvp disconnect propagation will revisit this. --- .../Hosting/BattleNodeWebSocketHandler.cs | 19 ++++++++++++++++--- SVSim.BattleNode/Sessions/BattleSessionV2.cs | 14 +++++++------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/SVSim.BattleNode/Hosting/BattleNodeWebSocketHandler.cs b/SVSim.BattleNode/Hosting/BattleNodeWebSocketHandler.cs index 008e5eb..2c00b0d 100644 --- a/SVSim.BattleNode/Hosting/BattleNodeWebSocketHandler.cs +++ b/SVSim.BattleNode/Hosting/BattleNodeWebSocketHandler.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using SVSim.BattleNode.Sessions; +using SVSim.BattleNode.Sessions.Participants; using SVSim.BattleNode.Wire; namespace SVSim.BattleNode.Hosting; @@ -104,9 +105,21 @@ public sealed class BattleNodeWebSocketHandler var ws = await ctx.WebSockets.AcceptWebSocketAsync(); _store.RemovePending(battleId); - // Phase 1: handler still constructs the old single-WS BattleSession. - // Task 9 switches to BattleSessionV2 + RealParticipant + ScriptedBotParticipant. - var session = new BattleSession(ws, battleId, viewerId, pending.P1.Context, _loggerFactory.CreateLogger()); + + // Phase 1: only Scripted is wired; Pvp + Bot land in subsequent phases. + if (pending.Type != BattleType.Scripted) + { + _log.LogWarning( + "WS upgrade for BattleId={Bid} with unsupported type={Type} (Phase 1 only handles Scripted).", + battleId, pending.Type); + return; + } + + var realParticipant = new RealParticipant(ws, viewerId, pending.P1.Context, + _loggerFactory.CreateLogger()); + var scriptedBot = new ScriptedBotParticipant(); + var session = new BattleSessionV2(battleId, pending.Type, realParticipant, scriptedBot, + _loggerFactory.CreateLogger()); await session.RunAsync(ctx.RequestAborted); } diff --git a/SVSim.BattleNode/Sessions/BattleSessionV2.cs b/SVSim.BattleNode/Sessions/BattleSessionV2.cs index 034ba63..4162695 100644 --- a/SVSim.BattleNode/Sessions/BattleSessionV2.cs +++ b/SVSim.BattleNode/Sessions/BattleSessionV2.cs @@ -42,13 +42,13 @@ public sealed class BattleSessionV2 public async Task RunAsync(CancellationToken cancellation) { - // Run both participants' inbound loops in parallel. First to complete cancels - // the session via the outer cancellation token. - using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellation); - var aTask = A.RunAsync(cts.Token); - var bTask = B.RunAsync(cts.Token); - await Task.WhenAny(aTask, bTask); - cts.Cancel(); + // Run both participants' inbound loops in parallel and wait for them all to + // complete. NoOp/Scripted bots return immediately; Real returns when the WS + // closes. Using WhenAny here would have killed the session as soon as the + // scripted bot's no-op RunAsync resolved. Phase 2's Pvp/Bot cases will need + // disconnect propagation; that's wired in their own task. + var aTask = A.RunAsync(cancellation); + var bTask = B.RunAsync(cancellation); try { await Task.WhenAll(aTask, bTask); } catch { /* swallow cancellation */ } await Task.WhenAll(