Patient walk-up: a pod at a dead relay WAITS for the session

User request after the first internet join attempt: 'should we modify
the program so it sits and retries with a waiting-for-session?'  The
arcade model is a pod that waits for the operator, not one that
aborts.

- RelayRequestSeat: retry loop (2s dials, 30 min cap) with live status
  into the join.bat console window (BTRelayWaitStatus: AttachConsole to
  the parent cmd; silent without one) -- banner + progress dots, a
  distinct 'game is FULL, waiting for a free seat' state, and 'seat
  assigned -- joining!' on success.  The unreachable MessageBox now
  only fires at the 30-minute give-up.
- BT_RELAY=auto discovery: same patient loop ('searching for a game on
  this network...'), LAN-only guidance in the give-up box.

Verified live: pod launched against a dead relay, waited 20+s, relay
started, pod seat-requested/registered/UDP-up automatically with no
user action.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-18 15:04:46 -05:00
co-authored by Claude Fable 5
parent 52ce7c58c8
commit f9f230c62b
+171 -69
View File
@@ -107,6 +107,31 @@ struct RelayUdpEnvelope
unsigned int sequence;
};
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// BTRelayWaitStatus -- progress text into the join.bat window during the
// patient walk-up wait. The exe is a GUI app with no window yet at seat-
// request time; attach to the parent cmd console so the player can see what
// the wait is doing. Quiet no-op without a parent console (double-clicked
// exe).
//
void BTRelayWaitStatus(const char *text)
{
static HANDLE s_out = INVALID_HANDLE_VALUE;
static int s_tried = 0;
if (!s_tried)
{
s_tried = 1;
AttachConsole(ATTACH_PARENT_PROCESS);
s_out = CreateFileA("CONOUT$", GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
}
if (s_out != INVALID_HANDLE_VALUE)
{
DWORD written = 0;
WriteFile(s_out, text, (DWORD)strlen(text), &written, NULL);
}
}
char GlobalEggFileName[80]; // NASTY HACK !!! should suffice till we get console up though
//struct RMREG //real mode registers structure
@@ -352,20 +377,48 @@ L4NetworkManager::L4NetworkManager():
// Zero-config for anyone on the operator's LAN (the 90s
// arcade model: cabinets find the operator station).
//
if (!RelayDiscover(&relayConsoleAddress))
//
// PATIENT WALK-UP: keep probing until the operator starts
// the session (status in the join_lan.bat window), 30 min cap.
//
{
// Release Fail() is a bare abort() -- without this box a
// player just sees a "crash" (field report 2026-07-18)
MessageBoxA(NULL,
"No game server answered on this network.\n\n"
"join_lan.bat only works on the SAME network as the\n"
"operator's machine. Joining over the internet?\n"
"Use join.bat instead.\n\n"
"On the operator's own network: make sure the\n"
"operator has pressed Start Session, then try again.",
"BattleTech -- can't find the game server",
MB_OK | MB_ICONERROR);
Fail("BT_RELAY=auto: no relay answered on this LAN\n");
extern void BTRelayWaitStatus(const char *); // below
Logical found = False;
unsigned long give_up =
GetTickCount() + 30UL * 60UL * 1000UL;
int shown = 0;
while (GetTickCount() < give_up)
{
if (RelayDiscover(&relayConsoleAddress))
{
found = True;
break;
}
if (!shown)
{
BTRelayWaitStatus(
"\nSearching for a game on this network ...\n"
"(joins automatically when the operator "
"starts the session;\n close this window to "
"give up. Joining over the INTERNET? Use "
"join.bat.)\n");
shown = 1;
}
BTRelayWaitStatus(".");
}
if (!found)
{
// Release Fail() is a bare abort() -- without this
// box a player just sees a "crash"
MessageBoxA(NULL,
"No game server answered on this network.\n\n"
"join_lan.bat only works on the SAME network as\n"
"the operator's machine. Joining over the\n"
"internet? Use join.bat instead.",
"BattleTech -- can't find the game server",
MB_OK | MB_ICONERROR);
Fail("BT_RELAY=auto: no relay answered on this LAN\n");
}
}
}
else
@@ -1384,72 +1437,121 @@ void L4NetworkManager::HostDisconnectedMessageHandler(HostDisconnectedMessage* H
//
Logical L4NetworkManager::RelayRequestSeat()
{
SOCKET seat_socket = RelayDialTcp(relayGameAddress, 60);
if (seat_socket == INVALID_SOCKET)
//
// PATIENT WALK-UP (user request 2026-07-18): one bounded dial + abort
// read as "it just unceremoniously crashes" when the operator had not
// started the session yet. The arcade model is a pod that WAITS: keep
// dialing with visible status until the session appears or a seat frees
// up; the player gives up by closing the join.bat window. 30 min cap
// so an abandoned window cannot camp a future session's seat forever.
//
char banner[256];
sprintf(banner,
"\nWaiting for the operator's session at %s:%d ...\n"
"(this joins automatically when the session starts;\n"
" close this window to give up)\n",
inet_ntoa(relayGameAddress.sin_addr),
(int)ntohs(relayGameAddress.sin_port) - 1);
unsigned long give_up = GetTickCount() + 30UL * 60UL * 1000UL;
int banner_shown = 0, full_shown = 0, dots = 0;
while (GetTickCount() < give_up)
{
return False;
}
Logical got_seat = False;
RelayTcpEnvelope request;
request.route = RELAY_ROUTE_SEAT_REQUEST;
request.length = 0;
if (RelaySendAll(seat_socket, (const char *)&request, sizeof(request)))
{
char reply[sizeof(RelayTcpEnvelope) + 80];
int have = 0;
unsigned long deadline = GetTickCount() + 10000UL;
while (GetTickCount() < deadline && !got_seat)
SOCKET seat_socket = RelayDialTcp(relayGameAddress, 2);
if (seat_socket == INVALID_SOCKET)
{
fd_set read_set;
FD_ZERO(&read_set);
FD_SET(seat_socket, &read_set);
timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 200000;
if (select(0, &read_set, NULL, NULL, &tv) != 1)
if (!banner_shown)
{
continue;
BTRelayWaitStatus(banner);
banner_shown = 1;
}
int received = recv(seat_socket, reply + have,
(int)sizeof(reply) - have, 0);
if (received <= 0)
BTRelayWaitStatus(".");
if ((++dots % 30) == 0)
{
break;
BTRelayWaitStatus("\n");
}
have += received;
if (have < (int)sizeof(RelayTcpEnvelope))
Sleep(2000);
continue;
}
Logical got_seat = False;
Logical roster_full = False;
RelayTcpEnvelope request;
request.route = RELAY_ROUTE_SEAT_REQUEST;
request.length = 0;
if (RelaySendAll(seat_socket, (const char *)&request, sizeof(request)))
{
char reply[sizeof(RelayTcpEnvelope) + 80];
int have = 0;
unsigned long deadline = GetTickCount() + 10000UL;
while (GetTickCount() < deadline && !got_seat)
{
continue;
}
RelayTcpEnvelope *envelope = (RelayTcpEnvelope *)reply;
if (envelope->route == RELAY_ROUTE_SEAT_FULL)
{
DEBUG_STREAM << "[relay] seat request: ROSTER FULL\n"
<< std::flush;
break;
}
if (envelope->route != RELAY_ROUTE_SEAT_ASSIGN)
{
break;
}
if (have < (int)(sizeof(RelayTcpEnvelope) + envelope->length))
{
continue; // partial payload; keep reading
}
const char *tag = reply + sizeof(RelayTcpEnvelope) + 4;
int tag_max = (int)envelope->length - 4;
if (tag_max > 0 && tag_max < (int)sizeof(relaySelf))
{
memcpy(relaySelf, tag, tag_max);
relaySelf[tag_max] = '\0';
DEBUG_STREAM << "[relay] seat ASSIGNED: '" << relaySelf
<< "'\n" << std::flush;
got_seat = True;
fd_set read_set;
FD_ZERO(&read_set);
FD_SET(seat_socket, &read_set);
timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 200000;
if (select(0, &read_set, NULL, NULL, &tv) != 1)
{
continue;
}
int received = recv(seat_socket, reply + have,
(int)sizeof(reply) - have, 0);
if (received <= 0)
{
break;
}
have += received;
if (have < (int)sizeof(RelayTcpEnvelope))
{
continue;
}
RelayTcpEnvelope *envelope = (RelayTcpEnvelope *)reply;
if (envelope->route == RELAY_ROUTE_SEAT_FULL)
{
DEBUG_STREAM << "[relay] seat request: ROSTER FULL\n"
<< std::flush;
roster_full = True;
break;
}
if (envelope->route != RELAY_ROUTE_SEAT_ASSIGN)
{
break;
}
if (have < (int)(sizeof(RelayTcpEnvelope) + envelope->length))
{
continue; // partial payload; keep reading
}
const char *tag = reply + sizeof(RelayTcpEnvelope) + 4;
int tag_max = (int)envelope->length - 4;
if (tag_max > 0 && tag_max < (int)sizeof(relaySelf))
{
memcpy(relaySelf, tag, tag_max);
relaySelf[tag_max] = '\0';
DEBUG_STREAM << "[relay] seat ASSIGNED: '" << relaySelf
<< "'\n" << std::flush;
got_seat = True;
}
}
}
closesocket(seat_socket);
if (got_seat)
{
char joined[128];
sprintf(joined, "\nSeat assigned (%s) -- joining!\n", relaySelf);
BTRelayWaitStatus(joined);
return True;
}
if (roster_full && !full_shown)
{
BTRelayWaitStatus(
"\nThe game is FULL -- waiting for a seat to free up ...\n");
full_shown = 1;
banner_shown = 1; // suppress the banner after this
}
Sleep(3000);
}
closesocket(seat_socket);
return got_seat;
return False;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~