D1: relay-assigned seats -- players never need a player number

A pod launched with no BT_SELF asks the relay for a seat before joining:
new control frames SEAT_REQUEST (-6) -> relay reserves the lowest roster
seat not claimed or reserved (60s reservation so simultaneous joiners
can't race onto one seat) -> SEAT_ASSIGN (-7, int32 hostID + NUL tag)
becomes relaySelf; everything downstream (egg self-match, HELLO) runs
exactly as if BT_SELF had been set. Roster full -> SEAT_FULL (-8) ->
clean Fail(). A real HELLO pops the reservation; a pod drop frees the
seat. Explicit BT_SELF still claims a specific seat (the operator's
local launches use it).

Client: L4NetworkManager::RelayRequestSeat (throwaway TCP dial to the
relay game port, 10s reply window). Relay: SEAT_REQUEST branch in
_handle_game_frame. Operator exporter collapsed from per-seat
join_as_playerN.bat to ONE universal join.bat (+ join_lan.bat with
BT_RELAY=auto) -- every player gets the same file, first come first
served, the arcade walk-up-to-a-pod model. players/ regenerated.

Verified: 8/8 seat stub tests (distinct seats in roster order, FULL on
exhaustion, claimed+reserved stays FULL, HELLO claim accepted, duplicate
HELLO refused); 2-node localhost e2e with NO BT_SELF on either pod ->
both seated, full ladder, RunMission pair, UDP flowing post-launch;
regression smoke: explicit-BT_SELF relay session clean with zero seat
requests, classic mesh (no BT_RELAY) un-regressed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-18 12:11:03 -05:00
co-authored by Claude Fable 5
parent efd440c64b
commit f04e8019c2
7 changed files with 195 additions and 36 deletions
+98 -4
View File
@@ -84,7 +84,10 @@ enum
RELAY_ROUTE_HELLO = -2, // first frame: magic + our hostID
RELAY_ROUTE_PEER_UP = -3, // relay->pod: hostID registered
RELAY_ROUTE_PEER_DOWN = -4, // relay->pod: hostID gone
RELAY_ROUTE_UDP_ACK = -5 // relay->pod: UDP HELLO acknowledged
RELAY_ROUTE_UDP_ACK = -5, // relay->pod: UDP HELLO acknowledged
RELAY_ROUTE_SEAT_REQUEST= -6, // pod->relay: assign me a free seat
RELAY_ROUTE_SEAT_ASSIGN = -7, // relay->pod: int32 hostID + NUL tag
RELAY_ROUTE_SEAT_FULL = -8 // relay->pod: roster full
};
#define RELAY_HELLO_MAGIC 0x31525442 // 'BTR1' little-endian
// LAN auto-discovery (BT_RELAY=auto): probe broadcast on this UDP port; the
@@ -388,12 +391,25 @@ L4NetworkManager::L4NetworkManager():
relayGameAddress.sin_port =
htons((unsigned short)(ntohs(relayConsoleAddress.sin_port) + 1));
//
// Identity: an explicit BT_SELF claims a specific seat; absent
// (or "auto"), the RELAY assigns the next free roster seat --
// players never need to know a player number.
//
const char *self_env = getenv("BT_SELF");
if (self_env == NULL || self_env[0] == '\0')
if (self_env == NULL || self_env[0] == '\0'
|| _stricmp(self_env, "auto") == 0)
{
Fail("BT_RELAY requires BT_SELF=<your [pilots] entry>\n");
if (!RelayRequestSeat())
{
Fail("relay seat request failed (roster full or relay "
"unreachable)\n");
}
}
else
{
Str_Copy(relaySelf, self_env, sizeof(relaySelf));
}
Str_Copy(relaySelf, self_env, sizeof(relaySelf));
relayMode = True;
DEBUG_STREAM << "[relay] mode ON: relay "
<< inet_ntoa(relayConsoleAddress.sin_addr) << ":"
@@ -1335,6 +1351,84 @@ void L4NetworkManager::HostDisconnectedMessageHandler(HostDisconnectedMessage* H
// D1 RELAY MODE support units. Everything below is dead unless BT_RELAY set.
//#############################################################################
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// RelayRequestSeat -- SERVER-ASSIGNED SEATS: no BT_SELF means "give me any
// free seat". A short throwaway connection to the relay game port sends
// SEAT_REQUEST; the relay reserves the lowest unclaimed roster seat and
// answers SEAT_ASSIGN {int32 hostID, NUL-terminated tag}. The tag becomes
// relaySelf and everything downstream (egg self-match, HELLO) proceeds as if
// BT_SELF had been set. SEAT_FULL / timeout => False.
//
Logical L4NetworkManager::RelayRequestSeat()
{
SOCKET seat_socket = RelayDialTcp(relayGameAddress, 60);
if (seat_socket == INVALID_SOCKET)
{
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)
{
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;
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);
return got_seat;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// RelayDialTcp -- outbound TCP dial with a BOUNDED retry window (unlike
// OpenConnection's unbounded 10061 busy-loop), then nonblocking + NODELAY.
+2
View File
@@ -338,6 +338,8 @@ private:
//
Logical
RelayDiscover(SOCKADDR_IN *console_endpoint);
Logical
RelayRequestSeat();
SOCKET
RelayDialTcp(const SOCKADDR_IN &endpoint, int timeout_seconds);
Logical