feat: add player-scoped assignment recovery

This commit is contained in:
Josh Creek
2026-08-31 22:54:16 +01:00
parent 99680dbf6f
commit 69f1ef6be1
6 changed files with 181 additions and 1 deletions
+45
View File
@@ -0,0 +1,45 @@
class_name AssignmentState
extends RefCounted
# Verified assignment-ready manifest returned by the control plane. The join
# authorisation is retained in memory only and is never written to the restart
# snapshot; transport installation belongs to the explicit ENet/Steam layer.
var available := false
var match_id := ""
var server_id := ""
var slot := -1
var expires_at := ""
var protocol_version := 0
var transport := ""
var join_authorisation := ""
var error_message := ""
func apply(payload: Dictionary) -> bool:
for key in ["match_id", "server_id", "player_id", "slot", "expires_at", "protocol_version", "transport", "join_authorisation"]:
if not payload.has(key):
return _reject("Assignment response is missing " + key)
if not payload["match_id"] is String or not payload["server_id"] is String or not payload["player_id"] is String or not payload["slot"] is int or not payload["expires_at"] is String or not payload["protocol_version"] is int or not payload["transport"] is String or not payload["join_authorisation"] is String:
return _reject("Assignment response contains invalid types")
var next_match_id := String(payload["match_id"])
var next_server_id := String(payload["server_id"])
var next_transport := String(payload["transport"])
if next_match_id.is_empty() or next_server_id.is_empty() or String(payload["player_id"]).is_empty() or int(payload["slot"]) < 0 or int(payload["slot"]) > 5 or int(payload["protocol_version"]) < 1 or (next_transport != "enet" and next_transport != "steam_sdr") or String(payload["expires_at"]).is_empty() or String(payload["join_authorisation"]).is_empty():
return _reject("Assignment response contains invalid values")
match_id = next_match_id
server_id = next_server_id
slot = int(payload["slot"])
expires_at = String(payload["expires_at"])
protocol_version = int(payload["protocol_version"])
transport = next_transport
join_authorisation = String(payload["join_authorisation"])
available = true
error_message = ""
return true
func _reject(reason: String) -> bool:
available = false
error_message = reason
return false
+12
View File
@@ -19,6 +19,7 @@ var player_id := ""
var session_expires_at := ""
var state: MatchmakingState
var ranked_profile: RankedProfileState
var assignment: AssignmentState
var _request: HTTPRequest
var _operation := ""
@@ -28,6 +29,7 @@ var _last_queue_create: Dictionary = {}
func _ready() -> void:
state = MatchmakingState.new()
ranked_profile = RankedProfileState.new()
assignment = AssignmentState.new()
_load_persisted_state()
state.changed.connect(_persist_state)
_request = HTTPRequest.new()
@@ -99,6 +101,12 @@ func fetch_ranked_profile() -> Error:
return _start_request("ranked_profile", HTTPClient.METHOD_GET, "/v1/profile/ranked", {}, "")
func fetch_assignment(match_id: String) -> Error:
if match_id.is_empty():
return ERR_INVALID_PARAMETER
return _start_request("assignment", HTTPClient.METHOD_GET, "/v1/assignments/" + match_id, {}, "")
func heartbeat(ticket_id: String, expected_revision: int) -> Error:
if ticket_id.is_empty() or expected_revision < 0:
return ERR_INVALID_PARAMETER
@@ -227,6 +235,10 @@ func _on_request_completed(result: HTTPRequest.Result, response_code: int, _head
if not ranked_profile.apply(payload):
request_failed.emit(operation, response_code, ranked_profile.error_message)
return
elif operation == "assignment":
if not assignment.apply(payload):
request_failed.emit(operation, response_code, assignment.error_message)
return
request_succeeded.emit(operation, payload)