Walk-up callsign/mech request: join menu -> relay -> the session egg

The 1995 front-desk conversation, over the internet.  join.bat/
join_lan.bat now open a JOIN-GAME menu (the FE in BT_FE_JOIN trim:
CALLSIGN edit + the 18-mech list + JOIN); the choice relaunches into the
normal join with BT_CALLSIGN/BT_MECH env, rides the relay SEAT_REQUEST
as {callsign NUL mech NUL} (empty payload = roster defaults, wire-
compatible), and the relay validates the mech tag, HOLDS egg delivery
until every pod's console pad is connected, rewrites the egg via
eggmodel (vehicle= + rasterized callsign bitmaps, graceful no-PySide6
fallback) and streams it to all pads -- so every player's egg copy
carries every player's callsign.

The hold is gated on CONSOLE-PAD count, not game-side registration: a
pod HELLOs only after parsing the egg's roster, so a registration gate
deadlocks (hit live in the first run; pods animate in WAITING FOR
MISSION ASSIGNMENT during the hold).

Verified 2-node e2e (env-injected requests): thor/vulture requested over
roster defaults bhk1/ava1 -> relay held 1/2, released at 2/2, rewrote
both seats (relay log), the delivered egg carries vehicle=thor/vulture +
name=VIPER/MONGOOSE, both pods launched and built [cyl] tables for both
requested mechs.  Join-menu layout screenshot-verified; the menu's
click-through relaunch awaits live human verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-22 13:58:51 -05:00
co-authored by Claude Opus 4.8
parent eb618fe9e6
commit fcedc046cf
9 changed files with 239 additions and 25 deletions
+108 -7
View File
@@ -61,6 +61,12 @@ BT_SELF=<their [pilots] entry>.
import os
import random
import selectors
# player callsign/mech seat requests mutate the session egg via eggmodel;
# import guarded so a stripped install still relays with roster defaults.
try:
import eggmodel
except ImportError:
eggmodel = None
import socket
import struct
import sys
@@ -220,6 +226,10 @@ ROUTE_SEAT_REQUEST = -6 # client->relay: assign me a free roster seat
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)
# 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",
"sunder", "snd1", "vulture", "vul1", "lok2", "mad2"}
MATCHLOG_CAP = 8 * 1024 * 1024 # matchlog upload frame cap (client caps at 8MB)
SEAT_RESERVE_SECONDS = 60.0
HELLO_MAGIC = 0x31525442 # 'BTR1' little-endian
@@ -294,6 +304,15 @@ class Relay:
self.stop_sent = False
self.eggs_done_at = None
self.egg_bytes = egg_wire_bytes(open(egg_path, "rb").read())
self.egg_path = egg_path
# WALK-UP SEAT PREFS (2026-07-22): callsign/mech requested in the
# SEAT_REQUEST payload, keyed by assigned host id. Eggs are HELD
# until the roster completes so every pod's egg carries every
# player's callsign (an early pod's egg would otherwise miss late
# joiners' labels); pods animate in WAITING FOR MISSION ASSIGNMENT
# meanwhile.
self.seat_prefs = {}
self.eggs_released = False
self.roster = parse_egg_roster(egg_path)
self.expected_ids = set(range(FIRST_GAME_HOST_ID,
FIRST_GAME_HOST_ID + len(self.roster)))
@@ -389,13 +408,27 @@ class Relay:
conn = RelayConsoleConn(sock, addr)
self.console_conns.append(conn)
self.sel.register(sock, selectors.EVENT_READ, ("console", conn))
# Stream the egg immediately (blocking sendall is fine: the pod's console
# pad drains fast and the egg is a few KB).
sock.setblocking(True)
# Egg delivery: HELD until every pod's console pad is connected
# (walk-up seat prefs must be in every pod's egg copy). NOT gated on
# game-side registration -- a pod HELLOs only after it parses the
# egg's roster, so that gate would deadlock. Seat requests precede
# the console pad, so by the Nth pad every pref is already in.
if self.eggs_released:
self._send_egg(conn)
elif len(self.console_conns) >= len(self.roster):
self._release_eggs()
else:
print(f"[relay] console conn {addr[0]}:{addr[1]}: egg HELD "
f"({len(self.console_conns)}/{len(self.roster)} pods "
f"present)", flush=True)
def _send_egg(self, conn):
addr = conn.sock.getpeername()
conn.sock.setblocking(True)
try:
n = 0
for pkt in egg_packets(self.egg_bytes):
sock.sendall(pkt)
conn.sock.sendall(pkt)
n += 1
conn.egg_sent = True
print(f"[relay] console conn {addr[0]}:{addr[1]}: egg sent "
@@ -407,10 +440,60 @@ class Relay:
return
finally:
try:
sock.setblocking(False)
conn.sock.setblocking(False)
except OSError:
pass
def _release_eggs(self):
if self.eggs_released:
return
self.eggs_released = True
if self.seat_prefs:
self._apply_seat_prefs()
for conn in list(self.console_conns):
if not conn.egg_sent:
self._send_egg(conn)
def _apply_seat_prefs(self):
# Rewrite vehicle= + the callsign system from the walk-up prefs.
# eggmodel's rasterizer needs PySide6 (present when launched from the
# operator app); without it the vehicle still applies and the
# callsign falls back to the text name= (labels keep the roster
# default bitmaps).
if eggmodel is None:
print("[relay] eggmodel unavailable -- seat prefs NOT applied",
flush=True)
return
try:
doc = eggmodel.EggDoc.load(self.egg_path)
pilots = doc.pilots()
names = [p.get("name") or p["address"] for p in pilots]
for host_id, (callsign, mech) in sorted(self.seat_prefs.items()):
i = host_id - FIRST_GAME_HOST_ID
if not (0 <= i < len(pilots)):
continue
if mech:
doc.set_kv(pilots[i]["address"], "vehicle", mech)
if callsign:
names[i] = callsign
try:
doc.set_callsigns(eggmodel.make_callsigns(names))
except Exception as e:
print(f"[relay] callsign rasterizer unavailable ({e!r}) -- "
f"vehicles applied, callsign bitmaps unchanged",
flush=True)
self.egg_bytes = egg_wire_bytes(
doc.emit().encode("latin-1", "replace"))
for host_id, (callsign, mech) in sorted(self.seat_prefs.items()):
i = host_id - FIRST_GAME_HOST_ID
if 0 <= i < len(pilots):
print(f"[relay] seat {host_id} egg: "
f"vehicle={mech or pilots[i].get('vehicle')} "
f"callsign={callsign or names[i]!r}", flush=True)
except Exception as e:
print(f"[relay] seat-pref egg rewrite FAILED ({e!r}) -- "
f"serving the original egg", 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)."""
@@ -665,12 +748,28 @@ class Relay:
return
host_id, tag = assigned
self.seat_reservations[host_id] = now + SEAT_RESERVE_SECONDS
# WALK-UP PREFS: optional payload {callsign NUL mech NUL}
pref_note = ""
if payload:
parts = payload.split(b"\0")
callsign = parts[0].decode("ascii", "replace").strip()[:15]
mech = (parts[1].decode("ascii", "replace").strip().lower()
if len(parts) > 1 else "")
callsign = "".join(c for c in callsign
if c.isalnum() or c in " -_.")
if mech and mech not in VEHICLE_TAGS:
print(f"[relay] {conn.name()} requested unknown mech "
f"{mech!r} -- keeping the roster default", flush=True)
mech = ""
if callsign or mech:
self.seat_prefs[host_id] = (callsign, mech)
pref_note = f" callsign={callsign!r} mech={mech or '(default)'}"
payload_out = struct.pack("<i", host_id) + tag.encode() + b"\0"
self._send_raw(conn, struct.pack(ENV_FMT_TCP, ROUTE_SEAT_ASSIGN,
len(payload_out)) + payload_out)
print(f"[relay] {conn.name()} seat request -> assigned host "
f"{host_id} '{tag}' (reserved {SEAT_RESERVE_SECONDS:.0f}s)",
flush=True)
f"{host_id} '{tag}' (reserved {SEAT_RESERVE_SECONDS:.0f}s)"
f"{pref_note}", flush=True)
return
if conn.host_id is None:
# First frame MUST be a valid HELLO.
@@ -694,6 +793,8 @@ class Relay:
self.seat_reservations.pop(host_id, None) # claim clears reserve
print(f"[relay] {conn.name()} REGISTERED "
f"({len(self.by_host)}/{len(self.roster)})", flush=True)
if len(self.by_host) >= len(self.roster):
self._release_eggs() # fallback; normally already fired
# PEER_UP exchange: newcomer learns everyone; everyone learns newcomer.
for other_id in sorted(self.by_host):
if other_id != host_id: