fix(multiplayer): validate ticket playlists

This commit is contained in:
Josh Creek
2026-09-01 22:02:01 +01:00
parent 0ce2a49419
commit 277ad4bf98
3 changed files with 16 additions and 0 deletions
+6
View File
@@ -51,6 +51,8 @@ func begin_queue(new_ticket_id: String, new_playlist: String) -> bool:
func apply_ticket_update(update: Dictionary) -> bool:
if not _has_string(update, "ticket_id") or not update.has("revision") or not _valid_revision(update["revision"]) or not update.has("state"):
return _request_resync(self.ticket_id)
if update.has("playlist") and not _valid_playlist(String(update["playlist"])):
return _request_resync(self.ticket_id)
if ticket_id.is_empty() or String(update["ticket_id"]) != ticket_id:
return _request_resync(self.ticket_id)
var incoming_revision := int(update["revision"])
@@ -299,3 +301,7 @@ func _valid_revision(value: Variant) -> bool:
if value is float:
return is_finite(float(value)) and float(value) >= 0.0 and float(value) == floor(float(value))
return false
func _valid_playlist(value: String) -> bool:
return value == "casual" or value == "ranked"
@@ -82,6 +82,14 @@ func test_ticket_and_proposal_revisions_must_be_nonnegative_integers() -> void:
assert_true(not state.apply_proposal_update({"proposal_id": "proposal-revision", "revision": -1, "state": "OPEN"}), "negative proposal revision is rejected")
func test_ticket_update_rejects_invalid_playlist_without_mutation() -> void:
var state := MatchmakingState.new()
state.begin_queue("ticket-playlist", "casual")
assert_true(not state.apply_ticket_update({"ticket_id": "ticket-playlist", "revision": 1, "state": "PROPOSED", "playlist": "admin"}), "unknown playlist is rejected")
assert_eq(state.phase, MatchmakingState.QUEUED, "invalid playlist cannot change phase")
assert_eq(state.playlist, "casual", "invalid playlist cannot change playlist")
func test_proposal_terminal_states_are_visible_and_not_cancellable() -> void:
var state := MatchmakingState.new()
state.begin_queue("ticket-1", "casual")
+2
View File
@@ -1546,3 +1546,5 @@ Reconnect recovery now treats `COMPLETED` as terminal, avoiding a needless queue
Ticket, proposal, and WebSocket revisions now fail closed unless they are finite, non-negative integers; fractional values are no longer silently truncated into valid revisions. Adversarial client tests cover fractional and negative inputs.
Proposal updates now enforce the documented `OPEN → ACCEPTED/DECLINED/EXPIRED/CANCELLED` graph, including rejecting higher-revision reopen/accept attempts after terminal decisions while preserving same-state duplicates. Adversarial proposal-transition tests cover accepted and declined terminal paths.
Ticket projections now validate playlist metadata on every update, rejecting unknown values before either phase or playlist state can mutate. An adversarial higher-revision update test covers this boundary.