extends Control const CLIENT_BUILD := "dev" const PROTOCOL_VERSION := 1 const HEARTBEAT_SECONDS := 10.0 const RECOVERY_POLL_SECONDS := 2.0 @onready var playlist_dropdown: OptionButton = %PlaylistDropdown @onready var status_label: Label = %StatusLabel @onready var detail_label: Label = %DetailLabel @onready var ranked_profile_label: Label = %RankedProfileLabel @onready var queue_button: Button = %QueueButton @onready var cancel_button: Button = %CancelButton @onready var accept_button: Button = %AcceptButton @onready var decline_button: Button = %DeclineButton @onready var back_button: Button = %BackButton var _elapsed_seconds := 0.0 var _heartbeat_seconds := 0.0 var _recovery_poll_seconds := 0.0 func _ready() -> void: playlist_dropdown.add_item("Casual") playlist_dropdown.set_item_metadata(0, "casual") playlist_dropdown.add_item("Ranked") playlist_dropdown.set_item_metadata(1, "ranked") playlist_dropdown.item_selected.connect(_on_playlist_selected) 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()) func _process(delta: float) -> void: if ControlPlaneClient.state.phase in [MatchmakingState.QUEUED, MatchmakingState.PROPOSED, MatchmakingState.ALLOCATING]: _elapsed_seconds += delta _heartbeat_seconds += delta _recovery_poll_seconds += delta if _recovery_poll_seconds >= RECOVERY_POLL_SECONDS: _recovery_poll_seconds = 0.0 var recovery_err := ControlPlaneClient.recover_proposal(ControlPlaneClient.state.proposal_id) if not ControlPlaneClient.state.proposal_id.is_empty() else ControlPlaneClient.recover_queue(ControlPlaneClient.state.ticket_id) if recovery_err != OK and recovery_err != ERR_BUSY: _on_local_error("State recovery unavailable: %s" % error_string(recovery_err)) if ControlPlaneClient.state.phase == MatchmakingState.QUEUED and _heartbeat_seconds >= HEARTBEAT_SECONDS: _heartbeat_seconds = 0.0 var err := ControlPlaneClient.heartbeat(ControlPlaneClient.state.ticket_id, ControlPlaneClient.state.revision) if err != OK: _on_local_error("Heartbeat unavailable: %s" % error_string(err)) _render(ControlPlaneClient.state.snapshot()) func _on_queue_pressed() -> void: if ControlPlaneClient.can_retry_queue_create(): var retry_err := ControlPlaneClient.retry_queue_create() 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 _heartbeat_seconds = 0.0 _recovery_poll_seconds = 0.0 var playlist := String(playlist_dropdown.get_selected_metadata()) var ticket_id := "ticket-%s-%s" % [str(Time.get_ticks_usec()), str(randi())] var err := ControlPlaneClient.queue_create(ticket_id, playlist, CLIENT_BUILD, PROTOCOL_VERSION) if err != OK: _on_local_error("Could not start matchmaking: %s" % error_string(err)) func _on_cancel_pressed() -> void: if not ControlPlaneClient.state.can_cancel(): return var err := ControlPlaneClient.cancel_queue(ControlPlaneClient.state.ticket_id, ControlPlaneClient.state.revision) if err != OK: _on_local_error("Could not cancel matchmaking: %s" % error_string(err)) func _on_accept_pressed() -> void: var err := ControlPlaneClient.respond_to_proposal(ControlPlaneClient.state.proposal_id, true, ControlPlaneClient.state.proposal_revision) if err != OK: _on_local_error("Could not accept proposal: %s" % error_string(err)) func _on_decline_pressed() -> void: var err := ControlPlaneClient.respond_to_proposal(ControlPlaneClient.state.proposal_id, false, ControlPlaneClient.state.proposal_revision) if err != OK: _on_local_error("Could not decline proposal: %s" % error_string(err)) func _on_playlist_selected(_index: int) -> void: _refresh_ranked_profile() func _refresh_ranked_profile() -> void: var ranked := String(playlist_dropdown.get_selected_metadata()) == "ranked" ranked_profile_label.visible = ranked if not ranked: return var err := ControlPlaneClient.fetch_ranked_profile() if err != OK and err != ERR_BUSY: ranked_profile_label.text = "Ranked profile unavailable: %s" % error_string(err) func _on_back_pressed() -> void: if ControlPlaneClient.state.can_cancel(): status_label.text = "Cancel the active search before leaving" return get_tree().change_scene_to_file(ScenePaths.MAIN_MENU) func _on_state_changed(snapshot: Dictionary) -> void: _render(snapshot) func _on_request_succeeded(_operation: String, _payload: Dictionary) -> void: ranked_profile_label.text = ControlPlaneClient.ranked_profile.display_text() _render(ControlPlaneClient.state.snapshot()) func _on_request_failed(_operation: String, _http_code: int, detail: String) -> void: detail_label.text = detail ranked_profile_label.text = ControlPlaneClient.ranked_profile.display_text() _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 static func phase_label(phase: String) -> String: match phase: MatchmakingState.IDLE: return "Ready to search" MatchmakingState.QUEUED: return "Searching for players" MatchmakingState.PROPOSED: return "Match found — confirm" MatchmakingState.ALLOCATING: return "Preparing match server" MatchmakingState.PROCESS_READY: return "Match server started" MatchmakingState.ASSIGNMENT_READY: return "Match assigned" MatchmakingState.CONNECTING: return "Connecting to match" MatchmakingState.LIVE: return "Match in progress" MatchmakingState.CANCELLED: return "Search cancelled" MatchmakingState.EXPIRED: return "Search expired" MatchmakingState.FAILED: return "Matchmaking unavailable" _: return "Recovering matchmaking state" func _render(snapshot: Dictionary) -> void: var phase := String(snapshot.get("phase", MatchmakingState.IDLE)) status_label.text = phase_label(phase) if String(snapshot.get("message", "")) != "": detail_label.text = String(snapshot["message"]) elif phase == MatchmakingState.QUEUED: detail_label.text = "Elapsed %.0fs · revision %d" % [_elapsed_seconds, int(snapshot.get("revision", 0))] elif phase == MatchmakingState.PROPOSED: detail_label.text = "Review the proposal before the countdown expires" elif phase == MatchmakingState.IDLE: detail_label.text = "Choose a playlist to begin" cancel_button.visible = ControlPlaneClient.state.can_cancel() accept_button.visible = phase == MatchmakingState.PROPOSED decline_button.visible = phase == MatchmakingState.PROPOSED 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: return phase in [MatchmakingState.CANCELLED, MatchmakingState.EXPIRED, MatchmakingState.FAILED, MatchmakingState.LIVE] static func _can_start_new_search(phase: String) -> bool: return phase == MatchmakingState.IDLE or phase in [MatchmakingState.CANCELLED, MatchmakingState.EXPIRED, MatchmakingState.FAILED]