mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 20:13:43 +00:00
fix(multiplayer): reclaim slots by signed identity
This commit is contained in:
@@ -37,12 +37,14 @@ const MAX_PLAYER_NAME_LENGTH := 24
|
||||
class PlayerInfo:
|
||||
var peer_id: int
|
||||
var player_name: String
|
||||
var player_identity: String
|
||||
var team: int = 0
|
||||
var ready: bool = false
|
||||
|
||||
func _init(p_peer_id: int, p_player_name: String, p_team: int = 0, p_ready: bool = false) -> void:
|
||||
func _init(p_peer_id: int, p_player_name: String, p_team: int = 0, p_ready: bool = false, p_player_identity: String = "") -> void:
|
||||
peer_id = p_peer_id
|
||||
player_name = p_player_name
|
||||
player_identity = p_player_identity
|
||||
team = p_team
|
||||
ready = p_ready
|
||||
|
||||
@@ -117,6 +119,20 @@ func configure_join_authorisations(tokens: Array, context: Dictionary, signing_k
|
||||
return true
|
||||
|
||||
|
||||
func player_identity(peer_id: int) -> String:
|
||||
if not roster.has(peer_id):
|
||||
return ""
|
||||
return String((roster[peer_id] as PlayerInfo).player_identity)
|
||||
|
||||
|
||||
static func reservation_identity_matches(slot_identity: String, incoming_identity: String, slot_name: String, incoming_name: String) -> bool:
|
||||
# Authenticated allocations must never fall back to a client-chosen display
|
||||
# name. The name fallback exists only for direct, unauthenticated servers.
|
||||
if not slot_identity.is_empty() or not incoming_identity.is_empty():
|
||||
return not slot_identity.is_empty() and slot_identity == incoming_identity
|
||||
return slot_name == incoming_name
|
||||
|
||||
|
||||
# Server only: a raw ENet disconnect (crash, timeout) that never sent a
|
||||
# proper hello just needs its (possibly absent) roster entry cleaned up.
|
||||
# The normal leave path also goes through here after the server erases it,
|
||||
@@ -219,6 +235,10 @@ func _hello(protocol_version: int, tick_hz: int, player_name: String, supplied_j
|
||||
await _reject(peer_id, "player name too long")
|
||||
return
|
||||
var clean_name := _sanitize_player_name(player_name)
|
||||
var identity := _join_identity(supplied_join_authorisation) if require_join_authorisation else clean_name
|
||||
if identity.is_empty():
|
||||
await _reject(peer_id, "join authorisation rejected")
|
||||
return
|
||||
|
||||
# Tell the new peer about everyone already here before anyone is told
|
||||
# about them, so no client ever observes an unknown peer_id in a
|
||||
@@ -228,7 +248,7 @@ func _hello(protocol_version: int, tick_hz: int, player_name: String, supplied_j
|
||||
_player_joined.rpc_id(peer_id, existing_id, existing.player_name, existing.team, existing.ready)
|
||||
|
||||
var team := _pick_balanced_team()
|
||||
roster[peer_id] = PlayerInfo.new(peer_id, clean_name, team, false)
|
||||
roster[peer_id] = PlayerInfo.new(peer_id, clean_name, team, false, identity)
|
||||
if require_join_authorisation:
|
||||
# _reserve_join_authorisation already owns the active peer reservation;
|
||||
# keeping the generation in the history makes fencing auditable without
|
||||
@@ -254,6 +274,8 @@ func _valid_join_authorisation(token: String) -> bool:
|
||||
var claims = envelope["Authorisation"]
|
||||
if not claims is Dictionary:
|
||||
return false
|
||||
if str(claims.get("PlayerID", "")).is_empty():
|
||||
return false
|
||||
var protocol := str(claims.get("Protocol", ""))
|
||||
var expires_at := str(claims.get("ExpiresAt", ""))
|
||||
var expiry := Time.get_unix_time_from_datetime_string(expires_at)
|
||||
@@ -285,6 +307,21 @@ func _valid_join_authorisation(token: String) -> bool:
|
||||
and expiry > Time.get_unix_time_from_system()
|
||||
|
||||
|
||||
func _join_identity(token: String) -> String:
|
||||
if token.is_empty():
|
||||
return ""
|
||||
var standard_token := token.replace("-", "+").replace("_", "/")
|
||||
while standard_token.length() % 4 != 0:
|
||||
standard_token += "="
|
||||
var decoded := Marshalls.base64_to_raw(standard_token)
|
||||
if decoded.is_empty():
|
||||
return ""
|
||||
var envelope = JSON.parse_string(decoded.get_string_from_utf8())
|
||||
if not envelope is Dictionary or not envelope.has("Authorisation") or not envelope["Authorisation"] is Dictionary:
|
||||
return ""
|
||||
return str(envelope["Authorisation"].get("PlayerID", ""))
|
||||
|
||||
|
||||
func is_join_authorisation_active(token: String) -> bool:
|
||||
return not token.is_empty() and _active_join_peers.has(token)
|
||||
|
||||
|
||||
@@ -124,7 +124,8 @@ class SlotInfo:
|
||||
# §6.4 (tasks 5.6/5.7). A ship is NEVER despawned on disconnect — the slot
|
||||
# keeps its ship and swaps the controller, so body order (and therefore
|
||||
# every snapshot index) stays stable for the whole match.
|
||||
var player_name := "" # identity key for reconnect; peer_id changes across a reconnect
|
||||
var player_name := "" # display name only; never authoritative for allocated reclaim
|
||||
var player_identity := "" # signed allocation identity; peer_id changes across a reconnect
|
||||
var disconnected := false
|
||||
var reserved_until_tick := -1 # server only: slot held for this player until here
|
||||
var interpolator := NetInterpolator.new() # client only
|
||||
@@ -461,6 +462,7 @@ func _start_server() -> void:
|
||||
slot.team = info.team
|
||||
slot.spawn_index = spawn_index
|
||||
slot.player_name = info.player_name
|
||||
slot.player_identity = MatchNet.player_identity(peer_id)
|
||||
slot.controller = RLShipController.new()
|
||||
slot.ship = spawn_ship(info.team, spawn_index, slot.controller)
|
||||
_slots.append(slot)
|
||||
@@ -1206,12 +1208,12 @@ func _build_takeover_controller() -> ShipController:
|
||||
|
||||
# Called when a peer joins while this match is already running. Returns true if
|
||||
# it reclaimed a reserved slot (§6.4's 30s identity-keyed reservation).
|
||||
func _try_reclaim_slot(peer_id: int, player_name: String) -> bool:
|
||||
func _try_reclaim_slot(peer_id: int, player_identity: String, player_name: String) -> bool:
|
||||
if not multiplayer.is_server():
|
||||
return false
|
||||
var now := Engine.get_physics_frames()
|
||||
for slot in _slots:
|
||||
if not slot.disconnected or slot.player_name == "" or slot.player_name != player_name:
|
||||
if not slot.disconnected or slot.player_name == "" or not MatchNet.reservation_identity_matches(slot.player_identity, player_identity, slot.player_name, player_name):
|
||||
continue
|
||||
if slot.reserved_until_tick >= 0 and now > slot.reserved_until_tick:
|
||||
continue # reservation lapsed; this is a fresh joiner, not a return
|
||||
@@ -1246,7 +1248,7 @@ func _try_reclaim_slot(peer_id: int, player_name: String) -> bool:
|
||||
func _on_player_joined_midmatch(peer_id: int, player_name: String) -> void:
|
||||
if not multiplayer.is_server() or _slots.is_empty():
|
||||
return
|
||||
if _try_reclaim_slot(peer_id, player_name):
|
||||
if _try_reclaim_slot(peer_id, MatchNet.player_identity(peer_id), player_name):
|
||||
return
|
||||
if _max_spectators >= 0 and _spectator_count() > _max_spectators:
|
||||
print("NetworkedMatch: spectator cap (%d) reached, disconnecting peer %d" % [_max_spectators, peer_id])
|
||||
@@ -1262,7 +1264,7 @@ func _on_player_joined_midmatch(peer_id: int, player_name: String) -> void:
|
||||
# in _promote_late_joiners(). Queued in arrival order and consumed from the
|
||||
# front, so waiting is first-come-first-served rather than whichever slot
|
||||
# index happens to free up first.
|
||||
_late_joiners.append({"peer_id": peer_id, "player_name": player_name})
|
||||
_late_joiners.append({"peer_id": peer_id, "player_name": player_name, "player_identity": MatchNet.player_identity(peer_id)})
|
||||
print("NetworkedMatch: peer %d (%s) joined mid-match; spectating until the next kickoff" % [peer_id, player_name])
|
||||
|
||||
|
||||
@@ -1302,6 +1304,7 @@ func _promote_late_joiners() -> void:
|
||||
var joiner_peer := int(joiner["peer_id"])
|
||||
slot.peer_id = joiner_peer
|
||||
slot.player_name = String(joiner["player_name"])
|
||||
slot.player_identity = String(joiner.get("player_identity", ""))
|
||||
slot.disconnected = false
|
||||
slot.reserved_until_tick = -1
|
||||
# Same reasoning as the reclaim path: the arriving client numbers its
|
||||
|
||||
Reference in New Issue
Block a user