From 0ecd565774a694fb0aaebeac19d694caebcc9c7a Mon Sep 17 00:00:00 2001 From: gamer147 Date: Mon, 1 Jun 2026 22:50:48 -0400 Subject: [PATCH] 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 --- .../ArenaTwoPickBattleController.cs | 10 ++++++++-- .../ArenaTwoPickBattleControllerTests.cs | 20 ++++++++++++------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/SVSim.EmulatedEntrypoint/Controllers/ArenaTwoPickBattleController.cs b/SVSim.EmulatedEntrypoint/Controllers/ArenaTwoPickBattleController.cs index 75fa780..3e372fa 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/ArenaTwoPickBattleController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/ArenaTwoPickBattleController.cs @@ -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 = "", }); } diff --git a/SVSim.UnitTests/Controllers/ArenaTwoPickBattleControllerTests.cs b/SVSim.UnitTests/Controllers/ArenaTwoPickBattleControllerTests.cs index f14f3f2..32435db 100644 --- a/SVSim.UnitTests/Controllers/ArenaTwoPickBattleControllerTests.cs +++ b/SVSim.UnitTests/Controllers/ArenaTwoPickBattleControllerTests.cs @@ -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));