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
+10
View File
@@ -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
View File
@@ -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;
+3
View File
@@ -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
};
//