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.
|
/// reproduction that exposed the inversion.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// The pre-2026-06-02 <c>Lose = 0</c> / <c>Win = 1</c> / <c>Consistency = 2</c>
|
/// The legacy <c>Lose = 0</c> / <c>Win = 1</c> / <c>Consistency = 2</c> values
|
||||||
/// values are kept for the existing Retire/Kill Scripted flow (which ships
|
/// (client <c>RESULT_CODE.NotFinish</c> / <c>NoContest</c> / <c>Invalid</c>) are
|
||||||
/// <c>Win = 1</c>, parsed client-side as <c>RESULT_CODE.NoContest</c> and
|
/// no longer emitted by any dispatch arm: the Retire/Kill flow now ships the
|
||||||
/// rendered as "battle ended in no contest" — works in that the battle
|
/// proper <c>RetireWin</c> / <c>RetireLose</c> codes, and the involuntary-drop
|
||||||
/// terminates, but shows the wrong text). To be removed once that flow is
|
/// cascade ships <c>DisconnectWin</c>. They survive only as wire-constant
|
||||||
/// rewritten to use the proper retire codes.
|
/// 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>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Always serialize as the int value, not the name; see the
|
/// 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>
|
/// <summary>Player lost by retiring. Pushed to the retirer on <c>Retire</c>/<c>Kill</c>.</summary>
|
||||||
RetireLose = 106,
|
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)
|
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
|
try
|
||||||
{
|
{
|
||||||
await survivor.PushAsync(
|
await survivor.PushAsync(
|
||||||
BuildBattleFinish(BattleResult.Win), noStock: true, cancellation)
|
BuildBattleFinish(BattleResult.DisconnectWin), noStock: true, cancellation)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -334,17 +336,6 @@ public sealed class BattleSession
|
|||||||
PlaySeq: null,
|
PlaySeq: null,
|
||||||
Body: new ResultCodeOnlyBody());
|
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(
|
private MsgEnvelope BuildTurnEndBroadcast() => new(
|
||||||
NetworkBattleUri.TurnEnd,
|
NetworkBattleUri.TurnEnd,
|
||||||
ViewerId: ScriptedLifecycle.FakeOpponentViewerId,
|
ViewerId: ScriptedLifecycle.FakeOpponentViewerId,
|
||||||
|
|||||||
@@ -320,11 +320,11 @@ public class BattleNodeFlowTests
|
|||||||
// Abruptly close A's WS (no Retire).
|
// Abruptly close A's WS (no Retire).
|
||||||
await clientA.DisposeAsync();
|
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);
|
var bFinish = await clientB.ReceiveSynchronizeAsync(ct);
|
||||||
Assert.That(bFinish.Uri, Is.EqualTo(NetworkBattleUri.BattleFinish));
|
Assert.That(bFinish.Uri, Is.EqualTo(NetworkBattleUri.BattleFinish));
|
||||||
var bBody = (RawBody)bFinish.Body;
|
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.
|
// PendingBattle should be evicted by the second arriver's RemovePending.
|
||||||
var store = factory.Services.GetRequiredService<SVSim.BattleNode.Sessions.IBattleSessionStore>();
|
var store = factory.Services.GetRequiredService<SVSim.BattleNode.Sessions.IBattleSessionStore>();
|
||||||
|
|||||||
Reference in New Issue
Block a user