From e1807d2492891d9fd12bcba8d731c0a12de9488c Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Mon, 31 Aug 2026 22:47:31 +0100 Subject: [PATCH] fix: handle matchmaking session expiry --- Game/scripts/control_plane_client.gd | 6 ++++++ Game/scripts/matchmaking.gd | 9 ++++++++- multiplayer-todo.md | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Game/scripts/control_plane_client.gd b/Game/scripts/control_plane_client.gd index d38d863d..4f78ff0e 100644 --- a/Game/scripts/control_plane_client.gd +++ b/Game/scripts/control_plane_client.gd @@ -6,12 +6,14 @@ extends Node signal request_succeeded(operation: String, payload: Dictionary) signal request_failed(operation: String, http_code: int, detail: String) +signal session_expired() const DEFAULT_BASE_URL := "http://127.0.0.1:8080" const PERSIST_PATH := "user://matchmaking_state.cfg" var base_url := DEFAULT_BASE_URL var access_token := "" +var auth_expired := false var state: MatchmakingState var ranked_profile: RankedProfileState @@ -38,6 +40,7 @@ func configure(url: String, token: String) -> bool: return false base_url = normalized access_token = normalized_token + auth_expired = false return true @@ -161,8 +164,11 @@ func _on_request_completed(result: HTTPRequest.Result, response_code: int, _head if response_code < 200 or response_code >= 300: var detail := String(parsed.get("error", "request rejected")) if response_code == HTTPClient.RESPONSE_UNAUTHORIZED: + access_token = "" + auth_expired = true state.fail("Session expired; sign in again") ranked_profile.set_error("Session expired; sign in again") + session_expired.emit() elif response_code == HTTPClient.RESPONSE_GONE and operation == "queue_recover": state.expire("Queue ticket expired") elif response_code == HTTPClient.RESPONSE_SERVICE_UNAVAILABLE: diff --git a/Game/scripts/matchmaking.gd b/Game/scripts/matchmaking.gd index edb999b3..20885534 100644 --- a/Game/scripts/matchmaking.gd +++ b/Game/scripts/matchmaking.gd @@ -29,6 +29,7 @@ func _ready() -> void: ControlPlaneClient.state.changed.connect(_on_state_changed) ControlPlaneClient.request_failed.connect(_on_request_failed) ControlPlaneClient.request_succeeded.connect(_on_request_succeeded) + ControlPlaneClient.session_expired.connect(_on_session_expired) _refresh_ranked_profile() _render(ControlPlaneClient.state.snapshot()) @@ -125,6 +126,12 @@ func _on_request_failed(_operation: String, _http_code: int, detail: String) -> _render(ControlPlaneClient.state.snapshot()) +func _on_session_expired() -> void: + status_label.text = "Session expired" + detail_label.text = "Sign in again before searching for a match" + queue_button.disabled = true + + func _on_local_error(detail: String) -> void: detail_label.text = detail @@ -171,7 +178,7 @@ 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 = not (_can_start_new_search(phase) or ControlPlaneClient.can_retry_queue_create()) + 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" diff --git a/multiplayer-todo.md b/multiplayer-todo.md index 0288d21c..b6c45bd3 100644 --- a/multiplayer-todo.md +++ b/multiplayer-todo.md @@ -1230,7 +1230,7 @@ the local/CI/community transport, not a silent production fallback. | 8.40 `[D:8.3,8.14]` | **IN PROGRESS.** Pure Go revisioned replica reducer rejects gaps for REST resync, makes duplicate/out-of-order events idempotent, and resumes from the authoritative snapshot revision; authenticated queue-ticket recovery now has an owner-checked REST read; Godot client projection persists non-secret ticket/proposal state, forces authoritative recovery after restart, and can replay a lost queue-create response with the original ticket/idempotency key | `server/domain/sync.go`, `server/api/service.go`, `matchmaking_state.gd` and `control_plane_client.gd` cover gap, snapshot, replay, same-revision conflict, owner-only ticket recovery, expired-ticket terminal handling, malformed restart snapshots and duplicate-create retry identity; authenticated WebSocket transport and live Godot verification remain | | 8.41 `[D:7.8,8.9,8.31,8.40]` | After assignment-ready, install SDR relay ticket before connect and send match-scoped join authorisation in `hello`; retain ENet assignments locally | Production connects/reconnects/fences old generation through SDR, never before assignment-ready; allocated/direct ENet and community flows remain compatible | | 8.42 `[D:8.22,8.23,8.24,8.40]` | **IN PROGRESS.** `RankedProfileState` and `ControlPlaneClient.fetch_ranked_profile()` expose the backend-authoritative rating/RD/volatility/games/tier/provisional/season view; matchmaking UI displays provisional/tier status without client-side rating math | `test_control_plane_client.gd` validates profile shape, numeric safety and provisional display; ranked profile fetch/display, committed revision after reconnect, abandon status and season countdown remain dependent on live auth/backend events and Godot runtime verification | -| 8.43 `[D:8.39,8.40,8.41]` | **IN PROGRESS.** Matchmaking client now distinguishes expired queue recovery, session expiry, missing records and retryable control-plane outages; terminal messages remain visible and active searches are not falsely failed on transient errors | `MatchmakingState` and `ControlPlaneClient` tests cover explicit expiry and the existing terminal/retry-safe state paths; decline, version mismatch, regional outage retry UI, failed reconnect, duplicate-action recovery and live Godot verification remain | +| 8.43 `[D:8.39,8.40,8.41]` | **IN PROGRESS.** Matchmaking client now distinguishes expired queue recovery, session expiry, missing records and retryable control-plane outages; a 401 clears the in-memory token, emits `session_expired` and disables retry until a new session is configured; terminal messages remain visible and active searches are not falsely failed on transient errors | `MatchmakingState` and `ControlPlaneClient` tests cover explicit expiry and the existing terminal/retry-safe state paths; decline, version mismatch, regional outage retry UI, failed reconnect, duplicate-action recovery and live Godot verification remain | #### 8F — Observability, verification, cost and rollout