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 endpoint := "" var join_authorisation := "" var error_message := "" func apply(payload: Dictionary, expected_player_id: String = "") -> bool: for key in ["match_id", "server_id", "player_id", "slot", "expires_at", "protocol_version", "transport", "endpoint", "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 _valid_nonnegative_integer(payload["slot"]) or not payload["expires_at"] is String or not _valid_nonnegative_integer(payload["protocol_version"]) or not payload["transport"] is String or not payload["endpoint"] 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"]) var next_endpoint := String(payload["endpoint"]) var next_player_id := String(payload["player_id"]) var next_expires_at := String(payload["expires_at"]) if not is_valid_expiry_timestamp(next_expires_at): return _reject("Assignment response contains invalid expiry") var expiry_unix := Time.get_unix_time_from_datetime_string(next_expires_at) if not is_valid_opaque_id(next_match_id) or not is_valid_opaque_id(next_server_id) or not is_valid_opaque_id(next_player_id) or (not expected_player_id.is_empty() and next_player_id != expected_player_id) 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 not _valid_endpoint(next_endpoint) or expiry_unix <= Time.get_unix_time_from_system() 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 = next_expires_at protocol_version = int(payload["protocol_version"]) transport = next_transport endpoint = next_endpoint join_authorisation = String(payload["join_authorisation"]) available = true error_message = "" return true static func is_valid_expiry_timestamp(value: String) -> bool: if value.is_empty(): return false var timestamp_pattern := RegEx.create_from_string("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$") return timestamp_pattern.search(value) != null static func is_valid_opaque_id(value: String) -> bool: if value.length() < 16 or value.length() > 128: return false var resource_pattern := RegEx.create_from_string("^[A-Za-z0-9_-]+$") return resource_pattern.search(value) != null static func _valid_endpoint(value: String) -> bool: if value.is_empty() or value.contains("/") or value.contains("?") or value.contains("#"): return false var separator := value.rfind(":") if separator <= 0 or separator >= value.length() - 1: return false var port := value.substr(separator + 1) return port.is_valid_int() and int(port) >= 1 and int(port) <= 65535 static func _valid_nonnegative_integer(value: Variant) -> bool: if value is int: return int(value) >= 0 if value is float: return is_finite(float(value)) and float(value) >= 0.0 and float(value) == floor(float(value)) return false func _reject(reason: String) -> bool: available = false error_message = reason return false