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>
244 lines
6.0 KiB
C++
244 lines
6.0 KiB
C++
#include "munga.h"
|
|
#pragma hdrstop
|
|
|
|
#include "appmgr.h"
|
|
|
|
HWND ghWnd = 0;
|
|
|
|
// per-frame observer slot (see RunMissions); the game layer registers
|
|
// the single-player local-console marshal here
|
|
void (*gPerFrameHook)() = NULL;
|
|
|
|
// an in-process console owns the launch (hosted network races)
|
|
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)
|
|
{
|
|
frameRate = frame_rate;
|
|
frameDuration = 1.0f/frameRate;
|
|
mHInstance = hInstance;
|
|
ghWnd = hWnd;
|
|
ApplicationManager::CurrentAppManager = this;
|
|
Check_Fpu();
|
|
}
|
|
|
|
ApplicationManager::~ApplicationManager()
|
|
{
|
|
}
|
|
|
|
void ApplicationManager::StartApplication(Application *new_app)
|
|
{
|
|
Check(this);
|
|
Check(new_app);
|
|
|
|
runningApplications.Add(new_app);
|
|
application = new_app;
|
|
new_app->Initialize();
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// RunMissions
|
|
//#############################################################################
|
|
//
|
|
void ApplicationManager::RunMissions()
|
|
{
|
|
Check(this);
|
|
int backgroundTasksRun = 0;
|
|
int foregroundTasksRun = 0;
|
|
Time beginFrameTimestamp, lastFrameTimestamp;
|
|
|
|
MSG msg;
|
|
SChainIteratorOf<Application*> current_application(runningApplications);
|
|
SChainOf<Application*> endedApplications(NULL);
|
|
SChainIteratorOf<Application*> endingApplication(endedApplications);
|
|
|
|
Start_Of_Frame:
|
|
backgroundTasksRun = 0;
|
|
foregroundTasksRun = 0;
|
|
beginFrameTimestamp = Now();
|
|
|
|
current_application.First();
|
|
|
|
lastFrameTimestamp = Now();
|
|
|
|
Time end_of_frame = Now();
|
|
end_of_frame += frameDuration;
|
|
#if defined(LAB_ONLY)
|
|
int bad_count = 0;
|
|
#endif
|
|
|
|
//
|
|
//----------------------------------
|
|
// Run all relevant foreground loops
|
|
//----------------------------------
|
|
//
|
|
|
|
Time startForeground = Now();
|
|
while ((application = current_application.ReadAndNext()) != NULL)
|
|
{
|
|
Check(application);
|
|
foregroundTasksRun++;
|
|
|
|
//------------------------------------------------------------------
|
|
// Give the per-frame observer a slice while 'application' is live
|
|
// (the loop condition NULLs the global on exit). The single-player
|
|
// local-console marshal hangs off this; NULL when unregistered.
|
|
//------------------------------------------------------------------
|
|
if (gPerFrameHook != NULL)
|
|
{
|
|
(*gPerFrameHook)();
|
|
}
|
|
|
|
if (!application->ExecuteForeground(end_of_frame, frameDuration))
|
|
{
|
|
if (!application->Shutdown(current_application.GetSize()))
|
|
{
|
|
endedApplications.Add(application);
|
|
|
|
if (current_application.GetCurrent() == NULL)
|
|
{
|
|
current_application.Last();
|
|
} else
|
|
{
|
|
current_application.Previous();
|
|
}
|
|
current_application.Remove();
|
|
}
|
|
}
|
|
}
|
|
Time endForeground = Now();
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// Check the Windows message queue for messages to be processed
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
//InvalidateRect(ghWnd, NULL, false);
|
|
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
|
|
{
|
|
if (msg.message == WM_QUIT)
|
|
{
|
|
endingApplication.First();
|
|
|
|
while (application = endingApplication.ReadAndNext())
|
|
{
|
|
application->Terminate();
|
|
delete application;
|
|
}
|
|
|
|
return;
|
|
}
|
|
TranslateMessage(&msg);
|
|
DispatchMessage(&msg);
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// Run all relevant background loops until it is time for the next frame.
|
|
// If no applications remain, exit the whole loop
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
current_application.First();
|
|
application = current_application.GetCurrent();
|
|
if (!application)
|
|
{
|
|
endingApplication.First();
|
|
|
|
while (application = endingApplication.ReadAndNext())
|
|
{
|
|
application->Terminate();
|
|
delete application;
|
|
}
|
|
return;
|
|
}
|
|
|
|
do
|
|
{
|
|
while (application->GetNetworkManager()->RoutePacket())
|
|
;
|
|
|
|
current_application.Next();
|
|
application = current_application.GetCurrent();
|
|
} while (application);
|
|
|
|
current_application.First();
|
|
application = current_application.GetCurrent();
|
|
if (!application)
|
|
{
|
|
endingApplication.First();
|
|
|
|
while (application = endingApplication.ReadAndNext())
|
|
{
|
|
application->Terminate();
|
|
delete application;
|
|
}
|
|
return;
|
|
}
|
|
|
|
//
|
|
//------------------------------------------------------------------
|
|
// Give each application a chance to do at least one background task
|
|
//------------------------------------------------------------------
|
|
//
|
|
Time startBackground = Now();
|
|
Background_Loop:
|
|
do
|
|
{
|
|
Check(application);
|
|
application->ExecuteBackgroundTask();
|
|
backgroundTasksRun++;
|
|
|
|
//
|
|
// Move to the next application
|
|
//
|
|
current_application.Next();
|
|
application = current_application.GetCurrent();
|
|
} while (application);
|
|
|
|
//
|
|
//---------------------------------------------------------------------
|
|
// If time remains before the next frame is due, do another pass on the
|
|
// background tasks
|
|
//---------------------------------------------------------------------
|
|
//
|
|
Time t2 = Now();
|
|
Time lateTime = Now();
|
|
lateTime += 15L;
|
|
|
|
current_application.First();
|
|
application = current_application.GetCurrent();
|
|
|
|
if (t2 < end_of_frame)
|
|
{
|
|
|
|
#if defined(LAB_ONLY)
|
|
if (end_of_frame - t2 > 0.1f)
|
|
{
|
|
DEBUG_STREAM << t2 << ',' << end_of_frame << endl << std::flush;
|
|
if (++bad_count == 1000)
|
|
{
|
|
Fail("End-of-frame cannot be correctly calculated!");
|
|
}
|
|
}
|
|
#endif
|
|
goto Background_Loop;
|
|
}
|
|
Time endBackground = Now();
|
|
|
|
//char str[256];
|
|
//Scalar lastFrameLength = Now() - beginFrameTimestamp;
|
|
//sprintf(str, "RPL4 - %.2f FPS", 1.0f / lastFrameLength);
|
|
//SetWindowTextA(ghWnd, str);
|
|
|
|
goto Start_Of_Frame;
|
|
}
|