D1 phase 7: LAN auto-discovery (BT_RELAY=auto) + operator LAN scripts
LAN players now find the game with zero configuration -- the 90s arcade model (cabinets locate the operator station) restored: - Client (L4NET RelayDiscover): BT_RELAY=auto broadcasts 'BTR1DISC' on udp/15999 (255.255.255.255 AND 127.0.0.1 -- broadcast doesn't reliably loop back on Windows; covers same-box sessions), 5 x 1s attempts; the answering relay's SOURCE IP + its advertised console port become the relay address. Explicit <host>:<port> path unchanged; mesh untouched. - Relay (btconsole.py): best-effort discovery responder on udp/15999 answers 'BTR1HERE' + <u16 consolePort>; degrades gracefully (logged) if the port is taken. - Operator app: Export player scripts now writes a join_as_playerN_lan.bat pair (BT_RELAY=auto) next to each internet script -- LAN guests double-click and are found; internet guests use the public-host script. Verified 2-node: both pods BT_RELAY=auto -> probe answered through a real interface (not just loopback) -> discovered 172.19.x.x:1500 -> full session to RunningMission. KB: multiplayer.md D1 section updated (+ the operator console entry). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
43fa53d542
commit
1436d27ac6
+128
-27
@@ -87,6 +87,11 @@ enum
|
||||
RELAY_ROUTE_UDP_ACK = -5 // relay->pod: UDP HELLO acknowledged
|
||||
};
|
||||
#define RELAY_HELLO_MAGIC 0x31525442 // 'BTR1' little-endian
|
||||
// LAN auto-discovery (BT_RELAY=auto): probe broadcast on this UDP port; the
|
||||
// relay answers "BTR1HERE" + <u16 consolePort>. Must match btconsole.py.
|
||||
#define RELAY_DISCOVERY_PORT 15999
|
||||
#define RELAY_DISC_PROBE "BTR1DISC"
|
||||
#define RELAY_DISC_REPLY "BTR1HERE"
|
||||
struct RelayTcpEnvelope
|
||||
{
|
||||
int route;
|
||||
@@ -336,35 +341,52 @@ L4NetworkManager::L4NetworkManager():
|
||||
const char *relay_env = getenv("BT_RELAY");
|
||||
if (relay_env != NULL && relay_env[0] != '\0')
|
||||
{
|
||||
char host_part[128];
|
||||
int relay_port = 0;
|
||||
const char *colon = strrchr(relay_env, ':');
|
||||
if (colon == NULL || colon == relay_env
|
||||
|| (relay_port = atoi(colon + 1)) <= 0
|
||||
|| (colon - relay_env) >= (int)sizeof(host_part))
|
||||
if (_stricmp(relay_env, "auto") == 0)
|
||||
{
|
||||
Fail("BT_RELAY must be <host>:<consolePort>\n");
|
||||
}
|
||||
memcpy(host_part, relay_env, colon - relay_env);
|
||||
host_part[colon - relay_env] = '\0';
|
||||
|
||||
unsigned long relay_ip = inet_addr(host_part);
|
||||
if (relay_ip == INADDR_NONE)
|
||||
{
|
||||
hostent *he = gethostbyname(host_part);
|
||||
if (he == NULL || he->h_addr_list[0] == NULL)
|
||||
//
|
||||
// LAN AUTO-DISCOVERY: broadcast a probe; the relay answers
|
||||
// with its console port; its source IP is the relay host.
|
||||
// Zero-config for anyone on the operator's LAN (the 90s
|
||||
// arcade model: cabinets find the operator station).
|
||||
//
|
||||
if (!RelayDiscover(&relayConsoleAddress))
|
||||
{
|
||||
DEBUG_STREAM << "ERROR: BT_RELAY host '" << host_part
|
||||
<< "' did not resolve\n" << std::flush;
|
||||
Fail("BT_RELAY host unresolvable\n");
|
||||
Fail("BT_RELAY=auto: no relay answered on this LAN\n");
|
||||
}
|
||||
relay_ip = *(unsigned long *)he->h_addr_list[0];
|
||||
}
|
||||
relayConsoleAddress.sin_family = AF_INET;
|
||||
relayConsoleAddress.sin_addr.S_un.S_addr = relay_ip;
|
||||
relayConsoleAddress.sin_port = htons((unsigned short)relay_port);
|
||||
else
|
||||
{
|
||||
char host_part[128];
|
||||
int relay_port = 0;
|
||||
const char *colon = strrchr(relay_env, ':');
|
||||
if (colon == NULL || colon == relay_env
|
||||
|| (relay_port = atoi(colon + 1)) <= 0
|
||||
|| (colon - relay_env) >= (int)sizeof(host_part))
|
||||
{
|
||||
Fail("BT_RELAY must be <host>:<consolePort> or 'auto'\n");
|
||||
}
|
||||
memcpy(host_part, relay_env, colon - relay_env);
|
||||
host_part[colon - relay_env] = '\0';
|
||||
|
||||
unsigned long relay_ip = inet_addr(host_part);
|
||||
if (relay_ip == INADDR_NONE)
|
||||
{
|
||||
hostent *he = gethostbyname(host_part);
|
||||
if (he == NULL || he->h_addr_list[0] == NULL)
|
||||
{
|
||||
DEBUG_STREAM << "ERROR: BT_RELAY host '" << host_part
|
||||
<< "' did not resolve\n" << std::flush;
|
||||
Fail("BT_RELAY host unresolvable\n");
|
||||
}
|
||||
relay_ip = *(unsigned long *)he->h_addr_list[0];
|
||||
}
|
||||
relayConsoleAddress.sin_family = AF_INET;
|
||||
relayConsoleAddress.sin_addr.S_un.S_addr = relay_ip;
|
||||
relayConsoleAddress.sin_port = htons((unsigned short)relay_port);
|
||||
}
|
||||
relayGameAddress = relayConsoleAddress;
|
||||
relayGameAddress.sin_port = htons((unsigned short)(relay_port + 1));
|
||||
relayGameAddress.sin_port =
|
||||
htons((unsigned short)(ntohs(relayConsoleAddress.sin_port) + 1));
|
||||
|
||||
const char *self_env = getenv("BT_SELF");
|
||||
if (self_env == NULL || self_env[0] == '\0')
|
||||
@@ -373,13 +395,92 @@ L4NetworkManager::L4NetworkManager():
|
||||
}
|
||||
Str_Copy(relaySelf, self_env, sizeof(relaySelf));
|
||||
relayMode = True;
|
||||
DEBUG_STREAM << "[relay] mode ON: relay " << host_part << ":"
|
||||
<< relay_port << " (game " << (relay_port + 1) << "), self='"
|
||||
<< relaySelf << "'\n" << std::flush;
|
||||
DEBUG_STREAM << "[relay] mode ON: relay "
|
||||
<< inet_ntoa(relayConsoleAddress.sin_addr) << ":"
|
||||
<< (int)ntohs(relayConsoleAddress.sin_port)
|
||||
<< " (game " << (int)ntohs(relayGameAddress.sin_port)
|
||||
<< "), self='" << relaySelf << "'\n" << std::flush;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// RelayDiscover -- BT_RELAY=auto: find the relay on the LAN by UDP broadcast.
|
||||
// Sends the probe to 255.255.255.255 AND 127.0.0.1 (same-box sessions --
|
||||
// broadcast does not reliably loop back on Windows), ~5 attempts at 1s. The
|
||||
// answering relay's source address + its advertised console port fill
|
||||
// *console_endpoint.
|
||||
//
|
||||
Logical L4NetworkManager::RelayDiscover(SOCKADDR_IN *console_endpoint)
|
||||
{
|
||||
SOCKET probe_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if (probe_socket == INVALID_SOCKET)
|
||||
{
|
||||
return False;
|
||||
}
|
||||
BOOL allow_broadcast = TRUE;
|
||||
setsockopt(probe_socket, SOL_SOCKET, SO_BROADCAST,
|
||||
(char *)&allow_broadcast, sizeof(allow_broadcast));
|
||||
unsigned long enable = 1;
|
||||
ioctlsocket(probe_socket, FIONBIO, &enable);
|
||||
|
||||
SOCKADDR_IN broadcast_addr, loopback_addr;
|
||||
memset(&broadcast_addr, 0, sizeof(broadcast_addr));
|
||||
broadcast_addr.sin_family = AF_INET;
|
||||
broadcast_addr.sin_addr.S_un.S_addr = INADDR_BROADCAST;
|
||||
broadcast_addr.sin_port = htons(RELAY_DISCOVERY_PORT);
|
||||
loopback_addr = broadcast_addr;
|
||||
loopback_addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
|
||||
|
||||
const int probe_len = (int)strlen(RELAY_DISC_PROBE);
|
||||
const int reply_len = (int)strlen(RELAY_DISC_REPLY);
|
||||
Logical found = False;
|
||||
|
||||
DEBUG_STREAM << "[relay] BT_RELAY=auto: probing the LAN (udp/"
|
||||
<< RELAY_DISCOVERY_PORT << ")...\n" << std::flush;
|
||||
for (int attempt = 0; attempt < 5 && !found; ++attempt)
|
||||
{
|
||||
sendto(probe_socket, RELAY_DISC_PROBE, probe_len, 0,
|
||||
(const sockaddr *)&broadcast_addr, sizeof(broadcast_addr));
|
||||
sendto(probe_socket, RELAY_DISC_PROBE, probe_len, 0,
|
||||
(const sockaddr *)&loopback_addr, sizeof(loopback_addr));
|
||||
|
||||
unsigned long wait_until = GetTickCount() + 1000UL;
|
||||
while (GetTickCount() < wait_until && !found)
|
||||
{
|
||||
fd_set read_set;
|
||||
FD_ZERO(&read_set);
|
||||
FD_SET(probe_socket, &read_set);
|
||||
timeval tv;
|
||||
tv.tv_sec = 0;
|
||||
tv.tv_usec = 100000;
|
||||
if (select(0, &read_set, NULL, NULL, &tv) != 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
char reply[32];
|
||||
SOCKADDR_IN from;
|
||||
int from_len = sizeof(from);
|
||||
int received = recvfrom(probe_socket, reply, sizeof(reply), 0,
|
||||
(sockaddr *)&from, &from_len);
|
||||
if (received >= reply_len + 2
|
||||
&& memcmp(reply, RELAY_DISC_REPLY, reply_len) == 0)
|
||||
{
|
||||
unsigned short console_port;
|
||||
memcpy(&console_port, reply + reply_len, 2);
|
||||
*console_endpoint = from;
|
||||
console_endpoint->sin_port = htons(console_port);
|
||||
DEBUG_STREAM << "[relay] discovered relay at "
|
||||
<< inet_ntoa(from.sin_addr) << ":" << (int)console_port
|
||||
<< "\n" << std::flush;
|
||||
found = True;
|
||||
}
|
||||
}
|
||||
}
|
||||
closesocket(probe_socket);
|
||||
return found;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// L4NetworkManager::MakeNotationFileEgg This routine creates the console remote
|
||||
// host
|
||||
|
||||
Reference in New Issue
Block a user