Round reset + device-creation retry + load-time pump (rejoin trilogy)

1. ROUND RESET (the "picked Hellbringer, got a Blackhawk" field bug):
   trim/prefs finalize the session egg for ONE round, and that was
   permanent for the relay's lifetime -- a rejoin after a mission or
   crash got the stale finalized egg and its new callsign/mech request
   was recorded but never applied.  _maybe_reset_round() (both drop
   paths) restores the original egg/roster/launch state once every pod
   is gone; live beacons re-key by tag position (conn.seat_tag).
   Verified two-round e2e: blkhawk mission -> exit -> RESET -> lok1
   request -> egg rewritten -> the Hellbringer spawned.

2. DEVICE-CREATION RETRY: CreateDevice(HARDWARE_VERTEXPROCESSING) fails
   transiently when a just-exited instance still holds the adapter
   (caught live; also the parked "exit 139" join crash).  The old
   double-failure path called PostQuitMessage(1) -- which does NOT stop
   execution -- and the NULL mDevice deref right after was the real
   crash.  Now: HW->SW retry with 500ms backoff and a 20s deadline,
   then a clean error box + ExitProcess.

3. LOAD-TIME PUMP: mission load is thousands of back-to-back
   synchronous model loads; the starved message pump ghosted the window
   "Not Responding" mid-load.  BTLoadPump() (hooked from
   d3d_OBJECT::LoadObject) pumps + repaints the wait screen at most
   every 150ms pre-run; no-op once the scene is live.

Also: round-reset originals captured after the roster parse (the first
cut crashed the relay constructor -- caught by the two-round test).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-22 18:57:50 -05:00
co-authored by Claude Opus 4.8
parent 638cfca18d
commit 3a9b35fc06
4 changed files with 134 additions and 7 deletions
+51
View File
@@ -313,6 +313,13 @@ class Relay:
# meanwhile.
self.seat_prefs = {}
self.eggs_released = False
# ROUND RESET originals (2026-07-22): trim/prefs FINALIZE the egg for
# one round; when every pod leaves, the round resets to these so the
# NEXT round's joins hold/rewrite a FRESH egg. (Field report: a
# rejoin after a crash got the stale finalized egg -- the new mech
# request was recorded but never applied.)
self.orig_egg_path = egg_path
self.orig_egg_bytes = self.egg_bytes
# PRESENCE BEACONS (2026-07-22, "launch with whoever connects"): the
# pod KEEPS its seat-request TCP connection open until it exits; a
# live beacon = a seated player. A beacon dropping before the seat
@@ -320,6 +327,7 @@ class Relay:
# LAUNCH trims the roster to the seats present.
self.seat_beacons = {}
self.roster = parse_egg_roster(egg_path)
self.orig_roster = list(self.roster) # round-reset restore point
self.expected_ids = set(range(FIRST_GAME_HOST_ID,
FIRST_GAME_HOST_ID + len(self.roster)))
# OPERATOR-RESERVED SEATS: roster seats the operator flies LOCALLY (with
@@ -545,6 +553,46 @@ class Relay:
print(f"[relay] seat-pref egg rewrite FAILED ({e!r}) -- "
f"serving the original egg", flush=True)
def _maybe_reset_round(self):
# All pods gone from a FINALIZED round -> restore the template so the
# next round's joins are honored (held egg, fresh prefs/trim). Live
# beacons (players already waiting for the next round) are re-keyed
# by their TAG's position in the restored roster.
if not self.eggs_released:
return
if self.by_host or any(c.acked for c in self.console_conns):
return
self.eggs_released = False
self.egg_path = self.orig_egg_path
self.egg_bytes = self.orig_egg_bytes
self.roster = list(self.orig_roster)
self.expected_ids = set(range(FIRST_GAME_HOST_ID,
FIRST_GAME_HOST_ID + len(self.roster)))
self.launches_sent = 0
self.launch_at = None
self.launch_requested = False
self.eggs_done_at = None
self._launch_blocked_warned = False
self.stop_sent = False
self.stop_requested = False
self.mission_started_at = None
old_beacons = self.seat_beacons
old_prefs = self.seat_prefs
self.seat_beacons = {}
self.seat_prefs = {}
self.seat_reservations = {}
for old_id, conn in old_beacons.items():
tag = getattr(conn, "seat_tag", None)
if tag in self.roster:
new_id = FIRST_GAME_HOST_ID + self.roster.index(tag)
conn.seat_host = new_id
self.seat_beacons[new_id] = conn
if old_id in old_prefs:
self.seat_prefs[new_id] = old_prefs[old_id]
print(f"[relay] round RESET -- roster/egg restored "
f"({len(self.seat_beacons)} player(s) already waiting)",
flush=True)
def _pods_ready(self):
"""REAL pods = console connections that ACKED the egg (scanners never
speak the protocol, so they can't hold a roster slot)."""
@@ -620,6 +668,7 @@ class Relay:
pass
if conn in self.console_conns:
self.console_conns.remove(conn)
self._maybe_reset_round()
def _log_launch_readiness(self):
# PRE-LAUNCH READINESS (2026-07-18): make the "launching short" footgun
@@ -834,6 +883,7 @@ class Relay:
self._send_raw(conn, struct.pack(ENV_FMT_TCP, ROUTE_SEAT_ASSIGN,
len(payload_out)) + payload_out)
conn.seat_host = host_id # this conn IS the beacon now
conn.seat_tag = tag # tag survives a round reset
self.seat_beacons[host_id] = conn
print(f"[relay] {conn.name()} seat request -> assigned host "
f"{host_id} '{tag}' (reserved {SEAT_RESERVE_SECONDS:.0f}s)"
@@ -929,6 +979,7 @@ class Relay:
self.udp_endpoint.pop(conn.host_id, None)
for other in self.by_host.values():
self._send_control(other, ROUTE_PEER_DOWN, conn.host_id)
self._maybe_reset_round()
# ---------------- game UDP side ----------------