#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. // char text[96]; int local_state = (application != NULL) ? application->GetApplicationState() : -1; sprintf(text, "%-14s %s", (gPilotNameCount > 0) ? gPilotNames[0] : "HOST", StateWord(local_state)); SetTextColor(mem, (local_state == 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(gRemotePods[i].state)); SetTextColor(mem, (gRemotePods[i].state == 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; } } }