fix(arena-tk2): park returns 3002 RETRY + empty node_server_url

Two client-crash bugs in the do_matching response when no partner is
waiting:

1. matching_state was 3001 (RC_BATTLE_MATCHING_ILLEGAL); the client's
   Matching.OnFinishedDoMatching switch maps that to an error dialog,
   not a retry. The retry state is 3002 (RC_BATTLE_MATCHING_RETRY).

2. node_server_url was omitted entirely. The client's
   DoMatchingBase.SettingDoMatchingData reads it via
   data["node_server_url"].ToString() with no Keys.Contains guard, so
   absence throws KeyNotFoundException out of NetworkManager.Connect
   before the matching_state switch is even reached. Prod RETRY
   captures send "" while waiting and the real URL only on SUCCEEDED;
   match that.

battle_id stays absent; its accessor IS guarded.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-01 22:50:48 -04:00
parent 43c0a6cf31
commit 0ecd565774
2 changed files with 21 additions and 9 deletions

View File

@@ -62,10 +62,16 @@ public class ArenaTwoPickBattleController : SVSimController
ct);
if (paired is null)
{
// 3002 = RC_BATTLE_MATCHING_RETRY: client polls again. 3001 is ILLEGAL
// and shows an error dialog on the client side. node_server_url must be
// present (the client's DoMatchingBase.SettingDoMatchingData calls
// .ToString() on it without a Keys.Contains guard); prod sends "" while
// waiting and the real URL only on SUCCEEDED. battle_id stays absent
// (its accessor IS guarded).
return Ok(new DoMatchingResponseDto
{
MatchingState = 3001,
// BattleId / NodeServerUrl null — client polls again.
MatchingState = 3002,
NodeServerUrl = "",
});
}

View File

@@ -40,7 +40,7 @@ public class ArenaTwoPickBattleControllerTests
}
[Test]
public async Task DoMatching_solo_poller_returns_3001_RETRY_with_no_BattleId()
public async Task DoMatching_solo_poller_returns_3002_RETRY_with_no_BattleId_but_empty_NodeServerUrl()
{
using var factory = new SVSimTestFactory();
var vid = await factory.SeedViewerAsync();
@@ -58,11 +58,17 @@ public class ArenaTwoPickBattleControllerTests
using var doc = JsonDocument.Parse(body);
var root = doc.RootElement;
Assert.That(root.GetProperty("matching_state").GetInt32(), Is.EqualTo(3001));
// BattleId should be ABSENT from the JSON (WhenWritingNull) — TryGetProperty
// returns false when the key isn't present.
// 3002 = RC_BATTLE_MATCHING_RETRY (client polls again). 3001 is ILLEGAL and
// pops an error dialog on the client.
Assert.That(root.GetProperty("matching_state").GetInt32(), Is.EqualTo(3002));
// battle_id must be ABSENT from the JSON; the client's accessor IS guarded with
// Keys.Contains so absence is the safe shape (matches prod RETRY captures).
Assert.That(root.TryGetProperty("battle_id", out _), Is.False,
"battle_id must be absent from the wire when matching_state==3001 RETRY.");
"battle_id must be absent from the wire when matching_state==3002 RETRY.");
// node_server_url MUST be present (empty string while waiting, the real URL on
// SUCCEEDED). Client's DoMatchingBase.SettingDoMatchingData calls .ToString() on
// it without a Keys.Contains guard, so absence throws KeyNotFoundException.
Assert.That(root.GetProperty("node_server_url").GetString(), Is.EqualTo(""));
}
[Test]
@@ -107,8 +113,8 @@ public class ArenaTwoPickBattleControllerTests
// A polls first (parks).
var respA1 = await clientA.PostAsync("/arena_two_pick_battle/do_matching", JsonContent.Create(req));
using var docA1 = JsonDocument.Parse(await respA1.Content.ReadAsStringAsync());
Assert.That(docA1.RootElement.GetProperty("matching_state").GetInt32(), Is.EqualTo(3001),
"A's first poll parks.");
Assert.That(docA1.RootElement.GetProperty("matching_state").GetInt32(), Is.EqualTo(3002),
"A's first poll parks (3002 = RETRY).");
// B polls (pairs).
var respB = await clientB.PostAsync("/arena_two_pick_battle/do_matching", JsonContent.Create(req));