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:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user