Files
RP412/MUNGA/APPMGR.h
T
CydandClaude Opus 5 68f5780efa 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) <noreply@anthropic.com>
2026-08-05 15:39:19 -05:00

73 lines
2.5 KiB
C++

#pragma once
#include "app.h"
extern HWND ghWnd;
// per-frame observer called once per RunMissions frame, after the
// foreground pass (the single-player local-console marshal uses this)
extern void (*gPerFrameHook)();
// True when an in-process console marshals the launch: suppresses the
// no-console self-run in CheckLoadMessageHandler so a hosted network
// race stages at WaitingForLaunch until every pod is ready. Plain
// single player leaves it False and auto-runs exactly as always.
extern Logical gConsoleMarshalsLaunch;
// True for lobby-member races: losing the console mid-mission ends the
// mission (the owner left - without a console the timer counts up
// forever). Arcade -net pods leave it False and re-listen for their
// 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:
ApplicationManager(HINSTANCE hInstance, HWND hWnd, Scalar frame_rate);
~ApplicationManager();
void StartApplication(Application *new_app);
void RunMissions();
Scalar GetFrameRate()
{
Check(this);
return frameRate;
}
HINSTANCE GetHInstance() { return mHInstance; }
HWND GetHWnd() { return ghWnd; }
static ApplicationManager* GetCurrentManager() { return CurrentAppManager; }
protected:
static ApplicationManager* CurrentAppManager;
Scalar frameDuration, frameRate;
SChainOf<Application*> runningApplications;
HINSTANCE mHInstance;
};