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();