refactor(battle-node): cut handler over to BattleSessionV2 + participants
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.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SVSim.BattleNode.Sessions;
|
using SVSim.BattleNode.Sessions;
|
||||||
|
using SVSim.BattleNode.Sessions.Participants;
|
||||||
using SVSim.BattleNode.Wire;
|
using SVSim.BattleNode.Wire;
|
||||||
|
|
||||||
namespace SVSim.BattleNode.Hosting;
|
namespace SVSim.BattleNode.Hosting;
|
||||||
@@ -104,9 +105,21 @@ public sealed class BattleNodeWebSocketHandler
|
|||||||
|
|
||||||
var ws = await ctx.WebSockets.AcceptWebSocketAsync();
|
var ws = await ctx.WebSockets.AcceptWebSocketAsync();
|
||||||
_store.RemovePending(battleId);
|
_store.RemovePending(battleId);
|
||||||
// Phase 1: handler still constructs the old single-WS BattleSession.
|
|
||||||
// Task 9 switches to BattleSessionV2 + RealParticipant + ScriptedBotParticipant.
|
// Phase 1: only Scripted is wired; Pvp + Bot land in subsequent phases.
|
||||||
var session = new BattleSession(ws, battleId, viewerId, pending.P1.Context, _loggerFactory.CreateLogger<BattleSession>());
|
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<RealParticipant>());
|
||||||
|
var scriptedBot = new ScriptedBotParticipant();
|
||||||
|
var session = new BattleSessionV2(battleId, pending.Type, realParticipant, scriptedBot,
|
||||||
|
_loggerFactory.CreateLogger<BattleSessionV2>());
|
||||||
await session.RunAsync(ctx.RequestAborted);
|
await session.RunAsync(ctx.RequestAborted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,13 +42,13 @@ public sealed class BattleSessionV2
|
|||||||
|
|
||||||
public async Task RunAsync(CancellationToken cancellation)
|
public async Task RunAsync(CancellationToken cancellation)
|
||||||
{
|
{
|
||||||
// Run both participants' inbound loops in parallel. First to complete cancels
|
// Run both participants' inbound loops in parallel and wait for them all to
|
||||||
// the session via the outer cancellation token.
|
// complete. NoOp/Scripted bots return immediately; Real returns when the WS
|
||||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
|
// closes. Using WhenAny here would have killed the session as soon as the
|
||||||
var aTask = A.RunAsync(cts.Token);
|
// scripted bot's no-op RunAsync resolved. Phase 2's Pvp/Bot cases will need
|
||||||
var bTask = B.RunAsync(cts.Token);
|
// disconnect propagation; that's wired in their own task.
|
||||||
await Task.WhenAny(aTask, bTask);
|
var aTask = A.RunAsync(cancellation);
|
||||||
cts.Cancel();
|
var bTask = B.RunAsync(cancellation);
|
||||||
try { await Task.WhenAll(aTask, bTask); } catch { /* swallow cancellation */ }
|
try { await Task.WhenAll(aTask, bTask); } catch { /* swallow cancellation */ }
|
||||||
|
|
||||||
await Task.WhenAll(
|
await Task.WhenAll(
|
||||||
|
|||||||
Reference in New Issue
Block a user