//===========================================================================// // btl4console.cpp -- BT412 in-process LocalConsole marshal (solo path). // // See btl4console.hpp. Models RP412's RPL4CONSOLE ConsoleTick, reduced to the // single-player case: no remote pods, no wire -- just own the mission clock and // dispatch StopMissionMessage at expiry. The tick runs on the game thread // (gPerFrameHook), so the engine call (Dispatch) is safe. //===========================================================================// #include // Application, application, DEBUG_STREAM #pragma hdrstop #include // strncpy (result names from the wire) #include "appmsg.hpp" // full Application::StopMissionMessage definition #include "appmgr.hpp" // gPerFrameHook #include "btl4console.hpp" // The pilot roster + score bridges (btl4gau3.cpp / mechmppr.cpp / btplayer.cpp): // the pilotList reads them for the Comm MFD, and the marshal snapshots them at // the stop so the results screen has the final scores. extern void *BTResolveRosterPilot(int slot); extern int BTPilotKills(void *pilot); extern int BTPilotDeaths(void *pilot); namespace { enum Phase { PhaseIdle, PhaseWaiting, PhaseRunning, PhaseStopped }; Phase gPhase = PhaseIdle; int gMissionSeconds = 0; unsigned long gStartTick = 0; Application *gWatched = 0; // Final scores snapshotted at the stop (roster order; slot 0 = local). enum { maxResults = 8 }; struct Result { int kills, deaths; char name[24]; }; Result gResults[maxResults]; int gResultCount = 0; void SnapshotResults() { gResultCount = 0; for (int slot = 0; slot < maxResults; ++slot) { void *pilot = BTResolveRosterPilot(slot); if (pilot == 0) break; // past the last occupied roster slot gResults[gResultCount].kills = BTPilotKills(pilot); gResults[gResultCount].deaths = BTPilotDeaths(pilot); gResults[gResultCount].name[0] = 0; // filled by the lobby (owner) if networked ++gResultCount; } DEBUG_STREAM << "[marshal] snapshot " << gResultCount << " pilot score(s)\n" << std::flush; } // // The game-thread tick (called every frame while an application is live). // void ConsoleTick() { if (application == 0) return; int state = application->GetApplicationState(); switch (gPhase) { case PhaseWaiting: // The mission has staged and started running -> start the clock. if (state == Application::RunningMission) { gPhase = PhaseRunning; gWatched = application; gStartTick = GetTickCount(); DEBUG_STREAM << "[marshal] mission running -- length " << gMissionSeconds << "s\n" << std::flush; } break; case PhaseRunning: if (application != gWatched) return; // only marshal our own mission if (state != Application::RunningMission) { // ended some other way (pilot abort, teardown) gPhase = PhaseStopped; DEBUG_STREAM << "[marshal] mission ended (state change)\n" << std::flush; } else if (gMissionSeconds > 0 && (GetTickCount() - gStartTick) >= (unsigned long)(gMissionSeconds * 1000)) { // The console clock expired: snapshot the final scores (the // game state is still live here), then stop the race exactly // as the arcade console did (StopMissionMessage on our pod). DEBUG_STREAM << "[marshal] time expired -- stopping mission\n" << std::flush; SnapshotResults(); Application::StopMissionMessage message(0); application->Dispatch(&message); gPhase = PhaseStopped; } break; default: break; } } } void BTLocalConsole_Install(int mission_seconds) { gMissionSeconds = mission_seconds; gPhase = PhaseWaiting; gStartTick = 0; gWatched = 0; // Marshaled: the in-process console owns the clock, so a console-loss // (there is none locally) would end the race -- harmless for solo. gConsoleLossEndsMission = True; gPerFrameHook = &ConsoleTick; DEBUG_STREAM << "[marshal] installed (length " << mission_seconds << "s)\n" << std::flush; } int BTLocalConsole_MissionCompleted() { return (gPhase == PhaseStopped) ? 1 : 0; } int BTLocalConsole_ResultCount() { return gResultCount; } int BTLocalConsole_GetResult(int index, int *kills, int *deaths) { if (index < 0 || index >= gResultCount) return 0; if (kills) *kills = gResults[index].kills; if (deaths) *deaths = gResults[index].deaths; return 1; } const char *BTLocalConsole_GetResultName(int index) { if (index < 0 || index >= gResultCount) return ""; return gResults[index].name; } // The lobby rebuilds the sheet from wire data (owner collates every pod's // scores) before showing the shared results, so it needs to clear + refill. void BTLocalConsole_ClearResults() { gResultCount = 0; } void BTLocalConsole_InjectResult(int slot, int kills, int deaths, const char *name) { if (slot < 0 || slot >= maxResults) return; gResults[slot].kills = kills; gResults[slot].deaths = deaths; gResults[slot].name[0] = 0; if (name) { strncpy(gResults[slot].name, name, sizeof(gResults[slot].name) - 1); gResults[slot].name[sizeof(gResults[slot].name) - 1] = 0; } if (slot >= gResultCount) gResultCount = slot + 1; } void BTLocalConsole_Uninstall() { gPerFrameHook = 0; gPhase = PhaseIdle; }