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>
This commit is contained in:
Cyd
2026-07-12 21:08:18 -05:00
co-authored by Claude Fable 5
parent a8053ad784
commit c66cb00a7e
8 changed files with 810 additions and 97 deletions
+4
View File
@@ -81,6 +81,10 @@ public:
static bool GetFullscreen() { return mFullscreen; }
static Logical GetSeeSolids() { return seeSolids; }
static unsigned long GetNetworkCommonFlatAddress() { return networkCommonFlatAddress; }
// The front end's multiplayer path turns network mode on at launch
// time (the owner pod meshes like any other pod); command-line -net
// keeps working exactly as before.
static void SetNetworkCommonFlatAddress(unsigned long address) { networkCommonFlatAddress = address; }
static CString GetEggNotationFileName() { return eggNotationFileName; }
// the in-game front end builds an egg and injects it here
static void SetEggNotationFileName(const char *name) { eggNotationFileName = name; }
+5
View File
@@ -378,6 +378,11 @@ void
Register_Object(networkEggNotationFile);
networkEggNotationFile->WriteFile("last.egg");
// In network mode (the owner pod of a multiplayer race) the state
// gate must open or CheckBuffers keeps dropping mesh packets - a
// console-fed pod gets this from the chunked egg path instead.
currentNetworkState = NormalState;
ReceiveEggFileMessage egg_message(-1, 10, "local egg", 10);
application->Post(DefaultEventPriority, this, &egg_message);
}
+59 -53
View File
@@ -112,66 +112,72 @@ namespace
<< 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)
//
// 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 (;;)
{
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)
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == INVALID_SOCKET)
{
wsa_error = WSAGetLastError();
DEBUG_STREAM << "ERROR: socket() failed with "
<< WSAGetLastError() << "!\n";
return InvalidConnection;
}
} while (result != 0 && wsa_error == WSAECONNREFUSED);
if (result != 0)
{
DEBUG_STREAM << "ERROR: connect() failed with "
<< wsa_error << "!\n" << std::flush;
closesocket(sock);
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;
}
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;
// 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);
return InvalidConnection;
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
}
return (Connection) sock;
}
Connection
+29 -1
View File
@@ -256,7 +256,35 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
// Front-end games are marshaled by the in-process console: it
// ends the race when the selected time expires. (Hand-fed -egg
// runs stay unmarshaled - the developer shortcut.)
RPL4LocalConsole_Install(RPL4FrontEnd_LastMissionSeconds());
//
// RP412HOSTPODS turns the launch into a hosted network race:
// this pod switches to network mode (it meshes like any pod)
// and the console also marshals the listed member pods over
// the wire. (The Steam lobby will feed this same path.)
const char *host_pods = getenv("RP412HOSTPODS");
if (host_pods != NULL && host_pods[0] != '\0')
{
int host_port = 1501;
const char *port_override = getenv("RP412HOSTPORT");
if (port_override != NULL && atoi(port_override) > 0)
{
host_port = atoi(port_override);
}
L4Application::SetNetworkCommonFlatAddress(host_port);
if (!RPL4LocalConsole_InstallNetworkRace(
RPL4FrontEnd_LastMissionSeconds(), frontend_egg,
host_pods, RPL4FrontEnd_LastPilotNames()))
{
DEBUG_STREAM << "Network race setup failed - back to the menu\n" << std::flush;
L4Application::SetNetworkCommonFlatAddress(0);
continue;
}
}
else
{
L4Application::SetNetworkCommonFlatAddress(0);
RPL4LocalConsole_Install(RPL4FrontEnd_LastMissionSeconds());
}
}
//
+518 -35
View File
@@ -5,9 +5,13 @@
#include "rpl4fe.h"
#include "..\munga\appmgr.h"
#include "..\munga\appmsg.h"
#include "..\munga\console.h"
#include "..\rp\rpcnsl.h"
#include "..\munga_l4\l4app.h"
#include "..\munga_l4\l4net.h"
#include "..\munga_l4\l4nettransport.h"
#define CONSOLE_NET_PORT 1501 // arcade default (matches L4NET.CPP)
//########################################################################
// The local console runs on ITS OWN THREAD, like the real console: it
@@ -18,8 +22,18 @@
// requested StopMissionMessage dispatch (the engine is single
// threaded; cross-thread dispatch is not safe).
//
// Results flow in through gConsoleScoreSink (RP layer): the same final
// scores RPPlayer sent the arcade console at mission end.
// NETWORK RACES (lobby owner as console): the same tick additionally
// marshals REMOTE pods over the NetTransport wire, speaking the exact
// arcade console protocol - egg chunks + ACK, state queries,
// RunMission when everyone reaches WaitingForLaunch, StopMission at
// expiry, EndMission score intake. The owner's own pod runs in
// network mode (it meshes like any pod) but is fed its egg locally
// and driven by direct engine calls, so the console never needs a
// connection to itself.
//
// Results flow in through gConsoleScoreSink (RP layer) for the local
// pod and EndMission wire messages for remote pods: the same final
// scores every pod sent the arcade console at mission end.
//########################################################################
namespace
@@ -54,6 +68,44 @@ namespace
FinalScore gResults[maxResults];
int gResultCount = 0;
//---------------------------------------------------------------
// Network race state: remote pods marshaled over the wire
//---------------------------------------------------------------
enum { maxRemotePods = 8 };
enum { remoteRxSize = 8192 };
struct RemotePod
{
char address[64]; // console channel, "ip[:port]"
NetTransport::Connection
connection;
int state; // last reported application state (-1 unknown)
Logical eggAcknowledged;
DWORD lastQueryTick;
DWORD eggSentTick; // 0 = never sent
Logical scored;
char rx[remoteRxSize]; // wire frame reassembly
int rxCount;
};
RemotePod gRemotePods[maxRemotePods];
int gRemotePodCount = 0;
Logical gNetworkRace = False;
char gEggPath[MAX_PATH] = "";
char *gEggWire = NULL; // newline->NUL image for chunking
int gEggWireSize = 0;
Logical gLocalEggFed = False;
Logical gRunSent = False;
Logical gRemoteStopsSent = False;
DWORD gRemoteStopTick = 0;
// pilot names in [pilots] order; host IDs start at FirstLegalHostID+1
// (the console reserves the first), so host 2 = pilot index 0
enum { firstPilotHostID = 2 };
char gPilotNames[maxRemotePods + 1][32];
int gPilotNameCount = 0;
//---------------------------------------------------------------
// The console thread: the mission clock lives here
//---------------------------------------------------------------
@@ -77,8 +129,8 @@ namespace
}
//---------------------------------------------------------------
// Final-score intake (called on the game thread from RPPlayer's
// mission-ending path, via the RP-layer sink)
// Final-score intake (game thread: the RP-layer sink for the
// local pod, the wire pump for remote pods)
//---------------------------------------------------------------
void CollectFinalScore(int host_ID, int score)
{
@@ -92,6 +144,214 @@ namespace
<< " = " << score << "\n" << std::flush;
}
//---------------------------------------------------------------
// The wire: the arcade console protocol over NetTransport
//---------------------------------------------------------------
void SendWire(RemotePod *pod, int client_ID, const void *message, int size)
{
char packet[sizeof(NetworkPacketHeader) + 1400];
if (size > (int) sizeof(packet) - (int) sizeof(NetworkPacketHeader))
{
return;
}
memset(packet, 0, sizeof(NetworkPacketHeader));
NetworkPacketHeader *header = (NetworkPacketHeader *) packet;
header->clientID = (NetworkClient::ClientID) client_ID;
header->gameID = 0;
header->fromHost = 1; // the console's reserved host ID
memcpy(packet + sizeof(NetworkPacketHeader), message, size);
NetTransport_Get()->Send(
pod->connection, packet, (int) sizeof(NetworkPacketHeader) + size);
}
void SendEggTo(RemotePod *pod)
{
int chunk_count = (gEggWireSize + 999) / 1000;
for (int i = 0; i < chunk_count; ++i)
{
int offset = i * 1000;
int length = gEggWireSize - offset;
if (length > 1000)
{
length = 1000;
}
NetworkManager__ReceiveEggFileMessage chunk(
i, gEggWireSize, gEggWire + offset, length);
SendWire(pod, NetworkClient::NetworkManagerClientID,
&chunk, (int) chunk.messageLength);
}
pod->eggSentTick = GetTickCount();
DEBUG_STREAM << "LocalConsole: egg sent to " << pod->address
<< " (" << chunk_count << " chunks)\n" << std::flush;
}
void PumpRemote(RemotePod *pod)
{
//
// Read whatever the wire has pending
//
for (;;)
{
int space = remoteRxSize - pod->rxCount;
if (space <= 0)
{
break;
}
int received = NetTransport_Get()->Receive(
pod->connection, pod->rx + pod->rxCount, space);
if (received <= 0)
{
break; // no data / disconnected
}
pod->rxCount += received;
if (received < space)
{
break;
}
}
//
// Parse complete frames: NetworkPacketHeader + engine message
//
const int header_size = (int) sizeof(NetworkPacketHeader);
const int base_size = (int) sizeof(Receiver__Message);
for (;;)
{
if (pod->rxCount < header_size + base_size)
{
break;
}
NetworkPacketHeader *header = (NetworkPacketHeader *) pod->rx;
Receiver__Message *base = (Receiver__Message *)(pod->rx + header_size);
int total = header_size + (int) base->messageLength;
if (total < header_size + base_size || total > remoteRxSize)
{
DEBUG_STREAM << "LocalConsole: garbage frame from "
<< pod->address << " - dropping buffer\n" << std::flush;
pod->rxCount = 0;
break;
}
if (pod->rxCount < total)
{
break;
}
if ((int) header->clientID == (int) NetworkClient::ConsoleClientID)
{
if ((int) base->messageID == ConsoleApplicationStateResponseMessageID)
{
ConsoleApplicationStateResponseMessage *message =
(ConsoleApplicationStateResponseMessage *) base;
if (pod->state != (int) message->GetApplicationState())
{
DEBUG_STREAM << "LocalConsole: " << pod->address
<< " state -> " << (int) message->GetApplicationState()
<< "\n" << std::flush;
}
pod->state = (int) message->GetApplicationState();
}
else if ((int) base->messageID == ConsoleApplicationEndMissionMessageID)
{
ConsoleApplicationEndMissionMessage *message =
(ConsoleApplicationEndMissionMessage *) base;
CollectFinalScore(
(int) message->GetPlayerHostID(),
(int) message->GetFinalScore());
pod->scored = True;
}
// VTV telemetry (IDs 2-6) skips through for now
}
else if ((int) header->clientID == (int) NetworkClient::NetworkManagerClientID)
{
if ((int) base->messageID == (int) L4NetworkManager::AcknowledgeEggFileMessageID)
{
if (!pod->eggAcknowledged)
{
DEBUG_STREAM << "LocalConsole: " << pod->address
<< " EGG ACK (mesh complete)\n" << std::flush;
}
pod->eggAcknowledged = True;
}
}
memmove(pod->rx, pod->rx + total, pod->rxCount - total);
pod->rxCount -= total;
}
}
void MarshalRemotes()
{
DWORD now = GetTickCount();
for (int i = 0; i < gRemotePodCount; ++i)
{
RemotePod *pod = &gRemotePods[i];
// state poll, once a second (the arcade console's cadence)
if ((LONG)(now - pod->lastQueryTick) >= 1000)
{
Application::StateQueryMessage query(1);
SendWire(pod, NetworkClient::ApplicationClientID,
&query, (int) query.messageLength);
pod->lastQueryTick = now;
}
PumpRemote(pod);
// egg feed: 5s retry until the pod ACKs (post-mesh)
if (pod->state == (int) Application::WaitingForEgg &&
!pod->eggAcknowledged &&
(pod->eggSentTick == 0 || (LONG)(now - pod->eggSentTick) >= 5000))
{
SendEggTo(pod);
}
}
}
Logical AllRemotesInState(int state)
{
for (int i = 0; i < gRemotePodCount; ++i)
{
if (gRemotePods[i].state != state)
{
return False;
}
}
return True;
}
Logical AllRemotesScored()
{
for (int i = 0; i < gRemotePodCount; ++i)
{
if (!gRemotePods[i].scored)
{
return False;
}
}
return True;
}
void DisconnectRemotes()
{
for (int i = 0; i < gRemotePodCount; ++i)
{
if (gRemotePods[i].connection != NetTransport::InvalidConnection)
{
NetTransport_Get()->Close(gRemotePods[i].connection);
gRemotePods[i].connection = NetTransport::InvalidConnection;
}
}
}
void DispatchLocalStop()
{
DEBUG_STREAM << "LocalConsole: stopping local pod\n" << std::flush;
InterlockedExchange(&gMissionRunning, 0);
Application::StopMissionMessage message(0);
application->Dispatch(&message);
gPhase = PhaseStopped;
}
//---------------------------------------------------------------
// The game-thread tick: state reporting + engine-safe execution
//---------------------------------------------------------------
@@ -111,6 +371,46 @@ namespace
switch (gPhase)
{
case PhaseWaiting:
if (gNetworkRace)
{
MarshalRemotes();
//
// Feed our own pod its egg locally: it meshes like any
// pod but the console drives it without a connection
//
if (!gLocalEggFed && state == Application::WaitingForEgg)
{
L4NetworkManager *network_manager =
(L4NetworkManager *) application->GetNetworkManager();
if (network_manager != NULL)
{
network_manager->FeedLocalEgg(gEggPath);
gLocalEggFed = True;
DEBUG_STREAM << "LocalConsole: local egg fed\n" << std::flush;
}
}
//
// Everyone staged: launch the race everywhere
//
if (!gRunSent &&
state == Application::WaitingForLaunch &&
AllRemotesInState(Application::WaitingForLaunch))
{
DEBUG_STREAM << "LocalConsole: all pods staged - RUN\n" << std::flush;
for (int i = 0; i < gRemotePodCount; ++i)
{
Application::RunMissionMessage run;
SendWire(&gRemotePods[i], NetworkClient::ApplicationClientID,
&run, (int) run.messageLength);
}
Application::RunMissionMessage local_run;
application->Dispatch(&local_run);
gRunSent = True;
}
}
if (state == Application::RunningMission)
{
gPhase = PhaseRunning;
@@ -125,24 +425,53 @@ namespace
break;
case PhaseRunning:
if (gNetworkRace)
{
// telemetry + final scores keep flowing during the race
for (int i = 0; i < gRemotePodCount; ++i)
{
PumpRemote(&gRemotePods[i]);
}
}
if (state != Application::RunningMission)
{
// mission ended some other way (pilot exit etc.)
InterlockedExchange(&gMissionRunning, 0);
gPhase = PhaseStopped;
DisconnectRemotes();
}
else if (gStopRequested)
{
//-----------------------------------------------------
// The console clock expired: end the race exactly the
// way the arcade console did, otherwise the mission
// clock rolls past 00:00 and counts up forever.
// way the arcade console did. Remote pods stop first;
// the local pod holds on briefly so their EndMission
// scores can land before our own teardown.
//-----------------------------------------------------
DEBUG_STREAM << "LocalConsole: time expired - stopping mission\n" << std::flush;
InterlockedExchange(&gMissionRunning, 0);
Application::StopMissionMessage message(0);
application->Dispatch(&message);
gPhase = PhaseStopped;
if (!gNetworkRace)
{
DEBUG_STREAM << "LocalConsole: time expired - stopping mission\n" << std::flush;
DispatchLocalStop();
}
else if (!gRemoteStopsSent)
{
DEBUG_STREAM << "LocalConsole: time expired - stopping remote pods\n" << std::flush;
for (int i = 0; i < gRemotePodCount; ++i)
{
Application::StopMissionMessage stop(0);
SendWire(&gRemotePods[i], NetworkClient::ApplicationClientID,
&stop, (int) stop.messageLength);
}
gRemoteStopsSent = True;
gRemoteStopTick = GetTickCount();
}
else if (AllRemotesScored() ||
(LONG)(GetTickCount() - gRemoteStopTick) >= 5000)
{
DispatchLocalStop();
DisconnectRemotes();
}
}
break;
@@ -154,41 +483,184 @@ namespace
break;
}
}
//---------------------------------------------------------------
// Shared install plumbing
//---------------------------------------------------------------
void InstallCommon(int mission_seconds)
{
// debug: L4CONSOLELEN overrides the mission length (test races)
const char *override_string = getenv("L4CONSOLELEN");
if (override_string != NULL && atoi(override_string) > 0)
{
mission_seconds = atoi(override_string);
DEBUG_STREAM << "LocalConsole: L4CONSOLELEN override, "
<< mission_seconds << "s\n" << std::flush;
}
gMissionSeconds = mission_seconds;
InterlockedExchange(&gLengthMs, (LONG) mission_seconds * 1000);
gPhase = PhaseWaiting;
gWatchedApp = NULL;
gRunSent = False;
gRemoteStopsSent = False;
gLocalEggFed = False;
// game-thread execution point
gPerFrameHook = &ConsoleTick;
// results intake from the RP layer
gConsoleScoreSink = &CollectFinalScore;
// the console itself lives on its own thread, like the real one
if (gConsoleThread == NULL)
{
gConsoleThread = CreateThread(
NULL, 0, ConsoleThreadProc, NULL, 0, NULL);
}
DEBUG_STREAM << "LocalConsole: installed (length "
<< mission_seconds << "s, console thread "
<< (gConsoleThread != NULL ? "up" : "FAILED") << ")\n" << std::flush;
}
}
void
RPL4LocalConsole_Install(int mission_seconds)
{
// debug: L4CONSOLELEN overrides the mission length (test races)
const char *override_string = getenv("L4CONSOLELEN");
if (override_string != NULL && atoi(override_string) > 0)
gNetworkRace = False;
gRemotePodCount = 0;
gPilotNameCount = 0;
InstallCommon(mission_seconds);
}
Logical
RPL4LocalConsole_InstallNetworkRace(
int mission_seconds,
const char *egg_path,
const char *remote_pod_list,
const char *pilot_names
)
{
gNetworkRace = True;
gRemotePodCount = 0;
gPilotNameCount = 0;
strncpy(gEggPath, egg_path, sizeof(gEggPath) - 1);
gEggPath[sizeof(gEggPath) - 1] = '\0';
//
// The wire image of the egg: file newlines become NULs, exactly
// what the arcade console sent (RPMission.ToEggFileMessages)
//
if (gEggWire != NULL)
{
mission_seconds = atoi(override_string);
DEBUG_STREAM << "LocalConsole: L4CONSOLELEN override, "
<< mission_seconds << "s\n" << std::flush;
delete[] gEggWire;
gEggWire = NULL;
gEggWireSize = 0;
}
FILE *egg_file = fopen(egg_path, "rb");
if (egg_file == NULL)
{
DEBUG_STREAM << "LocalConsole: cannot read egg " << egg_path << "\n" << std::flush;
return False;
}
fseek(egg_file, 0, SEEK_END);
long raw_size = ftell(egg_file);
fseek(egg_file, 0, SEEK_SET);
char *raw = new char[raw_size];
fread(raw, 1, raw_size, egg_file);
fclose(egg_file);
gEggWire = new char[raw_size];
gEggWireSize = 0;
for (long b = 0; b < raw_size; ++b)
{
if (raw[b] == '\r')
{
continue; // \r\n collapses to one NUL
}
gEggWire[gEggWireSize++] = (raw[b] == '\n') ? '\0' : raw[b];
}
delete[] raw;
//
// Pilot names in [pilots] order (results screen labels)
//
if (pilot_names != NULL)
{
const char *cursor = pilot_names;
while (*cursor != '\0' && gPilotNameCount < maxRemotePods + 1)
{
int length = 0;
while (cursor[length] != '\0' && cursor[length] != ',' &&
length < (int) sizeof(gPilotNames[0]) - 1)
{
gPilotNames[gPilotNameCount][length] = cursor[length];
++length;
}
gPilotNames[gPilotNameCount][length] = '\0';
++gPilotNameCount;
cursor += length;
if (*cursor == ',')
{
++cursor;
}
}
}
gMissionSeconds = mission_seconds;
InterlockedExchange(&gLengthMs, (LONG) mission_seconds * 1000);
gPhase = PhaseWaiting;
gWatchedApp = NULL;
// game-thread execution point
gPerFrameHook = &ConsoleTick;
// results intake from the RP layer
gConsoleScoreSink = &CollectFinalScore;
// the console itself lives on its own thread, like the real one
if (gConsoleThread == NULL)
//
// Connect to every remote pod's console channel. Blocking with
// retry, like the arcade console redialing a pod that is still
// booting; runs before the engine block so nothing is waiting.
//
NetTransport_Get()->Startup();
const char *cursor = remote_pod_list;
while (*cursor != '\0' && gRemotePodCount < maxRemotePods)
{
gConsoleThread = CreateThread(
NULL, 0, ConsoleThreadProc, NULL, 0, NULL);
RemotePod *pod = &gRemotePods[gRemotePodCount];
memset(pod, 0, sizeof(*pod));
pod->state = -1;
pod->connection = NetTransport::InvalidConnection;
int length = 0;
while (cursor[length] != '\0' && cursor[length] != ',' &&
length < (int) sizeof(pod->address) - 1)
{
pod->address[length] = cursor[length];
++length;
}
pod->address[length] = '\0';
cursor += length;
if (*cursor == ',')
{
++cursor;
}
SOCKADDR_IN console_address;
NetTransport_Get()->Resolve(pod->address, &console_address);
if (console_address.sin_port == 0)
{
console_address.sin_port = htons(CONSOLE_NET_PORT);
}
DEBUG_STREAM << "LocalConsole: connecting to pod " << pod->address
<< "...\n" << std::flush;
pod->connection = NetTransport_Get()->Connect(&console_address, 0);
if (pod->connection == NetTransport::InvalidConnection)
{
DEBUG_STREAM << "LocalConsole: could not reach pod "
<< pod->address << "\n" << std::flush;
return False;
}
++gRemotePodCount;
}
DEBUG_STREAM << "LocalConsole: installed (length "
<< mission_seconds << "s, console thread "
<< (gConsoleThread != NULL ? "up" : "FAILED") << ")\n" << std::flush;
DEBUG_STREAM << "LocalConsole: network race, " << gRemotePodCount
<< " remote pod(s) connected\n" << std::flush;
InstallCommon(mission_seconds);
return True;
}
Logical
@@ -214,3 +686,14 @@ Logical
*score = gResults[index].score;
return True;
}
const char *
RPL4LocalConsole_GetResultName(int host_ID)
{
int index = host_ID - firstPilotHostID;
if (index < 0 || index >= gPilotNameCount)
{
return NULL;
}
return gPilotNames[index];
}
+22
View File
@@ -26,6 +26,23 @@ void
Logical
RPL4LocalConsole_MissionCompleted();
// Network race: the owner pod meshes like any pod (network mode, egg
// fed locally) while this console marshals the REMOTE pods over the
// NetTransport wire with the arcade protocol - egg chunks + ACK,
// state polling, RunMission when everyone stages, StopMission at
// expiry, EndMission score intake. remote_pod_list is a comma list of
// console channels ("ip[:port]", default CONSOLE_NET_PORT);
// pilot_names is a comma list in [pilots] order for the results
// screen. Connects synchronously (retry, like the arcade console
// redialing a booting pod); False when a pod cannot be reached.
Logical
RPL4LocalConsole_InstallNetworkRace(
int mission_seconds,
const char *egg_path,
const char *remote_pod_list,
const char *pilot_names
);
// The last completed mission's final scores (what each pod reported at
// mission end). Valid until the next mission starts running.
int
@@ -33,3 +50,8 @@ int
Logical
RPL4LocalConsole_GetResult(int index, int *host_ID, int *score);
// Pilot name for a host ID ([pilots] order; host 2 = first pilot).
// NULL when unknown - the results screen falls back to pod numbers.
const char *
RPL4LocalConsole_GetResultName(int host_ID);
+168 -8
View File
@@ -161,6 +161,87 @@ namespace
Logical gHavePersist = False;
char gLastPilotName[24] = "Pilot";
// [pilots]-order names of the last launched race (owner first),
// comma separated - the network console labels results with them
char gLastPilotNamesCsv[256] = "";
//---------------------------------------------------------------
// Multiplayer host mode (lobby stand-in): RP412HOSTPODS lists the
// member pods' console channels ("ip[:port]" comma separated) and
// the egg gains one pilot per member. RP412HOSTPORT is the
// owner's console port (default 1501; game port is +1) and
// RP412HOSTADDR the owner's mesh address (default 127.0.0.1 -
// the Steam lobby supplies the FakeIP instead).
//---------------------------------------------------------------
enum { maxExtraPilots = 8 };
struct ExtraPilot
{
char address[64]; // mesh (game port) address for [pilots]
char name[32];
const char *vehicle;
const char *color;
};
int CollectExtraPilots(const FEState *fe, ExtraPilot *extras, char *owner_address)
{
const char *host_pods = getenv("RP412HOSTPODS");
if (host_pods == NULL || host_pods[0] == '\0')
{
return -1; // single player: standalone address
}
int host_port = 1501;
const char *port_override = getenv("RP412HOSTPORT");
if (port_override != NULL && atoi(port_override) > 0)
{
host_port = atoi(port_override);
}
const char *host_address = getenv("RP412HOSTADDR");
if (host_address == NULL || host_address[0] == '\0')
{
host_address = "127.0.0.1";
}
sprintf(owner_address, "%s:%d", host_address, host_port + 1);
int extra_count = 0;
const char *cursor = host_pods;
while (*cursor != '\0' && extra_count < maxExtraPilots)
{
char entry[64];
int length = 0;
while (cursor[length] != '\0' && cursor[length] != ',' &&
length < (int) sizeof(entry) - 1)
{
entry[length] = cursor[length];
++length;
}
entry[length] = '\0';
cursor += length;
if (*cursor == ',')
{
++cursor;
}
int console_port = 1501;
char *colon = strrchr(entry, ':');
if (colon != NULL)
{
*colon = '\0';
console_port = atoi(colon + 1);
}
ExtraPilot *pilot = &extras[extra_count];
sprintf(pilot->address, "%s:%d", entry, console_port + 1);
sprintf(pilot->name, "PILOT %d", extra_count + 2);
pilot->vehicle = kVehicles[
(fe->selection[GroupVehicle] + extra_count + 1) % FE_COUNT(kVehicles)].key;
pilot->color = kColors[
(fe->selection[GroupColor] + extra_count + 1) % FE_COUNT(kColors)].key;
++extra_count;
}
return extra_count;
}
const COLORREF kGreenBright = RGB(64, 255, 64);
const COLORREF kGreenDim = RGB(24, 140, 24);
const COLORREF kBlack = RGB(0, 0, 0);
@@ -518,13 +599,23 @@ namespace
extern const char *kOrdinalsBlock;
//---------------------------------------------------------------
// Egg assembly (console RPMission.ToEggString, race scenario,
// one local pilot)
// Egg assembly (console RPMission.ToEggString, race scenario).
// Single player uses the standalone address; a hosted network
// race lists the owner first plus one pilot per member pod.
//---------------------------------------------------------------
void BuildEggText(const FEState *fe, std::string &egg)
{
char line[256];
ExtraPilot extras[maxExtraPilots];
char owner_address[64];
strcpy(owner_address, kLocalPilotAddress);
int extra_count = CollectExtraPilots(fe, extras, owner_address);
if (extra_count < 0)
{
extra_count = 0; // single player
}
egg += "[mission]\n";
egg += "adventure=Red Planet\n";
sprintf(line, "map=%s\n", kMaps[fe->selection[GroupMap]].key);
@@ -544,10 +635,15 @@ namespace
}
egg += "[pilots]\n";
sprintf(line, "pilot=%s\n", kLocalPilotAddress);
sprintf(line, "pilot=%s\n", owner_address);
egg += line;
for (int p = 0; p < extra_count; ++p)
{
sprintf(line, "pilot=%s\n", extras[p].address);
egg += line;
}
sprintf(line, "[%s]\n", kLocalPilotAddress);
sprintf(line, "[%s]\n", owner_address);
egg += line;
egg += "hostType=0\n";
egg += "dropzone=one\n";
@@ -562,22 +658,75 @@ namespace
sprintf(line, "badge=%s\n", kBadges[fe->selection[GroupBadge]].key);
egg += line;
sprintf(line, "[largebitmap]\nbitmap=BitMap::Large::%s\n", fe->pilotName);
for (int p = 0; p < extra_count; ++p)
{
sprintf(line, "[%s]\n", extras[p].address);
egg += line;
egg += "hostType=0\n";
egg += "dropzone=one\n";
sprintf(line, "name=%s\n", extras[p].name);
egg += line;
sprintf(line, "bitmapindex=%d\n", p + 2);
egg += line;
egg += "loadzones=1\n";
sprintf(line, "vehicle=%s\n", extras[p].vehicle);
egg += line;
sprintf(line, "color=%s\n", extras[p].color);
egg += line;
egg += "badge=None\n";
}
egg += "[largebitmap]\n";
sprintf(line, "bitmap=BitMap::Large::%s\n", fe->pilotName);
egg += line;
sprintf(line, "[smallbitmap]\nbitmap=BitMap::Small::%s\n", fe->pilotName);
for (int p = 0; p < extra_count; ++p)
{
sprintf(line, "bitmap=BitMap::Large::%s\n", extras[p].name);
egg += line;
}
egg += "[smallbitmap]\n";
sprintf(line, "bitmap=BitMap::Small::%s\n", fe->pilotName);
egg += line;
for (int p = 0; p < extra_count; ++p)
{
sprintf(line, "bitmap=BitMap::Small::%s\n", extras[p].name);
egg += line;
}
sprintf(line, "[BitMap::Large::%s]\n", fe->pilotName);
egg += line;
AppendNameBitmap(egg, 128, 32, fe->pilotName);
egg += "width=8\n";
sprintf(line, "[BitMap::Small::%s]\n", fe->pilotName);
egg += line;
AppendNameBitmap(egg, 64, 16, fe->pilotName);
egg += "width=4\n";
for (int p = 0; p < extra_count; ++p)
{
sprintf(line, "[BitMap::Large::%s]\n", extras[p].name);
egg += line;
AppendNameBitmap(egg, 128, 32, extras[p].name);
egg += "width=8\n";
sprintf(line, "[BitMap::Small::%s]\n", extras[p].name);
egg += line;
AppendNameBitmap(egg, 64, 16, extras[p].name);
egg += "width=4\n";
}
egg += kOrdinalsBlock;
// [pilots]-order names for the network console's results
strcpy(gLastPilotNamesCsv, fe->pilotName);
for (int p = 0; p < extra_count; ++p)
{
if (strlen(gLastPilotNamesCsv) + strlen(extras[p].name) + 2 <
sizeof(gLastPilotNamesCsv))
{
strcat(gLastPilotNamesCsv, ",");
strcat(gLastPilotNamesCsv, extras[p].name);
}
}
}
}
@@ -744,6 +893,12 @@ int
return gLastMissionSeconds;
}
const char *
RPL4FrontEnd_LastPilotNames()
{
return gLastPilotNamesCsv;
}
//########################################################################
//######################## RPL4FrontEnd_ShowResults ######################
//########################################################################
@@ -915,7 +1070,12 @@ Logical
continue;
}
ResultRow *row = &rs.rows[rs.rowCount++];
if (result_count == 1)
const char *pilot_name = RPL4LocalConsole_GetResultName(host_ID);
if (pilot_name != NULL)
{
sprintf(row->name, "%.30s", pilot_name);
}
else if (result_count == 1)
{
sprintf(row->name, "%s", gLastPilotName);
}
+5
View File
@@ -29,6 +29,11 @@ Logical
int
RPL4FrontEnd_LastMissionSeconds();
// [pilots]-order pilot names of the last launched race (owner first),
// comma separated - labels for the network console's results intake.
const char *
RPL4FrontEnd_LastPilotNames();
// The between-races results screen: shows the last mission's final
// scores (from the local console) with a CONTINUE button. Returns
// immediately when there are no results (first boot). False only if