D1 phase 2+3: client TCP relay mode (L4NET) + 2-node verification

The pod can now run entirely over the relay: one outbound TCP connection
carries both the console protocol (egg/launch) and all game traffic
(envelope-multiplexed), so internet play works behind NAT with no port
forwarding.  All new code is behind 'if (relayMode)' (BT_RELAY unset => mesh
mode byte-for-byte untouched; verified un-regressed).

Env gates (parsed once in the ctor, after WSAStartup so gethostbyname works):
- BT_RELAY=<host>:<consolePort>  (host may be a DNS name; game port = +1)
- BT_SELF=<exact [pilots] entry> (NIC matching can't identify us across NAT)

L4NET changes:
- CreateConsoleHost: relay branch dials OUT to the relay console port
  (bounded retry; console-less continue on a re-dial) instead of listening.
- StartConnecting: relay self-match by BT_SELF string; peers become VIRTUAL
  hosts (INVALID_SOCKET, NoNetworkConnectionStatus) flipped online by the
  relay's PEER_UP; then ConnectRelayGame dials the game port + HELLOs.  HostID
  assignment / remoteHostCount / the connection gate / app ladder unchanged.
- CheckRelay (new): the receive seam -- drains {route,length} envelopes,
  synthesizes HostConnected/Disconnected from PEER_UP/DOWN (same messages the
  mesh accept path routes), returns game frames envelope-stripped (they
  self-identify via NetworkPacketHeader.fromHost; consumers route by payload).
- Send: game-host traffic -> relay unicast envelope (console host excluded --
  its legacy protocol is relay-TERMINATED, not routed).
- ExclusiveBroadcast: build ONCE, send ONCE with the broadcast route -- the
  relay fans out, killing the mesh's (N-1)x upload duplication.
- RelaySendAll (new): partial-send-safe transmit (required on the multiplexed
  socket -- a partial write would desync framing for all peers).
- CheckBuffers: polls CheckRelay first; skips recv on virtual game hosts.
- RelayGameDown: relay-loss synthesizes all-peers-disconnected (match
  continues peer-less; pod never exits mid-match).
- Mode(): now STORES the reliable/unreliable mode (was ignored) for the UDP
  phase's authentic mode+flag routing.

Verified 2-node localhost relay (MP_RELAY.EGG, tagged [pilots]):
- both pods reach 'All connections completed!' via PEER_UP, then
  RunningMission; both mechs' MakeMessages cross the relay (paint x2 each) and
  bidirectional 148-byte update records flow (net-tx/net-rx traces; relay
  stats tcp rx==tx, registered [2,3]); cockpit/HUD render; no crashes.
- driving one node transmits pose/damage/death frames through the relay.
- mesh smoke (no BT_RELAY): 2-node session still forms + simulates unchanged.

Plan: ~/.claude/plans/partitioned-snuggling-piglet.md.  Next: UDP channel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-18 09:01:54 -05:00
co-authored by Claude Fable 5
parent bf8c2ffaae
commit c029df6e6b
3 changed files with 908 additions and 0 deletions
+244
View File
@@ -0,0 +1,244 @@
[mission]
adventure=BattleTech
map=cavern
scenario=freeforall
time=night
weather=clear
temperature=27
length=600
[ordinals]
bitmap=Ordinal::BitMap::1
bitmap=Ordinal::BitMap::2
bitmap=Ordinal::BitMap::3
bitmap=Ordinal::BitMap::4
[Ordinal::BitMap::1]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=000038000003C000001FF00000000F00
bitmap=0000F8000003C000003FF80000000F00
bitmap=0001F8000003C00000707C0000000F00
bitmap=0001F8000003C00000603C0000000F00
bitmap=00007801FC0FF00000003C3DF807FF00
bitmap=00007803FE0FF00000003C3FFC0FFF00
bitmap=00007803C703C00000003C3E3C1F0F00
bitmap=000078078303C0000000783C1E1E0F00
bitmap=000078078003C0000000783C1E1E0F00
bitmap=00007807C003C0000000F03C1E1E0F00
bitmap=00007807F003C0000001E03C1E1E0F00
bitmap=00007803FC03C0000003C03C1E1E0F00
bitmap=00007801FE03C0000007803C1E1E0F00
bitmap=000078007F03C000000F003C1E1E0F00
bitmap=000078001F03C000001E003C1E1E0F00
bitmap=000078000F03C000003C003C1E1E0F00
bitmap=000078060F03C0000078003C1E1E0F00
bitmap=000078071E03E0000078003C1E1F1F00
bitmap=00007803FE01F000007FFC3C1E0FFF00
bitmap=00007801FC00F000007FFC3C1E07EF00
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[Ordinal::BitMap::2]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=001FFF0000003C0000003C03C0780000
bitmap=001FFF0000003C0000007C03C0780000
bitmap=00000E0000003C000000FC03C0780000
bitmap=00003C0000003C000001BC03C0780000
bitmap=0000700F3E1FFC000003BC0FF07BF000
bitmap=0001E00F7E3FFC0000073C0FF07FF800
bitmap=0003800FFE7C3C0000063C03C07C7800
bitmap=0007F80FFE783C00000C3C03C0783C00
bitmap=0007FE0F80783C0000183C03C0783C00
bitmap=00001E0F00783C0000383C03C0783C00
bitmap=00000F0F00783C0000703C03C0783C00
bitmap=00000F0F00783C00007FFF03C0783C00
bitmap=00000F0F00783C00007FFF03C0783C00
bitmap=00000F0F00783C0000003C03C0783C00
bitmap=00000F0F00783C0000003C03C0783C00
bitmap=00000F0F00783C0000003C03C0783C00
bitmap=00180F0F00783C0000003C03C0783C00
bitmap=001C1F0F007C7C0000003C03E0783C00
bitmap=000FFE0F003FFC0000003C01F0783C00
bitmap=0007FC0F001FBC0000003C00F0783C00
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[Ordinal::BitMap::3]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=001FFF03C07800000000FC03C0780000
bitmap=001FFF03C07800000003FC03C0780000
bitmap=001E0003C07800000007C003C0780000
bitmap=001E0003C0780000000F0003C0780000
bitmap=001E000FF07BF000000F000FF07BF000
bitmap=001E000FF07FF800001E000FF07FF800
bitmap=001E0003C07C7800001E0003C07C7800
bitmap=001FFC03C0783C00001EFC03C0783C00
bitmap=001FFE03C0783C00001FFE03C0783C00
bitmap=00001F03C0783C00001F1F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00180F03C0783C00001E0F03C0783C00
bitmap=001C1F03E0783C00001F1F03E0783C00
bitmap=000FFE01F0783C00000FFE01F0783C00
bitmap=0007FC00F0783C000007FC00F0783C00
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[Ordinal::BitMap::4]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=001FFF03C07800000007FC03C0780000
bitmap=001FFF03C0780000000FFE03C0780000
bitmap=00000F03C0780000001F1F03C0780000
bitmap=00000F03C0780000001E0F03C0780000
bitmap=00000F0FF07BF000001E0F0FF07BF000
bitmap=00001F0FF07FF800001E0F0FF07FF800
bitmap=00001E03C07C7800001E0F03C07C7800
bitmap=00003E03C0783C00001E0F03C0783C00
bitmap=00003C03C0783C00000F1E03C0783C00
bitmap=00003C03C0783C000007FC03C0783C00
bitmap=00007803C0783C000007FC03C0783C00
bitmap=00007803C0783C00000F1E03C0783C00
bitmap=00007803C0783C00001E0F03C0783C00
bitmap=0000F003C0783C00001E0F03C0783C00
bitmap=0000F003C0783C00001E0F03C0783C00
bitmap=0000F003C0783C00001E0F03C0783C00
bitmap=0000F003C0783C00001E0F03C0783C00
bitmap=0000F003E0783C00000F1E03E0783C00
bitmap=0000F001F0783C00000FFE01F0783C00
bitmap=0000F000F0783C000007FC00F0783C00
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[pilots]
pilot=10.99.0.1:1502
pilot=10.99.0.2:1502
[10.99.0.1:1502]
hostType=0
advancedDamage=1
loadzones=1
name=Aeolus
bitmapindex=1
experience=expert
badge=VGL
patch=Yellow
role=Role::Default
dropzone=one
vehicle=bhk1
vehicleValue=1000
color=White
[10.99.0.2:1502]
hostType=0
advancedDamage=1
loadzones=1
name=Boreas
bitmapindex=2
experience=expert
badge=VGL
patch=Yellow
role=Role::Default
dropzone=one
vehicle=madcat
vehicleValue=1000
color=Crimson
[largebitmap]
bitmap=BitMap::Large::Aeolus
[BitMap::Large::Aeolus]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=000003FE0000000001E0000000000000
bitmap=000007FF0000000001E0000000000000
bitmap=00000F8F8000000001E0000000000000
bitmap=00000F078000000001E0000000000000
bitmap=00000F0781FF00FF81E0F0781FC00000
bitmap=00000F0783FF81FFC1E0F0783FE00000
bitmap=00000F0787C7C3E3E1E0F0783C700000
bitmap=00000F078783C3C1E1E0F07878300000
bitmap=00000F078783C3C1E1E0F07878000000
bitmap=00000FFF8783C3C1E1E0F0787C000000
bitmap=00000FFF8783C3C1E1E0F0787F000000
bitmap=00000F0787FFC3C1E1E0F0783FC00000
bitmap=00000F0787FFC3C1E1E0F0781FE00000
bitmap=00000F07878003C1E1E0F07807F00000
bitmap=00000F07878003C1E1E0F07801F00000
bitmap=00000F07878003C1E1E0F07800F00000
bitmap=00000F078780C3C1E1E0F07860F00000
bitmap=00000F0787C1C3E3E1E078F871E00000
bitmap=00000F0783FF81FFC1E07FF83FE00000
bitmap=00000F0781FF00FF81E03F781FC00000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[smallbitmap]
bitmap=BitMap::Small::Aeolus
[BitMap::Small::Aeolus]
bitmap=00000000000000000000000000000000
bitmap=0000000000000000003E00000C000000
bitmap=006300000C00000000631F0F8CC63C00
bitmap=00633198CCC66200007F3198CCC66000
bitmap=00633F98CCC6780000633018CCC61E00
bitmap=00633018CCC6060000633098CCC64600
bitmap=00631F0F8C7E3C000000000000000000
bitmap=00000000000000000000000000000000
x=64
y=16
width=4
[Role::Default]
model=dfltrole
[Role::NoReturn]
model=noretun
+623
View File
@@ -71,6 +71,34 @@
#define CONSOLE_NET_PORT 1501 // Port number the console will connect on
#define GAME_NET_PORT 1502 // Port number the game will connect on
//
// D1 RELAY-MODE wire constants (must match tools/btconsole.py --relay).
// TCP envelope on the game relay connection: { int32 route; uint32 length; }
// followed by `length` payload bytes (the byte-exact classic frame:
// NetworkPacketHeader + Receiver::Message). UDP envelope (later phase):
// { int32 route; int32 fromHost; uint32 seq; } + frame.
//
enum
{
RELAY_ROUTE_BROADCAST = -1, // to every registered pod except the sender
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
};
#define RELAY_HELLO_MAGIC 0x31525442 // 'BTR1' little-endian
struct RelayTcpEnvelope
{
int route;
unsigned int length;
};
struct RelayUdpEnvelope
{
int route;
int fromHost;
unsigned int sequence;
};
char GlobalEggFileName[80]; // NASTY HACK !!! should suffice till we get console up though
//struct RMREG //real mode registers structure
@@ -188,6 +216,21 @@ L4NetworkManager::L4NetworkManager():
remoteHostCount = 0x7FFFFFFF;
wsaData = new WSADATA;
// D1 relay-mode defaults: inert until BT_RELAY is parsed at ctor end.
relayMode = False;
relaySelf[0] = '\0';
memset(&relayConsoleAddress, 0, sizeof(relayConsoleAddress));
memset(&relayGameAddress, 0, sizeof(relayGameAddress));
relayGameSocket = INVALID_SOCKET;
relayUdpSocket = INVALID_SOCKET;
relayPadTail = 0;
relayConsoleDialedOnce = False;
udpUp = False;
lastUdpKeepaliveTick = 0;
udpTxSeq = 0;
memset(udpRxSeq, 0, sizeof(udpRxSeq));
currentNetworkMode = NetworkManager::ReliableMode;
//
// If there is no net common, set things up for single user mode
//
@@ -279,6 +322,60 @@ L4NetworkManager::L4NetworkManager():
// Force us into reliable mode
//
Mode(NetworkManager::ReliableMode);
//
// D1 RELAY MODE: BT_RELAY=<host>:<consolePort> switches the whole net layer
// to outbound-only (the pod dials the relay for console AND game traffic).
// The host part may be a DNS name (internet play: one shared hostname).
// BT_SELF must name this pod's exact [pilots] entry (NIC matching cannot
// identify us across NAT). Parsed here, AFTER WSAStartup (gethostbyname).
//
{
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))
{
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)
{
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));
const char *self_env = getenv("BT_SELF");
if (self_env == NULL || self_env[0] == '\0')
{
Fail("BT_RELAY requires BT_SELF=<your [pilots] entry>\n");
}
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;
}
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -309,6 +406,50 @@ void L4NetworkManager::CreateConsoleHost()
numberOfConsoleHostsConnected = 0; // Shouldn't be any consoles yet
numberOfMungaHostsConnected = 0; // No game machines either
//
// D1 RELAY MODE: dial OUT to the relay's console port instead of listening
// (outbound = NAT-friendly; the relay terminates the same legacy console
// protocol, so the egg/ack/launch handlers are untouched). The console
// host starts in OpeningConnectionStatus; CheckBuffers' existing Opening
// completion path (getpeername + address match, which holds by
// construction) fires the HostConnectedMessage.
//
if (relayMode)
{
// First dial waits for the relay to come up (like mesh peers wait on
// each other); a mid-game RE-dial (console-loss handler) is bounded
// short so a dead relay can't hang the pod -- the app's no-console
// self-launch guard (APP.cpp CheckLoadMessageHandler) and the mesh
// console-loss semantics both tolerate a console-less pod.
int timeout_seconds = relayConsoleDialedOnce ? 3 : 60;
SOCKET relay_console_socket =
RelayDialTcp(relayConsoleAddress, timeout_seconds);
if (relay_console_socket == INVALID_SOCKET && !relayConsoleDialedOnce)
{
DEBUG_STREAM << "ERROR: BT_RELAY set but the relay console port "
"did not answer within " << timeout_seconds << "s\n" << std::flush;
PostQuitMessage(AbortExitCodeID);
return;
}
relayConsoleDialedOnce = True;
myConsoleHost = new L4Host(
FirstLegalHostID,
ConsoleHostType,
&relayConsoleAddress,
relay_console_socket,
"Console");
Register_Object(myConsoleHost);
myConsoleHost->SetConnectStatus(
(relay_console_socket == INVALID_SOCKET)
? L4Host::NoNetworkConnectionStatus // console-less continue
: L4Host::OpeningConnectionStatus);
Check(application);
Check(application->GetHostManager());
application->GetHostManager()->AdoptRemoteHost(myConsoleHost);
networkEggNotationFile = NULL;
return;
}
//
// Post a listen for a console with an unknown network address
// this will return a stream that the console will eventually be on
@@ -484,8 +625,19 @@ void
//
// See if this host is us or someone else
//
// D1 RELAY MODE: local-NIC matching cannot identify us across NAT (our
// public address differs from every NIC), so the pod self-identifies by
// the BT_SELF string matching its [pilots] entry EXACTLY (the entries
// become arbitrary unique ip:port-shaped tags; ports/NICs irrelevant --
// which also lets N instances share one box with identical -net).
//
//WinSock support :ADB 01/06/07
bool addressFound = false;
if (relayMode)
{
addressFound = (_stricmp((const char *)host_name, relaySelf) == 0);
}
else
for(int i=0; i<num_addresses; i++)
{
if(addresses[i] == net_address.sin_addr.S_un.S_addr && localGamePort == ntohs(net_address.sin_port))
@@ -515,6 +667,30 @@ void
Check(application->GetHostManager());
application->GetHostManager()->AdoptLocalHost(my_l4host);
}
else if (relayMode)
{
//
// D1 RELAY MODE: peers are VIRTUAL hosts -- no per-peer socket, no
// connect/listen. They sit in NoNetworkConnectionStatus (which the
// CheckBuffers loop skips) until the relay's PEER_UP control frame
// synthesizes the HostConnectedMessage that flips them OnLine. The
// hostID assignment (egg order) and remoteHostCount stay identical
// to the mesh, so the "All connections completed!" gate and the
// whole app ladder run UNMODIFIED.
//
remoteHostCount++;
my_l4host = new L4Host(
nextOpenHostID,
mission_host_data->GetHostType(),
&net_address,
INVALID_SOCKET,
host_name);
Register_Object(my_l4host);
my_l4host->SetConnectStatus(L4Host::NoNetworkConnectionStatus);
Check(application);
Check(application->GetHostManager());
application->GetHostManager()->AdoptRemoteHost(my_l4host);
}
else
{
//
@@ -555,6 +731,20 @@ void
nextOpenHostID++;
}
//
// D1 RELAY MODE: one outbound game connection to the relay carries every
// peer. BT_SELF must have matched an egg entry (the local host drives our
// hostID + fromHost stamping).
//
if (relayMode)
{
if (!listen)
{
Fail("BT_SELF matched no [pilots] entry in the egg\n");
}
ConnectRelayGame(application->GetHostManager()->GetLocalHostID());
}
//
// All the hosts are created, the connects/listens done.
//
@@ -639,6 +829,26 @@ Logical
return True;
}
//
// D1 RELAY MODE: drop the relay game/UDP sockets (virtual game hosts hold
// INVALID_SOCKET; the loop below CloseConnection()s them harmlessly).
//
if (relayMode)
{
if (relayGameSocket != INVALID_SOCKET)
{
closesocket(relayGameSocket);
relayGameSocket = INVALID_SOCKET;
}
if (relayUdpSocket != INVALID_SOCKET)
{
closesocket(relayUdpSocket);
relayUdpSocket = INVALID_SOCKET;
}
relayPadTail = 0;
udpUp = False;
}
//
// If we get here there was a network setup, we must drop all connections
// deregister all the remote hosts and then deregister the local host.
@@ -1018,6 +1228,350 @@ void L4NetworkManager::HostDisconnectedMessageHandler(HostDisconnectedMessage* H
DEBUG_STREAM << addressString << "\n" << std::flush;
}
//#############################################################################
// D1 RELAY MODE support units. Everything below is dead unless BT_RELAY set.
//#############################################################################
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// RelayDialTcp -- outbound TCP dial with a BOUNDED retry window (unlike
// OpenConnection's unbounded 10061 busy-loop), then nonblocking + NODELAY.
//
SOCKET L4NetworkManager::RelayDialTcp(
const SOCKADDR_IN &endpoint,
int timeout_seconds)
{
unsigned long deadline = GetTickCount() + (unsigned long)timeout_seconds * 1000UL;
for (;;)
{
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == INVALID_SOCKET)
{
return INVALID_SOCKET;
}
if (connect(sock, (const sockaddr *)&endpoint, sizeof(endpoint)) == 0)
{
unsigned long enable = 1;
ioctlsocket(sock, FIONBIO, &enable);
BOOL noDelay = TRUE;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY,
(char *)&noDelay, sizeof(noDelay));
return sock;
}
int wsa_error = WSAGetLastError();
closesocket(sock);
if (GetTickCount() >= deadline
|| (wsa_error != WSAECONNREFUSED && wsa_error != WSAETIMEDOUT))
{
DEBUG_STREAM << "[relay] dial failed (WSA " << wsa_error
<< ")\n" << std::flush;
return INVALID_SOCKET;
}
Sleep(250);
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// RelaySendAll -- partial-send-safe transmit on the multiplexed relay socket.
// REQUIRED correctness over the mesh path's fire-and-forget send(): a partial
// write here would desync the envelope framing for every peer at once.
//
Logical L4NetworkManager::RelaySendAll(
SOCKET sock,
const char *buffer,
int length)
{
int sent_total = 0;
while (sent_total < length)
{
int sent = send(sock, buffer + sent_total, length - sent_total, 0);
if (sent > 0)
{
sent_total += sent;
continue;
}
int wsa_error = WSAGetLastError();
if (sent == SOCKET_ERROR && wsa_error == WSAEWOULDBLOCK)
{
// Kernel buffer full: wait (bounded) for writability.
fd_set write_set;
FD_ZERO(&write_set);
FD_SET(sock, &write_set);
timeval tv;
tv.tv_sec = 5;
tv.tv_usec = 0;
if (select(0, NULL, &write_set, NULL, &tv) == 1)
{
continue;
}
DEBUG_STREAM << "[relay] send stalled >5s\n" << std::flush;
return False;
}
DEBUG_STREAM << "[relay] send failed (WSA " << wsa_error << ")\n"
<< std::flush;
return False;
}
return True;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// BuildRelayPacket -- the classic frame builder (the malloc + header fill the
// live mesh Send() does inline; kept as a SEPARATE unit so the verified mesh
// path stays byte-for-byte untouched). Caller frees.
//
NetworkPacket *L4NetworkManager::BuildRelayPacket(
Message *message,
ClientID client,
int *packet_size)
{
Check_Pointer(message);
Verify(message->messageLength >= 8);
NetworkPacket *packet =
(NetworkPacket *)malloc(message->messageLength + sizeof(NetworkPacketHeader));
memcpy(&packet->messageData, message, message->messageLength);
packet->clientID = client;
packet->gameID = gameID;
packet->timeStamp = Now();
Host *local_host = application->GetHostManager()->GetLocalHost();
packet->fromHost = (local_host == NULL) ? 0 : local_host->GetHostID();
*packet_size = message->messageLength + sizeof(NetworkPacketHeader);
return packet;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// RelaySendFrame -- envelope + frame on the relay game connection.
//
Logical L4NetworkManager::RelaySendFrame(
int route,
const NetworkPacket *packet,
int packet_size)
{
if (relayGameSocket == INVALID_SOCKET)
{
return False;
}
char wire[sizeof(RelayTcpEnvelope) + NETWORKMANAGER_BUFFER_SIZE];
Verify(packet_size <= NETWORKMANAGER_BUFFER_SIZE);
RelayTcpEnvelope *envelope = (RelayTcpEnvelope *)wire;
envelope->route = route;
envelope->length = (unsigned int)packet_size;
memcpy(wire + sizeof(RelayTcpEnvelope), packet, packet_size);
if (!RelaySendAll(relayGameSocket,
wire, (int)sizeof(RelayTcpEnvelope) + packet_size))
{
RelayGameDown("send failure");
return False;
}
if (getenv("BT_NET_TRACE"))
{
DEBUG_STREAM << "[net-tx] relay route=" << route
<< " client=" << (int)packet->clientID
<< " msgID=" << (int)packet->messageData.messageID
<< " len=" << (int)packet->messageData.messageLength
<< "\n" << std::flush;
}
return True;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ConnectRelayGame -- dial the relay's game port and register with HELLO.
// (UDP channel comes in the later phase; TCP carries everything until then.)
//
void L4NetworkManager::ConnectRelayGame(HostID local_host_ID)
{
relayGameSocket = RelayDialTcp(relayGameAddress, 60);
if (relayGameSocket == INVALID_SOCKET)
{
Fail("BT_RELAY set but the relay game port did not answer\n");
}
relayPadTail = 0;
struct
{
RelayTcpEnvelope envelope;
int magic;
unsigned int hostID;
} hello;
hello.envelope.route = RELAY_ROUTE_HELLO;
hello.envelope.length = 8;
hello.magic = RELAY_HELLO_MAGIC;
hello.hostID = (unsigned int)local_host_ID;
if (!RelaySendAll(relayGameSocket, (const char *)&hello, sizeof(hello)))
{
Fail("relay HELLO transmit failed\n");
}
DEBUG_STREAM << "[relay] game connection up, HELLO sent (hostID "
<< (int)local_host_ID << ")\n" << std::flush;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// RelayGameDown -- the relay game connection died: close it and synthesize a
// HostDisconnectedMessage for every online game host (the match continues
// peer-less, exactly like an all-peers mesh disconnect; the pod never exits
// mid-match).
//
void L4NetworkManager::RelayGameDown(const char *why)
{
if (relayGameSocket == INVALID_SOCKET)
{
return;
}
DEBUG_STREAM << "[relay] game connection DOWN (" << why << ")\n" << std::flush;
closesocket(relayGameSocket);
relayGameSocket = INVALID_SOCKET;
relayPadTail = 0;
udpUp = False;
HostManager::RemoteHostIterator remote_hosts(application->GetHostManager());
Host *host;
while ((host = remote_hosts.ReadAndNext()) != NULL)
{
if (host->GetHostType() == ConsoleHostType)
{
continue;
}
L4Host *l4host = Cast_Object(L4Host*, host);
if (l4host->GetConnectStatus() != L4Host::OnLineConnectionStatus)
{
continue;
}
// INVALID_SOCKET stream pointer: the disconnect handler's
// CloseConnection(streamPointer) must not touch a real socket.
HostDisconnectedMessage host_down(host->GetHostID(),
(unsigned long)INVALID_SOCKET);
NetworkClient *client = GetNetworkClientPointer(NetworkManagerClientID);
Check(client);
client->ReceiveNetworkPacket(NULL, &host_down);
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CheckRelay -- THE relay receive seam, called from the top of CheckBuffers.
// Drains complete envelopes from the relay pad: control frames (PEER_UP/DOWN)
// are handled inline by synthesizing the SAME HostConnected/Disconnected
// messages the mesh accept path routes, so the connection gate and app ladder
// run unmodified; game frames are copied out envelope-stripped (they
// self-identify via NetworkPacketHeader.fromHost -- every consumer routes by
// payload ids, never by arrival socket). Returns True with one game frame.
//
Logical L4NetworkManager::CheckRelay(NetworkPacket *network_packet)
{
if (!relayMode || relayGameSocket == INVALID_SOCKET)
{
return False;
}
for (;;)
{
//
// Drain complete envelopes already buffered.
//
while (relayPadTail >= (int)sizeof(RelayTcpEnvelope))
{
RelayTcpEnvelope *envelope = (RelayTcpEnvelope *)relayPad;
int frame_length = (int)envelope->length;
if (frame_length > NETWORKMANAGER_BUFFER_SIZE)
{
RelayGameDown("oversize frame (protocol desync)");
return False;
}
int whole = (int)sizeof(RelayTcpEnvelope) + frame_length;
if (relayPadTail < whole)
{
break; // partial frame; need more bytes
}
int route = envelope->route;
char *payload = relayPad + sizeof(RelayTcpEnvelope);
Logical is_game_frame = False;
if (route == RELAY_ROUTE_PEER_UP && frame_length >= 4)
{
HostID peer_id = (HostID)(*(int *)payload);
Host *peer = application->GetHostManager()->GetRemoteHost(peer_id);
if (peer != NULL)
{
L4Host *l4peer = Cast_Object(L4Host*, peer);
HostConnectedMessage peer_up(peer_id,
*l4peer->GetNetworkAddress(), // the egg-parsed tag address
(unsigned long)INVALID_SOCKET);
NetworkClient *client =
GetNetworkClientPointer(NetworkManagerClientID);
Check(client);
client->ReceiveNetworkPacket(NULL, &peer_up);
}
}
else if (route == RELAY_ROUTE_PEER_DOWN && frame_length >= 4)
{
HostID peer_id = (HostID)(*(int *)payload);
Host *peer = application->GetHostManager()->GetRemoteHost(peer_id);
if (peer != NULL
&& Cast_Object(L4Host*, peer)->GetConnectStatus()
== L4Host::OnLineConnectionStatus)
{
HostDisconnectedMessage peer_down(peer_id,
(unsigned long)INVALID_SOCKET);
NetworkClient *client =
GetNetworkClientPointer(NetworkManagerClientID);
Check(client);
client->ReceiveNetworkPacket(NULL, &peer_down);
}
}
else if (route >= FirstLegalHostID)
{
is_game_frame = True;
Mem_Copy(network_packet, payload, frame_length,
NETWORKMANAGER_BUFFER_SIZE);
}
// (unknown negative routes: ignore -- forward compatibility)
int remainder = relayPadTail - whole;
if (remainder > 0)
{
memmove(relayPad, relayPad + whole, remainder);
}
relayPadTail = remainder;
if (is_game_frame)
{
if (getenv("BT_NET_TRACE"))
{
DEBUG_STREAM << "[net-rx] relay client="
<< (int)network_packet->clientID
<< " msgID=" << (int)network_packet->messageData.messageID
<< " len=" << (int)network_packet->messageData.messageLength
<< " from host " << (int)network_packet->fromHost
<< "\n" << std::flush;
}
return True;
}
}
//
// Refill from the socket.
//
int space = (int)sizeof(relayPad) - relayPadTail;
int received = recv(relayGameSocket, relayPad + relayPadTail, space, 0);
if (received > 0)
{
relayPadTail += received;
continue;
}
if (received == 0)
{
RelayGameDown("closed by relay");
return False;
}
int wsa_error = WSAGetLastError();
if (wsa_error == WSAEWOULDBLOCK)
{
return False; // no more data this poll
}
if (wsa_error == WSAECONNRESET)
{
RelayGameDown("connection reset");
return False;
}
DEBUG_STREAM << "[relay] recv error WSA " << wsa_error << "\n" << std::flush;
return False;
}
}
#if MESSAGE_BUFFERING
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// L4NetworkManager::Send
@@ -1098,6 +1652,23 @@ void L4NetworkManager::Send(
return;
}
Verify(l4host->GetConnectStatus() == L4Host::OnLineConnectionStatus);
//
// D1 RELAY MODE: game-host traffic rides the ONE relay connection inside a
// {route, length} envelope (route = destination hostID; the relay demuxes).
// The CONSOLE host is excluded -- its connection speaks the legacy raw
// protocol which the relay TERMINATES rather than routes (egg ack, etc).
//
if (relayMode && l4host->GetHostType() != ConsoleHostType)
{
int relay_packet_size;
NetworkPacket *relay_packet =
BuildRelayPacket(message, client, &relay_packet_size);
RelaySendFrame((int)host_ID, relay_packet, relay_packet_size);
free(relay_packet);
CLEAR_SEND_PACKET();
return;
}
//
// figure out where to put the packet we're assembling
//
@@ -1507,6 +2078,26 @@ void
#if !BATCHED_TRANSMIT
Host
*this_host;
//
// D1 RELAY MODE: build ONCE, send ONCE with the broadcast route -- the
// relay fans out server-side. This kills the mesh's (N-1)x upload
// duplication at the exact seam where EntityBroadcastToReplicants already
// serializes the update record once.
//
if (relayMode)
{
if (relayGameSocket != INVALID_SOCKET)
{
int relay_packet_size;
NetworkPacket *relay_packet =
BuildRelayPacket(message, client, &relay_packet_size);
RelaySendFrame(RELAY_ROUTE_BROADCAST, relay_packet, relay_packet_size);
free(relay_packet);
}
return;
}
//
// Iterate through the host list, resolving network addresses, sending and
// making sure we don't send to ourselves.
@@ -2020,6 +2611,19 @@ Logical L4NetworkManager::CheckBuffers(NetworkPacket *network_packet)
//
Check(application);
Check(application->GetHostManager());
//
// D1 RELAY MODE: the relay game connection is polled FIRST (control frames
// synthesize HostConnected/Disconnected inline; game frames return here).
// The classic per-host loop below still services the CONSOLE host; game
// hosts are virtual (skipped via the relay gate in the OnLine recv block).
//
if (relayMode && CheckRelay(network_packet))
{
CLEAR_CHECK_BUFFERS();
return True;
}
//
// Make an iterator to take us through all the hosts
//
@@ -2239,6 +2843,16 @@ Logical L4NetworkManager::CheckBuffers(NetworkPacket *network_packet)
break;
}
#if !BATCHED_RECEIVE
//
// D1 RELAY MODE: online GAME hosts are virtual (no socket of their own;
// their traffic arrives via CheckRelay above) -- recv'ing their
// INVALID_SOCKET would just spam WSAENOTSOCK. The console host keeps
// its real socket and is polled normally.
//
if (relayMode && remote_host->GetHostType() != ConsoleHostType)
{
continue;
}
//
// If we get this far the host is on line, if there is room to do a receive
// into this buffer then do one.
@@ -2921,6 +3535,15 @@ void L4NetworkManager::CloseConnection(SOCKET socket_ptr) // socket address fro
//WinSock support :ADB 01/06/07
void L4NetworkManager::Mode(NetworkMode myMode)
{
//
// D1: the mode is now STORED (the 2007 port ignored it). The app ladder
// drives it authentically -- UnreliableMode at LoadingMission, back to
// Reliable at mission end (APP.cpp) -- and the relay UDP phase routes
// unreliable-window traffic by (mode == UnreliableMode && !ReliableFlag),
// restoring the 1995 NETNUB reliable/unreliable split.
//
currentNetworkMode = myMode;
//SetSwitchesRequestPtr set_switches_request;
//
// No netnub present? Just return
+41
View File
@@ -327,6 +327,30 @@ private:
DroppedMessageHostSocket *dropped_message_host_socket
);
//
// D1 RELAY MODE (BT_RELAY=host:port + BT_SELF=<[pilots] entry>): outbound-
// only topology -- the pod dials the relay (extended btconsole.py) for BOTH
// the console protocol (egg/launch; legacy raw frames, direction flipped)
// and the game traffic (envelope-framed, one TCP connection multiplexing
// every peer; UDP for the unreliable channel in a later phase). BT_RELAY
// unset => relayMode False => every relay branch is dead and the classic
// mesh is untouched.
//
SOCKET
RelayDialTcp(const SOCKADDR_IN &endpoint, int timeout_seconds);
Logical
RelaySendAll(SOCKET sock, const char *buffer, int length);
NetworkPacket *
BuildRelayPacket(Message *message, ClientID client, int *packet_size);
Logical
RelaySendFrame(int route, const NetworkPacket *packet, int packet_size);
void
ConnectRelayGame(HostID local_host_ID);
void
RelayGameDown(const char *why);
Logical
CheckRelay(NetworkPacket *network_packet);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Private Data
//
@@ -353,6 +377,23 @@ private:
addrinfo* thisHost;
SOCKET gameListenerSocket;
SOCKET consoleListenerSocket;
// D1 relay mode state (see the private-methods block above). All inert
// unless BT_RELAY is set.
Logical relayMode;
char relaySelf[64]; // BT_SELF -- our [pilots] entry string
SOCKADDR_IN relayConsoleAddress; // relay console endpoint (BT_RELAY port)
SOCKADDR_IN relayGameAddress; // relay game endpoint (port + 1, tcp+udp)
SOCKET relayGameSocket; // the ONE multiplexed game connection
SOCKET relayUdpSocket; // unreliable channel (later phase)
char relayPad[16384]; // relay-connection reassembly buffer
int relayPadTail;
Logical relayConsoleDialedOnce; // first dial gets the long timeout
Logical udpUp; // UDP HELLO acked by the relay
unsigned long lastUdpKeepaliveTick;
unsigned long udpTxSeq;
unsigned long udpRxSeq[64]; // per-fromHost last-seen sequence
NetworkMode currentNetworkMode; // Reliable/Unreliable (Mode() stores)
};
//~~~~~~~~~~~~~~~~~~~~~~ L4NetworkManager inlines ~~~~~~~~~~~~~~~~~~~~~~~~~~