Files
RP412/RP_L4/RPL4CONSOLE.cpp
T
CydandClaude Opus 5 46418ebb5c The console speaks to the room it is standing in
Cyd's point: a real pod bay console is its own machine, and every station
- pod, Live Cam, mission review - hears it over the wire. That is why the
spooler expects LoadMission and RunMission to arrive as packets. Ours is
colocated, and the network stack is quite right not to push bytes through
a socket to reach a client in the same process.

But the console had gone further than that. It called
application->Dispatch, stepping past the client's receive entry
altogether, so nothing watching packets ever saw the console speak. The
first Live Cam recording is the evidence: 14,342 packets, every one of
them from the racer via the interest manager, and not one LoadMission,
RunMission or StopMission - because those came from inside the house.

The tee was not in the wrong layer for the wire; it was in the right place
and the console was walking around it.

So build the packet SendWire would have built and hand it to the client's
own front door. NetworkClient::ReceiveNetworkPacket is Dispatch plus
whatever is watching, and Application IS the client for
ApplicationClientID, so this is the same delivery arriving where deliveries
arrive - no socket, no loopback, no second copy of the protocol.

Correcting myself twice over. RunMission does NOT have to be the first
packet in the spool: playback reads spool->GetPointer(), the current
cursor, by which time the task has consumed what came before, so it checks
that the cursor has REACHED RunMission rather than that the file starts
with it. And the ordering is therefore not "the whole job" as I claimed -
the recorded timings already show a normal shape, fifteen entity creations
inside the first twenty milliseconds and updates running from 0.110s to
308.940s of a five minute race.

Still open: the egg feed is local too (FeedLocalEgg), so a spool carries
no egg. Whether that matters depends on how the playback application loads
its mission, which is the next thing to find out rather than guess at.

WATCH ON NEXT LAUNCH: this changes how the local station is told to start
and stop a race. It should be identical - ReceiveNetworkPacket dispatches
the same message to the same object - but it is the launch path, so a race
that does not start is this commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 12:26:04 -05:00

856 lines
24 KiB
C++

#include "..\munga_l4\mungal4.h"
#pragma hdrstop
#include "rpl4console.h"
#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
// stays alive across the whole session, owns the mission clock, and
// raises the stop request when the selected length expires. The game
// thread's per-frame tick is the only place engine calls happen - it
// reports state transitions to the console thread and executes the
// requested StopMissionMessage dispatch (the engine is single
// threaded; cross-thread dispatch is not safe).
//
// 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
{
enum ConsolePhase
{
PhaseWaiting = 0, // waiting for the mission to start running
PhaseRunning, // mission running, console thread watching the clock
PhaseStopped // stop dispatched, waiting for teardown
};
ConsolePhase gPhase = PhaseWaiting;
int gMissionSeconds = 0;
Application *gWatchedApp = NULL;
HANDLE gConsoleThread = NULL;
// shared with the console thread
volatile LONG gMissionRunning = 0;
volatile LONG gStopRequested = 0;
volatile LONG gShuttingDown = 0;
volatile LONG gRunStartTick = 0;
volatile LONG gLengthMs = 0;
// collected mission results (this session's last race)
enum { maxResults = 16 };
struct FinalScore
{
int hostID;
int score;
};
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
//---------------------------------------------------------------
DWORD WINAPI ConsoleThreadProc(LPVOID)
{
while (!gShuttingDown)
{
Sleep(250);
if (gMissionRunning && !gStopRequested)
{
LONG length_ms = gLengthMs;
if (length_ms > 0 &&
(LONG)(GetTickCount() - (DWORD) gRunStartTick) >= length_ms)
{
InterlockedExchange(&gStopRequested, 1);
}
}
}
return 0;
}
//---------------------------------------------------------------
// 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)
{
if (gResultCount < maxResults)
{
gResults[gResultCount].hostID = host_ID;
gResults[gResultCount].score = score;
++gResultCount;
}
DEBUG_STREAM << "LocalConsole: final score, host " << host_ID
<< " = " << 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);
}
//---------------------------------------------------------------
// The same packet, delivered to the station on this machine.
//
// A real pod bay console sits on its own machine and every station
// hears it over the wire. Here it is colocated, and the network stack
// is quite right not to push bytes through a socket to reach a client
// in the same process - but the console had gone further than that and
// called application->Dispatch, stepping past the client's receive
// entry altogether. Anything watching packets therefore never saw the
// console speak: the first Live Cam recording held all 14,342 of the
// racer's packets and not one LoadMission, RunMission or StopMission,
// because those were the messages that came from inside the house.
//
// So build the packet SendWire would have built and hand it to the
// client's own front door. No socket, no wire, no copy of the protocol
// - just the delivery arriving where a delivery arrives.
//---------------------------------------------------------------
void DeliverLocal(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);
NetworkPacket *received = (NetworkPacket *) packet;
Check(application);
application->ReceiveNetworkPacket(received, &received->messageData);
}
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);
DeliverLocal(
NetworkClient::ApplicationClientID,
&message,
(int) message.messageLength
);
gPhase = PhaseStopped;
}
//---------------------------------------------------------------
// The countdown the engine shows, taken from the clock that will
// actually end the race (gMissionClockHook - see APPMGR.h).
//
// Called on the game thread, reading two volatile LONGs the console
// thread writes with InterlockedExchange. Aligned 32-bit reads, and
// a torn value could only mistime the cockpit clock by one tick of
// a countdown nobody reads to the millisecond - not worth a lock on
// the frame path.
//---------------------------------------------------------------
Logical MissionClock(Scalar *seconds_remaining)
{
//
// Only answer for the race this console is actually marshalling.
// Nothing ever uninstalls the hook, so a player who hosts a race
// and then joins somebody else's lobby still has it wired up -
// and in that race the console is a bystander whose gLengthMs and
// gRunStartTick belong to the previous mission entirely.
//
if (gWatchedApp == NULL || gWatchedApp != application)
{
return False;
}
if (!gMissionRunning)
{
return False; // not started, or already stopped
}
LONG length_ms = gLengthMs;
if (length_ms <= 0)
{
return False; // endless: nothing to count down
}
// DWORD subtraction, so a GetTickCount wrap costs nothing
LONG elapsed_ms = (LONG)(GetTickCount() - (DWORD) gRunStartTick);
LONG left_ms = length_ms - elapsed_ms;
if (left_ms < 0)
{
//
// The console polls at 250 ms, so the clock reaches zero
// slightly before the stop is dispatched. Hold at zero
// rather than showing negative time in the cockpit.
//
left_ms = 0;
}
*seconds_remaining = (Scalar) left_ms / 1000.0f;
return True;
}
//---------------------------------------------------------------
// The game-thread tick: state reporting + engine-safe execution
//---------------------------------------------------------------
void ConsoleTick()
{
if (application == NULL)
{
return;
}
if (gPhase != PhaseWaiting && application != gWatchedApp)
{
return;
}
int state = application->GetApplicationState();
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;
DeliverLocal(
NetworkClient::ApplicationClientID,
&local_run,
(int) local_run.messageLength
);
gRunSent = True;
}
}
if (state == Application::RunningMission)
{
gPhase = PhaseRunning;
gWatchedApp = application;
gResultCount = 0;
InterlockedExchange(&gRunStartTick, (LONG) GetTickCount());
InterlockedExchange(&gStopRequested, 0);
InterlockedExchange(&gMissionRunning, 1);
DEBUG_STREAM << "LocalConsole: mission running, length "
<< gMissionSeconds << "s\n" << std::flush;
}
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. Remote pods stop first;
// the local pod holds on briefly so their EndMission
// scores can land before our own teardown.
//-----------------------------------------------------
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;
case PhaseStopped:
// The application tears itself down after a stop (arcade
// pods were relaunched per mission). WinMain's race loop
// asks MissionCompleted() and cycles back to the setup
// screen in the same process.
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;
}
//
// RP412MISSIONSECONDS overrides the menu's game length. The shortest
// the menu offers is 3:00, which is a long wait when what you are
// testing is what happens at the buzzer.
//
const char *seconds_override = getenv("RP412MISSIONSECONDS");
if (seconds_override != NULL && atoi(seconds_override) > 0)
{
mission_seconds = atoi(seconds_override);
DEBUG_STREAM << "LocalConsole: length overridden to "
<< mission_seconds << "s by RP412MISSIONSECONDS\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;
// the cockpit clock now counts down the same clock that will stop
// the race, rather than the engine's own reckoning of it
gMissionClockHook = &MissionClock;
// 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)
{
gNetworkRace = False;
gRemotePodCount = 0;
gPilotNameCount = 0;
// single player launches itself (the engine's no-console self-run)
gConsoleMarshalsLaunch = False;
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;
// the owner pod must stage at WaitingForLaunch with everyone else -
// this console launches the whole mesh at once
gConsoleMarshalsLaunch = True;
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)
{
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;
}
}
}
//
// 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();
//
// An escape pressed during the last race must not cancel this one.
//
NetTransport_ClearWaitCancel();
const char *cursor = remote_pod_list;
while (*cursor != '\0' && gRemotePodCount < maxRemotePods)
{
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: network race, " << gRemotePodCount
<< " remote pod(s) connected\n" << std::flush;
InstallCommon(mission_seconds);
return True;
}
Logical
RPL4LocalConsole_MissionCompleted()
{
return gPhase == PhaseStopped;
}
int
RPL4LocalConsole_ResultCount()
{
return gResultCount;
}
Logical
RPL4LocalConsole_GetResult(int index, int *host_ID, int *score)
{
if (index < 0 || index >= gResultCount)
{
return False;
}
*host_ID = gResults[index].hostID;
*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];
}
void
RPL4LocalConsole_ClearResults()
{
gResultCount = 0;
gPilotNameCount = 0;
}
void
RPL4LocalConsole_InjectResult(int host_ID, int score, const char *name)
{
if (gResultCount >= maxResults)
{
return;
}
gResults[gResultCount].hostID = host_ID;
gResults[gResultCount].score = score;
++gResultCount;
int index = host_ID - firstPilotHostID;
if (name != NULL && index >= 0 && index < maxRemotePods + 1)
{
strncpy(gPilotNames[index], name, sizeof(gPilotNames[index]) - 1);
gPilotNames[index][sizeof(gPilotNames[index]) - 1] = '\0';
if (index >= gPilotNameCount)
{
gPilotNameCount = index + 1;
}
}
}