diff --git a/content/MP_RELAY.EGG b/content/MP_RELAY.EGG new file mode 100644 index 0000000..af7a9b6 --- /dev/null +++ b/content/MP_RELAY.EGG @@ -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 diff --git a/engine/MUNGA_L4/L4NET.CPP b/engine/MUNGA_L4/L4NET.CPP index bd638c7..1a954b7 100644 --- a/engine/MUNGA_L4/L4NET.CPP +++ b/engine/MUNGA_L4/L4NET.CPP @@ -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=: 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 :\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=\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; iGetHostManager()); 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 diff --git a/engine/MUNGA_L4/L4NET.H b/engine/MUNGA_L4/L4NET.H index 607f778..d4f5328 100644 --- a/engine/MUNGA_L4/L4NET.H +++ b/engine/MUNGA_L4/L4NET.H @@ -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 ~~~~~~~~~~~~~~~~~~~~~~~~~~