Files
RP412/RP_L4/RPL4APP.cpp
T
CydandClaude Fable 5 858fb7fb42 Fade into the Winners Circle instead of cutting to it
The podium arrived as a hard cut: the race was still on screen one frame
and the stand was there the next. The race has its own fade-to-black
already, and it was being suppressed to keep the fade from blacking out
the podium behind it - which threw away the transition along with the
problem.

Now the two are sequenced. StopMission lets the race fade out as it
always did and posts the podium to itself for when that fade has landed
on black; the handler stands the finishers up behind the black and ramps
back in. The fade-in is the end-of-mission fade run backwards - the same
multiply on the fog colour and both fog distances, from nothing up to
what the Winners Circle asked for.

Timings: 0.7s of fade-out and black, then a 0.45s fade in. Both come out
of the 11 second hold, leaving about ten seconds of podium.
RP412PODIUMFADEIN sets the ramp.

Verified by measuring frame brightness across the transition. The race
falls away and the screen reaches black, then the stand comes up - and
with the ramp stretched to 3s to make it resolvable at a half-second
sampling interval, it climbs 71.8, 77, 78.7, 79.7, 80.5 rather than
stepping, so it is a real fade and not a cut arriving late.

The camera also comes down and tilts up across the tiers, which is how a
podium wants to be shot. It is a balance in both directions: drop it
further or tilt harder and the sky takes the top half while the winner's
spot slides off the bottom of the frame; tilt down instead and the shot
turns into a floor plan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 10:18:51 -05:00

745 lines
22 KiB
C++

#include "rpl4.h"
#pragma hdrstop
#include "rpl4app.h"
#include "rpl4vid.h"
#include "rpl4arnd.h"
#include "rpl4grnd.h"
#include "rpl4mssn.h"
#include "..\munga_l4\l4gauge.h"
#include "..\rp\rpreg.h"
#include "..\munga_l4\l4ctrl.h"
#include "..\munga_l4\l4mppr.h"
#include "..\munga\appmgr.h"
#include "rpl4mode.h"
#include "..\rp\vtv.h"
#include "rpl4mppr.h"
#include "..\rp\rpplayer.h"
#include "..\munga_l4\l4video.h"
#include "..\munga\dropzone.h"
#include "..\munga\director.h"
#include "..\munga\nttmgr.h"
#include "..\munga_l4\l4vb16.h"
//
//#############################################################################
// RPL4Application
//#############################################################################
//
const Receiver::HandlerEntry
RPL4Application::MessageHandlerEntries[]=
{
MESSAGE_ENTRY(RPL4Application, StopMission),
MESSAGE_ENTRY(RPL4Application, WinnersCircle)
};
Receiver::MessageHandlerSet& RPL4Application::GetMessageHandlers()
{
static Receiver::MessageHandlerSet messageHandlers(
ELEMENTS(RPL4Application::MessageHandlerEntries),
RPL4Application::MessageHandlerEntries,
L4Application::GetMessageHandlers());
return messageHandlers;
}
Derivation* RPL4Application::GetClassDerivations()
{
static Derivation classDerivations(
L4Application::GetClassDerivations(), "RPL4Application");
return &classDerivations;
}
RPL4Application::SharedData
RPL4Application::DefaultData(
RPL4Application::GetClassDerivations(),
RPL4Application::GetMessageHandlers()
);
//
// How long to sit on black between the race fading out and the podium
// fading in. The race fade is FADE_OUT_TIME (half a second), so this is
// that plus a beat, to land on black rather than on the tail of it.
//
const Scalar winnersCircleFadeOutTime = 0.7f;
// and how quickly the stand comes up out of the black afterwards
const Scalar winnersCircleFadeInTime = 0.45f;
RPL4Application::RPL4Application(
HINSTANCE hInstance,
HWND hWnd,
ResourceFile *resource_file,
ClassID class_ID,
SharedData &shared_data
):
L4Application(hInstance, hWnd, resource_file, RPL4, class_ID, shared_data)
{
Check_Fpu();
}
//
//#############################################################################
// ~RPL4Application
//#############################################################################
//
RPL4Application::~RPL4Application()
{
Check_Fpu();
}
//
//#############################################################################
// ShowWinnersCircle
//#############################################################################
//
// The pod hall's award platform. Every map carries a "wcircle" stand at
// (1200,0,0) with eight ranked dropzones named win1..win8 on it - rank 1 at
// the front on the low tier, ranks 4-8 across the back on the high one. All
// this shipped; only the mission-review build ever drove it.
//
// Stand the finishers on their spots in finishing order, freeze them, and
// look back at the stand from in front of it.
//
void
RPL4Application::ShowWinnersCircle()
{
Check(this);
// RP412PODIUM=0 skips the whole thing
const char *podium_mode = getenv("RP412PODIUM");
if (podium_mode != NULL && atoi(podium_mode) == 0)
{
DEBUG_STREAM << "WinnersCircle: disabled\n" << std::flush;
return;
}
EntityManager *entity_manager = GetEntityManager();
if (entity_manager == NULL)
{
return;
}
EntityGroup *dropzones = entity_manager->FindGroup("DropZones");
if (dropzones == NULL)
{
DEBUG_STREAM << "WinnersCircle: no DropZones group\n" << std::flush;
return;
}
//
//---------------------------------------------------------------------
// Walk the finishing order. Player::CalcRanking has been ranking every
// scoring player by score each frame, in football as much as in a race
// (CalcFootballRanking exists but is never called), so rank order is
// meaningful in both.
//---------------------------------------------------------------------
//
char winners_spot[] = "win?";
char *place = winners_spot + 3;
int placed = 0;
Point3D standFront(0.0f, 0.0f, 0.0f);
Point3D standCentre(0.0f, 0.0f, 0.0f);
Player *p;
for (int rank = 0; (p = CameraDirector::FindPlayerByRank(rank)) != NULL; ++rank)
{
if (rank > 7)
{
break; // only eight spots exist on the stand
}
*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 == NULL)
{
continue;
}
Entity *vehicle = p->GetPlayerVehicle();
if (vehicle == NULL || vehicle->GetClassID() != VTVClassID)
{
continue;
}
VTV *vtv = (VTV*) vehicle;
vtv->Reset(dropzone->localOrigin, VTV::MissionReviewReset);
vtv->SetPerformance(&VTV::DoNothing);
vtv->FlushEvents();
Point3D spot = dropzone->localOrigin.linearPosition;
if (placed == 0)
{
// remember where the front of the stand is - the shot is framed
// off it rather than off hardcoded map coordinates
standFront = spot;
}
standCentre += spot;
DEBUG_STREAM << "WinnersCircle: " << winners_spot << " at "
<< spot.x << "," << spot.y << "," << spot.z << "\n" << std::flush;
++placed;
}
if (placed == 0)
{
DEBUG_STREAM << "WinnersCircle: nobody to place\n" << std::flush;
return;
}
//
//---------------------------------------------------------------------
// Switch the world back on.
//
// Dying collapses the view and sets the application "dead" until the
// pilot reincarnates - while that flag is up the renderer skips every
// static object, which is the whole map. A pilot killed near the buzzer
// is still waiting for a respawn that will never come, so the flag is
// still up and the podium would play out against a black screen.
//
// The mission-review build never hit this: it watches from a CameraShip,
// which cannot die, so nothing ever set the flag there.
//
// Standing the finishers up IS the resurrection, so clear it.
//---------------------------------------------------------------------
//
SetIsDead(false);
DPLRenderer *dpl_renderer = GetVideoRenderer();
if (dpl_renderer == NULL)
{
return;
}
//
//---------------------------------------------------------------------
// The name plates are drawn per rank slot, so they have to be re-sorted
// now that the finishing order is final - otherwise the signs read in
// whatever order the players were created.
//---------------------------------------------------------------------
//
dpl_renderer->SortAndReloadNameBitmaps();
//
//---------------------------------------------------------------------
// Widen to 45 degrees and pull back in front of the stand so the whole
// line-up frames up. The stand runs from z~3 (rank 1, low) to z~39
// (ranks 4-8, high) and x~1180..1219; at 45 degrees that needs roughly
// 30 units of standoff. Eye is above the top tier looking slightly
// down at the middle of the group.
//---------------------------------------------------------------------
//
standCentre.x /= (Scalar) placed;
standCentre.y /= (Scalar) placed;
standCentre.z /= (Scalar) placed;
//
// Stand off along the line from the middle of the group out through the
// front spot, so the shot faces the stand however the map has it turned,
// and lift the eye above the top tier to look down on the line-up.
//
Vector3D facing;
facing.x = standFront.x - standCentre.x;
facing.y = 0.0f;
facing.z = standFront.z - standCentre.z;
Scalar reach = (Scalar) sqrt(facing.x * facing.x + facing.z * facing.z);
if (reach < 0.01f)
{
facing.x = 0.0f; facing.z = -1.0f; reach = 1.0f;
}
//
// Framing is tunable while the shot is being dialled in:
// RP412PODIUMSTANDOFF distance out in front of the stand
// RP412PODIUMHEIGHT eye height above the group
// RP412PODIUMAIM height of the aim point above the group
//
//
// Framed off the stand itself: down low and tilted up across the tiers,
// which is how you photograph a podium. It is a balance in both
// directions - drop the camera further or tilt harder and the sky takes
// the top half while the winner's spot slides off the bottom; tilt down
// instead and it becomes a floor plan. Pillarboxing to 4:3 is what lets
// it sit this close without the platform trailing off at the sides.
//
Scalar standoff = 36.0f;
Scalar height = 12.0f;
Scalar aim_lift = 2.0f;
const char *tune = getenv("RP412PODIUMSTANDOFF");
if (tune != NULL && atof(tune) != 0.0) standoff = (Scalar) atof(tune);
tune = getenv("RP412PODIUMHEIGHT");
if (tune != NULL && atof(tune) != 0.0) height = (Scalar) atof(tune);
tune = getenv("RP412PODIUMAIM");
if (tune != NULL) aim_lift = (Scalar) atof(tune);
Point3D eye;
eye.x = standCentre.x + (facing.x / reach) * standoff;
eye.y = standCentre.y + height;
eye.z = standCentre.z + (facing.z / reach) * standoff;
standCentre.y += aim_lift;
//
// RP412PODIUMCAM=0 leaves the view in the cockpit, which is also the
// way to tell a bad camera from a scene that is not drawing at all.
//
//
// Clear the cockpit glass away. The MFDs and the radar sit over the
// viewscreen like the pod's bezels and have nothing to say once the
// race is over; the podium gets the whole canvas.
//
SVGA16 *cockpit = SVGA16::GetCockpit();
if (cockpit != NULL)
{
cockpit->HideSecondaryDisplays();
}
//
// Fog first: it pulls the clip plane in to 1100, and SetViewAngle is what
// rebuilds the projection that reads it.
//
dpl_renderer->SetFogStyle(DPLRenderer::winnersCircleFogStyle);
const char *camera_mode = getenv("RP412PODIUMCAM");
if (camera_mode == NULL || atoi(camera_mode) != 0)
{
dpl_renderer->SetViewAngle(Degree(45.0f));
dpl_renderer->SetPresentationCamera(eye, standCentre);
}
else
{
// still rebuild the projection so the new clip plane takes effect
dpl_renderer->SetViewAngle(Degree(40.0f));
}
if (camera_mode != NULL && atoi(camera_mode) == 0)
{
DEBUG_STREAM << "WinnersCircle: presentation camera disabled\n"
<< std::flush;
}
//
// Everything is in place behind the black - bring it up.
// RP412PODIUMFADEIN sets the ramp in seconds.
//
Scalar fade_in = winnersCircleFadeInTime;
const char *fade_tune = getenv("RP412PODIUMFADEIN");
if (fade_tune != NULL && atof(fade_tune) > 0.0)
{
fade_in = (Scalar) atof(fade_tune);
}
dpl_renderer->StartPresentationFadeIn(fade_in);
DEBUG_STREAM << "WinnersCircle: " << placed << " placed; centre "
<< standCentre.x << "," << standCentre.y << "," << standCentre.z
<< " eye " << eye.x << "," << eye.y << "," << eye.z
<< "\n" << std::flush;
}
//
//#############################################################################
// StopMissionMessageHandler
//#############################################################################
//
// The mission is over. Show the podium, then let the base handler run - it
// puts the player into MissionEndingState, whose fade timer is what actually
// keeps the sim and the renderer alive until teardown.
//
void
RPL4Application::StopMissionMessageHandler(StopMissionMessage *message)
{
Check(this);
//
// StopMission arrives twice: once from the console at the buzzer, and
// again from the player when the ending fade runs out - that second one
// is what actually retires the application. Only the first is the end of
// the race.
//
if (GetApplicationState() != Application::EndingMission)
{
//
//-------------------------------------------------------------
// Don't cut straight to the podium. The race gets its own
// fade-to-black first - that fade is already running by the time
// this returns - and the Winners Circle comes up out of the
// black afterwards. Standing everyone up now would just fade out
// the podium instead of the race.
//
// FADE_OUT_TIME is half a second; a beat more than that lands on
// black rather than on the tail of the fade.
//-------------------------------------------------------------
//
Receiver::Message podium_message(
WinnersCircleMessageID, sizeof(Receiver::Message));
Time event_time;
event_time = Now();
event_time += winnersCircleFadeOutTime;
Post(LowEventPriority, this, &podium_message, event_time);
DEBUG_STREAM << "WinnersCircle: race over, fading out\n" << std::flush;
}
L4Application::StopMissionMessageHandler(message);
Check_Fpu();
}
//
//#############################################################################
// WinnersCircleMessageHandler
//#############################################################################
//
// The race has faded to black. Set the stand up behind the black and fade
// back in to it.
//
void
RPL4Application::WinnersCircleMessageHandler(Receiver::Message *)
{
Check(this);
DEBUG_STREAM << "WinnersCircle: standing the finishers up\n" << std::flush;
ShowWinnersCircle();
Check_Fpu();
}
//
//#############################################################################
// MakeRegistry
//#############################################################################
//
Registry*
RPL4Application::MakeRegistry()
{
Check_Fpu();
return new RPRegistry(GetResourceFile());
}
//
//#############################################################################
// MakeVideoRenderer
//#############################################################################
//
VideoRenderer*
RPL4Application::MakeVideoRenderer()
{
if (divisionParameters)
{
return
new RPL4VideoRenderer(
ghWnd,
L4Application::mScreenWidth,
L4Application::mScreenHeight,
L4Application::mFullscreen,
VisualInterestType,
DefaultInterestDepth
);
}
return NULL;
}
//
//#############################################################################
// MakeAudioRenderer
//#############################################################################
//
AudioRenderer*
RPL4Application::MakeAudioRenderer()
{
Check_Fpu();
// char *blaster1, *blaster2;
// blaster1 = getenv(FRONT_CARD_ENV_VAR);
// blaster2 = getenv(REAR_CARD_ENV_VAR);
//if (blaster1 != NULL && blaster2 != NULL)
//{
return new RPL4AudioRenderer(
1000.0f,
GetMissionReviewMode()
);
//}
// return NULL;
}
//#############################################################################
// MakeModeManager
//#############################################################################
ModeManager*
RPL4Application::MakeModeManager()
{
RPL4ModeManager
*manager = new RPL4ModeManager(RPL4ModeManager::ModeInitial);
Check(manager);
return manager;
}
//
//#############################################################################
// MakeGaugeRenderer
//#############################################################################
//
GaugeRenderer*
RPL4Application::MakeGaugeRenderer(int *secondaryIndex, int *aux1Index, int *aux2Index)
{
char *mode_string = getenv("L4GAUGE");
if (mode_string != NULL && (secondaryIndex != NULL || aux1Index != NULL || aux2Index != NULL))
{
return new RPL4GaugeRenderer(!L4Application::mFullscreen, secondaryIndex, aux1Index, aux2Index);
}
return NULL;
}
//
//#############################################################################
// MakeMission
//#############################################################################
//
Mission*
RPL4Application::MakeMission(
NotationFile *notation_file,
ResourceFile *resources
)
{
Check(this);
Check(notation_file);
Check(resources);
Check_Fpu();
return new RPL4Mission(notation_file, resources);
}
//
//#############################################################################
// MakeViewpointEntity
//#############################################################################
//
Entity*
RPL4Application::MakeViewpointEntity(Entity::MakeMessage *message)
{
Check(this);
// Entity
// *viewing_entity = L4Application::MakeViewpointEntity(message);
//
// if(viewing_entity)
// {
// Check_Fpu();
// return viewing_entity;
// }
LBE4ControlsManager* controls =
(LBE4ControlsManager *)GetControlsManager();
Check(controls);
Entity
*viewing_entity = NULL;
char
*primary_mapping_name = NULL;
switch(message->classToCreate)
{
case CameraShipClassID:
//=======================================================================
// Create Camera ship
//=======================================================================
{
CameraShip
*viewing_camera =
CameraShip::Make((CameraShip::MakeMessage*)message);
Check(viewing_camera);
viewing_entity = viewing_camera;
//--------------------------------------------------
// Create controls mapping object
//--------------------------------------------------
CameraControlsMapper::SubsystemResource
control_subsystem_resource;
Str_Copy(
control_subsystem_resource.subsystemName,
"ControlsMapper",
sizeof(control_subsystem_resource.subsystemName)
);
control_subsystem_resource.classID = TrivialSubsystemClassID;
control_subsystem_resource.subsystemModelSize =
sizeof(control_subsystem_resource);
CameraControlsMapper
*camera_mapper;
switch (controls->primaryControlType)
{
case LBE4ControlsManager::PrimaryThrustMaster:
Tell("RPL4Application, CameraShip using ThrustMaster\n");
camera_mapper = new CameraThrustmasterMapper(
viewing_camera,
CameraShip::ControlsMapperSubsystem,
&control_subsystem_resource
);
primary_mapping_name = "Thrustmaster";
break;
case LBE4ControlsManager::PrimaryRIO:
Tell("RPL4Application, CameraShip using RIO\n");
camera_mapper = new CameraRIOMapper(
viewing_camera,
CameraShip::ControlsMapperSubsystem,
&control_subsystem_resource
);
primary_mapping_name = "L4";
break;
default:
Tell("RPL4Application, CameraShip using default mapper\n");
camera_mapper = new CameraL4Mapper(
viewing_camera,
CameraShip::ControlsMapperSubsystem,
&control_subsystem_resource
);
// primary mapping name is NULL
break;
}
//-------------------------------------------
// Set mapping subsystem for this entity
//-------------------------------------------
Register_Object(camera_mapper);
viewing_camera->SetMappingSubsystem(camera_mapper);
}
break;
case VTVClassID:
//=======================================================================
// Create VTV
//=======================================================================
{
VTV
*viewing_VTV = VTV::Make((VTV::MakeMessage*)message);
Check(viewing_VTV);
viewing_entity = viewing_VTV;
//--------------------------------------------------
// Set control deadbanding
//--------------------------------------------------
controls->SetJoystickDeadBand(.1);
controls->SetThrottleDeadBand(.1);
controls->SetPedalsDeadBand(.1);
//--------------------------------------------------
// Create controls mapping object
//--------------------------------------------------
VTVControlsMapper::SubsystemResource
control_subsystem_resource;
Str_Copy(
control_subsystem_resource.subsystemName,
"ControlsMapper",
sizeof(control_subsystem_resource.subsystemName)
);
control_subsystem_resource.classID = TrivialSubsystemClassID;
control_subsystem_resource.subsystemModelSize =
sizeof(control_subsystem_resource);
control_subsystem_resource.segmentIndex = -1;
control_subsystem_resource.subsystemFlags = 0;
VTVControlsMapper
*vtv_mapper;
switch (controls->primaryControlType)
{
case LBE4ControlsManager::PrimaryThrustMaster:
Tell("RPL4Application, VTV using ThrustMaster\n");
vtv_mapper = new VTVThrustmasterMapper(
viewing_VTV,
VTV::ControlsMapperSubsystem,
&control_subsystem_resource
);
primary_mapping_name = "Thrustmaster";
break;
case LBE4ControlsManager::PrimaryRIO:
Tell("RPL4Application, VTV using RIO\n");
vtv_mapper = new VTVRIOMapper(
viewing_VTV,
VTV::ControlsMapperSubsystem,
&control_subsystem_resource
);
primary_mapping_name = "L4";
break;
default:
DEBUG_STREAM << "*****VTV has no controls mapping!*****" << std::flush;
Fail("RPL4Application::MakeViewpointEntity");
vtv_mapper = NULL;
// primary mapping name is NULL
break;
}
//-------------------------------------------
// Set mapping subsystem for this entity
//-------------------------------------------
Register_Object(vtv_mapper);
viewing_VTV->SetMappingSubsystem(vtv_mapper);
//-------------------------------------------
// Enable collision detection for this entity
//-------------------------------------------
viewing_VTV->StartCollisionAssistant();
}
break;
}
Check(viewing_entity);
//------------------------------------------------------------------------
// Load up the control mapping stream for the entity
//------------------------------------------------------------------------
if (primary_mapping_name != NULL)
{
ResourceDescription
*resource_description =
application->GetResourceFile()->SearchList(
viewing_entity->GetResourceID(),
ResourceDescription::ControlMappingsListResourceType
);
if (resource_description != NULL)
{
Check(resource_description);
Check(GetResourceFile());
resource_description =
GetResourceFile()->SearchList(
resource_description->resourceID,
primary_mapping_name
);
if (resource_description != NULL)
{
Check(resource_description);
resource_description->Lock();
controls->CreateStreamedMappings(
viewing_entity,
(int*)resource_description->resourceAddress
);
resource_description->Unlock();
}
}
}
//------------------------------------------------------------------------
// Create gauges for this entity
//------------------------------------------------------------------------
RPL4GaugeRenderer
*gauge_renderer = (RPL4GaugeRenderer *) GetGaugeRenderer();
if (gauge_renderer != NULL)
{
Check(gauge_renderer);
gauge_renderer->ConfigureForModel("Init", viewing_entity);
}
Check_Fpu();
return viewing_entity;
}