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>
579 lines
15 KiB
C++
579 lines
15 KiB
C++
#include "rpl4.h"
|
|
#pragma hdrstop
|
|
|
|
#include "rpl4pb.h"
|
|
#include "..\munga_l4\l4video.h"
|
|
#include "..\munga\mission.h"
|
|
#include "..\munga_l4\l4net.h"
|
|
#include "..\munga\update.h"
|
|
#include "..\munga\renderer.h"
|
|
#include "..\munga\cammppr.h"
|
|
#include "..\munga\icom.h"
|
|
#include "..\munga\nttmgr.h"
|
|
#include "..\rp\rpplayer.h"
|
|
#include "..\munga\dropzone.h"
|
|
#include "..\rp\vtv.h"
|
|
#include "..\munga\director.h"
|
|
|
|
// RB 1/14/07
|
|
//#include <dpl\matrix.h>
|
|
|
|
//#############################################################################
|
|
//########################## RPSpoolerTask ##############################
|
|
//#############################################################################
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
RPSpoolerTask::RPSpoolerTask()
|
|
{
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
RPSpoolerTask::DispatchPacket(NetworkPacket *packet)
|
|
{
|
|
RPL4PlaybackApplication *playback_app =
|
|
Cast_Object(RPL4PlaybackApplication*,application);
|
|
playback_app->DispatchPacket(packet);
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
long
|
|
RPSpoolerTask::GetTimeBias()
|
|
{
|
|
RPL4PlaybackApplication *playback_app =
|
|
Cast_Object(RPL4PlaybackApplication*,application);
|
|
Check_Fpu();
|
|
return playback_app->GetTimeBias();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
RPSpoolerTask::~RPSpoolerTask()
|
|
{
|
|
Check_Fpu();
|
|
}
|
|
|
|
RPL4IdleApplication::RPL4IdleApplication(HINSTANCE hInstance, HWND hWnd, ResourceFile *resource)
|
|
:RPL4Application(hInstance, hWnd, resource)
|
|
{
|
|
}
|
|
|
|
RPL4IdleApplication::~RPL4IdleApplication()
|
|
{
|
|
}
|
|
|
|
NetworkManager *RPL4IdleApplication::MakeNetworkManager()
|
|
{
|
|
Check_Fpu();
|
|
return new L4PlaybackNetworkManager;
|
|
}
|
|
|
|
Registry *RPL4IdleApplication::MakeRegistry()
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
void RPL4IdleApplication::LoadBackgroundTasks()
|
|
{
|
|
Check(this);
|
|
|
|
ApplicationTask *application_task;
|
|
|
|
application_task = new GaugeRendererTask;
|
|
Register_Object(application_task);
|
|
backgroundTasks->AddTask(application_task);
|
|
}
|
|
|
|
Logical RPL4IdleApplication::ExecuteForeground(Time start_of_frame, Scalar frame_duration)
|
|
{
|
|
RPL4Application::ExecuteForeground(start_of_frame, frame_duration);
|
|
return 0;
|
|
}
|
|
|
|
Logical RPL4IdleApplication::Shutdown(int remainingApps)
|
|
{
|
|
if (remainingApps > 1)
|
|
{
|
|
return 1;
|
|
} else
|
|
{
|
|
RPL4Application::Shutdown(remainingApps);
|
|
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
void
|
|
RPL4IdleApplication::Initialize()
|
|
{
|
|
Check(this);
|
|
|
|
L4Application::InitializeTillConsole();
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
//####################### RPL4PlaybackApplication #######################
|
|
//#############################################################################
|
|
|
|
const Receiver::HandlerEntry
|
|
RPL4PlaybackApplication::MessageHandlerEntries[]=
|
|
{
|
|
MESSAGE_ENTRY(RPL4PlaybackApplication, RunMission),
|
|
MESSAGE_ENTRY(RPL4PlaybackApplication, StopMission)
|
|
};
|
|
|
|
Receiver::MessageHandlerSet& RPL4PlaybackApplication::GetMessageHandlers()
|
|
{
|
|
static Receiver::MessageHandlerSet messageHandlers(ELEMENTS(RPL4PlaybackApplication::MessageHandlerEntries), RPL4PlaybackApplication::MessageHandlerEntries, RPL4Application::GetMessageHandlers());
|
|
return messageHandlers;
|
|
}
|
|
|
|
Derivation* RPL4PlaybackApplication::GetClassDerivations()
|
|
{
|
|
static Derivation classDerivations(RPL4Application::GetClassDerivations(), "RPL4PlaybackApplication");
|
|
return &classDerivations;
|
|
}
|
|
|
|
RPL4PlaybackApplication::SharedData
|
|
RPL4PlaybackApplication::DefaultData(
|
|
RPL4PlaybackApplication::GetClassDerivations(),
|
|
RPL4PlaybackApplication::GetMessageHandlers()
|
|
);
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
RPL4PlaybackApplication::RPL4PlaybackApplication(
|
|
HINSTANCE hInstance,
|
|
HWND hWnd,
|
|
ResourceFile *resource_file,
|
|
SpoolFile *spool_file
|
|
):
|
|
RPL4Application(hInstance, hWnd, resource_file, L4ApplicationClassID, DefaultData)
|
|
{
|
|
Check(spool_file);
|
|
spoolFile = spool_file;
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
NetworkManager*
|
|
RPL4PlaybackApplication::MakeNetworkManager()
|
|
{
|
|
Check_Fpu();
|
|
return new L4PlaybackNetworkManager;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
RPL4PlaybackApplication::Initialize()
|
|
{
|
|
Check(this);
|
|
|
|
L4Application::InitializeTillConsole();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
RPL4PlaybackApplication::LoadBackgroundTasks()
|
|
{
|
|
Check(this);
|
|
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
// Add background tasks
|
|
//--------------------------------------------------------------------------
|
|
//
|
|
ApplicationTask *application_task;
|
|
|
|
application_task = new RPSpoolerTask;
|
|
Register_Object(application_task);
|
|
backgroundTasks->AddTask(application_task);
|
|
|
|
application_task = new ProcessEventTask;
|
|
Register_Object(application_task);
|
|
backgroundTasks->AddTask(application_task);
|
|
|
|
application_task = new GaugeRendererTask;
|
|
Register_Object(application_task);
|
|
backgroundTasks->AddTask(application_task);
|
|
|
|
application_task = new CompleteCyclesTask;
|
|
Register_Object(application_task);
|
|
backgroundTasks->AddTask(application_task);
|
|
|
|
application_task = new FryDeathRowTask;
|
|
Register_Object(application_task);
|
|
backgroundTasks->AddTask(application_task);
|
|
|
|
application_task = new AudioRendererTask;
|
|
Register_Object(application_task);
|
|
backgroundTasks->AddTask(application_task);
|
|
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Logical
|
|
RPL4PlaybackApplication::ExecuteForeground(
|
|
Time start_of_frame,
|
|
Scalar frame_duration
|
|
)
|
|
{
|
|
SET_FOREGROUND_PROCESSING();
|
|
|
|
Check(this);
|
|
Verify(application == this);
|
|
|
|
if (!executeFrames || GetApplicationState() == WaitingForEgg)
|
|
{
|
|
videoRenderer->ExecuteIdle();
|
|
CLEAR_FOREGROUND_PROCESSING();
|
|
return executeFrames && !Exit_Code;
|
|
}
|
|
|
|
Time
|
|
frame_ticks;
|
|
frame_ticks = frame_duration;
|
|
|
|
if (RPL4Application::GetMissionReviewMode() == 2)
|
|
{
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
// Controls Manager
|
|
//
|
|
// Poll all devices, update all control variables.
|
|
//
|
|
// This is executed before the update manager so that the
|
|
// models have valid control values. It is not necessary for
|
|
// the controls to operate at the frame rate of this loop. If
|
|
// the controls manager can run run at a lower rate it can
|
|
// throttle itself internally.
|
|
//--------------------------------------------------------------------------
|
|
//
|
|
Check(controlsManager);
|
|
controlsManager->Execute();
|
|
}
|
|
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
// Update Manager
|
|
//
|
|
// Execute replicants.
|
|
// Execute master entities if they are interesting here or
|
|
// elsewhere.
|
|
// Inform interest manager of possible interest zone change.
|
|
// If the master entity has provided and update message then
|
|
// send it to replicants.
|
|
//
|
|
// This is executed before the interest manager so that the
|
|
// interest manager can merge all interest zone changes before
|
|
// broadcasting interest arena deltas and building interesting
|
|
// entity lists.
|
|
//
|
|
// This is executed before the renderers so that the watchers
|
|
// have executed on the model and are ready for use by the
|
|
// renderers.
|
|
//--------------------------------------------------------------------------
|
|
//
|
|
SET_UPDATE_MANAGER();
|
|
|
|
Check(updateManager);
|
|
updateManager->Execute(start_of_frame);
|
|
|
|
CLEAR_UPDATE_MANAGER();
|
|
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
// Interest Manager
|
|
//
|
|
// Update the net interest arena.
|
|
// Calculate which interest zones should be released and which
|
|
// interest zones should be loaded
|
|
// Send a becoming uninteresting and becoming interesting
|
|
// message to the entity.
|
|
// Broadcast the interest arena change
|
|
//
|
|
// The renderer manager call, update interest origins will
|
|
// update only those interest origins required by the renderers
|
|
// executing this frame.
|
|
//
|
|
// This is executed before the renderer manager so that when
|
|
// the renderers execute they execute upon interest lists that
|
|
// are valid as of the end of a model/update frame.
|
|
//
|
|
// In theory the interest manager does not have to run every
|
|
// frame. If throttled less, this would cause fuzz at interest
|
|
// zone borders, which may or may not be acceptable depending
|
|
// on the size of the interest arena.
|
|
//--------------------------------------------------------------------------
|
|
//
|
|
Check(rendererManager);
|
|
rendererManager->UpdateInterestOrigins(start_of_frame);
|
|
Check(interestManager);
|
|
interestManager->Execute();
|
|
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
// Renderer Manager
|
|
//
|
|
// Execute renderers.
|
|
// Get interest list from interest manager.
|
|
// Poll watchers.
|
|
//
|
|
// It is not necessary for all renderers to operate at the frame
|
|
// rate of this loop. If a renderer can run at a lower rate it
|
|
// does so via the renderer manager which will govern if a
|
|
// renderer executes this frame.
|
|
//--------------------------------------------------------------------------
|
|
//
|
|
SET_RENDERER_MANAGER();
|
|
|
|
Check(rendererManager);
|
|
rendererManager->Execute(start_of_frame, frame_ticks, frame_ticks);
|
|
|
|
CLEAR_RENDERER_MANAGER();
|
|
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
// Intercom Manager
|
|
//--------------------------------------------------------------------------
|
|
//
|
|
Check(intercomManager);
|
|
intercomManager->Execute();
|
|
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
// Execution statistics
|
|
//--------------------------------------------------------------------------
|
|
//
|
|
if (GetApplicationState() == RunningMission)
|
|
{
|
|
// 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();
|
|
Check_Fpu();
|
|
return executeFrames && !Exit_Code;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// ExecuteBackgroundTask
|
|
//#############################################################################
|
|
//
|
|
void
|
|
RPL4PlaybackApplication::ExecuteBackgroundTask()
|
|
{
|
|
Check(this);
|
|
|
|
//
|
|
// If there exists a high priority event, execute it
|
|
//
|
|
if (ProcessOneEvent(HighEventPriority))
|
|
{
|
|
return;
|
|
}
|
|
|
|
//
|
|
// Execute lower priority tasks
|
|
//
|
|
Check(backgroundTasks);
|
|
backgroundTasks->Execute();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
RPL4PlaybackApplication::~RPL4PlaybackApplication()
|
|
{
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
RPL4PlaybackApplication::DispatchPacket(NetworkPacket *packet)
|
|
{
|
|
Check(this);
|
|
NetworkManager *net_mgr = GetNetworkManager();
|
|
Check(net_mgr);
|
|
NetworkClient *client = net_mgr->GetNetworkClientPointer(packet->clientID);
|
|
Check(client);
|
|
client->ReceiveNetworkPacket(packet, &packet->messageData);
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
RPL4PlaybackApplication::RunMissionMessageHandler(
|
|
RunMissionMessage *message
|
|
)
|
|
{
|
|
Check(this);
|
|
//
|
|
//------------------------------------------------------------------------
|
|
// If the application is waiting to launch, synchronize the clock stuff up
|
|
//------------------------------------------------------------------------
|
|
//
|
|
if (GetApplicationState() == WaitingForLaunch)
|
|
{
|
|
SpoolFile *spool = GetSpoolFile();
|
|
Check(spool);
|
|
NetworkPacket *packet = (NetworkPacket*)spool->GetPointer();
|
|
Verify(packet->messageData.messageID == RunMissionMessageID);
|
|
timeBias = Now().ticks - packet->timeStamp.ticks;
|
|
spool->NextPacket();
|
|
}
|
|
|
|
RPL4Application::RunMissionMessageHandler(message);
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
char winners_spot[]="win?";
|
|
|
|
void
|
|
RPL4PlaybackApplication::StopMissionMessageHandler(
|
|
StopMissionMessage *message
|
|
)
|
|
{
|
|
//STUBBED: DPL RB 1/14/07
|
|
MissionReviewApplicationManager *app_mgr = GetApplicationManager();
|
|
Check(app_mgr);
|
|
SpoolFile *spool = GetSpoolFile();
|
|
if (spool)
|
|
{
|
|
Check(spool);
|
|
app_mgr->ReleaseSpoolFile(spool);
|
|
spoolFile = NULL;
|
|
|
|
//
|
|
//------------------------
|
|
// Find the dropzone group
|
|
//------------------------
|
|
//
|
|
EntityManager *entity_manager = application->GetEntityManager();
|
|
Check(entity_manager);
|
|
EntityGroup *dropzones = entity_manager->FindGroup("DropZones");
|
|
Check(dropzones);
|
|
char *place = winners_spot+3;
|
|
|
|
Player *p;
|
|
//
|
|
//----------------------------------------------------------------------
|
|
// Move all the players into their final positions in the winners circle
|
|
//----------------------------------------------------------------------
|
|
//
|
|
for (
|
|
int rank=0;
|
|
(p = CameraDirector::FindPlayerByRank(rank)) != NULL;
|
|
++rank
|
|
)
|
|
{
|
|
//
|
|
//-----------------------------------------------------------------
|
|
// Now find the dropzone appropriate to the rank. If one cannot be
|
|
// found, ignore this vehicle
|
|
//-----------------------------------------------------------------
|
|
//
|
|
Verify(rank<10);
|
|
*place = (char)('1' + rank);
|
|
ChainIteratorOf<Node*> iterator(dropzones->groupMembers);
|
|
DropZone *dropzone;
|
|
while ((dropzone = (DropZone*)iterator.ReadAndNext()) != NULL)
|
|
{
|
|
if (!strcmp(dropzone->GetDropZoneName(), winners_spot))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
if (!dropzone)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
//
|
|
//---------------------------------------------------------------------
|
|
// Get the location of this dropzone, and reset the vehicle to place it
|
|
// where specified
|
|
//---------------------------------------------------------------------
|
|
//
|
|
VTV *vtv = (VTV*)p->GetPlayerVehicle();
|
|
if (vtv->GetClassID() == VTVClassID)
|
|
{
|
|
vtv->Reset(dropzone->localOrigin, VTV::MissionReviewReset);
|
|
vtv->SetPerformance(&VTV::DoNothing);
|
|
vtv->FlushEvents();
|
|
}
|
|
}
|
|
|
|
//
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Reload the NameBitmaps for the Winners Circle
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
DPLRenderer *dpl_renderer = GetVideoRenderer();
|
|
Check(dpl_renderer);
|
|
dpl_renderer->SortAndReloadNameBitmaps();
|
|
|
|
//
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Set the Viewing Angle to 45 Degrees for Winners Circle
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
dpl_renderer->SetViewAngle(Degree(45.0f));
|
|
|
|
//
|
|
//~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Set the Lighting Angle
|
|
//~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
for(int ii=0;ii<dpl_renderer->sceneLightCount;++ii)
|
|
{
|
|
//dpl_RotateDCS(dpl_renderer->sceneLightDCS[ii], 0.0, dpl_Z);
|
|
//dpl_RotateDCS(dpl_renderer->sceneLightDCS[ii], 150.0, dpl_Y);
|
|
//dpl_RotateDCS(dpl_renderer->sceneLightDCS[ii], -30.0, dpl_X);
|
|
//dpl_FlushDCS (dpl_renderer->sceneLightDCS[ii]);
|
|
}
|
|
}
|
|
|
|
//
|
|
//---------------------------------------
|
|
// Let the application stop regularly now
|
|
//---------------------------------------
|
|
//
|
|
RPL4Application::StopMissionMessageHandler(message);
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Logical
|
|
RPL4PlaybackApplication::TestInstance() const
|
|
{
|
|
return IsDerivedFrom(*GetClassDerivations());
|
|
}
|