Files
RP412/RP_L4/RPL4CONSOLE.cpp
T
CydandClaude Opus 5 50399e016e The board repeats what the tick decided
Cyd's screenshot: the commit board's LAUNCH button lit - which requires
the local pod to be staged - while the host's own row read 'connecting',
which is the word for a state of -1. Two reads of the same fact from two
places, disagreeing: the tick read the application state and armed on it,
and the paint handler read it AGAIN, separately, and got something else.

Why the paint's read returned -1 is not proven, and it does not need to
be: re-deriving state at paint time was the mistake, the same one that
broke five instruments in one night of the render-tick hunt. The tick
already stores every state it acted on in gShownStates, for change
detection - the paint now draws those values and reads nothing else. The
board and the arming can no longer disagree, because they are the same
read.

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

1212 lines
35 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)
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 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)
{
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;
}
}
}