From the playtest: a member who Alt+Q's mid-race vanishes from the host's
score sheet, as though they had not raced at all. A pod that leaves never
sends its EndMission score - that message belongs to the buzzer - and the
sheet only ever held what EndMission delivered.
The pods have been telling the console their score all along:
ConsolePlayerVTVScoreUpdate telemetry fed the arcade console's status
board, and this console skipped it ('IDs 2-6 skip through for now'). It
now keeps the last score each pod reported, together with the host ID the
pod itself put in the message - no arithmetic about egg order to get
wrong - and at the buzzer any pod that is not there to answer has its
last telemetry score stood in and named in the log. Leaving early keeps
what was earned; it just stops earning.
Also on the leaver's own machine: RPL4Lobby_PullRaceResults gave every
member eight seconds of frozen window waiting for a score sheet - but an
aborter leaves a race still running everywhere else, whose sheet will not
exist for minutes. Exit_Code carries the abort at that moment (the race
loop resets it after the menu), so the wait becomes one free look. The
sheet's nonce keeps it eligible to show once the race really ends.
The member's drop-to-desktop after Alt+Q is NOT fixed here - it is not
yet diagnosed, and this session has spent enough confident guesses. The
next piece of evidence that names it is the member machine's rpl4.log
tail from right after an abort.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1256 lines
37 KiB
C++
1256 lines
37 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 "..\munga\spooler.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)
|
|
int lastScore; // last telemetry score heard mid-race
|
|
int lastScoreHostID; // the host ID the pod itself reported
|
|
Logical haveScore; // any telemetry score at all yet
|
|
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;
|
|
}
|
|
//
|
|
// Running telemetry. The pods have been sending it all
|
|
// along (it fed the arcade console's status board); this
|
|
// console skipped it - until a player Alt+Q'ed mid-race
|
|
// and vanished from the score sheet, because a pod that
|
|
// leaves never sends its EndMission score. Keep the last
|
|
// score heard, and the host ID the pod itself reported
|
|
// with it, so the buzzer can stand it in for a pod that
|
|
// is not there to answer. Leaving early keeps the points
|
|
// earned; it just stops earning more.
|
|
//
|
|
else if ((int) base->messageID ==
|
|
ConsolePlayerVTVScoreUpdateMessageID)
|
|
{
|
|
ConsolePlayerVTVScoreUpdateMessage *message =
|
|
(ConsolePlayerVTVScoreUpdateMessage *) base;
|
|
pod->lastScore = (int) message->GetPlayerScore();
|
|
pod->lastScoreHostID = (int) message->GetPlayerHostID();
|
|
pod->haveScore = True;
|
|
}
|
|
// the rest of the telemetry (IDs 2-4, 6) skips through
|
|
}
|
|
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 commit board: the pod bay ritual, on screen.
|
|
//
|
|
// In the pod bay the mission is COMMITTED first - every pod preps
|
|
// up to, but not past, the launch - and then the operator presses
|
|
// the launch button. The prep window is not dead time: the MFDs
|
|
// and map buttons are alive before the mission starts, so pilots
|
|
// use it to set maps and presets, and the wait is part of the
|
|
// game's social fabric.
|
|
//
|
|
// This is that ritual for a lobby race. The host's menu button
|
|
// says COMMIT; committing marshals everyone exactly as before, but
|
|
// where the console used to fire RunMission the instant all pods
|
|
// staged, it now arms a LAUNCH button on a small status board and
|
|
// waits for the operator. The board lists every pod and what it is
|
|
// doing - connecting, loading, READY - so the host can see who the
|
|
// room is waiting on, and it gets out of the way the moment the
|
|
// mission drops.
|
|
//
|
|
// Game thread throughout: created, painted, clicked and destroyed
|
|
// inside ConsoleTick, so the game's own message pump (already
|
|
// alive - that is why the MFDs work) delivers its input, and no
|
|
// state crosses a thread. WS_EX_NOACTIVATE keeps the game window
|
|
// focused: PadRIO controls answer only while it is, and a launch
|
|
// click must not cost the host their controls.
|
|
//---------------------------------------------------------------
|
|
|
|
HWND gStatusWindow = NULL;
|
|
Logical gLaunchArmed = False; // everyone staged: button lit
|
|
Logical gLaunchRequested = False; // the operator pressed it
|
|
RECT gLaunchRect; // client coords, hit-tested
|
|
int gShownStates[maxRemotePods + 2];
|
|
|
|
const COLORREF kBoardGreen = RGB(64, 255, 64);
|
|
const COLORREF kBoardGreenDim = RGB(24, 140, 24);
|
|
|
|
const char *StateWord(int state)
|
|
{
|
|
switch (state)
|
|
{
|
|
case -1: return "connecting";
|
|
case Application::WaitingForEgg: return "waiting for egg";
|
|
case Application::CreatingMission:
|
|
case Application::LoadingMission: return "loading";
|
|
case Application::WaitingForLaunch: return "READY";
|
|
case Application::LaunchingMission:
|
|
case Application::RunningMission: return "running";
|
|
default: return "starting";
|
|
}
|
|
}
|
|
|
|
LRESULT CALLBACK StatusBoardWndProc(
|
|
HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|
{
|
|
switch (message)
|
|
{
|
|
case WM_MOUSEACTIVATE:
|
|
// take the click, leave the focus with the game
|
|
return MA_NOACTIVATE;
|
|
|
|
case WM_ERASEBKGND:
|
|
return 1;
|
|
|
|
case WM_LBUTTONDOWN:
|
|
{
|
|
int x = (int)(short) LOWORD(lParam);
|
|
int y = (int)(short) HIWORD(lParam);
|
|
|
|
if (gLaunchArmed &&
|
|
x >= gLaunchRect.left && x < gLaunchRect.right &&
|
|
y >= gLaunchRect.top && y < gLaunchRect.bottom)
|
|
{
|
|
gLaunchRequested = True;
|
|
}
|
|
}
|
|
return 0;
|
|
|
|
case WM_PAINT:
|
|
{
|
|
PAINTSTRUCT ps;
|
|
HDC hdc = BeginPaint(hwnd, &ps);
|
|
RECT client;
|
|
GetClientRect(hwnd, &client);
|
|
|
|
HDC mem = CreateCompatibleDC(hdc);
|
|
HBITMAP surface = CreateCompatibleBitmap(
|
|
hdc, client.right, client.bottom);
|
|
HGDIOBJ old_surface = SelectObject(mem, surface);
|
|
|
|
FillRect(mem, &client,
|
|
(HBRUSH) GetStockObject(BLACK_BRUSH));
|
|
HBRUSH frame = CreateSolidBrush(kBoardGreenDim);
|
|
FrameRect(mem, &client, frame);
|
|
DeleteObject(frame);
|
|
|
|
HFONT font = CreateFontA(-14, 0, 0, 0, FW_NORMAL,
|
|
FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS,
|
|
CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
|
|
DEFAULT_PITCH | FF_MODERN, "Consolas");
|
|
HGDIOBJ old_font = SelectObject(mem, font);
|
|
SetBkMode(mem, TRANSPARENT);
|
|
|
|
int row_h = 20;
|
|
RECT line;
|
|
line.left = 10;
|
|
line.right = client.right - 10;
|
|
line.top = 8;
|
|
line.bottom = line.top + row_h;
|
|
|
|
SetTextColor(mem, kBoardGreen);
|
|
DrawTextA(mem, "MISSION COMMITTED", -1, &line,
|
|
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
|
line.top += row_h + 4;
|
|
line.bottom += row_h + 4;
|
|
|
|
//
|
|
// One row per pod: the host first, then the remotes in
|
|
// [pilots] order. READY rows bright, the rest dim, so
|
|
// who the room is waiting on reads at a glance.
|
|
//
|
|
// Every state on this board comes from gShownStates -
|
|
// the values the TICK stored when it decided whether to
|
|
// arm - and nothing is re-read at paint time. The first
|
|
// version re-read application->GetApplicationState()
|
|
// here, and the host's own row said 'connecting' under
|
|
// a lit LAUNCH button: two reads of one fact from two
|
|
// places, disagreeing - the exact mistake behind five
|
|
// broken instruments in one night of this project. One
|
|
// frame of reference: the tick decides, the paint
|
|
// repeats what it decided.
|
|
//
|
|
char text[96];
|
|
|
|
sprintf(text, "%-14s %s",
|
|
(gPilotNameCount > 0) ? gPilotNames[0] : "HOST",
|
|
StateWord(gShownStates[0]));
|
|
SetTextColor(mem,
|
|
(gShownStates[0] == Application::WaitingForLaunch)
|
|
? kBoardGreen : kBoardGreenDim);
|
|
DrawTextA(mem, text, -1, &line,
|
|
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
|
|
|
for (int i = 0; i < gRemotePodCount; ++i)
|
|
{
|
|
line.top += row_h;
|
|
line.bottom += row_h;
|
|
|
|
const char *name = (i + 1 < gPilotNameCount)
|
|
? gPilotNames[i + 1] : gRemotePods[i].address;
|
|
|
|
sprintf(text, "%-14s %s", name,
|
|
StateWord(gShownStates[i + 1]));
|
|
SetTextColor(mem,
|
|
(gShownStates[i + 1] == Application::WaitingForLaunch)
|
|
? kBoardGreen : kBoardGreenDim);
|
|
DrawTextA(mem, text, -1, &line,
|
|
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
|
}
|
|
|
|
//
|
|
// The operator's button. Lit only when every system is
|
|
// ready - the same condition that used to fire the run
|
|
// automatically.
|
|
//
|
|
gLaunchRect.left = 10;
|
|
gLaunchRect.right = client.right - 10;
|
|
gLaunchRect.bottom = client.bottom - 8;
|
|
gLaunchRect.top = gLaunchRect.bottom - 26;
|
|
|
|
HBRUSH button = CreateSolidBrush(
|
|
gLaunchArmed ? kBoardGreen : kBoardGreenDim);
|
|
FrameRect(mem, &gLaunchRect, button);
|
|
DeleteObject(button);
|
|
SetTextColor(mem,
|
|
gLaunchArmed ? kBoardGreen : kBoardGreenDim);
|
|
DrawTextA(mem,
|
|
gLaunchArmed ? "L A U N C H" : "waiting for pods...",
|
|
-1, &gLaunchRect,
|
|
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
|
|
|
BitBlt(hdc, 0, 0, client.right, client.bottom,
|
|
mem, 0, 0, SRCCOPY);
|
|
SelectObject(mem, old_font);
|
|
DeleteObject(font);
|
|
SelectObject(mem, old_surface);
|
|
DeleteObject(surface);
|
|
DeleteDC(mem);
|
|
EndPaint(hwnd, &ps);
|
|
}
|
|
return 0;
|
|
}
|
|
return DefWindowProcA(hwnd, message, wParam, lParam);
|
|
}
|
|
|
|
void CreateStatusBoard()
|
|
{
|
|
static Logical class_registered = False;
|
|
|
|
if (!class_registered)
|
|
{
|
|
class_registered = True;
|
|
WNDCLASSA window_class;
|
|
memset(&window_class, 0, sizeof(window_class));
|
|
window_class.lpfnWndProc = StatusBoardWndProc;
|
|
window_class.hInstance = GetModuleHandleA(NULL);
|
|
window_class.hCursor = LoadCursor(NULL, IDC_ARROW);
|
|
window_class.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH);
|
|
window_class.lpszClassName = "RPCommitBoard";
|
|
RegisterClassA(&window_class);
|
|
}
|
|
|
|
int height = 8 + 24 + (gRemotePodCount + 1) * 20 + 12 + 26 + 8;
|
|
int width = 280;
|
|
|
|
//
|
|
// Top-right of the game window, inset a little - over the 3D
|
|
// view, clear of the instrument panes along the edges.
|
|
//
|
|
RECT game;
|
|
GetWindowRect(ghWnd, &game);
|
|
|
|
gStatusWindow = CreateWindowExA(
|
|
WS_EX_TOPMOST | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW,
|
|
"RPCommitBoard", "", WS_POPUP,
|
|
game.right - width - 48, game.top + 64,
|
|
width, height,
|
|
ghWnd, NULL, GetModuleHandleA(NULL), NULL);
|
|
if (gStatusWindow != NULL)
|
|
{
|
|
ShowWindow(gStatusWindow, SW_SHOWNOACTIVATE);
|
|
}
|
|
for (int i = 0; i < maxRemotePods + 2; ++i)
|
|
{
|
|
gShownStates[i] = -999;
|
|
}
|
|
}
|
|
|
|
void DestroyStatusBoard()
|
|
{
|
|
if (gStatusWindow != NULL)
|
|
{
|
|
DestroyWindow(gStatusWindow);
|
|
gStatusWindow = NULL;
|
|
}
|
|
gLaunchArmed = False;
|
|
gLaunchRequested = False;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// 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:
|
|
//
|
|
// A mission can die before it ever runs - Alt+Q during the
|
|
// commit hold, or while loading. The console only learned a
|
|
// mission was over from PhaseRunning, so an abort during prep
|
|
// left it in PhaseWaiting forever, MissionCompleted() answered
|
|
// False, and WinMain's single-binary loop fell out to the
|
|
// DESKTOP instead of returning to the setup screen. The commit
|
|
// hold makes the prep window somewhere players actually stand,
|
|
// so the hole finally had traffic. A prep death counts as a
|
|
// completed mission now: back to the menu, lobby intact.
|
|
//
|
|
if (state == Application::EndingMission ||
|
|
state == Application::StoppingMission ||
|
|
state == Application::AbortingMission)
|
|
{
|
|
DEBUG_STREAM << "LocalConsole: mission ended during prep - "
|
|
<< "back to the menu\n" << std::flush;
|
|
InterlockedExchange(&gMissionRunning, 0);
|
|
gPhase = PhaseStopped;
|
|
DestroyStatusBoard();
|
|
if (gNetworkRace)
|
|
{
|
|
DisconnectRemotes();
|
|
}
|
|
break;
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
|
|
//
|
|
// The commit board, from the moment there is anything to
|
|
// show. It repaints only when a state actually changes -
|
|
// this ticks every frame.
|
|
//
|
|
if (gRemotePodCount > 0 && !gRunSent)
|
|
{
|
|
if (gStatusWindow == NULL)
|
|
{
|
|
CreateStatusBoard();
|
|
}
|
|
if (gStatusWindow != NULL)
|
|
{
|
|
Logical changed = (gShownStates[0] != state);
|
|
|
|
gShownStates[0] = state;
|
|
for (int i = 0; i < gRemotePodCount; ++i)
|
|
{
|
|
if (gShownStates[i + 1] != gRemotePods[i].state)
|
|
{
|
|
gShownStates[i + 1] = gRemotePods[i].state;
|
|
changed = True;
|
|
}
|
|
}
|
|
if (changed)
|
|
{
|
|
InvalidateRect(gStatusWindow, NULL, FALSE);
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// Everyone staged: ARM. The console used to fire the run
|
|
// itself right here; the run belongs to the operator now,
|
|
// pod bay fashion, and the prep hold is the point - the
|
|
// MFDs and map buttons are alive, so pilots set maps and
|
|
// presets before the drop.
|
|
//
|
|
if (!gRunSent &&
|
|
state == Application::WaitingForLaunch &&
|
|
AllRemotesInState(Application::WaitingForLaunch))
|
|
{
|
|
if (!gLaunchArmed)
|
|
{
|
|
gLaunchArmed = True;
|
|
DEBUG_STREAM << "LocalConsole: all pods staged - "
|
|
<< "committed, LAUNCH is the operator's\n"
|
|
<< std::flush;
|
|
if (gStatusWindow != NULL)
|
|
{
|
|
InvalidateRect(gStatusWindow, NULL, FALSE);
|
|
}
|
|
}
|
|
|
|
//
|
|
// A solo commit (no remote pods) launches itself: with
|
|
// nobody to wait for there is no board, and the old
|
|
// instant start is what a lone pilot expects.
|
|
//
|
|
if (gLaunchRequested || gRemotePodCount == 0)
|
|
{
|
|
DEBUG_STREAM << "LocalConsole: 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;
|
|
|
|
// out of the way: the host has a race to fly
|
|
DestroyStatusBoard();
|
|
}
|
|
}
|
|
else if (gLaunchArmed && !gRunSent)
|
|
{
|
|
//
|
|
// Somebody fell back out of readiness (a reload, a
|
|
// drop). Disarm rather than launching a room that is
|
|
// no longer whole.
|
|
//
|
|
gLaunchArmed = False;
|
|
if (gStatusWindow != NULL)
|
|
{
|
|
InvalidateRect(gStatusWindow, NULL, FALSE);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (state == Application::RunningMission)
|
|
{
|
|
gPhase = PhaseRunning;
|
|
DestroyStatusBoard(); // however the run began
|
|
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;
|
|
DestroyStatusBoard();
|
|
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)
|
|
{
|
|
//
|
|
// Stand in for anyone who is not here to answer. A pod
|
|
// that left mid-race - Alt+Q, a crash, a dropped link -
|
|
// never sends EndMission, and used to vanish from the
|
|
// score sheet as though it had not raced at all. Its
|
|
// last telemetry score is the score it left with.
|
|
//
|
|
for (int i = 0; i < gRemotePodCount; ++i)
|
|
{
|
|
if (!gRemotePods[i].scored && gRemotePods[i].haveScore)
|
|
{
|
|
DEBUG_STREAM << "LocalConsole: "
|
|
<< gRemotePods[i].address
|
|
<< " never reported - keeping its last"
|
|
<< " telemetry score\n" << std::flush;
|
|
CollectFinalScore(
|
|
gRemotePods[i].lastScoreHostID,
|
|
gRemotePods[i].lastScore);
|
|
gRemotePods[i].scored = True;
|
|
}
|
|
}
|
|
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;
|
|
DestroyStatusBoard(); // no board survives into a new race
|
|
|
|
// 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 recording needs this too. A spool says what moved; the egg says
|
|
// what it moved through, and the console is where the egg's name is
|
|
// actually known.
|
|
//
|
|
SpoolRecorder_Get()->SetEggPath(gEggPath);
|
|
|
|
//
|
|
// 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;
|
|
}
|
|
}
|
|
}
|