mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-15 02:52:12 +00:00
feat(multiplayer): bind admissions to durable leases
This commit is contained in:
@@ -66,6 +66,8 @@ var _active_join_peers: Dictionary = {} # opaque authorisation -> peer_id
|
||||
var _join_history: Dictionary = {} # token -> {generation, lost_at}
|
||||
var _join_authorisation_context: Dictionary = {}
|
||||
var _join_signing_key := PackedByteArray()
|
||||
var _connection_lease_claim := Callable()
|
||||
var _connection_lease_disconnect := Callable()
|
||||
|
||||
# Test hook (tests/match_net_smoke.gd): set false before connecting to
|
||||
# suppress the automatic real hello, so a test can send a deliberately
|
||||
@@ -105,6 +107,8 @@ func _on_shutting_down() -> void:
|
||||
_join_history.clear()
|
||||
_join_authorisation_context.clear()
|
||||
_join_signing_key = PackedByteArray()
|
||||
_connection_lease_claim = Callable()
|
||||
_connection_lease_disconnect = Callable()
|
||||
require_join_authorisation = false
|
||||
admissions_open = true
|
||||
|
||||
@@ -184,6 +188,8 @@ func _remove_player(peer_id: int) -> void:
|
||||
var history: Dictionary = _join_history.get(token, {})
|
||||
history["lost_at"] = Time.get_unix_time_from_system()
|
||||
_join_history[token] = history
|
||||
if _connection_lease_disconnect.is_valid():
|
||||
_connection_lease_disconnect.call(_join_identity(token), int(history.get("generation", 0)))
|
||||
break
|
||||
if not roster.has(peer_id):
|
||||
return
|
||||
@@ -286,9 +292,9 @@ func _hello(protocol_version: int, tick_hz: int, player_name: String, supplied_j
|
||||
return
|
||||
var join_generation := 1
|
||||
if require_join_authorisation:
|
||||
join_generation = _reserve_join_authorisation(supplied_join_authorisation, peer_id)
|
||||
join_generation = await _claim_join_authorisation(supplied_join_authorisation, peer_id)
|
||||
if join_generation < 0:
|
||||
await _reject(peer_id, "join authorisation reclaim expired")
|
||||
await _reject(peer_id, "join authorisation lease rejected")
|
||||
return
|
||||
if player_name.length() > MAX_INPUT_LENGTH:
|
||||
await _reject(peer_id, "player name too long")
|
||||
@@ -432,16 +438,68 @@ func is_join_authorisation_active(token: String) -> bool:
|
||||
return not token.is_empty() and _active_join_peers.has(token)
|
||||
|
||||
|
||||
func _reserve_join_authorisation(token: String, peer_id: int) -> int:
|
||||
func configure_connection_lease_callbacks(claim: Callable, disconnect: Callable) -> void:
|
||||
_connection_lease_claim = claim
|
||||
_connection_lease_disconnect = disconnect
|
||||
|
||||
|
||||
func _claim_join_authorisation(token: String, peer_id: int) -> int:
|
||||
var expected_generation := _available_join_generation(token)
|
||||
if expected_generation < 0:
|
||||
return -1
|
||||
var generation := expected_generation + 1
|
||||
if _connection_lease_claim.is_valid():
|
||||
var response = await _connection_lease_claim.call(_join_identity(token), expected_generation)
|
||||
generation = lease_claim_generation(response, expected_generation)
|
||||
if generation < 0:
|
||||
return -1
|
||||
# The await above deliberately allows one bounded control-plane request.
|
||||
# Re-evaluate every local fact that can change during that suspension before
|
||||
# publishing the reservation. If a durable claim succeeded, close it again.
|
||||
# A concurrent same-token hello can receive the same idempotent claim; its
|
||||
# loser must not close the generation now owned by the local winner.
|
||||
if _active_join_peers.has(token):
|
||||
return -1
|
||||
if not admissions_open or not _valid_join_authorisation(token) or peer_id not in multiplayer.get_peers():
|
||||
if _connection_lease_disconnect.is_valid():
|
||||
_connection_lease_disconnect.call(_join_identity(token), generation)
|
||||
return -1
|
||||
_join_history[token] = {"generation": generation, "lost_at": 0.0}
|
||||
_active_join_peers[token] = peer_id
|
||||
return generation
|
||||
|
||||
|
||||
func _available_join_generation(token: String) -> int:
|
||||
if token.is_empty() or _active_join_peers.has(token):
|
||||
return -1
|
||||
var now := Time.get_unix_time_from_system()
|
||||
var history: Dictionary = _join_history.get(token, {})
|
||||
var lost_at := float(history.get("lost_at", 0.0))
|
||||
if lost_at > 0.0:
|
||||
if now < lost_at or now - lost_at > RECONNECT_GRACE_SECONDS:
|
||||
return -1
|
||||
var generation := int(history.get("generation", 0)) + 1
|
||||
if lost_at > 0.0 and (now < lost_at or now - lost_at > RECONNECT_GRACE_SECONDS):
|
||||
return -1
|
||||
return int(history.get("generation", 0))
|
||||
|
||||
|
||||
static func lease_claim_generation(response, expected_generation: int) -> int:
|
||||
if not response is Dictionary or expected_generation < 0:
|
||||
return -1
|
||||
var status := String(response.get("status", ""))
|
||||
if status not in ["claimed", "unavailable"] or not response.get("generation") is int:
|
||||
return -1
|
||||
var generation := int(response["generation"])
|
||||
if status == "unavailable":
|
||||
return generation if expected_generation > 0 and generation == expected_generation + 1 else -1
|
||||
# A durable backend may return a later generation only to a fresh process
|
||||
# recovering an already-disconnected lease. Locally known generations never
|
||||
# skip, and outage fallback never invents a jump.
|
||||
return generation if generation == expected_generation + 1 or (expected_generation == 0 and generation > 1) else -1
|
||||
|
||||
|
||||
func _reserve_join_authorisation(token: String, peer_id: int) -> int:
|
||||
var expected_generation := _available_join_generation(token)
|
||||
if expected_generation < 0:
|
||||
return -1
|
||||
var generation := expected_generation + 1
|
||||
_join_history[token] = {"generation": generation, "lost_at": 0.0}
|
||||
_active_join_peers[token] = peer_id
|
||||
return generation
|
||||
|
||||
Reference in New Issue
Block a user