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:
co-authored by
Claude Opus 4.8
parent
eb618fe9e6
commit
fcedc046cf
@@ -378,6 +378,22 @@ phase). Plan: `~/.claude/plans/partitioned-snuggling-piglet.md`.
|
||||
SEAT_REQUEST branch (btconsole.py). Verified: 8/8 stub tests (distinct seats, FULL on
|
||||
exhaustion, claim clears reserve, duplicate-HELLO refused) + 2-node localhost e2e with NO
|
||||
BT_SELF on either pod → both seated, full ladder, launch, UDP flowing.
|
||||
- **WALK-UP CALLSIGN/MECH REQUEST (2026-07-22) [T2]**: the 1995 front-desk conversation, over
|
||||
the wire. join.bat/join_lan.bat set `BT_FE_JOIN=1` and launch ZERO-ARG -> the FE menu in JOIN
|
||||
trim (btl4fe.cpp `FeJoinOnly()`: CALLSIGN edit + the 18-mech list + JOIN; screenshot-verified)
|
||||
-> `BTFeLaunchJoinRelay` relaunches with the universal join args + `BT_CALLSIGN`/`BT_MECH` env
|
||||
-> `RelayRequestSeat` sends them as the SEAT_REQUEST payload `{callsign NUL mech NUL}` (empty
|
||||
= roster defaults, wire-compatible) -> the relay validates the mech tag (the 18 certified
|
||||
codes), stores per-seat prefs, and **HOLDS egg delivery until every pod's console pad is
|
||||
connected** -- NOT until game-side registration: a pod HELLOs only after parsing the egg
|
||||
roster, so that gate DEADLOCKS (hit live, fixed; pods animate in WAITING FOR MISSION
|
||||
ASSIGNMENT during the hold) -- then rewrites the egg via eggmodel (vehicle= + rasterized
|
||||
callsign bitmaps; PySide6 absent -> vehicles still apply, bitmap fallback logged) and streams
|
||||
it to all pads. VERIFIED 2-node e2e: requested thor/vulture over roster defaults bhk1/ava1 ->
|
||||
relay held 1/2, released 2/2, rewrote both seats, LAST.EGG carries vehicle=thor/vulture +
|
||||
name=VIPER/MONGOOSE, both pods launched with `[cyl] table 'thor'/'vulture'` live. The FE
|
||||
menu->relaunch path itself: awaiting live human verification (env-injected path is what the
|
||||
e2e run proves). BT_SELF pods skip the request (defaults).
|
||||
- **PATIENT WALK-UP (2026-07-18) [T2]**: a pod dialed at a relay that isn't up yet no longer
|
||||
aborts (release `Fail()` is a bare `abort()` = looked like a "crash", field report). Both the
|
||||
seat request (`RelayRequestSeat`) and LAN discovery (`BT_RELAY=auto`) now RETRY (2s dials, 30
|
||||
|
||||
@@ -1719,10 +1719,33 @@ Logical L4NetworkManager::RelayRequestSeat()
|
||||
}
|
||||
Logical got_seat = False;
|
||||
Logical roster_full = False;
|
||||
// CALLSIGN/MECH REQUEST (2026-07-22, the walk-up desk over the wire):
|
||||
// the join menu (BT_FE_JOIN) stashes the player's picks in BT_CALLSIGN
|
||||
// / BT_MECH; they ride the seat request as {callsign NUL mech NUL} and
|
||||
// the relay writes them into the session egg before it releases eggs.
|
||||
// Empty payload (no env) = the operator's roster defaults, as before.
|
||||
char seat_payload[64];
|
||||
int seat_payload_length = 0;
|
||||
{
|
||||
const char *callsign = getenv("BT_CALLSIGN");
|
||||
const char *mech_tag = getenv("BT_MECH");
|
||||
if ((callsign != NULL && callsign[0] != '\0')
|
||||
|| (mech_tag != NULL && mech_tag[0] != '\0'))
|
||||
{
|
||||
seat_payload_length = _snprintf(seat_payload,
|
||||
sizeof(seat_payload) - 1, "%s%c%s%c",
|
||||
callsign != NULL ? callsign : "", 0,
|
||||
mech_tag != NULL ? mech_tag : "", 0);
|
||||
if (seat_payload_length < 0)
|
||||
seat_payload_length = sizeof(seat_payload) - 1;
|
||||
}
|
||||
}
|
||||
RelayTcpEnvelope request;
|
||||
request.route = RELAY_ROUTE_SEAT_REQUEST;
|
||||
request.length = 0;
|
||||
if (RelaySendAll(seat_socket, (const char *)&request, sizeof(request)))
|
||||
request.length = (unsigned int)seat_payload_length;
|
||||
if (RelaySendAll(seat_socket, (const char *)&request, sizeof(request))
|
||||
&& (seat_payload_length == 0
|
||||
|| RelaySendAll(seat_socket, seat_payload, seat_payload_length)))
|
||||
{
|
||||
char reply[sizeof(RelayTcpEnvelope) + 80];
|
||||
int have = 0;
|
||||
|
||||
@@ -457,6 +457,16 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
SetEnvironmentVariableA("BT_FE_EGG", NULL);
|
||||
sprintf(fe_arguments, "-net %d -platform glass", fe_spec.consolePort);
|
||||
break;
|
||||
case BTFeLaunchJoinRelay:
|
||||
// BT_FE_JOIN (join.bat): the callsign/mech request rides env
|
||||
// into the relaunched pod; L4NET's SEAT_REQUEST carries it to
|
||||
// the relay, which writes it into the session egg. Args = the
|
||||
// universal join contract (join.bat's own OPERATOR.EGG/1501).
|
||||
SetEnvironmentVariableA("BT_FE_EGG", NULL);
|
||||
SetEnvironmentVariableA("BT_CALLSIGN", fe_spec.joinCallsign);
|
||||
SetEnvironmentVariableA("BT_MECH", fe_spec.joinVehicle);
|
||||
strcpy(fe_arguments, "-egg OPERATOR.EGG -net 1501 -platform glass");
|
||||
break;
|
||||
#ifdef BT_STEAM
|
||||
case BTFeLaunchJoinSteam:
|
||||
SetEnvironmentVariableA("BT_FE_EGG", NULL);
|
||||
|
||||
+63
-11
@@ -588,6 +588,25 @@ static int
|
||||
return s;
|
||||
}
|
||||
|
||||
//
|
||||
// BT_FE_JOIN=1 (join.bat / join_lan.bat): the JOIN-GAME menu -- the walk-up
|
||||
// desk conversation of the 1995 centers, over the internet. Only CALLSIGN +
|
||||
// MECH are the player's to pick (the operator owns the mission + the rest of
|
||||
// the loadout); the choice rides the relay SEAT_REQUEST and the relay writes
|
||||
// it into the session egg (btconsole.py).
|
||||
//
|
||||
static int
|
||||
FeJoinOnly()
|
||||
{
|
||||
static int s = -1;
|
||||
if (s < 0)
|
||||
{
|
||||
const char *e = getenv("BT_FE_JOIN");
|
||||
s = (e != NULL && *e != '0') ? 1 : 0;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
static int
|
||||
GroupSize(int group)
|
||||
{
|
||||
@@ -719,6 +738,15 @@ static void
|
||||
{
|
||||
menu.itemCount = 0;
|
||||
int y;
|
||||
if (FeJoinOnly())
|
||||
{
|
||||
// JOIN trim: the mech list + the JOIN button; callsign is the edit
|
||||
// control (repositioned in BTFrontEnd_Run). Everything else is the
|
||||
// operator's call.
|
||||
y = MenuTopY; AddGroup(GroupVehicle, MenuCol2X, &y);
|
||||
AddButton(GroupLaunch, MenuCol5X, MenuClientH - 190, 220, 52);
|
||||
return;
|
||||
}
|
||||
// col1: the mission
|
||||
y = MenuTopY; AddGroup(GroupMap, MenuCol1X, &y);
|
||||
AddGroup(GroupScenario, MenuCol1X, &y);
|
||||
@@ -843,8 +871,10 @@ static void
|
||||
|
||||
HFONT old_font = (HFONT)SelectObject(dc, menu.titleFont);
|
||||
RECT title_rect = { MenuCol1X, 14, client.right - 24, 54 };
|
||||
DrawMenuText(dc, "BATTLETECH -- MISSION CONSOLE", &title_rect,
|
||||
kGreenBright, DT_LEFT | DT_TOP | DT_SINGLELINE);
|
||||
DrawMenuText(dc,
|
||||
FeJoinOnly() ? "BATTLETECH -- JOIN GAME"
|
||||
: "BATTLETECH -- MISSION CONSOLE",
|
||||
&title_rect, kGreenBright, DT_LEFT | DT_TOP | DT_SINGLELINE);
|
||||
|
||||
SelectObject(dc, menu.textFont);
|
||||
|
||||
@@ -896,12 +926,15 @@ static void
|
||||
// Edit labels (the controls themselves are child windows in col5).
|
||||
//
|
||||
RECT label = { MenuCol5X, MenuNameLabelY, MenuCol5X + 240, MenuNameLabelY + MenuRowH };
|
||||
DrawMenuText(dc, "PILOT NAME", &label, kGreenDim,
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
||||
label.top = MenuPeersLabelY;
|
||||
label.bottom = label.top + MenuRowH;
|
||||
DrawMenuText(dc, "LAN PEERS (host: ip:port,...)", &label, kGreenDim,
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
||||
DrawMenuText(dc, FeJoinOnly() ? "CALLSIGN" : "PILOT NAME", &label,
|
||||
kGreenDim, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
||||
if (!FeJoinOnly())
|
||||
{
|
||||
label.top = MenuPeersLabelY;
|
||||
label.bottom = label.top + MenuRowH;
|
||||
DrawMenuText(dc, "LAN PEERS (host: ip:port,...)", &label, kGreenDim,
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
||||
}
|
||||
|
||||
//
|
||||
// Buttons (framed, RP412 style).
|
||||
@@ -911,7 +944,7 @@ static void
|
||||
{
|
||||
const MenuItem &item = menu.items[i];
|
||||
const char *text =
|
||||
(item.group == GroupLaunch) ? "L A U N C H" :
|
||||
(item.group == GroupLaunch) ? (FeJoinOnly() ? "J O I N" : "L A U N C H") :
|
||||
(item.group == GroupSteamHost) ? "HOST STEAM LOBBY" :
|
||||
(item.group == GroupSteamJoin) ? "JOIN STEAM LOBBY" : NULL;
|
||||
if (text == NULL)
|
||||
@@ -926,7 +959,9 @@ static void
|
||||
DeleteObject(frame);
|
||||
|
||||
RECT help = { MenuCol1X, client.bottom - 30, client.right - 24, client.bottom - 6 };
|
||||
DrawMenuText(dc, "click to select ENTER = launch ESC = quit",
|
||||
DrawMenuText(dc,
|
||||
FeJoinOnly() ? "click to select ENTER = join ESC = quit"
|
||||
: "click to select ENTER = launch ESC = quit",
|
||||
&help, kGreenDim, DT_LEFT | DT_TOP | DT_SINGLELINE);
|
||||
|
||||
SelectObject(dc, old_font);
|
||||
@@ -1037,7 +1072,8 @@ int
|
||||
DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;
|
||||
AdjustWindowRect(&frame_rect, style, FALSE);
|
||||
menu.window = CreateWindowW(
|
||||
L"BTFrontEndWnd", L"BattleTech - Mission Console",
|
||||
L"BTFrontEndWnd",
|
||||
FeJoinOnly() ? L"BattleTech - Join Game" : L"BattleTech - Mission Console",
|
||||
style, 100, 80,
|
||||
frame_rect.right - frame_rect.left, frame_rect.bottom - frame_rect.top,
|
||||
NULL, NULL, GetModuleHandleW(NULL), NULL);
|
||||
@@ -1067,6 +1103,10 @@ int
|
||||
SendMessageW(menu.peersEdit, WM_SETFONT, (WPARAM)menu.textFont, TRUE);
|
||||
SetWindowTextA(menu.nameEdit, pilot_name);
|
||||
SetWindowTextA(menu.peersEdit, peers);
|
||||
if (FeJoinOnly())
|
||||
{
|
||||
ShowWindow(menu.peersEdit, SW_HIDE); // operator's concern, not the joiner's
|
||||
}
|
||||
|
||||
ShowWindow(menu.window, SW_SHOW);
|
||||
SetForegroundWindow(menu.window);
|
||||
@@ -1104,6 +1144,18 @@ int
|
||||
//
|
||||
// Resolve the launch spec + write the egg.
|
||||
//
|
||||
if (FeJoinOnly())
|
||||
{
|
||||
// JOIN GAME: no egg, no port math -- the relay owns the session. The
|
||||
// callsign + mech request rides the SEAT_REQUEST payload (L4NET reads
|
||||
// BT_CALLSIGN / BT_MECH, set by WinMain from this spec).
|
||||
spec->mode = BTFeLaunchJoinRelay;
|
||||
strncpy(spec->joinCallsign, pilot_name[0] ? pilot_name : "Pilot",
|
||||
sizeof(spec->joinCallsign) - 1);
|
||||
strncpy(spec->joinVehicle, kVehicles[menu.selection[GroupVehicle]].key,
|
||||
sizeof(spec->joinVehicle) - 1);
|
||||
return 0;
|
||||
}
|
||||
int console_port = kPorts[menu.selection[GroupPort]];
|
||||
const RoleEntry &scenario = kScenarios[menu.selection[GroupScenario]];
|
||||
BTFeMission mission;
|
||||
|
||||
@@ -55,6 +55,7 @@ enum BTFeLaunchMode
|
||||
BTFeLaunchRawSolo, // plain -egg endless solo (renderer work)
|
||||
BTFeLaunchHostLan, // marshal feeds self + remote pods
|
||||
BTFeLaunchJoinLan, // plain -net pod; a remote host's marshal feeds us
|
||||
BTFeLaunchJoinRelay, // BT_FE_JOIN: relay join with callsign/mech request
|
||||
BTFeLaunchHostSteam, // (BT_STEAM) lobby host: marshal over the Steam wire
|
||||
BTFeLaunchJoinSteam // (BT_STEAM) lobby member: -net pod on the Steam wire
|
||||
};
|
||||
@@ -68,6 +69,8 @@ struct BTFeLaunchSpec
|
||||
int missionSeconds;
|
||||
char steamMyToken[48]; // (BT_STEAM) my roster token ip
|
||||
char steamMap[512]; // (BT_STEAM) ip=steamid64;...
|
||||
char joinCallsign[32]; // (BT_FE_JOIN) requested callsign
|
||||
char joinVehicle[16]; // (BT_FE_JOIN) requested mech tag
|
||||
};
|
||||
|
||||
//
|
||||
|
||||
+6
-3
@@ -6,15 +6,18 @@ Do NOT run anything from inside the zip. The .bat files must sit next
|
||||
to the 'content' and 'build' folders.
|
||||
|
||||
TO JOIN AN INTERNET GAME: double-click join.bat
|
||||
(waits for the operator's session if it's not up yet)
|
||||
TO JOIN A LAN GAME: double-click join_lan.bat (operator's LAN)
|
||||
-- pick your CALLSIGN and MECH, click JOIN (your choice is sent to
|
||||
the operator; waits for the session if it's not up yet)
|
||||
TO JOIN A LAN GAME: double-click join_lan.bat (operator's LAN;
|
||||
same callsign/mech menu)
|
||||
SINGLE-PLAYER: double-click play_solo.bat -- opens the
|
||||
mission menu: pick your mech (all 18), map, experience level,
|
||||
colours, weather and mission length, then launch
|
||||
{STEAM}STEAM INTERNET PLAY: double-click play_steam.bat (EXPERIMENTAL --
|
||||
{STEAM} needs the Steam client running; host or join from the menu)
|
||||
|
||||
Your seat and mech are assigned automatically by the operator.
|
||||
Your seat is assigned automatically; you'll pilot the mech you picked
|
||||
in the join menu (the operator can override it).
|
||||
If the game closes unexpectedly, send the operator content\join.log.
|
||||
|
||||
AFTER A MULTIPLAYER SESSION: the game writes a small match report,
|
||||
|
||||
+4
-1
@@ -13,7 +13,10 @@ set BT_LOG=join.log
|
||||
set BT_START_INSIDE=1
|
||||
set BT_DEV_GAUGES=1
|
||||
cd content
|
||||
..\build\Release\btl4.exe -egg OPERATOR.EGG -net 1501
|
||||
rem JOIN-GAME menu (2026-07-22): pick your CALLSIGN + MECH, click JOIN --
|
||||
rem your choice is sent to the operator and lands in the mission roster.
|
||||
set BT_FE_JOIN=1
|
||||
..\build\Release\btl4.exe
|
||||
echo.
|
||||
echo The game has exited. If you did NOT quit on purpose
|
||||
echo (crash, or it never connected), send the operator the
|
||||
|
||||
@@ -16,7 +16,10 @@ set BT_LOG=join.log
|
||||
set BT_START_INSIDE=1
|
||||
set BT_DEV_GAUGES=1
|
||||
cd content
|
||||
..\build\Release\btl4.exe -egg OPERATOR.EGG -net 1501
|
||||
rem JOIN-GAME menu (2026-07-22): pick your CALLSIGN + MECH, click JOIN --
|
||||
rem your choice is sent to the operator and lands in the mission roster.
|
||||
set BT_FE_JOIN=1
|
||||
..\build\Release\btl4.exe
|
||||
echo.
|
||||
echo The game has exited. If you did NOT quit on purpose
|
||||
echo (crash, or it never connected), send the operator the
|
||||
|
||||
+108
-7
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user