Connection audit: every connect/disconnect scenario handled + verified
Systematic actor-death x lifecycle-phase sweep (operator request after the WaitingForEgg zombie). New handling, all live-verified: 1. PRE-EGG console-pad loss (the real zombie phase): before the egg the pod's only link is the console pad -- the game socket doesn't exist, so the earlier RelayGameDown hook could never fire (the verification run itself caught this). Detection now lives in HostDisconnectedMessageHandler ConsoleHostType (relayMode + pre-scene -> BTRelayRejoinNow); mesh keeps 1995 behavior. 2. MID-MISSION relay loss: the STOP is relay-sent (1995: the console owned the clock), so a pod losing its relay mid-match played a peer-less FOREVER-mission. RelayGameDown (scene presented) now posts StopMissionMessage at +15s -> normal end -> lobby relaunch. 3. POST-RELEASE pod death stalled the round (survivors wait forever on the dead peer's connection gate): relay _abort_round -> route -11 REJOIN to survivors -> everyone re-seats in seconds (reset-first ordering makes the drop cascade re-trigger-proof). 4. BTRelayRejoinNow carries BT_SEAT_CLAIM (mirrored tag): without it the rejoiner's own reclaim-hold blocked assignment -> ROSTER FULL loop for the 90s window (found by verification round 2). 5. Beacon NAT keepalive (SIO_KEEPALIVE_VALS 60s/10s) so long between-rounds waits can't be silently unseated. 6. BT_LOG_APPEND=1 preserves the log across lobby-loop generations (truncation erased round-1 verification evidence). Verified non-issues: relay pods never bind the -net listener (no double-launch collision on the internet path); mesh bind-fail exits cleanly. Full matrix + evidence: context/multiplayer.md; scratchpad/audit_verify.sh is the repeatable rig. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
56ca2f5c13
commit
c8cba8c764
@@ -227,6 +227,7 @@ ROUTE_SEAT_ASSIGN = -7 # relay->client: int32 hostID + NUL-terminated tag
|
||||
ROUTE_SEAT_FULL = -8 # relay->client: no free seats
|
||||
ROUTE_MATCHLOG = -9 # client->relay: matchlog upload (filename NUL + bytes)
|
||||
ROUTE_READY = -10 # client->relay: mission load complete (READY light)
|
||||
ROUTE_REJOIN = -11 # relay->client: abandon the round, rejoin (peer died mid-load)
|
||||
# the 18 certified mech tags (mirror of the FE kVehicles catalog)
|
||||
VEHICLE_TAGS = {"blkhawk", "loki", "bhk1", "madcat", "thor", "owens",
|
||||
"own1", "thr1", "lok1", "mad1", "avatar", "ava1",
|
||||
@@ -1040,6 +1041,30 @@ class Relay:
|
||||
except OSError as e:
|
||||
self._drop_game(conn, f"send failed {e!r}")
|
||||
|
||||
def _abort_round(self, why):
|
||||
"""ROUND ABORT (connection audit 2026-07-23): a registered pod died
|
||||
AFTER egg release but BEFORE launch -- the engine's all-connections
|
||||
gate would leave every survivor waiting on it forever. Bounce all
|
||||
remaining pods back to their seats (REJOIN) and reset the round; the
|
||||
reclaim window puts everyone back in the same seats in seconds."""
|
||||
if not self.eggs_released or self.launches_sent >= 2:
|
||||
return
|
||||
print(f"[relay] ROUND ABORT ({why}) -- sending every pod back to "
|
||||
f"the join wait for a fresh round", flush=True)
|
||||
# reset FIRST so the cascade of resulting drops can't re-trigger
|
||||
self.eggs_released = False
|
||||
self.launch_at = None
|
||||
self.launch_requested = False
|
||||
self.eggs_done_at = None
|
||||
self._launch_blocked_warned = False
|
||||
survivors = list(self.by_host.values())
|
||||
for conn in survivors:
|
||||
try:
|
||||
self._send_raw(conn, struct.pack(ENV_FMT_TCP, ROUTE_REJOIN, 0))
|
||||
except OSError:
|
||||
pass
|
||||
# the full state restore happens in _maybe_reset_round as they drop
|
||||
|
||||
def _drop_game(self, conn, why):
|
||||
print(f"[relay] {conn.name()} dropped: {why}", flush=True)
|
||||
try:
|
||||
@@ -1066,12 +1091,16 @@ class Relay:
|
||||
print(f"[relay] PLAYER {seat - FIRST_GAME_HOST_ID + 1} LEFT "
|
||||
f"(host {seat}) tag='{tag}' "
|
||||
f"({len(self.seat_beacons)} seated)", flush=True)
|
||||
was_registered = (conn.host_id is not None
|
||||
and self.by_host.get(conn.host_id) is conn)
|
||||
if conn.host_id is not None and self.by_host.get(conn.host_id) is conn:
|
||||
del self.by_host[conn.host_id]
|
||||
self.udp_endpoint.pop(conn.host_id, None)
|
||||
for other in list(self.by_host.values()): # snapshot: a failed
|
||||
# send drops that peer INSIDE the loop (mutates by_host)
|
||||
self._send_control(other, ROUTE_PEER_DOWN, conn.host_id)
|
||||
if was_registered:
|
||||
self._abort_round(f"host {conn.host_id} dropped mid-load")
|
||||
self._maybe_reset_round()
|
||||
|
||||
# ---------------- game UDP side ----------------
|
||||
|
||||
Reference in New Issue
Block a user