D1 phase 5+6: UDP unreliable channel (restores the 1995 reliable/unreliable split)

The ~60Hz entity update records now ride UDP in relay mode, so a lost/late
datagram is dropped (dead reckoning absorbs the gap) instead of head-of-line-
blocking the reliable TCP stream -- the fix for internet rubber-banding.
Reliable traffic (make/damage/death/control/egg/launch) stays on TCP.

This RESTORES the authentic 1995 NETNUB split, it doesn't invent one:
Receiver::Message defaults messageFlags=ReliableFlag; the update path clears
it (flags=0, ENTITY.cpp:590); Mode(UnreliableMode) fires at LoadingMission
(APP.cpp:704 -- the 2007 port's Mode() was a no-op, now it STORES the mode).
Routing gate = (mode==UnreliableMode && !(flags & ReliableFlag)) -- flag AND
mode, so the map-stream creation messages (also flags=0 but flowing during the
still-Reliable CreatingMission window) ride TCP for free.

Client (L4NET):
- Mode() stores currentNetworkMode (was ignored).
- ConnectRelayUdp: UDP socket connect()ed to the relay game port; HELLO
  (outbound punch-through, relay learns our endpoint); udpUp on HELLO-ACK.
  BT_RELAY_TCP_ONLY=1 disables the channel (UDP-blocked nets degrade to the
  pure-TCP checkpoint automatically -- no ACK => everything stays on TCP).
- RelayUdpSendFrame: {route, fromHost, seq} envelope + frame, best-effort.
- RelayUdpKeepalive: ~15s NAT-hold + ~1s HELLO retry until acked.
- CheckRelayUdp: HELLO-ACK sets udpUp; per-sender seq gate drops stale/
  out-of-order datagrams; frames handed up envelope-stripped like TCP.
- Send / ExclusiveBroadcast: route unreliable-window traffic to UDP, else TCP.
- CheckBuffers polls the UDP channel first, then TCP; keepalive tick.
(The relay-side UDP forwarder + endpoint learning + --udp-drop hook landed in
phase 1.)

Verified 2-node localhost relay: during a drive the update stream flows on UDP
(relay udp tx 2->190+, TCP frozen at 53) and the peer's replicant tracks the
master to sub-2u; 15% forced drop (--udp-drop 15) stays coherent (tracked to
0.1u); TCP-only fallback confirmed (udp-known drops the TCP-only pod); no
crashes; mesh mode un-regressed.  KB: context/multiplayer.md gains the D1
RELAY MODE section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-18 10:00:26 -05:00
co-authored by Claude Fable 5
parent c029df6e6b
commit dbb9af2dfa
3 changed files with 293 additions and 5 deletions
+18
View File
@@ -350,6 +350,22 @@ private:
RelayGameDown(const char *why);
Logical
CheckRelay(NetworkPacket *network_packet);
//
// UDP unreliable channel (restores the 1995 NETNUB reliable/unreliable
// split): the ~60Hz update records ride UDP so a lost/late datagram is
// dropped (dead reckoning absorbs the gap) instead of head-of-line-
// blocking the reliable stream. Reliable traffic + control stay on TCP.
//
void
ConnectRelayUdp(HostID local_host_ID);
Logical
RelayUdpSendFrame(int route, const NetworkPacket *packet, int packet_size);
void
RelayUdpKeepalive(HostID local_host_ID);
Logical
RelayShouldUseUdp(const Message *message) const;
Logical
CheckRelayUdp(NetworkPacket *network_packet);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Private Data
@@ -390,7 +406,9 @@ private:
int relayPadTail;
Logical relayConsoleDialedOnce; // first dial gets the long timeout
Logical udpUp; // UDP HELLO acked by the relay
HostID relayLocalHostID; // our hostID (for UDP HELLO/keepalive/tx)
unsigned long lastUdpKeepaliveTick;
unsigned long lastUdpHelloTick; // HELLO retry until acked
unsigned long udpTxSeq;
unsigned long udpRxSeq[64]; // per-fromHost last-seen sequence
NetworkMode currentNetworkMode; // Reliable/Unreliable (Mode() stores)