From d85c8b81946b0eeaea835af1176e0e90ea9df6fc Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Tue, 1 Sep 2026 17:03:31 +0100 Subject: [PATCH] feat(multiplayer): expose action retry in matchmaking UI --- Game/scripts/matchmaking.gd | 11 +++++++++-- multiplayer-next.md | 2 ++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Game/scripts/matchmaking.gd b/Game/scripts/matchmaking.gd index 20885534..b19aee2d 100644 --- a/Game/scripts/matchmaking.gd +++ b/Game/scripts/matchmaking.gd @@ -58,6 +58,11 @@ func _on_queue_pressed() -> void: if retry_err != OK: _on_local_error("Could not retry matchmaking: %s" % error_string(retry_err)) return + if ControlPlaneClient.can_retry_last_mutation(): + var mutation_err := ControlPlaneClient.retry_last_mutation() + if mutation_err != OK: + _on_local_error("Could not retry matchmaking action: %s" % error_string(mutation_err)) + return if not _can_start_new_search(ControlPlaneClient.state.phase): return _elapsed_seconds = 0.0 @@ -178,8 +183,10 @@ func _render(snapshot: Dictionary) -> void: cancel_button.visible = ControlPlaneClient.state.can_cancel() accept_button.visible = phase == MatchmakingState.PROPOSED decline_button.visible = phase == MatchmakingState.PROPOSED - queue_button.disabled = ControlPlaneClient.auth_expired or not (_can_start_new_search(phase) or ControlPlaneClient.can_retry_queue_create()) - queue_button.text = "Retry Search" if ControlPlaneClient.can_retry_queue_create() else "Search" + var retry_search := ControlPlaneClient.can_retry_queue_create() + var retry_mutation := ControlPlaneClient.can_retry_last_mutation() + queue_button.disabled = ControlPlaneClient.auth_expired or not (_can_start_new_search(phase) or retry_search or retry_mutation) + queue_button.text = "Retry Search" if retry_search else ("Retry Request" if retry_mutation else "Search") static func _is_terminal(phase: String) -> bool: diff --git a/multiplayer-next.md b/multiplayer-next.md index 6a6502a2..065adc5c 100644 --- a/multiplayer-next.md +++ b/multiplayer-next.md @@ -1440,3 +1440,5 @@ Allocator-selected region, build, protocol, and transport now travel with the al The same allocation path now carries the matcher-selected playlist, preventing a ranked match from inheriting the Fleet’s casual default. Durable allocation claims return the playlist, the worker includes it in Fleet selection metadata, Agones copies it to the allocated GameServer, and the supervisor overrides `--playlist` before launch; the existing compatibility tests remain green. The Godot control-plane client now retains the exact last idempotent mutation and exposes `retry_last_mutation()` for transport, timeout, rate-limit, and 5xx failures. Retries reuse the original idempotency key and expected revision, while 401 and 409 responses remain non-retryable; the harness covers the policy boundary. This closes the local duplicate-action recovery mechanism for heartbeat/cancel/proposal calls, with broader live UI retry verification still remaining. + +The matchmaking UI now exposes that retained replay through its existing action button as `Retry Request` while a heartbeat, cancellation, or proposal action has a retryable failure. Terminal, authentication, and revision-conflict paths remain ineligible, so the button cannot issue a stale blind command.