mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
fix(multiplayer): project accepted queue state
This commit is contained in:
@@ -35,7 +35,7 @@ func _ready() -> void:
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if ControlPlaneClient.state.phase in [MatchmakingState.QUEUED, MatchmakingState.PROPOSED, MatchmakingState.ALLOCATING]:
|
||||
if ControlPlaneClient.state.phase in [MatchmakingState.QUEUED, MatchmakingState.PROPOSED, MatchmakingState.ACCEPTED, MatchmakingState.ALLOCATING]:
|
||||
_elapsed_seconds += delta
|
||||
_heartbeat_seconds += delta
|
||||
_recovery_poll_seconds += delta
|
||||
@@ -149,6 +149,8 @@ static func phase_label(phase: String) -> String:
|
||||
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:
|
||||
@@ -181,6 +183,8 @@ func _render(snapshot: Dictionary) -> void:
|
||||
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.IDLE:
|
||||
detail_label.text = "Choose a playlist to begin"
|
||||
cancel_button.visible = ControlPlaneClient.state.can_cancel()
|
||||
|
||||
@@ -11,6 +11,7 @@ signal resync_required(resource_id: String)
|
||||
const IDLE := "IDLE"
|
||||
const QUEUED := "QUEUED"
|
||||
const PROPOSED := "PROPOSED"
|
||||
const ACCEPTED := "ACCEPTED"
|
||||
const ALLOCATING := "ALLOCATING"
|
||||
const PROCESS_READY := "PROCESS_READY"
|
||||
const ASSIGNMENT_READY := "ASSIGNMENT_READY"
|
||||
@@ -241,7 +242,7 @@ func _reset() -> void:
|
||||
|
||||
|
||||
func _is_ticket_state(value: String) -> bool:
|
||||
return value in [QUEUED, PROPOSED, ALLOCATING, PROCESS_READY, ASSIGNMENT_READY, CONNECTING, LIVE, CANCELLED, EXPIRED, FAILED]
|
||||
return value in [QUEUED, PROPOSED, ACCEPTED, ALLOCATING, PROCESS_READY, ASSIGNMENT_READY, CONNECTING, LIVE, CANCELLED, EXPIRED, FAILED]
|
||||
|
||||
|
||||
func _has_string(value: Dictionary, key: String) -> bool:
|
||||
|
||||
@@ -46,6 +46,9 @@ func test_ticket_normalization_derives_authoritative_enqueue_time() -> void:
|
||||
func test_websocket_event_validation_requires_contract_specific_fields() -> void:
|
||||
var envelope := {"event": "state_changed", "revision": 1, "resource_id": "ticket-1", "occurred_at": "2026-08-31T12:00:00Z", "state": "QUEUED"}
|
||||
assert_true(ControlPlaneClient._valid_websocket_event(envelope), "valid state event is accepted")
|
||||
var accepted := envelope.duplicate()
|
||||
accepted["state"] = "ACCEPTED"
|
||||
assert_true(ControlPlaneClient._valid_websocket_event(accepted), "authoritative accepted queue event is accepted")
|
||||
var bad_state := envelope.duplicate()
|
||||
bad_state["state"] = "SECRET"
|
||||
assert_true(not ControlPlaneClient._valid_websocket_event(bad_state), "unknown state event is rejected")
|
||||
|
||||
@@ -11,6 +11,14 @@ func test_ticket_projection_accepts_ordered_updates_and_exposes_cancel() -> void
|
||||
assert_true(state.can_cancel(), "authoritative cancel remains available before allocation")
|
||||
|
||||
|
||||
func test_ticket_projection_accepts_authoritative_accepted_phase() -> void:
|
||||
var state := MatchmakingState.new()
|
||||
assert_true(state.begin_queue("ticket-accepted", "ranked"), "queue setup succeeds")
|
||||
assert_true(state.apply_ticket_update({"ticket_id": "ticket-accepted", "revision": 1, "state": "ACCEPTED", "playlist": "ranked"}), "accepted queue phase is valid")
|
||||
assert_eq(state.phase, MatchmakingState.ACCEPTED, "accepted phase remains visible instead of forcing resync")
|
||||
assert_true(not state.can_cancel(), "accepted match cannot be cancelled as a queue ticket")
|
||||
|
||||
|
||||
func test_ticket_projection_rejects_gap_and_wrong_ticket_without_mutation() -> void:
|
||||
var state := MatchmakingState.new()
|
||||
assert_true(state.begin_queue("ticket-1", "casual"), "queue setup succeeds")
|
||||
|
||||
@@ -1534,3 +1534,5 @@ The ranked profile projection now also carries the active season's authoritative
|
||||
The versioned OpenAPI contract now declares the implemented `/profile/ranked` surface and its server-authoritative ranked profile schema, including optional active-season metadata. Contract tests reject omission of this operation, extra response fields, and credential leakage.
|
||||
|
||||
The Godot client now defers reconnect-triggered authoritative recovery when an HTTP mutation is still in flight, closing the `ERR_BUSY` recovery-drop race. An adversarial client test verifies that the active ticket remains queued for recovery rather than silently staying stale.
|
||||
|
||||
The Godot queue projection now includes the contract's `ACCEPTED` ticket phase. Accepted events are no longer rejected as an unknown state; the UI keeps the accepted status visible and proceeds through allocation recovery. State and WebSocket vocabulary tests cover the transition.
|
||||
|
||||
Reference in New Issue
Block a user