mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
215 lines
8.5 KiB
GDScript
215 lines
8.5 KiB
GDScript
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.ACCEPTED, 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 ControlPlaneClient.state.has_open_proposal() 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.ACCEPTED:
|
|
return "Match accepted — preparing server"
|
|
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.RESULT_PENDING:
|
|
return "Recording match result"
|
|
MatchmakingState.COMPLETED:
|
|
return "Match complete"
|
|
MatchmakingState.ASSIGNED:
|
|
return "Match assigned"
|
|
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:
|
|
var waited := _elapsed_seconds
|
|
if int(snapshot.get("enqueued_at_unix", 0)) > 0:
|
|
waited = float(ControlPlaneClient.state.waited_seconds(int(Time.get_unix_time_from_system())))
|
|
detail_label.text = "Waiting %.0fs · revision %d" % [waited, int(snapshot.get("revision", 0))]
|
|
elif phase == MatchmakingState.PROPOSED:
|
|
detail_label.text = "Review the proposal before the countdown expires"
|
|
elif phase == MatchmakingState.ACCEPTED:
|
|
detail_label.text = "All players accepted; preparing the match server"
|
|
elif phase == MatchmakingState.RESULT_PENDING:
|
|
detail_label.text = "The server is confirming the final result"
|
|
elif phase == MatchmakingState.COMPLETED:
|
|
detail_label.text = "The match result has been recorded"
|
|
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, MatchmakingState.COMPLETED]
|
|
|
|
|
|
static func _can_start_new_search(phase: String) -> bool:
|
|
return phase == MatchmakingState.IDLE or phase in [MatchmakingState.CANCELLED, MatchmakingState.EXPIRED, MatchmakingState.FAILED, MatchmakingState.COMPLETED]
|