mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
177 lines
5.3 KiB
GDScript
177 lines
5.3 KiB
GDScript
class_name MatchmakingState
|
|
extends RefCounted
|
|
|
|
# Client-side projection of the authenticated control-plane lifecycle. The
|
|
# server remains authoritative; this object only decides what the UI may show
|
|
# and refuses stale, gapped, or conflicting revisions instead of guessing.
|
|
|
|
signal changed(snapshot: Dictionary)
|
|
signal resync_required(resource_id: String)
|
|
|
|
const IDLE := "IDLE"
|
|
const QUEUED := "QUEUED"
|
|
const PROPOSED := "PROPOSED"
|
|
const ALLOCATING := "ALLOCATING"
|
|
const PROCESS_READY := "PROCESS_READY"
|
|
const ASSIGNMENT_READY := "ASSIGNMENT_READY"
|
|
const CONNECTING := "CONNECTING"
|
|
const LIVE := "LIVE"
|
|
const CANCELLED := "CANCELLED"
|
|
const EXPIRED := "EXPIRED"
|
|
const FAILED := "FAILED"
|
|
|
|
var phase := IDLE
|
|
var ticket_id := ""
|
|
var playlist := ""
|
|
var revision := 0
|
|
var expires_at_unix := 0
|
|
var proposal_id := ""
|
|
var proposal_revision := 0
|
|
var proposal_state := ""
|
|
var message := ""
|
|
var needs_resync := false
|
|
|
|
|
|
func begin_queue(new_ticket_id: String, new_playlist: String) -> bool:
|
|
if new_ticket_id.is_empty() or (new_playlist != "casual" and new_playlist != "ranked"):
|
|
return false
|
|
_reset()
|
|
ticket_id = new_ticket_id
|
|
playlist = new_playlist
|
|
phase = QUEUED
|
|
_emit_changed()
|
|
return true
|
|
|
|
|
|
func apply_ticket_update(update: Dictionary) -> bool:
|
|
if not _has_string(update, "ticket_id") or not update.has("revision") or not update.has("state"):
|
|
return _request_resync(ticket_id)
|
|
if ticket_id.is_empty() or String(update["ticket_id"]) != ticket_id:
|
|
return _request_resync(ticket_id)
|
|
var incoming_revision := int(update["revision"])
|
|
if incoming_revision < revision:
|
|
return false
|
|
if incoming_revision == revision:
|
|
if _ticket_differs(update):
|
|
return _request_resync(ticket_id)
|
|
return true
|
|
if incoming_revision > revision + 1:
|
|
return _request_resync(ticket_id)
|
|
var incoming_state := String(update["state"])
|
|
if not _is_ticket_state(incoming_state):
|
|
return _request_resync(ticket_id)
|
|
revision = incoming_revision
|
|
phase = incoming_state
|
|
if update.has("playlist"):
|
|
playlist = String(update["playlist"])
|
|
if update.has("expires_at_unix"):
|
|
expires_at_unix = int(update["expires_at_unix"])
|
|
if update.has("message"):
|
|
message = String(update["message"])
|
|
_emit_changed()
|
|
return true
|
|
|
|
|
|
func apply_proposal_update(update: Dictionary) -> bool:
|
|
if not _has_string(update, "proposal_id") or not update.has("revision") or not update.has("state"):
|
|
return _request_resync(proposal_id)
|
|
var incoming_id := String(update["proposal_id"])
|
|
if proposal_id.is_empty():
|
|
proposal_id = incoming_id
|
|
elif proposal_id != incoming_id:
|
|
return _request_resync(proposal_id)
|
|
var incoming_revision := int(update["revision"])
|
|
if incoming_revision < proposal_revision:
|
|
return false
|
|
if incoming_revision == proposal_revision and not proposal_state.is_empty():
|
|
if String(update["state"]) != proposal_state:
|
|
return _request_resync(proposal_id)
|
|
return true
|
|
if not proposal_state.is_empty() and incoming_revision > proposal_revision + 1:
|
|
return _request_resync(proposal_id)
|
|
var incoming_proposal_state := String(update["state"])
|
|
if incoming_proposal_state == "OPEN":
|
|
phase = PROPOSED
|
|
elif incoming_proposal_state == "ACCEPTED":
|
|
phase = ALLOCATING
|
|
elif incoming_proposal_state == "DECLINED":
|
|
phase = FAILED
|
|
message = "A player declined the match proposal"
|
|
elif incoming_proposal_state == "EXPIRED":
|
|
phase = EXPIRED
|
|
message = "The match proposal expired"
|
|
else:
|
|
return _request_resync(proposal_id)
|
|
proposal_revision = incoming_revision
|
|
proposal_state = incoming_proposal_state
|
|
if update.has("expires_at_unix"):
|
|
expires_at_unix = int(update["expires_at_unix"])
|
|
_emit_changed()
|
|
return true
|
|
|
|
|
|
func mark_assignment_ready() -> void:
|
|
phase = ASSIGNMENT_READY
|
|
message = "Match server is ready"
|
|
_emit_changed()
|
|
|
|
|
|
func mark_connecting() -> void:
|
|
phase = CONNECTING
|
|
message = "Connecting to match server"
|
|
_emit_changed()
|
|
|
|
|
|
func mark_live() -> void:
|
|
phase = LIVE
|
|
message = "Match in progress"
|
|
_emit_changed()
|
|
|
|
|
|
func fail(reason: String) -> void:
|
|
phase = FAILED
|
|
message = reason if not reason.is_empty() else "Matchmaking failed"
|
|
_emit_changed()
|
|
|
|
|
|
func can_cancel() -> bool:
|
|
return phase == QUEUED or phase == PROPOSED or phase == ALLOCATING
|
|
|
|
|
|
func snapshot() -> Dictionary:
|
|
return {"phase": phase, "ticket_id": ticket_id, "playlist": playlist, "revision": revision, "expires_at_unix": expires_at_unix, "proposal_id": proposal_id, "proposal_revision": proposal_revision, "proposal_state": proposal_state, "message": message, "needs_resync": needs_resync}
|
|
|
|
|
|
func _ticket_differs(update: Dictionary) -> bool:
|
|
return String(update["state"]) != phase or (update.has("playlist") and String(update["playlist"]) != playlist) or (update.has("expires_at_unix") and int(update["expires_at_unix"]) != expires_at_unix)
|
|
|
|
|
|
func _request_resync(resource_id: String) -> bool:
|
|
needs_resync = true
|
|
resync_required.emit(resource_id)
|
|
return false
|
|
|
|
|
|
func _emit_changed() -> void:
|
|
changed.emit(snapshot())
|
|
|
|
|
|
func _reset() -> void:
|
|
phase = IDLE
|
|
playlist = ""
|
|
revision = 0
|
|
expires_at_unix = 0
|
|
proposal_id = ""
|
|
proposal_revision = 0
|
|
proposal_state = ""
|
|
message = ""
|
|
needs_resync = false
|
|
|
|
|
|
func _is_ticket_state(value: String) -> bool:
|
|
return value in [QUEUED, PROPOSED, ALLOCATING, PROCESS_READY, ASSIGNMENT_READY, CONNECTING, LIVE, CANCELLED, EXPIRED, FAILED]
|
|
|
|
|
|
func _has_string(value: Dictionary, key: String) -> bool:
|
|
return value.has(key) and value[key] is String and not String(value[key]).is_empty()
|