fix(multiplayer): reclaim slots by signed identity

This commit is contained in:
Josh Creek
2026-09-01 16:08:01 +01:00
parent bdb3c8f4a7
commit b4ea50d76a
4 changed files with 56 additions and 7 deletions
+8 -5
View File
@@ -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