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
+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 ~~~~~~~~~~~~~~~~~~~~~~~~~~