Files
RP412/MUNGA_L4/L4NETTRANSPORT.cpp
CydandClaude Fable 5 c66cb00a7e Network races: the local console marshals remote pods over the wire
The lobby-owner-as-console architecture, in-engine. RPL4CONSOLE gains
RPL4LocalConsole_InstallNetworkRace: the owner pod switches to network
mode and meshes like any pod (egg fed locally via FeedLocalEgg, which
now opens the ConsoleOnly state gate), while the console tick also
marshals REMOTE pods over NetTransport speaking the exact arcade
protocol - egg chunks with 5s-retry-until-ACK, 1Hz state polling,
RunMission once every pod stages at WaitingForLaunch, StopMission at
expiry (remotes first, local pod holds until their EndMission scores
land), score intake labeled with [pilots]-order names on the results
screen.

The front end builds the multi-pilot egg: RP412HOSTPODS lists member
console channels (lobby stand-in; the Steam lobby feeds the same
path), RP412HOSTPORT/RP412HOSTADDR set the owner side.

Winsock Connect now redials with a fresh socket per attempt (a refused
TCP socket is dead; the old loop reused it) bounded at 120s - needed
whenever a peer boots after the caller, which is the normal Steam
lobby launch order.

Verified on loopback: member pod in -net, owner hosting from its menu;
mesh completed both sides, 30s race, remote score collected over the
wire (host 3), local stop after the drain, results screen shows both
pilots by name in one process that returns to the menu.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:08:18 -05:00

332 lines
8.3 KiB
C++

#include "mungal4.h"
#pragma hdrstop
#include "l4nettransport.h"
#include <Ws2tcpip.h>
//########################################################################
// 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;
//
// Retry-while-refused, bounded: the egg-ACK ordering means
// the peer may not be listening yet. A refused TCP socket
// is dead - every attempt needs a fresh one.
//
DWORD deadline = GetTickCount() + 120 * 1000;
for (;;)
{
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
// (port 0 = ephemeral, for console channels)
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;
}
if (connect(sock, (sockaddr *) remote, sizeof(SOCKADDR_IN)) == 0)
{
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;
}
int wsa_error = WSAGetLastError();
closesocket(sock);
if (wsa_error != WSAECONNREFUSED ||
(LONG)(GetTickCount() - deadline) >= 0)
{
DEBUG_STREAM << "ERROR: connect() failed with "
<< wsa_error << "!\n" << std::flush;
return InvalidConnection;
}
Sleep(250); // peer not up yet - redial
}
}
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;
}