feat(battle-node): RealParticipant session-finished signal + Pvp cascade

RealParticipant gains _sessionFinished TCS + MarkSessionFinished /
AwaitSessionFinishedAsync. PvP first-arriver's handler awaits the
signal instead of calling self.RunAsync (which the session does
internally on the same instance — double-call would race the WS read).

BattleSession.RunAsync branches on Type: Pvp uses WhenAny + synthesize
BattleFinish(Win) to survivor + WhenAll(drain); Scripted/Bot keep
Phase 1's WhenAll-everything semantics. Disconnect cascade now drives
end-of-battle when a WS drops without a graceful Retire.
This commit is contained in:
gamer147
2026-06-01 21:58:47 -04:00
parent 2789dc08cb
commit ca5a1e926d
3 changed files with 113 additions and 9 deletions

View File

@@ -57,6 +57,33 @@ public sealed class RealParticipant : IBattleParticipant, IHasHandshakePhase
public event Func<MsgEnvelope, CancellationToken, Task>? FrameEmitted;
private readonly TaskCompletionSource<bool> _sessionFinished
= new(TaskCreationOptions.RunContinuationsAsynchronously);
/// <summary>Called by the second arriver's handler (in a finally block) after
/// session.RunAsync completes. Signals the first arriver's handler that it can
/// return and let the HTTP request complete (which closes the WS).</summary>
internal void MarkSessionFinished() => _sessionFinished.TrySetResult(true);
/// <summary>Awaited by the first arriver's handler instead of calling RunAsync
/// (the session already calls RunAsync on this instance from the second arriver's
/// handler context — calling it twice would race the WS read loop). Returns when
/// either MarkSessionFinished fires or the passed CT cancels.</summary>
internal Task AwaitSessionFinishedAsync(CancellationToken ct)
{
if (_sessionFinished.Task.IsCompleted) return _sessionFinished.Task;
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
var reg = ct.Register(() => tcs.TrySetCanceled(ct));
_sessionFinished.Task.ContinueWith(t =>
{
reg.Dispose();
if (t.IsCompletedSuccessfully) tcs.TrySetResult(true);
else if (t.IsFaulted) tcs.TrySetException(t.Exception!.InnerExceptions);
else tcs.TrySetCanceled();
}, TaskContinuationOptions.ExecuteSynchronously);
return tcs.Task;
}
public RealParticipant(WebSocket ws, long viewerId, MatchContext context,
ILogger<RealParticipant> log)
{