Files
SVSimServer/SVSim.BattleNode/Protocol/BattleResult.cs
gamer147 ee23985055 fix(battle-node): push BattleFinish on Scripted TurnEndFinal so the client doesn't park on the disconnect-checker
Before: when the player declared their final winning turn (TurnEndFinal),
Scripted mode forwarded it to the bot — which fired a useless 3-frame
TurnStart/TurnEnd/Judge burst as if the game were continuing. No
BattleFinish was ever pushed, so the client's
BattleFinishToOpponentDisConnectChecker (NetworkBattleManagerBase.cs:1640
+ BattleFinishToOpponentDisConnectChecker.cs) parked the player on a
"waiting for opponent" dialog for 128 seconds, eventually falling through
to a synthetic OnDisConnectWin. The user could see "opponent defeated"
animations but couldn't proceed to the post-battle screen.

After: Scripted TurnEndFinal pushes BattleFinish with result=LifeLose=102
to the player (matches the RESULT_CODE the client expects per
NetworkBattleReceiver.cs:963-986; client maps LifeLose → "opponent's life
ran out, PLAYER WIN" UI per NetworkBattleManagerBase.cs:1450-1459). Phase
transitions to Terminal so RunAsync's PvP-disconnect cascade doesn't
synthesize a second BattleFinish on top. No bot burst — the game is
over.

Wire reference: prod TK2 capture battle-traffic_tk2_regular.ndjson:273-274
shows server pushing TurnEndFinal followed immediately by BattleFinish
result:102.

BattleResult enum gets the LifeWin=101 / LifeLose=102 values and a
corrected docstring. The pre-existing Lose=0 / Win=1 / Consistency=2
values stay (Retire/Kill flow ships them today and works as "no contest"
end-of-battle), but their docstring no longer claims they're the WS
shape — they were always the HTTP /finish shape, mislabeled.

TurnEnd (regular, not final) keeps the existing forward-to-bot behavior
in Scripted mode — that's a normal turn boundary, not game end.

PvP TurnEndFinal still broadcasts the same TurnEnd+Judge as regular
TurnEnd; the actual game-end BattleFinish push in PvP rides the loser's
Retire/Kill or the disconnect cascade in RunAsync.

177 battle-node tests passing (was 176; +1 covering the new dispatch arm).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 17:48:20 -04:00

56 lines
2.7 KiB
C#

namespace SVSim.BattleNode.Protocol;
/// <summary>
/// Wire value of <c>result</c> on a WS <c>BattleFinish</c> frame.
/// <para>
/// Maps to the client's <c>NetworkBattleReceiver.RESULT_CODE</c> enum at
/// <c>NetworkBattleReceiver.cs:963-986</c>. The client extracts this via
/// <c>NetworkBattleReceiver.cs:1170-1172</c>:
/// <code>
/// case NetworkParameter.result:
/// _receiveData.result = (RESULT_CODE)ConvertToInt(data.Value);
/// </code>
/// then dispatches through <c>NetworkBattleManagerBase.JudgeResultReceive</c>
/// (lines 1412-1488). The wire codes are categorized by HOW the battle ended
/// (life / deckout / retire / disconnect / timeout) and are named from the
/// OPPONENT'S perspective:
/// </para>
/// <list type="bullet">
/// <item><c>LifeLose</c> = "opponent's life ran out" → PLAYER WIN UI</item>
/// <item><c>LifeWin</c> = "opponent's life win condition met" → PLAYER LOSE UI</item>
/// </list>
/// <para>
/// Prior to 2026-06-02 the docstring here claimed the values were 0/1/2 mapping
/// LOSE/WIN/CONSISTENCY — that was the HTTP <c>/finish</c> response shape, not the
/// WS frame. The previous values are kept for backwards compat with the
/// Retire/Kill scripted flow (which has shipped emitting <c>Win = 1</c>, parsed
/// client-side as <c>RESULT_CODE.NoContest</c> and rendered as "battle ended in
/// no contest" — a working-but-not-quite-right scripted-bot terminator).
/// </para>
/// <para>
/// Always serialize as the int value, not the name; see the
/// <c>JsonNumberEnumConverter</c> on <see cref="Bodies.BattleFinishBody.Result"/>.
/// </para>
/// </summary>
public enum BattleResult
{
/// <summary>Pre-2026-06-02 value, kept for the existing Retire/Kill Scripted flow.
/// Wire-equivalent of <c>RESULT_CODE.NotFinish = 0</c>. Don't use for new code.</summary>
Lose = 0,
/// <summary>Pre-2026-06-02 value, kept for the existing Retire/Kill Scripted flow.
/// Wire-equivalent of <c>RESULT_CODE.NoContest = 1</c>. Don't use for new code.</summary>
Win = 1,
/// <summary>Pre-2026-06-02 value, kept for the existing Retire/Kill Scripted flow.
/// Wire-equivalent of <c>RESULT_CODE.Invalid = 2</c>. Don't use for new code.</summary>
Consistency = 2,
/// <summary>Opponent's life ran out (PLAYER WIN). Pushed by Scripted mode on
/// the player's <c>TurnEndFinal</c> emit. Matches the prod TK2 capture at
/// <c>data_dumps/captures/battle-traffic_tk2_regular.ndjson:274</c>.</summary>
LifeLose = 102,
/// <summary>Opponent's life-win condition met (PLAYER LOSE). Not currently emitted
/// by Scripted mode — the bot can't kill the player — but listed for completeness.</summary>
LifeWin = 101,
}