feat(battle-node): WS handler Pvp branch with WaitingRoom
Pvp arrivers Pair-or-Park: second arriver constructs the session; first arriver awaits self.AwaitSessionFinishedAsync (never calls self.RunAsync directly because the session does). Park-race retries Pair once. Bot type still stubbed for Phase 3. Scripted path unchanged. Viewer-id validation extended to accept either P1 or P2 (PvP sessions have both).
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SVSim.BattleNode.Bridge;
|
||||
using SVSim.BattleNode.Sessions;
|
||||
using SVSim.BattleNode.Sessions.Participants;
|
||||
using SVSim.BattleNode.Wire;
|
||||
@@ -31,12 +32,20 @@ namespace SVSim.BattleNode.Hosting;
|
||||
public sealed class BattleNodeWebSocketHandler
|
||||
{
|
||||
private readonly IBattleSessionStore _store;
|
||||
private readonly IWaitingRoom _waitingRoom;
|
||||
private readonly BattleNodeOptions _options;
|
||||
private readonly ILogger<BattleNodeWebSocketHandler> _log;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
|
||||
public BattleNodeWebSocketHandler(IBattleSessionStore store, ILoggerFactory loggerFactory)
|
||||
public BattleNodeWebSocketHandler(
|
||||
IBattleSessionStore store,
|
||||
IWaitingRoom waitingRoom,
|
||||
BattleNodeOptions options,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
_store = store;
|
||||
_waitingRoom = waitingRoom;
|
||||
_options = options;
|
||||
_loggerFactory = loggerFactory;
|
||||
_log = loggerFactory.CreateLogger<BattleNodeWebSocketHandler>();
|
||||
}
|
||||
@@ -94,33 +103,104 @@ public sealed class BattleNodeWebSocketHandler
|
||||
ctx.Response.StatusCode = StatusCodes.Status404NotFound;
|
||||
return;
|
||||
}
|
||||
if (pending.P1.ViewerId != viewerId)
|
||||
var isP1 = viewerId == pending.P1.ViewerId;
|
||||
var isP2 = pending.P2 is not null && viewerId == pending.P2.ViewerId;
|
||||
if (!isP1 && !isP2)
|
||||
{
|
||||
_log.LogWarning(
|
||||
"WS upgrade viewer-id mismatch on BattleId={Bid}: bridge expected={Expected}, decrypted={Got}.",
|
||||
battleId, pending.P1.ViewerId, viewerId);
|
||||
"WS upgrade viewer-id mismatch on BattleId={Bid}: bridge expected={P1}/{P2}, decrypted={Got}.",
|
||||
battleId, pending.P1.ViewerId, pending.P2?.ViewerId, viewerId);
|
||||
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
return;
|
||||
}
|
||||
|
||||
var ws = await ctx.WebSockets.AcceptWebSocketAsync();
|
||||
_store.RemovePending(battleId);
|
||||
|
||||
// Phase 1: only Scripted is wired; Pvp + Bot land in subsequent phases.
|
||||
if (pending.Type != BattleType.Scripted)
|
||||
switch (pending.Type)
|
||||
{
|
||||
_log.LogWarning(
|
||||
"WS upgrade for BattleId={Bid} with unsupported type={Type} (Phase 1 only handles Scripted).",
|
||||
battleId, pending.Type);
|
||||
return;
|
||||
}
|
||||
case BattleType.Scripted:
|
||||
{
|
||||
_store.RemovePending(battleId);
|
||||
var realParticipant = new RealParticipant(ws, viewerId, pending.P1.Context,
|
||||
_loggerFactory.CreateLogger<RealParticipant>());
|
||||
var scriptedBot = new ScriptedBotParticipant();
|
||||
var session = new BattleSession(battleId, pending.Type, realParticipant, scriptedBot,
|
||||
_loggerFactory.CreateLogger<BattleSession>());
|
||||
await session.RunAsync(ctx.RequestAborted);
|
||||
break;
|
||||
}
|
||||
|
||||
var realParticipant = new RealParticipant(ws, viewerId, pending.P1.Context,
|
||||
_loggerFactory.CreateLogger<RealParticipant>());
|
||||
var scriptedBot = new ScriptedBotParticipant();
|
||||
var session = new BattleSession(battleId, pending.Type, realParticipant, scriptedBot,
|
||||
_loggerFactory.CreateLogger<BattleSession>());
|
||||
await session.RunAsync(ctx.RequestAborted);
|
||||
case BattleType.Pvp:
|
||||
{
|
||||
// Pick this connection's MatchContext (P1's if isP1, P2's if isP2).
|
||||
var selfCtx = isP1 ? pending.P1.Context : pending.P2!.Context;
|
||||
var self = new RealParticipant(ws, viewerId, selfCtx,
|
||||
_loggerFactory.CreateLogger<RealParticipant>());
|
||||
|
||||
var firstArriver = _waitingRoom.Pair(battleId, self);
|
||||
|
||||
if (firstArriver is not null)
|
||||
{
|
||||
// We are the SECOND arriver. Construct and drive the session.
|
||||
_store.RemovePending(battleId);
|
||||
var session = new BattleSession(
|
||||
battleId, BattleType.Pvp, firstArriver, self,
|
||||
_loggerFactory.CreateLogger<BattleSession>());
|
||||
try
|
||||
{
|
||||
await session.RunAsync(ctx.RequestAborted);
|
||||
}
|
||||
finally
|
||||
{
|
||||
firstArriver.MarkSessionFinished();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// We are the FIRST arriver. Park; ParkAsync returns the second arriver
|
||||
// on pairing, null on timeout / cancellation / TryAdd race.
|
||||
var second = await _waitingRoom.ParkAsync(
|
||||
battleId, self, _options.WaitingRoomTimeout, ctx.RequestAborted);
|
||||
|
||||
if (second is null)
|
||||
{
|
||||
// Either timeout (most common) or Park/Park race. Retry Pair once.
|
||||
second = _waitingRoom.Pair(battleId, self);
|
||||
if (second is null)
|
||||
{
|
||||
_log.LogWarning(
|
||||
"PvP waiting-room timeout or race on BattleId={Bid}; first arriver disconnected.",
|
||||
battleId);
|
||||
_store.RemovePending(battleId);
|
||||
return;
|
||||
}
|
||||
// Retry succeeded — we're the de-facto second arriver now. Own the session.
|
||||
_store.RemovePending(battleId);
|
||||
var raceSession = new BattleSession(
|
||||
battleId, BattleType.Pvp, second, self,
|
||||
_loggerFactory.CreateLogger<BattleSession>());
|
||||
try { await raceSession.RunAsync(ctx.RequestAborted); }
|
||||
finally { second.MarkSessionFinished(); }
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal first-arriver path: session is being constructed/driven by the
|
||||
// second arriver. Hold this HTTP request open until they signal completion.
|
||||
// Do NOT call self.RunAsync — the session already does.
|
||||
await self.AwaitSessionFinishedAsync(ctx.RequestAborted);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case BattleType.Bot:
|
||||
// Phase 3 deliverable.
|
||||
_log.LogWarning("BattleType.Bot not yet supported (Phase 3); BattleId={Bid}", battleId);
|
||||
return;
|
||||
|
||||
default:
|
||||
_log.LogError("Unknown BattleType={Type} for BattleId={Bid}; closing WS", pending.Type, battleId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ReadCredential(HttpContext ctx, string name)
|
||||
|
||||
Reference in New Issue
Block a user