mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
feat(multiplayer): bind admissions to durable leases
This commit is contained in:
+21
-47
@@ -4,6 +4,7 @@ const NetCodec = preload("res://scripts/net_codec.gd")
|
||||
const ServerControlScript = preload("res://scripts/server_control.gd")
|
||||
const AgonesSDKScript = preload("res://scripts/agones_sdk.gd")
|
||||
const AssignmentState = preload("res://scripts/assignment_state.gd")
|
||||
const ConnectionLeaseClientScript = preload("res://scripts/connection_lease_client.gd")
|
||||
|
||||
# Headless dedicated server entry point (task 1.6). Parses CLI args, hosts
|
||||
# via NetworkManager, logs structured lines, and watches for physics-tick
|
||||
@@ -32,9 +33,8 @@ var _watchdog_armed := false # skip the first _process(): engine startup sched
|
||||
var _control: ServerControl = null
|
||||
var _match_loop: ServerMatchLoop = null
|
||||
var _agones = null
|
||||
var _connection_leases = null
|
||||
var _drain_requested := false
|
||||
var _connection_reports_inflight: Dictionary = {}
|
||||
var _connection_reports_complete: Dictionary = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -105,6 +105,18 @@ func _ready() -> void:
|
||||
get_tree().root.add_child.call_deferred(_agones)
|
||||
if _agones.configure_from_environment():
|
||||
_agones.start_health()
|
||||
_connection_leases = ConnectionLeaseClientScript.new()
|
||||
_connection_leases.name = "ConnectionLeases"
|
||||
var lease_url := OS.get_environment("COSMIC_CLASH_CONTROL_PLANE_URL")
|
||||
var lease_token := OS.get_environment("COSMIC_CLASH_WORKLOAD_TOKEN")
|
||||
if _connection_leases.configure(lease_url, lease_token, String(config.get_value("match-id")), String(config.get_value("server-id"))):
|
||||
_connection_leases.reconciliation_failed.connect(_on_connection_lease_reconciliation_failed)
|
||||
get_tree().root.add_child.call_deferred(_connection_leases)
|
||||
MatchNet.configure_connection_lease_callbacks(_connection_leases.claim, _connection_leases.record_disconnect)
|
||||
else:
|
||||
_connection_leases.queue_free()
|
||||
_connection_leases = null
|
||||
ServerLog.warn("connection_lease_backend_unavailable", {"reason": "invalid_or_missing_configuration"})
|
||||
|
||||
NetworkManager.client_connected.connect(_on_client_connected)
|
||||
NetworkManager.client_disconnected.connect(_on_client_disconnected)
|
||||
@@ -188,8 +200,6 @@ func _on_client_disconnected(peer_id: int) -> void:
|
||||
|
||||
func _on_player_joined(peer_id: int, player_name: String) -> void:
|
||||
ServerLog.info("player_joined", {"peer_id": peer_id, "name": player_name, "roster": MatchNet.roster.size()})
|
||||
if config != null and bool(config.get_value("allocated-mode")):
|
||||
_report_player_connected(MatchNet.player_identity(peer_id))
|
||||
|
||||
|
||||
func _on_player_left(peer_id: int) -> void:
|
||||
@@ -209,52 +219,16 @@ func _on_initial_connect_ready() -> void:
|
||||
ServerLog.info("initial_connect_window_started", {"match_id": String(config.get_value("match-id"))})
|
||||
|
||||
|
||||
func _report_player_connected(player_id: String) -> void:
|
||||
var base_url := OS.get_environment("COSMIC_CLASH_CONTROL_PLANE_URL").strip_edges().trim_suffix("/")
|
||||
var workload_token := OS.get_environment("COSMIC_CLASH_WORKLOAD_TOKEN").strip_edges()
|
||||
var server_id := String(config.get_value("server-id"))
|
||||
var match_id := String(config.get_value("match-id"))
|
||||
if not valid_connection_report_configuration(base_url, workload_token, match_id, server_id, player_id) or _connection_reports_inflight.has(player_id) or _connection_reports_complete.has(player_id):
|
||||
return
|
||||
_connection_reports_inflight[player_id] = true
|
||||
var endpoint := "%s/v1/servers/%s/connect" % [base_url, server_id.uri_encode()]
|
||||
# A player can join many matches. Scope the durable key to this match so a
|
||||
# later valid report cannot conflict with an earlier match's stored digest.
|
||||
var idempotency_key := "server-connect-" + (match_id + "\n" + player_id).sha256_text()
|
||||
var payload := JSON.stringify({"player_id": player_id})
|
||||
for attempt in range(5):
|
||||
var request := HTTPRequest.new()
|
||||
request.timeout = 5.0
|
||||
add_child(request)
|
||||
var start_error := request.request(endpoint, [
|
||||
"Authorization: Bearer " + workload_token,
|
||||
"Content-Type: application/json",
|
||||
"Idempotency-Key: " + idempotency_key,
|
||||
], HTTPClient.METHOD_POST, payload)
|
||||
var response_code := 0
|
||||
if start_error == OK:
|
||||
var response: Array = await request.request_completed
|
||||
response_code = int(response[1])
|
||||
request.queue_free()
|
||||
if response_code == 204:
|
||||
_connection_reports_complete[player_id] = true
|
||||
_connection_reports_inflight.erase(player_id)
|
||||
ServerLog.debug("player_connection_recorded", {"player_id": player_id})
|
||||
return
|
||||
if response_code in [400, 401, 404, 409, 422]:
|
||||
break
|
||||
if attempt < 4 and is_inside_tree():
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
_connection_reports_inflight.erase(player_id)
|
||||
ServerLog.warn("player_connection_report_failed", {"player_id": player_id})
|
||||
func _on_connection_lease_reconciliation_failed(reason: String) -> void:
|
||||
# A durable/local divergence means this process can no longer prove that a
|
||||
# future generation is globally current. Preserve the live match but close
|
||||
# admission so it cannot mint additional ambiguous leases.
|
||||
MatchNet.admissions_open = false
|
||||
ServerLog.error("connection_lease_reconciliation_failed", {"reason": reason})
|
||||
|
||||
|
||||
static func valid_connection_report_configuration(base_url: String, workload_token: String, match_id: String, server_id: String, player_id: String) -> bool:
|
||||
if not (base_url.begins_with("http://") or base_url.begins_with("https://")) or base_url.contains("\n") or base_url.contains("\r") or base_url.contains("?") or base_url.contains("#"):
|
||||
return false
|
||||
if workload_token.is_empty() or workload_token.contains("\n") or workload_token.contains("\r"):
|
||||
return false
|
||||
return AssignmentState.is_valid_opaque_id(match_id) and AssignmentState.is_valid_opaque_id(server_id) and AssignmentState.is_valid_opaque_id(player_id)
|
||||
return ConnectionLeaseClientScript.valid_configuration(base_url, workload_token, match_id, server_id) and AssignmentState.is_valid_opaque_id(player_id)
|
||||
|
||||
|
||||
static func required_min_players(allocated: bool, roster_size: int, configured: int) -> int:
|
||||
|
||||
Reference in New Issue
Block a user