fix(battle-node): involuntary-drop survivor gets DisconnectWin, not Win=NoContest
Code-review follow-up to the dispatch unification (0a8a84b).
1. The RunAsync drop cascade synthesized BattleFinish(Win=1), which the client
renders as RESULT_CODE.NoContest ("battle ended in no contest") instead of a
win. Add DisconnectWin=201 (already in the client enum, routes to WIN UI) and
ship it for involuntary opponent drops. Update PvpMidGameDisconnect_FullCascade.
2. Remove BuildBattleFinishNoContest() — dead since the Retire/Kill arm moved to
RetireWin/RetireLose.
3. Correct the BattleResult docstring: Lose/Win/Consistency are no longer emitted
by any dispatch arm; they survive only as serialization-test constants.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -30,12 +30,15 @@ namespace SVSim.BattleNode.Protocol;
|
||||
/// reproduction that exposed the inversion.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The pre-2026-06-02 <c>Lose = 0</c> / <c>Win = 1</c> / <c>Consistency = 2</c>
|
||||
/// values are kept for the existing Retire/Kill Scripted flow (which ships
|
||||
/// <c>Win = 1</c>, parsed client-side as <c>RESULT_CODE.NoContest</c> and
|
||||
/// rendered as "battle ended in no contest" — works in that the battle
|
||||
/// terminates, but shows the wrong text). To be removed once that flow is
|
||||
/// rewritten to use the proper retire codes.
|
||||
/// The legacy <c>Lose = 0</c> / <c>Win = 1</c> / <c>Consistency = 2</c> values
|
||||
/// (client <c>RESULT_CODE.NotFinish</c> / <c>NoContest</c> / <c>Invalid</c>) are
|
||||
/// no longer emitted by any dispatch arm: the Retire/Kill flow now ships the
|
||||
/// proper <c>RetireWin</c> / <c>RetireLose</c> codes, and the involuntary-drop
|
||||
/// cascade ships <c>DisconnectWin</c>. They survive only as wire-constant
|
||||
/// references in serialization tests and can be deleted once those are dropped.
|
||||
/// (Historically <c>Win = 1</c> was shipped on Retire and rendered client-side
|
||||
/// as "battle ended in no contest" — the wrong text — which is why it was
|
||||
/// replaced.)
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Always serialize as the int value, not the name; see the
|
||||
@@ -71,4 +74,11 @@ public enum BattleResult
|
||||
|
||||
/// <summary>Player lost by retiring. Pushed to the retirer on <c>Retire</c>/<c>Kill</c>.</summary>
|
||||
RetireLose = 106,
|
||||
|
||||
/// <summary>Survivor wins because the opponent's socket dropped without a graceful
|
||||
/// <c>Retire</c>. Pushed to the survivor by the <see cref="Sessions.BattleSession"/>
|
||||
/// RunAsync drop cascade. Client <c>RESULT_CODE.DisconnectWin</c> renders the
|
||||
/// "opponent disconnected" result text → player WIN UI. Same player-perspective
|
||||
/// convention as the Life/Retire codes.</summary>
|
||||
DisconnectWin = 201,
|
||||
}
|
||||
|
||||
@@ -58,11 +58,13 @@ public sealed class BattleSession
|
||||
|
||||
if (Phase != BattleSessionPhase.Terminal)
|
||||
{
|
||||
// Involuntary drop (no graceful Retire): synthesize BattleFinish(Win) to survivor.
|
||||
// Involuntary drop (no graceful Retire): synthesize BattleFinish(DisconnectWin)
|
||||
// to survivor. DisconnectWin=201 → client renders "opponent disconnected" →
|
||||
// WIN UI; the legacy Win=1 used here previously rendered "no contest".
|
||||
try
|
||||
{
|
||||
await survivor.PushAsync(
|
||||
BuildBattleFinish(BattleResult.Win), noStock: true, cancellation)
|
||||
BuildBattleFinish(BattleResult.DisconnectWin), noStock: true, cancellation)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -334,17 +336,6 @@ public sealed class BattleSession
|
||||
PlaySeq: null,
|
||||
Body: new ResultCodeOnlyBody());
|
||||
|
||||
private MsgEnvelope BuildBattleFinishNoContest() => new(
|
||||
NetworkBattleUri.BattleFinish,
|
||||
ViewerId: ScriptedLifecycle.FakeOpponentViewerId,
|
||||
Uuid: WireConstants.ServerUuid,
|
||||
Bid: null,
|
||||
Try: 0,
|
||||
Cat: EmitCategory.Battle,
|
||||
PubSeq: null,
|
||||
PlaySeq: null,
|
||||
Body: new BattleFinishBody(Result: BattleResult.Win));
|
||||
|
||||
private MsgEnvelope BuildTurnEndBroadcast() => new(
|
||||
NetworkBattleUri.TurnEnd,
|
||||
ViewerId: ScriptedLifecycle.FakeOpponentViewerId,
|
||||
|
||||
@@ -320,11 +320,11 @@ public class BattleNodeFlowTests
|
||||
// Abruptly close A's WS (no Retire).
|
||||
await clientA.DisposeAsync();
|
||||
|
||||
// B should receive BattleFinish(Win) within a few seconds.
|
||||
// B should receive BattleFinish(DisconnectWin) within a few seconds.
|
||||
var bFinish = await clientB.ReceiveSynchronizeAsync(ct);
|
||||
Assert.That(bFinish.Uri, Is.EqualTo(NetworkBattleUri.BattleFinish));
|
||||
var bBody = (RawBody)bFinish.Body;
|
||||
Assert.That((long)bBody.Entries["result"]!, Is.EqualTo((long)SVSim.BattleNode.Protocol.BattleResult.Win));
|
||||
Assert.That((long)bBody.Entries["result"]!, Is.EqualTo((long)SVSim.BattleNode.Protocol.BattleResult.DisconnectWin));
|
||||
|
||||
// PendingBattle should be evicted by the second arriver's RemovePending.
|
||||
var store = factory.Services.GetRequiredService<SVSim.BattleNode.Sessions.IBattleSessionStore>();
|
||||
|
||||
Reference in New Issue
Block a user