From 68f5780efa4e297c0ae9a3ac6d6198f45c7ceb4f Mon Sep 17 00:00:00 2001 From: Cyd Date: Wed, 5 Aug 2026 15:39:19 -0500 Subject: [PATCH] The cockpit clock counts the console's clock A race ends when the console says so, but the countdown on the map display was computed from the engine clock and its own idea of when the race started - QueryPerformanceCounter from Application::gameStarted, against the console's GetTickCount from gRunStartTick. Two clocks, two epochs, two threads. They agreed to within a frame in the ordinary case, which is why nobody noticed. They do not agree at all when RP412MISSIONSECONDS is set: the override shortens the CONSOLE's length and leaves the egg's alone, so a 25-second test race displayed a clock counting down from 5:00 and was stopped with 4:35 still showing. gMissionClockHook (APPMGR.h, alongside the gPerFrameHook it mirrors) lets the console answer for the countdown when it is marshalling. NULL, or a console that has no answer yet, falls back to exactly the old computation - which is what the arcade -net pods, lobby members and mission review all take, none of them running a console locally. A member's clock is anchored by the console's RunMission arriving over the wire anyway, so it starts within one latency of correct and only drifts at the rate the two crystals differ. Two things come out of it beyond the clock itself. The camera directors switch behaviour at "30 seconds left" (DIRECTOR.cpp, RPDIRECT.cpp) and were reading the same free-running number, so the dramatic end-of-race camera and the actual buzzer were on different clocks too; they now share one. And the countdown holds at 00:00 instead of going negative - the console polls at 250 ms, so zero always arrives slightly before the stop is dispatched. The hook is guarded on gWatchedApp == application. Nothing ever uninstalls it, 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 holding the previous mission's gLengthMs and gRunStartTick. Verified by running a 25-second race with the menu still set to 5:00 and photographing the map display: 00:17, 00:01, then 00:00 held while "time expired - stopping mission" went to the log. Captures use PrintWindow rather than CopyFromScreen - the first attempt grabbed the desktop sitting in front of the Map window, which is somebody's screen contents written to disk, and those files were deleted. Co-Authored-By: Claude Opus 5 (1M context) --- MUNGA/APP.cpp | 19 ++++++++++++++-- MUNGA/APPMGR.cpp | 3 +++ MUNGA/APPMGR.h | 22 ++++++++++++++++++ RP_L4/RPL4CONSOLE.cpp | 53 +++++++++++++++++++++++++++++++++++++++++++ RP_L4/RPL4PB.cpp | 16 +++++++++++-- 5 files changed, 109 insertions(+), 4 deletions(-) diff --git a/MUNGA/APP.cpp b/MUNGA/APP.cpp index 24f0344..6fab4f7 100644 --- a/MUNGA/APP.cpp +++ b/MUNGA/APP.cpp @@ -649,8 +649,23 @@ Time endIntercom = Now(); // if (GetApplicationState() == RunningMission) { - secondsRemainingInGame = - currentMission->GetGameLength() - (Now() - gameStarted); + // + // Ask the console first: it owns the clock that actually ends the + // race, so this is the countdown the buzzer will agree with. Its + // own reckoning is the fallback for everything with no console of + // its own - see gMissionClockHook in APPMGR.h. + // + Scalar console_remaining; + if (gMissionClockHook != NULL && + (*gMissionClockHook)(&console_remaining)) + { + secondsRemainingInGame = console_remaining; + } + else + { + secondsRemainingInGame = + currentMission->GetGameLength() - (Now() - gameStarted); + } } routePacketFinished = False; diff --git a/MUNGA/APPMGR.cpp b/MUNGA/APPMGR.cpp index 6dbad56..0abfe37 100644 --- a/MUNGA/APPMGR.cpp +++ b/MUNGA/APPMGR.cpp @@ -15,6 +15,9 @@ Logical gConsoleMarshalsLaunch = False; // losing the console mid-mission ends it (lobby-member races) Logical gConsoleLossEndsMission = False; +// the console's countdown, when a console is marshalling (see APPMGR.h) +Logical (*gMissionClockHook)(Scalar *seconds_remaining) = NULL; + ApplicationManager* ApplicationManager::CurrentAppManager = NULL; ApplicationManager::ApplicationManager(HINSTANCE hInstance, HWND hWnd, Scalar frame_rate) : Node(ApplicationManagerClassID), runningApplications(this) diff --git a/MUNGA/APPMGR.h b/MUNGA/APPMGR.h index ac52fdc..4ac35c8 100644 --- a/MUNGA/APPMGR.h +++ b/MUNGA/APPMGR.h @@ -20,6 +20,28 @@ extern Logical gConsoleMarshalsLaunch; // console to return, exactly as always. extern Logical gConsoleLossEndsMission; +// +// The console's own countdown, when there is a console to ask. +// +// A mission ends when the console says so, but secondsRemainingInGame was +// computed here from the engine clock and its own idea of when the race +// started - a different clock, from a different epoch, than the one that +// actually fires the buzzer. The two agree to within a frame or so, which +// is why nobody noticed, but they are not the same number: the cockpit +// clock could read 0:00 with the race still running, and the camera +// directors' "last 30 seconds" behaviour switched on the engine's reading +// rather than on the real remaining time. +// +// Set by the console when it is marshalling; NULL restores the engine's +// own reckoning, which is what the arcade -net pods, lobby members and +// mission review all use (none of them run a console locally, and their +// clock is anchored by the console's RunMission arriving anyway). +// +// Returns False when it has no answer yet - the window between the +// application reaching RunningMission and the console noticing. +// +extern Logical (*gMissionClockHook)(Scalar *seconds_remaining); + class ApplicationManager : public Node { public: diff --git a/RP_L4/RPL4CONSOLE.cpp b/RP_L4/RPL4CONSOLE.cpp index e57f3d4..8377db9 100644 --- a/RP_L4/RPL4CONSOLE.cpp +++ b/RP_L4/RPL4CONSOLE.cpp @@ -352,6 +352,55 @@ namespace gPhase = PhaseStopped; } + //--------------------------------------------------------------- + // The countdown the engine shows, taken from the clock that will + // actually end the race (gMissionClockHook - see APPMGR.h). + // + // Called on the game thread, reading two volatile LONGs the console + // thread writes with InterlockedExchange. Aligned 32-bit reads, and + // a torn value could only mistime the cockpit clock by one tick of + // a countdown nobody reads to the millisecond - not worth a lock on + // the frame path. + //--------------------------------------------------------------- + Logical MissionClock(Scalar *seconds_remaining) + { + // + // Only answer for the race this console is actually marshalling. + // Nothing ever uninstalls the hook, so a player who hosts a race + // and then joins somebody else's lobby still has it wired up - + // and in that race the console is a bystander whose gLengthMs and + // gRunStartTick belong to the previous mission entirely. + // + if (gWatchedApp == NULL || gWatchedApp != application) + { + return False; + } + if (!gMissionRunning) + { + return False; // not started, or already stopped + } + LONG length_ms = gLengthMs; + if (length_ms <= 0) + { + return False; // endless: nothing to count down + } + + // DWORD subtraction, so a GetTickCount wrap costs nothing + LONG elapsed_ms = (LONG)(GetTickCount() - (DWORD) gRunStartTick); + LONG left_ms = length_ms - elapsed_ms; + if (left_ms < 0) + { + // + // The console polls at 250 ms, so the clock reaches zero + // slightly before the stop is dispatched. Hold at zero + // rather than showing negative time in the cockpit. + // + left_ms = 0; + } + *seconds_remaining = (Scalar) left_ms / 1000.0f; + return True; + } + //--------------------------------------------------------------- // The game-thread tick: state reporting + engine-safe execution //--------------------------------------------------------------- @@ -522,6 +571,10 @@ namespace // 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; diff --git a/RP_L4/RPL4PB.cpp b/RP_L4/RPL4PB.cpp index 4b9f8d5..86ae448 100644 --- a/RP_L4/RPL4PB.cpp +++ b/RP_L4/RPL4PB.cpp @@ -357,8 +357,20 @@ Logical // if (GetApplicationState() == RunningMission) { - secondsRemainingInGame = - currentMission->GetGameLength() - (Now() - gameStarted); + // same rule as Application::ExecuteForeground - the console's + // countdown when there is one, our own reckoning otherwise. There + // is no console in mission review, so this takes the fallback. + Scalar console_remaining; + if (gMissionClockHook != NULL && + (*gMissionClockHook)(&console_remaining)) + { + secondsRemainingInGame = console_remaining; + } + else + { + secondsRemainingInGame = + currentMission->GetGameLength() - (Now() - gameStarted); + } } CLEAR_FOREGROUND_PROCESSING();