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

@@ -127,6 +127,48 @@ public class RealParticipantTests
"B's Phase must not change when A's Phase is set.");
}
[Test]
public async Task AwaitSessionFinishedAsync_returns_when_MarkSessionFinished_fires()
{
var ws = new TestWebSocket();
var p = new RealParticipant(ws, viewerId: 1, FixtureCtx(),
NullLogger<RealParticipant>.Instance);
var awaiter = p.AwaitSessionFinishedAsync(CancellationToken.None);
p.MarkSessionFinished();
await awaiter; // should complete promptly
Assert.Pass();
}
[Test]
public void AwaitSessionFinishedAsync_cancels_on_token()
{
var ws = new TestWebSocket();
var p = new RealParticipant(ws, viewerId: 1, FixtureCtx(),
NullLogger<RealParticipant>.Instance);
using var cts = new CancellationTokenSource();
var awaiter = p.AwaitSessionFinishedAsync(cts.Token);
cts.Cancel();
Assert.That(async () => await awaiter, Throws.InstanceOf<OperationCanceledException>());
}
[Test]
public async Task MarkSessionFinished_is_idempotent()
{
var ws = new TestWebSocket();
var p = new RealParticipant(ws, viewerId: 1, FixtureCtx(),
NullLogger<RealParticipant>.Instance);
p.MarkSessionFinished();
p.MarkSessionFinished(); // should not throw
await p.AwaitSessionFinishedAsync(CancellationToken.None);
Assert.Pass();
}
private static MatchContext FixtureCtx() => new(
SelfDeckCardIds: Enumerable.Range(1, 30).Select(_ => 100_011_010L).ToList(),
ClassId: "1", CharaId: "1", CardMasterName: "card_master_node_10015",