From 22421fb41844dbf4d6b2e8b69685f557728110c7 Mon Sep 17 00:00:00 2001 From: Cyd Date: Sun, 12 Jul 2026 19:38:37 -0500 Subject: [PATCH] NetTransport seam: the wire moves behind an interface The Steam-multiplayer prerequisite from the design doc: L4NET keeps hosts, message queues, and the deterministic mesh ordering, while connect/listen/accept/close, send/receive, startup/cleanup, the local interface list, and ip[:port] parsing move behind NetTransport (L4NETTRANSPORT.h). WinsockNetTransport carries the existing TCP behavior over verbatim - including the connect retry-while-refused loop the egg-ACK ordering relies on - and is the process default; NetTransport_Set installs a replacement before the network manager comes up. L4STEAMTRANSPORT.h documents the ISteamNetworkingSockets mapping per method (FakeIP keeps [pilots] entries as IPv4 strings) behind RP412_STEAM until the Steamworks SDK lands. Also fixed in passing: the ExclusiveBroadcast path sent sizeof(network_packet) - four bytes of pointer - instead of the message size, which would have sheared the stream framing had it ever fired. Verified: single-player race cycle (menu, race, results, menu, race) green; -net 8000 boots, listens for a console through the transport, and idles stable. Co-Authored-By: Claude Fable 5 --- MUNGA_L4/L4NET.CPP | 296 +++++-------------------------- MUNGA_L4/L4NETTRANSPORT.cpp | 325 ++++++++++++++++++++++++++++++++++ MUNGA_L4/L4NETTRANSPORT.h | 126 +++++++++++++ MUNGA_L4/L4STEAMTRANSPORT.h | 128 +++++++++++++ MUNGA_L4/Munga_L4.vcxproj | 2 + docs/RP412-FRONTEND-DESIGN.md | 13 ++ 6 files changed, 640 insertions(+), 250 deletions(-) create mode 100644 MUNGA_L4/L4NETTRANSPORT.cpp create mode 100644 MUNGA_L4/L4NETTRANSPORT.h create mode 100644 MUNGA_L4/L4STEAMTRANSPORT.h diff --git a/MUNGA_L4/L4NET.CPP b/MUNGA_L4/L4NET.CPP index 2ef6d0b..26640ba 100644 --- a/MUNGA_L4/L4NET.CPP +++ b/MUNGA_L4/L4NET.CPP @@ -19,6 +19,7 @@ #include "l4app.h" #include "l4host.h" #include "l4net.h" +#include "l4nettransport.h" #include "..\munga\mission.h" #include "..\munga\notation.h" //#include @@ -226,11 +227,10 @@ L4NetworkManager::L4NetworkManager(): //Function_Ptr = &Net_Common_Ptr->Function; //Buffer_Length_Ptr = &Net_Common_Ptr->Buffer_Length; - int iResult = WSAStartup(MAKEWORD(2,2), wsaData); - if(iResult != NO_ERROR) + if (!NetTransport_Get()->Startup()) { - DEBUG_STREAM << "ERROR: WSAStartup() failed with " << iResult << "!\n" << std::flush; - WSACleanup(); + DEBUG_STREAM << "ERROR: network transport startup failed!\n" << std::flush; + NetTransport_Get()->Cleanup(); PostQuitMessage(AbortExitCodeID); } consoleListenerSocket = NULL; @@ -271,7 +271,7 @@ L4NetworkManager::L4NetworkManager(): if(addresses == NULL) { DEBUG_STREAM<<"ERROR: GetMyAddress() failed!\n"; - WSACleanup(); + NetTransport_Get()->Cleanup(); Fail("Unable to initialize the network"); } @@ -319,39 +319,11 @@ void L4NetworkManager::CreateConsoleHost() // CONSOLE_NET_PORT, // Local port // 0, // Remote port (don't care in this case) // 0); // Network address (don't care in this case) - consoleListenerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + consoleListenerSocket = (SOCKET) NetTransport_Get()->Listen(networkPort, 1); if(consoleListenerSocket == INVALID_SOCKET) { - DEBUG_STREAM << "ERROR: Could not create console listener socket; socket() failed with " << WSAGetLastError() << "!\n" << std::flush; - WSACleanup(); - PostQuitMessage(AbortExitCodeID); - return; - } - sockaddr_in localEndpoint; - memset(&localEndpoint, 0, sizeof(localEndpoint)); - localEndpoint.sin_family = AF_INET; - localEndpoint.sin_port = htons(networkPort); - localEndpoint.sin_addr.S_un.S_addr = INADDR_ANY; - if(bind(consoleListenerSocket, (sockaddr*)&localEndpoint, sizeof(localEndpoint))) - { - DEBUG_STREAM << "ERROR: Could not bind console listener socket; bind() failed with " << WSAGetLastError() << "!\n" << std::flush; - WSACleanup(); - PostQuitMessage(AbortExitCodeID); - return; - } - if(listen(consoleListenerSocket, 1)) - { - DEBUG_STREAM << "ERROR: Could not listen on console listener socket; listen() failed with " << WSAGetLastError() << "!\n" << std::flush; - WSACleanup(); - PostQuitMessage(AbortExitCodeID); - return; - } - //set to non blocking - unsigned long enable = 1; - if(ioctlsocket(consoleListenerSocket, FIONBIO, &enable)) - { - DEBUG_STREAM << "ERROR: Could not set console listener socket to nonblocking; ioctlsocket() failed with " << WSAGetLastError() << "!\n" << std::flush; - WSACleanup(); + DEBUG_STREAM << "ERROR: Could not open the console listener!\n" << std::flush; + NetTransport_Get()->Cleanup(); PostQuitMessage(AbortExitCodeID); return; } @@ -490,7 +462,6 @@ void // listen = False; remoteHostCount = 0; - int bufferSize = sizeof(SOCKADDR_IN); while ((mission_host_data = mission_host_iterator.ReadAndNext()) != NULL) { Check(mission_host_data); @@ -503,7 +474,7 @@ void //VERIFY: are these IP addresses? //net_address = ResolveAddress(host_name); //net_address = inet_addr((char *)host_name); - WSAStringToAddressA((LPSTR)host_name, AF_INET, NULL, (LPSOCKADDR)&net_address, &bufferSize); + NetTransport_Get()->Resolve((LPSTR)host_name, &net_address); if (net_address.sin_port == 0) net_address.sin_port = htons(localGamePort); @@ -760,7 +731,7 @@ L4NetworkManager::~L4NetworkManager() delete my_l4host; } - WSACleanup(); + NetTransport_Get()->Cleanup(); delete wsaData; wsaData = NULL; //Unregister_Pointer(Net_Common_Ptr); @@ -992,8 +963,7 @@ void L4NetworkManager::HostDisconnectedMessageHandler(HostDisconnectedMessage* H // // Post a listen for a console so it can reconnect if it wants to. // - shutdown(gameListenerSocket, SD_BOTH); - closesocket(gameListenerSocket); + NetTransport_Get()->Close(gameListenerSocket); gameListenerSocket = NULL; //unsigned long console_socket = OpenConnection( // NETNUB_TCP_LISTEN, @@ -1166,7 +1136,7 @@ void L4NetworkManager::Send( //Net_Common_Ptr->Buffer_Length = (short)SEND_BUFFER_SIZE(packet_size); //send_packet_request->Socket_Ptr = l4host->GetNetworkSocket(); //NetNub::SendCommand(); - send(l4host->GetNetworkSocket(), (char *)my_temp_packet, packet_size, 0); + NetTransport_Get()->Send(l4host->GetNetworkSocket(), my_temp_packet, packet_size); free(my_temp_packet); // Check for errors and abort if there were any //#if defined(TRACE_SEND_BUFFER) @@ -1325,12 +1295,12 @@ Logical L4NetworkManager::SendMessageToNetnub( // SendPacketReturnPtr send_packet_return = // (SendPacketReturnPtr)Net_Common_Ptr->Shared_Memory_Buffer; //#endif - //Net_Common_Ptr->Buffer_Length = + //Net_Common_Ptr->Buffer_Length = // (unsigned short)SEND_BUFFER_SIZE(munga_network_message_size); - //Net_Common_Ptr->Function = + //Net_Common_Ptr->Function = // NETNUB_SEND_PACKET; //NetNub::SendCommand(); - send(receiving_host->GetNetworkSocket(), (char *)network_packet, munga_network_message_size, 0); + NetTransport_Get()->Send(receiving_host->GetNetworkSocket(), network_packet, munga_network_message_size); free(network_packet); #if defined(TRACE_SEND_BUFFER) @@ -1876,7 +1846,10 @@ void //NetNub::SendCommand(); for(int i=0; iGetNetworkSocket(), (char *)network_packet, sizeof(network_packet), 0); + // was send(..., sizeof(network_packet), 0): sizeof a POINTER - + // four bytes of packet, which would shear the stream framing + // if this path ever fired + NetTransport_Get()->Send(l4host_array[i]->GetNetworkSocket(), network_packet, munga_network_message_size); } free(network_packet); @@ -2117,15 +2090,15 @@ Logical L4NetworkManager::CheckBuffers(NetworkPacket *network_packet) if(remote_host->GetHostType() == ConsoleHostType) { - if((tempSocket = accept(consoleListenerSocket, NULL, 0)) != INVALID_SOCKET) + if((tempSocket = (SOCKET) NetTransport_Get()->Accept(consoleListenerSocket)) != INVALID_SOCKET) { - closesocket(consoleListenerSocket); + NetTransport_Get()->Close(consoleListenerSocket); consoleListenerSocket = INVALID_SOCKET; } } else { - if((tempSocket = accept(gameListenerSocket, NULL, 0)) == INVALID_SOCKET) + if((tempSocket = (SOCKET) NetTransport_Get()->Accept(gameListenerSocket)) == INVALID_SOCKET) { continue; } @@ -2255,45 +2228,21 @@ Logical L4NetworkManager::CheckBuffers(NetworkPacket *network_packet) // call the network server //NetShare(); void* buffer = malloc(space_left); - int received = recv(remote_host->GetNetworkSocket(), (char*)buffer, space_left, 0); + // transport normalizes the would-block/reset cases: + // >0 = data, ReceiveDisconnected = drop, ReceiveNoData = nothing yet + int received = NetTransport_Get()->Receive(remote_host->GetNetworkSocket(), buffer, space_left); // if we received data, copy it over into the buffer for this host - //switch(Net_Common_Ptr->Status) if(received > 0) { - //case NETNUB_RECEIVED_PACKET: // copy the received data over to the host's data buffer Mem_Copy( &remote_host->pad_buffer[remote_host->pad_tail], - //Net_Common_Ptr->Shared_Memory_Buffer, buffer, - //Net_Common_Ptr->Buffer_Length, received, space_left); - //remote_host->pad_tail += Net_Common_Ptr->Buffer_Length; remote_host->pad_tail += received; - //break; } - //case NETNUB_OK: - else if(received == SOCKET_ERROR) - { - //this indicates that no data is available and the socket is non blocking - //break; - DWORD error = WSAGetLastError(); - switch (error) - { - case WSAECONNRESET: - // this will cause us to execute our disconnect code - received = 0; - break; - case WSAEWOULDBLOCK: - // this is expected when there is no data - break; - default: - DEBUG_STREAM << "L4NetworkManager::CheckBuffers: Socket recv returned an unexpected error: WSAGetLastError = " << error << std::endl << std::flush; - } - } - // case NETNUB_STREAM_DISCONNECTED: - if(received == 0) + if(received == NetTransport::ReceiveDisconnected) { { HostDisconnectedMessage myHostDisconnected( @@ -2585,31 +2534,21 @@ void //WinSock support :ADB 01/06/07 NetworkAddress* L4NetworkManager::GetMyAddress() { - char name[255]; - PHOSTENT hostinfo; + // the transport owns interface enumeration (it appends loopback) + enum { maxLocalAddresses = 16 }; + unsigned long local_addresses[maxLocalAddresses]; - if (gethostname(name, sizeof(name)) != 0) + int count = NetTransport_Get()->GetLocalAddresses(local_addresses, maxLocalAddresses); + if (count <= 0) { - DEBUG_STREAM << "ERROR: gethostname() failed!" << std::endl << std::flush; return NULL; } - if ((hostinfo = gethostbyname(name)) == NULL) - { - DEBUG_STREAM << "ERROR: gethostbyname() failed!" << std::endl << std::flush; - return NULL; - } + NetworkAddress* myAddresses = new NetworkAddress[count]; + for (int i=0; ih_addr_list[num_addresses]; num_addresses++); - - NetworkAddress* myAddresses = new NetworkAddress[num_addresses + 1]; - for (int i=0; ih_addr_list[i]); - - // add 127.0.0.1 to list - myAddresses[num_addresses++] = (NetworkAddress)0x0100007F; - return myAddresses; } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -2620,54 +2559,11 @@ NetworkAddress* L4NetworkManager::GetMyAddress() //WinSock support :ADB 01/06/07 bool L4NetworkManager::ResolveAddress(CString host_name, SOCKADDR_IN *address) { - //if (strstr(host_name, ":")) - { - // this address contains a colon so we'll do things a little differently - int bufferSize = sizeof(SOCKADDR_IN); - WSAStringToAddressA((LPSTR)host_name, AF_INET, NULL, (LPSOCKADDR)address, &bufferSize); - if (address->sin_port == 0) - address->sin_port = htons(GAME_NET_PORT); - return true; - } - - addrinfo* hostAddrinfo = NULL; - addrinfo aiHints; - memset(&aiHints, 0, sizeof(aiHints)); - aiHints.ai_family = AF_INET; - aiHints.ai_socktype = SOCK_STREAM; - aiHints.ai_protocol = IPPROTO_TCP; - - int iResult = getaddrinfo((char*)host_name, NULL, &aiHints , &hostAddrinfo); - if(iResult != 0) - { - DEBUG_STREAM<<"ERROR: getaddrinfo() failed with " << WSAGetLastError() << "!\n"; - return NULL; - } - - addrinfo* addr = hostAddrinfo; - while(addr != NULL) - { - if(addr->ai_addr->sa_family == AF_INET) - break; - addr = addr->ai_next; - } - - memset(address, 0, sizeof(SOCKADDR_IN)); - *address = *((sockaddr_in*)&addr->ai_addr->sa_data); - if (address->sin_port == 32778) - address->sin_port = htons(CONSOLE_NET_PORT); - - freeaddrinfo(hostAddrinfo); - - // - // Check the status returns and return the address if we got one ok - // - if(!address || address->sin_addr.S_un.S_addr == 0 || address->sin_port == 0) - { - DEBUG_STREAM << "Couldn't resolve " << host_name << " to a net address\n"; - Fail("unresolvable network address\n"); - } - + // numeric ip[:port]; the game port fills in when none was given + // (the old DNS/getaddrinfo path below the early return was dead) + NetTransport_Get()->Resolve((LPSTR)host_name, address); + if (address->sin_port == 0) + address->sin_port = htons(GAME_NET_PORT); return true; } @@ -2680,9 +2576,7 @@ bool L4NetworkManager::CheckSocket(SOCKET socket, SOCKADDR_IN *remoteEndpoint) { if (remoteEndpoint) { - int size = sizeof(SOCKADDR_IN); - memset(remoteEndpoint, 0, size); - if(!getpeername(socket, (sockaddr*)remoteEndpoint, &size)) + if (NetTransport_Get()->GetRemoteAddress(socket, remoteEndpoint)) return true; } return false; @@ -2738,126 +2632,29 @@ SOCKET L4NetworkManager::OpenConnection( if(connection_type == NETNUB_TCP_OPEN) { - DEBUG_STREAM << "Opening connection to " << inet_ntoa(*(in_addr*)&internet_address) << ":" << remote_port << "...\n" << std::flush; - - SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); - if(sock == INVALID_SOCKET) - { - DEBUG_STREAM << "ERROR: socket() failed with " << WSAGetLastError() << "!\n"; - return INVALID_SOCKET; - } - - bool reuseAddr = true; - if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char*)&reuseAddr, sizeof(bool))) - { - DEBUG_STREAM << "ERROR: Could not set SO_REUSEADDR on socket; setsockopt() failed with " << WSAGetLastError() << "!\n" << std::flush; - WSACleanup(); - PostQuitMessage(AbortExitCodeID); - return INVALID_SOCKET; - } - - sockaddr_in localEndpoint; - memset(&localEndpoint, 0, sizeof(localEndpoint)); - localEndpoint.sin_family = AF_INET; - localEndpoint.sin_port = htons(local_port); - localEndpoint.sin_addr.S_un.S_addr = INADDR_ANY; - if(bind(sock, (sockaddr*)&localEndpoint, sizeof(localEndpoint))) - { - DEBUG_STREAM << "ERROR: Could not bind local socket; bind() failed with " << WSAGetLastError() << "!\n" << std::flush; - WSACleanup(); - PostQuitMessage(AbortExitCodeID); - return INVALID_SOCKET; - } - sockaddr_in remoteEndpoint; memset(&remoteEndpoint, 0, sizeof(remoteEndpoint)); remoteEndpoint.sin_family = AF_INET; //VERIFY: Network vs Host byte order? remoteEndpoint.sin_addr.S_un.S_addr = internet_address; remoteEndpoint.sin_port = htons(remote_port); - int wsaError, iResult; - do - { - iResult = connect(sock, (sockaddr*)&remoteEndpoint, sizeof(remoteEndpoint)); - - if (iResult != 0) - { - wsaError = WSAGetLastError(); - } - } while (iResult != 0 && wsaError == 10061); - - if(iResult != 0) - { - DEBUG_STREAM << "ERROR: connect() failed with " << wsaError << "!\n" << std::flush; - return INVALID_SOCKET; - } - - //set to non blocking - unsigned long enable = 1; - if(ioctlsocket(sock, FIONBIO, &enable)) - { - DEBUG_STREAM << "ERROR: Could not set actively opened socket to nonblocking; ioctlsocket() failed with " << WSAGetLastError() << "!\n" << std::flush; - WSACleanup(); - PostQuitMessage(AbortExitCodeID); - } - return sock; + return (SOCKET) NetTransport_Get()->Connect(&remoteEndpoint, local_port); } else if(connection_type == NETNUB_TCP_LISTEN) { if(gameListenerSocket == NULL) { - DEBUG_STREAM << "Starting to listen on port " << local_port << "...\n" << std::flush; - - gameListenerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + gameListenerSocket = (SOCKET) NetTransport_Get()->Listen(local_port, 25); if(gameListenerSocket == INVALID_SOCKET) { - DEBUG_STREAM << "ERROR: Could not create game listener socket; socket() failed with " << WSAGetLastError() << "!\n" << std::flush; - WSACleanup(); + DEBUG_STREAM << "ERROR: Could not open the game listener on port " << local_port << "!\n" << std::flush; PostQuitMessage(AbortExitCodeID); } - - bool reuseAddr = true; - if (setsockopt(gameListenerSocket, SOL_SOCKET, SO_REUSEADDR, (char*)&reuseAddr, sizeof(bool))) - { - DEBUG_STREAM << "ERROR: Could not set SO_REUSEADDR on game listener socket; setsockopt() failed with " << WSAGetLastError() << "!\n" << std::flush; - WSACleanup(); - PostQuitMessage(AbortExitCodeID); - return INVALID_SOCKET; - } - - sockaddr_in localEndpoint; - memset(&localEndpoint, 0, sizeof(localEndpoint)); - localEndpoint.sin_family = AF_INET; - localEndpoint.sin_port = htons(local_port); - localEndpoint.sin_addr.S_un.S_addr = INADDR_ANY; - if(bind(gameListenerSocket, (sockaddr*)&localEndpoint, sizeof(localEndpoint))) - { - DEBUG_STREAM << "ERROR: Could not bind game listener socket; bind() failed with " << WSAGetLastError() << "!\n" << std::flush; - WSACleanup(); - PostQuitMessage(AbortExitCodeID); - return INVALID_SOCKET; - } - if(listen(gameListenerSocket, 25)) - { - DEBUG_STREAM << "ERROR: Could not listen on game listener socket; listen() failed with " << WSAGetLastError() << "!\n" << std::flush; - WSACleanup(); - PostQuitMessage(AbortExitCodeID); - return INVALID_SOCKET; - } - //set to non blocking - unsigned long enable = 1; - if(ioctlsocket(gameListenerSocket, FIONBIO, &enable)) - { - DEBUG_STREAM << "ERROR: Could not set game listener socket to nonblocking; ioctlsocket() failed with " << WSAGetLastError() << "!\n" << std::flush; - WSACleanup(); - PostQuitMessage(AbortExitCodeID); - return INVALID_SOCKET; - } } else DEBUG_STREAM << "Listen requested on port " << local_port << " but gameListenerSocket already existed!\n" << std::flush; - + return INVALID_SOCKET; } else @@ -2885,8 +2682,7 @@ void L4NetworkManager::CloseConnection(SOCKET socket_ptr) // socket address fro //close_request = (TCPCloseRequestPtr)Net_Common_Ptr->Shared_Memory_Buffer; //close_request->Socket_Ptr = socket_ptr; //NetNub::SendCommand(); - shutdown(socket_ptr, SD_BOTH); - closesocket(socket_ptr); + NetTransport_Get()->Close(socket_ptr); // Check for errors //if(Net_Common_Ptr->Status != NETNUB_OK) //{ diff --git a/MUNGA_L4/L4NETTRANSPORT.cpp b/MUNGA_L4/L4NETTRANSPORT.cpp new file mode 100644 index 0000000..5c5d548 --- /dev/null +++ b/MUNGA_L4/L4NETTRANSPORT.cpp @@ -0,0 +1,325 @@ +#include "mungal4.h" +#pragma hdrstop + +#include "l4nettransport.h" +#include + +//######################################################################## +// WinsockNetTransport - the TCP wire the arcade always used, moved +// verbatim out of L4NET.CPP behind the NetTransport seam. Behavior +// notes preserved from the original: +// - Connect retries while the remote refuses (WSAECONNREFUSED): the +// console feeds eggs in [pilots] order with ACKs, so earlier pods +// are listening before later pods open - the retry covers the race. +// - Sockets go nonblocking once connected; listeners are nonblocking +// from creation. +//######################################################################## + +namespace +{ + class WinsockNetTransport: + public NetTransport + { + public: + WinsockNetTransport(): + started(False) + { + } + + Logical + Startup() + { + if (started) + { + return True; + } + int result = WSAStartup(MAKEWORD(2, 2), &winsockData); + if (result != NO_ERROR) + { + DEBUG_STREAM << "ERROR: WSAStartup() failed with " << result + << "!\n" << std::flush; + return False; + } + started = True; + return True; + } + + void + Cleanup() + { + if (started) + { + WSACleanup(); + started = False; + } + } + + int + GetLocalAddresses( + unsigned long *addresses, + int max_count + ) + { + char name[255]; + PHOSTENT hostinfo; + + if (gethostname(name, sizeof(name)) != 0) + { + DEBUG_STREAM << "ERROR: gethostname() failed!" << std::endl << std::flush; + return 0; + } + if ((hostinfo = gethostbyname(name)) == NULL) + { + DEBUG_STREAM << "ERROR: gethostbyname() failed!" << std::endl << std::flush; + return 0; + } + + int count = 0; + for (int i = 0; hostinfo->h_addr_list[i] != NULL && count < max_count; ++i) + { + addresses[count++] = *((unsigned long *) hostinfo->h_addr_list[i]); + } + + // loopback rounds out the list (single-machine testing) + if (count < max_count) + { + addresses[count++] = 0x0100007F; + } + return count; + } + + Logical + Resolve( + const char *host_name, + SOCKADDR_IN *address + ) + { + // numeric ip[:port] only - the egg carries addresses, not + // names; port stays 0 when absent (caller applies default) + int buffer_size = sizeof(SOCKADDR_IN); + return WSAStringToAddressA( + (LPSTR) host_name, AF_INET, NULL, + (LPSOCKADDR) address, &buffer_size) == 0; + } + + Connection + Connect( + const SOCKADDR_IN *remote, + int local_port + ) + { + DEBUG_STREAM << "Opening connection to " + << inet_ntoa(remote->sin_addr) << ":" << ntohs(remote->sin_port) + << "...\n" << std::flush; + + SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (sock == INVALID_SOCKET) + { + DEBUG_STREAM << "ERROR: socket() failed with " + << WSAGetLastError() << "!\n"; + return InvalidConnection; + } + + bool reuse_address = true; + if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, + (char *) &reuse_address, sizeof(bool))) + { + DEBUG_STREAM << "ERROR: Could not set SO_REUSEADDR on socket; setsockopt() failed with " + << WSAGetLastError() << "!\n" << std::flush; + closesocket(sock); + return InvalidConnection; + } + + // bind the game port: the mesh identifies peers by + // address AND port, so the source port must be ours + sockaddr_in local_endpoint; + memset(&local_endpoint, 0, sizeof(local_endpoint)); + local_endpoint.sin_family = AF_INET; + local_endpoint.sin_port = htons((unsigned short) local_port); + local_endpoint.sin_addr.S_un.S_addr = INADDR_ANY; + if (bind(sock, (sockaddr *) &local_endpoint, sizeof(local_endpoint))) + { + DEBUG_STREAM << "ERROR: Could not bind local socket; bind() failed with " + << WSAGetLastError() << "!\n" << std::flush; + closesocket(sock); + return InvalidConnection; + } + + int wsa_error = 0, result; + do + { + result = connect(sock, (sockaddr *) remote, sizeof(SOCKADDR_IN)); + if (result != 0) + { + wsa_error = WSAGetLastError(); + } + } while (result != 0 && wsa_error == WSAECONNREFUSED); + + if (result != 0) + { + DEBUG_STREAM << "ERROR: connect() failed with " + << wsa_error << "!\n" << std::flush; + closesocket(sock); + return InvalidConnection; + } + + unsigned long enable = 1; + if (ioctlsocket(sock, FIONBIO, &enable)) + { + DEBUG_STREAM << "ERROR: Could not set actively opened socket to nonblocking; ioctlsocket() failed with " + << WSAGetLastError() << "!\n" << std::flush; + closesocket(sock); + return InvalidConnection; + } + return (Connection) sock; + } + + Connection + Listen( + int local_port, + int backlog + ) + { + DEBUG_STREAM << "Starting to listen on port " << local_port + << "...\n" << std::flush; + + SOCKET listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (listener == INVALID_SOCKET) + { + DEBUG_STREAM << "ERROR: Could not create listener socket; socket() failed with " + << WSAGetLastError() << "!\n" << std::flush; + return InvalidConnection; + } + + bool reuse_address = true; + if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, + (char *) &reuse_address, sizeof(bool))) + { + DEBUG_STREAM << "ERROR: Could not set SO_REUSEADDR on listener socket; setsockopt() failed with " + << WSAGetLastError() << "!\n" << std::flush; + closesocket(listener); + return InvalidConnection; + } + + sockaddr_in local_endpoint; + memset(&local_endpoint, 0, sizeof(local_endpoint)); + local_endpoint.sin_family = AF_INET; + local_endpoint.sin_port = htons((unsigned short) local_port); + local_endpoint.sin_addr.S_un.S_addr = INADDR_ANY; + if (bind(listener, (sockaddr *) &local_endpoint, sizeof(local_endpoint))) + { + DEBUG_STREAM << "ERROR: Could not bind listener socket; bind() failed with " + << WSAGetLastError() << "!\n" << std::flush; + closesocket(listener); + return InvalidConnection; + } + if (listen(listener, backlog)) + { + DEBUG_STREAM << "ERROR: Could not listen on listener socket; listen() failed with " + << WSAGetLastError() << "!\n" << std::flush; + closesocket(listener); + return InvalidConnection; + } + + unsigned long enable = 1; + if (ioctlsocket(listener, FIONBIO, &enable)) + { + DEBUG_STREAM << "ERROR: Could not set listener socket to nonblocking; ioctlsocket() failed with " + << WSAGetLastError() << "!\n" << std::flush; + closesocket(listener); + return InvalidConnection; + } + return (Connection) listener; + } + + Connection + Accept(Connection listener) + { + SOCKET accepted = accept((SOCKET) listener, NULL, 0); + if (accepted == INVALID_SOCKET) + { + return InvalidConnection; + } + return (Connection) accepted; + } + + void + Close(Connection connection) + { + shutdown((SOCKET) connection, SD_BOTH); + closesocket((SOCKET) connection); + } + + int + Send( + Connection connection, + const void *data, + int size + ) + { + return send((SOCKET) connection, (const char *) data, size, 0); + } + + int + Receive( + Connection connection, + void *buffer, + int size + ) + { + int received = recv((SOCKET) connection, (char *) buffer, size, 0); + if (received > 0) + { + return received; + } + if (received == 0) + { + return ReceiveDisconnected; + } + + DWORD error = WSAGetLastError(); + switch (error) + { + case WSAECONNRESET: + // hard drop reads the same as an orderly close upstairs + return ReceiveDisconnected; + case WSAEWOULDBLOCK: + return ReceiveNoData; + default: + DEBUG_STREAM << "WinsockNetTransport::Receive: recv returned an unexpected error: WSAGetLastError = " + << error << std::endl << std::flush; + return ReceiveNoData; + } + } + + Logical + GetRemoteAddress( + Connection connection, + SOCKADDR_IN *address + ) + { + int size = sizeof(SOCKADDR_IN); + memset(address, 0, size); + return getpeername((SOCKET) connection, (sockaddr *) address, &size) == 0; + } + + private: + Logical started; + WSADATA winsockData; + }; + + WinsockNetTransport gWinsockTransport; + NetTransport *gTransport = &gWinsockTransport; +} + +NetTransport * + NetTransport_Get() +{ + return gTransport; +} + +void + NetTransport_Set(NetTransport *transport) +{ + gTransport = (transport != NULL) ? transport : &gWinsockTransport; +} diff --git a/MUNGA_L4/L4NETTRANSPORT.h b/MUNGA_L4/L4NETTRANSPORT.h new file mode 100644 index 0000000..16827e7 --- /dev/null +++ b/MUNGA_L4/L4NETTRANSPORT.h @@ -0,0 +1,126 @@ +//===========================================================================// +// File: l4nettransport.h // +// Project: MUNGA Brick: Network Transport Seam // +// Contents: The wire interface under the network manager // +//---------------------------------------------------------------------------// +// Copyright (C) 1994-1995, Virtual World Entertainment, Inc. // +// PROPRIETARY AND CONFIDENTIAL // +//===========================================================================// + +#pragma once + +#include "..\munga\style.h" +#include + +//######################################################################## +// NetTransport - the seam between the network manager's mesh/console +// logic and the wire (docs/RP412-FRONTEND-DESIGN.md section 3). Mirrors +// the RIOBase pattern: L4NET keeps hosts, message queues, and the +// deterministic mesh ordering; only connect/listen/send/recv live +// behind this interface. +// +// Implementations: +// WinsockNetTransport (l4nettransport.cpp) - TCP; the arcade/LAN wire +// SteamNetTransport (future) - ISteamNetworkingSockets +// P2P over SDR +// +// Addresses stay SOCKADDR_IN-shaped on purpose: Steam's FakeIP system +// hands out fake IPv4 addresses for exactly this kind of engine, so +// the [pilots] list keeps working as ip[:port] strings in both worlds. +//######################################################################## + +class NetTransport +{ +public: + // Opaque connection handle. SOCKET for Winsock (SOCKET is UINT_PTR), + // HSteamNetConnection for Steam - both fit; callers must not + // interpret it. + typedef UINT_PTR Connection; + static const Connection InvalidConnection = (Connection) ~0; // == INVALID_SOCKET + + // Receive() results when no payload came back + enum + { + ReceiveDisconnected = 0, // orderly close or connection reset + ReceiveNoData = -1 // nothing pending (connections are nonblocking) + }; + + virtual ~NetTransport() + { + } + + //--------------------------------------------------------------- + // Lifecycle + //--------------------------------------------------------------- + virtual Logical + Startup() = 0; + virtual void + Cleanup() = 0; + + //--------------------------------------------------------------- + // Addressing: the local interface list (the mesh identifies + // "which [pilots] entry is me" against it) and numeric ip[:port] + // parsing. Port 0 in the result means "caller applies default". + //--------------------------------------------------------------- + virtual int + GetLocalAddresses( + unsigned long *addresses, + int max_count + ) = 0; + virtual Logical + Resolve( + const char *host_name, + SOCKADDR_IN *address + ) = 0; + + //--------------------------------------------------------------- + // Connections (the deterministic mesh + the console channel). + // Connect blocks until the remote end accepts (the mesh relies + // on retry-until-up ordering), then goes nonblocking. Listeners + // are nonblocking from the start; Accept polls one. + //--------------------------------------------------------------- + virtual Connection + Connect( + const SOCKADDR_IN *remote, + int local_port + ) = 0; + virtual Connection + Listen( + int local_port, + int backlog + ) = 0; + virtual Connection + Accept(Connection listener) = 0; + virtual void + Close(Connection connection) = 0; + + //--------------------------------------------------------------- + // Data plane (nonblocking) + //--------------------------------------------------------------- + virtual int + Send( + Connection connection, + const void *data, + int size + ) = 0; + virtual int + Receive( + Connection connection, + void *buffer, + int size + ) = 0; + + // remote endpoint of a live connection (mesh identity checks) + virtual Logical + GetRemoteAddress( + Connection connection, + SOCKADDR_IN *address + ) = 0; +}; + +// The process-wide transport. Defaults to Winsock TCP; a Steam build +// installs its transport BEFORE the network manager comes up. +NetTransport * + NetTransport_Get(); +void + NetTransport_Set(NetTransport *transport); diff --git a/MUNGA_L4/L4STEAMTRANSPORT.h b/MUNGA_L4/L4STEAMTRANSPORT.h new file mode 100644 index 0000000..0644081 --- /dev/null +++ b/MUNGA_L4/L4STEAMTRANSPORT.h @@ -0,0 +1,128 @@ +//===========================================================================// +// File: l4steamtransport.h // +// Project: MUNGA Brick: Steam Network Transport (skeleton) // +// Contents: NetTransport over ISteamNetworkingSockets - awaiting the SDK // +//---------------------------------------------------------------------------// +// Copyright (C) 1994-1995, Virtual World Entertainment, Inc. // +// PROPRIETARY AND CONFIDENTIAL // +//===========================================================================// + +#pragma once + +//######################################################################## +// SteamNetTransport - the retail wire. NOT BUILT YET: everything below +// is guarded by RP412_STEAM, which stays undefined until the Steamworks +// SDK is dropped into the tree (needs a Steamworks partner login to +// download; put it at extern\steamworks and add steam_api.lib + +// steam_api.dll to the link/dist). +// +// Method mapping (see also docs/RP412-FRONTEND-DESIGN.md section 3): +// +// Startup SteamAPI_Init + ISteamNetworkingUtils::InitRelay +// NetworkAccessAuthorization; then +// BeginAsyncRequestFakeIP so this pod has a fake +// IPv4 identity to put in the [pilots] list. +// Cleanup SteamAPI_Shutdown. +// GetLocalAddresses our allocated FakeIP (the mesh identifies +// "which pilots entry is me" against it, same as +// the LAN build matches its interface list). +// Resolve parse ip[:port] exactly like Winsock - FakeIPs +// ARE IPv4 strings, so egg text stays unchanged. +// Connect ConnectP2PByFakeIP (SDR picks the route, NAT +// traversal and IP privacy come free); block on +// the connection state callback until Connected, +// mirroring the TCP retry-until-accepted loop. +// Listen CreateListenSocketP2PFakeIP(port index); the +// game port / console port pair becomes fake-port +// index 0 / 1. +// Accept poll the connection-request callback queue, +// AcceptConnection, return the HSteamNetConnection +// (it fits the NetTransport::Connection handle). +// Close CloseConnection(reason 0, linger enabled). +// Send SendMessageToConnection with +// k_nSteamNetworkingSend_Reliable: the engine's +// stream framing (NetworkPacketHeader + payload) +// assumes ordered reliable delivery, exactly what +// a reliable Steam message lane provides. +// Receive ReceiveMessagesOnConnection; copy into the +// host's pad buffer, normalize "no messages" to +// ReceiveNoData and connection-closed callbacks +// to ReceiveDisconnected. +// GetRemoteAddress GetConnectionInfo -> m_addrRemote (the FakeIP), +// so the mesh's identity checks keep working on +// SOCKADDR_IN values. +// +// Lobby flow that drives this transport (front-end work, not here): +// the lobby owner collects members' FakeIPs + loadouts via lobby data, +// builds the canonical egg with the ordered [pilots] list of FakeIPs, +// distributes it, and doubles as the console (RPL4CONSOLE marshals the +// mission; StopMissionMessage goes out over the console channel). +// +// Install with NetTransport_Set(new SteamNetTransport) BEFORE the +// network manager constructs (front of WinMain, when -steam / a Steam +// launch is detected). +//######################################################################## + +#ifdef RP412_STEAM + +#include "l4nettransport.h" + +class SteamNetTransport: + public NetTransport +{ +public: + SteamNetTransport(); + ~SteamNetTransport(); + + Logical + Startup(); + void + Cleanup(); + + int + GetLocalAddresses( + unsigned long *addresses, + int max_count + ); + Logical + Resolve( + const char *host_name, + SOCKADDR_IN *address + ); + + Connection + Connect( + const SOCKADDR_IN *remote, + int local_port + ); + Connection + Listen( + int local_port, + int backlog + ); + Connection + Accept(Connection listener); + void + Close(Connection connection); + + int + Send( + Connection connection, + const void *data, + int size + ); + int + Receive( + Connection connection, + void *buffer, + int size + ); + + Logical + GetRemoteAddress( + Connection connection, + SOCKADDR_IN *address + ); +}; + +#endif // RP412_STEAM diff --git a/MUNGA_L4/Munga_L4.vcxproj b/MUNGA_L4/Munga_L4.vcxproj index e721fc9..3a9191d 100644 --- a/MUNGA_L4/Munga_L4.vcxproj +++ b/MUNGA_L4/Munga_L4.vcxproj @@ -248,6 +248,7 @@ + @@ -438,6 +439,7 @@ + diff --git a/docs/RP412-FRONTEND-DESIGN.md b/docs/RP412-FRONTEND-DESIGN.md index f401ec9..acc1530 100644 --- a/docs/RP412-FRONTEND-DESIGN.md +++ b/docs/RP412-FRONTEND-DESIGN.md @@ -92,6 +92,19 @@ Transport seam: mirror the `RIOBase` pattern at the L4NET layer — a `L4Host` keeps its identity/queue role; only connect/listen/send/recv move behind the interface. +**Status (2026-07-12): the seam is IN.** `MUNGA_L4/L4NETTRANSPORT.h/.cpp` +defines `NetTransport` (startup/cleanup, local-address list, ip[:port] +resolve, connect/listen/accept/close, send/receive, remote-address) with +`WinsockNetTransport` as the process default; L4NET.CPP contains no raw +Winsock calls anymore. `MUNGA_L4/L4STEAMTRANSPORT.h` documents the +per-method ISteamNetworkingSockets mapping (FakeIP keeps the `[pilots]` +list as IPv4 strings) and stays behind `RP412_STEAM` until the Steamworks +SDK is dropped at `extern\steamworks` (partner-login download). Remaining +Steam work: SDK drop → implement `SteamNetTransport` → lobby UI in the +front end (owner collects FakeIPs/loadouts via lobby data, builds and +distributes the canonical egg, runs the RPL4CONSOLE marshal) → install +with `NetTransport_Set` at WinMain when launched under Steam. + ## 4. Implementation options (decide here) **A. In-engine front end (recommended).** Port the egg builder (~300 lines: