8 Commits
Author SHA1 Message Date
CydandClaude Fable 5 1efd5137d6 Release 4.12.5
Version string, zip name and README, plus the new environ.ini options -
the podium and the mission-length override. Everything else in that file
documents itself, so these should not be the exception.

This one restores the Winners Circle: the race fades out and fades back
in on the award platform, finishers in finishing order with their
callsigns on the plates, held for a few seconds before the results.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 22:18:05 -05:00
CydandClaude Fable 5 b97dcce3a2 Pilot callsigns on the Winners Circle plates
The plates beside each spot came out blank. They ask for textures called
player1..player8, which are not files - the renderer draws each pilot's
callsign into a texture at run time - so the load failed and left them
untextured. Nothing bound the two together.

SortAndReloadNameBitmaps already builds those textures indexed by
finishing place, which is exactly how the plates are numbered, so the
plate beside each spot wants mNameTextures[place]. Binding them is the
whole fix, but it takes two steps rather than one.

The plates have to survive mesh consolidation first. Static geometry is
merged with D3DXConcatenateMeshes and its draw ops deduped by material -
and eight failed texture loads leave eight identical untextured ops, so
all eight plates collapse into one that could only ever carry a single
name. Each plate now gets a distinct 1x1 marker texture as it loads,
which keeps it a subset of its own. The marker is never seen.

Then the binding runs against the consolidated mesh, not the objects the
plates were loaded from - by podium time those have been merged away and
are no longer drawn, which is why re-pointing them changed nothing.

Verified on a race: 8 plates found in the consolidated mesh, 1 bound,
and the winner's plate reads their callsign under their vehicle. One
bound of eight is right for a single-pod race - the rest of the places
have nobody in them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 14:40:53 -05:00
CydandClaude Fable 5 f31c8401c7 No gunsight on the Winners Circle
The reticle was still drawn over the podium. It goes out in the 2D pass,
after everything the presentation turns off, so it survived. The race is
over and nothing is being aimed at.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 14:27:07 -05:00
CydandClaude Fable 5 e1a3ef7cf1 Put the pilot's own vehicle on the Winners Circle
Your own vehicle is built insideEntity - a cockpit and no hull, because
you are sitting in it and never see it. That is fine for a race and wrong
for a podium: from the presentation camera your spot on the stand was the
one that was empty, and on a single-pod race that is the whole picture.

The renderer now gives the viewpoint entity an exterior before the shot.
Disconnected_Eye is the engine's own switch for this case, documented as
being there "so higher level renderers can fix the eye in one spot and
watch the viewpoint entity drive around", and it is what makes
NotifyOfNewInterestingEntity choose outsideEntity.

The exterior is added alongside what is already there rather than by
tearing the entity down and rebuilding it. Teardown-and-rebuild is the
path the interest manager uses constantly for scenery dropping out of
range, so it looked safe, but it is not safe for the viewpoint entity:
that one is never uninteresting, the eye renderable goes down with it,
and doing it mid-mission stops the scene rendering at all - the screen
went black from the moment the podium came up and never came back.
Adding the renderables directly does the same job with nothing removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 10:50:06 -05:00
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
CydandClaude Fable 5 28df53aa31 Frame the Winners Circle for the shape it was built for
The stand was composed to be looked at from a 4:3 pod monitor. On a 16:9
canvas its platform runs out at the sides and the shot fills up with sky
and void, so the podium is now pillarboxed: the scene renders into a
centred 4:3 viewport with the surround left black.

The projection has to use the cropped shape too, or the scene comes out
squashed into the narrower viewport instead of cropped by it.
RP412PODIUMASPECT overrides the ratio, 0 turns it off.

The camera also came in closer, from 33 units rather than 45, and now
aims slightly below the group rather than above it. That tilt is what
buys back the sky above the grandstand - aiming above the group tips the
camera up instead and walks the winner's spot off the bottom of the
frame, which is the one position that has to be in shot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 10:05:45 -05:00
CydandClaude Fable 5 461dcfb6b9 Clear the cockpit glass for the Winners Circle
The six secondary displays sit over the viewscreen like the pod's bezels
and have nothing to say once the race is over. Worse, the radar sits dead
centre along the bottom edge - directly on top of the winner's spot, so
the one position that matters was the one you could not see.

They are hidden when the podium comes up, which uncovers the canvas the
3D is already being drawn on. No matching show: the mission is over by
then, and the next race builds a fresh cockpit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 09:53:29 -05:00
CydandClaude Fable 5 9389ec2003 Winners Circle: the award platform at the end of a race
The pod hall stood the finishers on a numbered platform when the race
ended. All of it shipped in this repo and none of it ever ran here: the
stand geometry, the eight ranked dropzones win1-win8 in every one of the
11 maps, and the sequence that places the racers on them. The sequence
lived on RPL4PlaybackApplication - the mission-review build - behind a
spool file, so the app the pods actually race has never called it.

RPL4Application now has its own StopMission handler that ranks the
finishers, drops each onto their spot, freezes them, re-sorts the name
plates into finishing order and frames a camera on the stand. It fires
once: StopMission arrives twice, from the console at the buzzer and again
from the player when the ending fade expires, and only the first is the
end of the race.

Ranking works in football as well as a race. CalcFootballRanking ranks
only the RunnerPlayers group, which would have placed the runners and
stopped - but nothing calls it. What runs is Player::CalcRanking, every
frame, over every scoring player by score.

Three pieces of the original had been stubbed out in the D3D9 port and
are restored:

  SetViewAngle was an empty function, so the 45 degrees the sequence asks
  for did nothing. It now rebuilds the projection the way DPLReadINIPage
  does and pushes it, and sets viewRatio, which nothing had written since
  the DPL body was commented out.

  winnersCircleFogStyle was an empty case. The stand sits far off the
  track in open ground where the track's own fog leaves it dark; this is
  the blue-violet the original used, with the fog pushed back to 100/1050
  and the clip plane pulled to 1100.

  The end-of-mission fade had to be told to stand down. It multiplies the
  fog colour and both fog distances toward zero every frame - correct when
  a race just ends, fatal to anything shown afterwards. That fade is what
  made the podium a black screen, and it took a while to find because
  every frame was being built and presented correctly the whole time.

The presentation camera overrides D3DTS_VIEW between the eye renderable
writing it and ExecuteImplementation reading it back for the draw calls,
so no CameraShip is needed. It builds with LookAt LH, not RH: the
projection is LH, and RH aims the camera the opposite way - ask to look
down at the stand and you get the sky behind you. The engine's own eye
renderable is right to use RH, because its forward and up come out of the
entity matrix already in that convention.

The mission is held open 11 seconds rather than 3. That fade timer is the
only thing keeping the simulation and the renderer alive once the race is
over, and it has no upper bound on the ending path.

Switches, all off-by-default behaviour aside: RP412PODIUM=0 skips it,
RP412PODIUMCAM=0 keeps the cockpit view, RP412PODIUMSTANDOFF/HEIGHT/AIM
frame the shot, RP412MISSIONSECONDS overrides the menu game length (the
shortest it offers is 3:00, a long wait when what you are testing is the
buzzer), and RP412RENDERDIAG=1 reports what a frame is made of.

Verified end to end on Wiseguy's Wake: the stand, its tiers, the blue 2
and 3, the red 4 through 8 and all eight name bays, held steady for the
full 11 seconds and then handing off to the results screen.

Known gaps: the name plates are blank, because the player1-8 textures are
runtime name bitmaps that do not resolve as files in this port, and your
own vehicle has no exterior model - you see the others, not yourself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 09:46:38 -05:00
16 changed files with 1161 additions and 26 deletions
+78 -1
View File
@@ -13,6 +13,7 @@ L4TEXOP::WrapType d3d_OBJECT::mLastWrapV = L4TEXOP::WrapType::REPEAT;
bool d3d_OBJECT::mLastTexturingState = true;
long d3d_OBJECT::mNextID = 1;
stdext::hash_map<std::string, L4TEXOP> d3d_OBJECT::mTextureCache;
LPDIRECT3DTEXTURE9 d3d_OBJECT::mNamePlateMarkers[d3d_OBJECT::kNamePlateCount + 1] = { NULL };
void chgext(char *filePath, const char *newExtension)
{
@@ -156,7 +157,36 @@ d3d_OBJECT *d3d_OBJECT::LoadObject(LPDIRECT3DDEVICE9 device, char *fileName)
{
sprintf(textureFilename, "VIDEO\\%s", materials[i].pTextureFilename);
}
object->mDrawOps[i].texture = LoadTexture(device, textureFilename);
//
// playerN is not a file - it is a pilot's callsign, drawn into
// a texture at run time, so loading it always fails. Give the
// plate its marker texture instead: it keeps the eight sign
// faces as eight separate draw ops through consolidation, and
// the Winners Circle swaps the real callsign in later.
//
const char *base = strrchr(textureFilename, '\\');
base = (base != NULL) ? base + 1 : textureFilename;
int plate = 0;
if (_strnicmp(base, "player", 6) == 0 &&
base[6] >= '1' && base[6] <= '8')
{
plate = base[6] - '0';
}
if (plate != 0)
{
memset(&object->mDrawOps[i].texture, 0, sizeof(L4TEXOP));
object->mDrawOps[i].texture.texture =
NamePlateMarker(device, plate);
if (object->mDrawOps[i].texture.texture != NULL)
{
object->mDrawOps[i].texture.texture->AddRef();
}
}
else
{
object->mDrawOps[i].texture = LoadTexture(device, textureFilename);
}
}
if (nextDetailOp < numDetailOps && detailOps[nextDetailOp] == i)
@@ -286,6 +316,53 @@ void d3d_OBJECT::FlushTextureCache()
(*iter).second.texture->Release();
}
mTextureCache.clear();
for (int plate = 0; plate <= kNamePlateCount; ++plate)
{
if (mNamePlateMarkers[plate] != NULL)
{
mNamePlateMarkers[plate]->Release();
mNamePlateMarkers[plate] = NULL;
}
}
}
//
// A distinct 1x1 texture per plate, so the eight sign faces stay eight
// separate draw ops through mesh consolidation instead of merging into one.
// Its content never shows - the callsign replaces it before the podium.
//
LPDIRECT3DTEXTURE9 d3d_OBJECT::NamePlateMarker(LPDIRECT3DDEVICE9 device, int plate)
{
if (plate < 1 || plate > kNamePlateCount || device == NULL)
{
return NULL;
}
if (mNamePlateMarkers[plate] == NULL)
{
device->CreateTexture(1, 1, 1, 0, D3DFMT_A4R4G4B4, D3DPOOL_MANAGED,
&mNamePlateMarkers[plate], NULL);
}
return mNamePlateMarkers[plate];
}
//
// Which plate a texture is the marker for, or 0 if it is not one.
//
int d3d_OBJECT::NamePlateFor(LPDIRECT3DTEXTURE9 texture)
{
if (texture == NULL)
{
return 0;
}
for (int plate = 1; plate <= kNamePlateCount; ++plate)
{
if (mNamePlateMarkers[plate] == texture)
{
return plate;
}
}
return 0;
}
d3d_OBJECT::d3d_OBJECT(LPDIRECT3DDEVICE9 device, int vertCount)
+24
View File
@@ -111,6 +111,27 @@ public:
// race loop tears the renderer down between missions).
static void FlushTextureCache();
//------------------------------------------------------------------
// Pilot name plates.
//
// The Winners Circle signs ask for textures called player1..player8,
// which are not files - the renderer draws each pilot's callsign into
// a texture at run time. Loading them fails and the plates come out
// blank, so the draw ops that wanted them are noted here as the
// geometry loads, and bound to the real textures once the finishing
// order is known.
//
// Cleared with the texture cache: the entries point at objects that
// belong to the mission being torn down.
//------------------------------------------------------------------
// Each plate is given a distinct marker texture as it loads. Without
// one they are eight identical untextured draw ops, and consolidation
// merges every static mesh by material - all eight plates would
// collapse into a single shared op that can only ever show one name.
enum { kNamePlateCount = 8 };
static LPDIRECT3DTEXTURE9 NamePlateMarker(LPDIRECT3DDEVICE9 device, int plate);
static int NamePlateFor(LPDIRECT3DTEXTURE9 texture);
private:
static d3d_OBJECT* LoadSpheres(LPDIRECT3DDEVICE9 device, char *fileName);
@@ -150,6 +171,9 @@ private:
static long mNextID;
static stdext::hash_map< std::string , L4TEXOP > mTextureCache;
// index 1..8; [0] unused so the index is the plate number
static LPDIRECT3DTEXTURE9 mNamePlateMarkers[kNamePlateCount + 1];
};
extern int gNumBatches;
+14
View File
@@ -490,6 +490,20 @@ void
// one message per frame, and queued WM_PAINTs (lowest priority) starve
// behind it - panes would freeze on their first frame.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Off screen, and stay off: the pane keeps its pixels and geometry, it
// simply stops being shown. Repaint() on a hidden window is harmless.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void
MFDSplitView::Hide()
{
Check_Pointer(this);
if (window != NULL)
{
ShowWindow((HWND) window, SW_HIDE);
}
}
void
MFDSplitView::SetPosition(int x, int y)
{
+6
View File
@@ -71,6 +71,12 @@ public:
void
Resize(int display_width, int display_height);
// Take the pane off screen. Used for the Winners Circle, which wants
// the whole viewscreen: the panes overlap it like the pod's bezels,
// so hiding them uncovers the full canvas underneath.
void
Hide();
~MFDSplitView();
Logical
+23
View File
@@ -4161,6 +4161,29 @@ void
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Clear the glass off the screen for the Winners Circle.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void
SVGA16::HideSecondaryDisplays()
{
Check_Pointer(this);
if (!splitViews)
{
return;
}
for (int view = 0; view < SplitViewCount; ++view)
{
if (splitView[view] != NULL)
{
splitView[view]->Hide();
}
}
DEBUG_STREAM << "SVGA16: secondary displays hidden for the podium\n"
<< std::flush;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Fit the 1920x1080 cockpit canvas into the given client area: one
// uniform scale (so the canvas never stretches - a wider desktop just
+12
View File
@@ -349,6 +349,18 @@ public:
GetCockpit()
{ return activeCockpit; }
//------------------------------------------------------------------
// Take the six secondary displays off screen and leave the whole
// canvas to the viewscreen. The Winners Circle wants the podium
// uncluttered, and the panes are only bezels over the 3D - hiding
// them uncovers what is already being drawn behind.
//
// There is no matching show: the mission is over by the time this
// is called, and the next race builds a fresh cockpit.
//------------------------------------------------------------------
void
HideSecondaryDisplays();
protected:
Logical
splitViews;
+437 -22
View File
@@ -1797,6 +1797,19 @@ DPLRenderer::DPLRenderer(
backgroundGreen = 0.0f;
backgroundBlue = 0.0f;
viewAngle = 30.0f;
// the Winners Circle camera is off until the podium asks for it. No
// restore path is needed: a fresh DPLRenderer is built per mission, so
// the next race re-reads viewangle from RPDPL.INI.
mPresentationCamera = False;
mPresentationFog = False;
mPresentationAspect = 0.0f;
mPresentationFadeTime = 0.0f;
mPresentationFogRed = 0.0f;
mPresentationFogGreen = 0.0f;
mPresentationFogBlue = 0.0f;
mPresentationFogNear = 0.0f;
mPresentationFogFar = 0.0f;
D3DXMatrixIdentity(&mPresentationView);
dplMainView = NULL;
dplDeathZone = NULL;
dplMainZone = NULL;
@@ -2343,10 +2356,53 @@ void
}
break;
case winnersCircleFogStyle:
//
// The presentation shot, restored from the DPL body below. The
// stand sits far off the track in open ground, so the track's own
// fog leaves it in the dark - this is the lighter blue-violet the
// original used to lift it, with the fog pushed back to 100/1050.
//
// HACK!! This really shouldn't reset the clip planes, but since
// it only happens at the end of the review, it should be safe for now.
//
fogRed = 0.32f;
fogGreen = 0.30f;
fogBlue = 0.65f;
fogNear = 100.0f;
fogFar = 1050.0f;
// the per-frame FOGSTART/FOGEND come from these
currentFogNear = fogNear;
currentFogFar = fogFar;
clipNear = 0.25f;
clipFar = 1100.0f;
// tells the end-of-mission fade to stand down
mPresentationFog = True;
//
// Crop to the shape the stand was composed for. 4:3 unless
// RP412PODIUMASPECT says otherwise; 0 turns it off and lets
// the podium run full width.
//
mPresentationAspect = 4.0f / 3.0f;
{
const char *aspect = getenv("RP412PODIUMASPECT");
if (aspect != NULL)
{
mPresentationAspect = (float) atof(aspect);
}
}
// unconditional: fogUpdating is off while a vehicle drives its own
// headlight fog, and the podium overrides all of that
if (mDevice != NULL)
{
mDevice->SetRenderState(D3DRS_FOGCOLOR,
D3DCOLOR_XRGB((int)(255 * fogRed), (int)(255 * fogGreen),
(int)(255 * fogBlue)));
}
// dpl_SetViewClipPlanes ( dplMainView, 0.25f, 1100.0f );
// dpl_SetViewFog(
// dplMainView,
@@ -5608,6 +5664,24 @@ void
intersect_mask = INTERSECT_ALL;
}
//
// RP412RENDERDIAG=1: name what actually gets built, so a missing
// Winners Circle can be told from one that is simply out of shot.
//
{
static const char *diag = getenv("RP412RENDERDIAG");
if (diag != NULL && atoi(diag) != 0)
{
DEBUG_STREAM << "RenderDiag: building class"
<< entity->GetClassID() << " res "
<< entity->GetResourceID() << " at "
<< entity->localOrigin.linearPosition.x << ","
<< entity->localOrigin.linearPosition.y << ","
<< entity->localOrigin.linearPosition.z
<< "\n" << std::flush;
}
}
Logical first_object = True;
video_iterator.First();
@@ -5997,12 +6071,110 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
if (mCamShipHUD)
mCamShipHUD->Execute();
//
// The eye renderable has just written D3DTS_VIEW from the viewpoint
// entity. For the Winners Circle the shot comes from off the stand
// instead, so stomp it here - before the read-back below that feeds
// every draw call.
//
if (mPresentationCamera)
{
mDevice->SetTransform(D3DTS_VIEW, &mPresentationView);
}
//
// RP412RENDERDIAG=1: report what the frame is actually made of once the
// mission is ending, which is the only way to tell an empty scene from a
// misaimed camera.
//
{
static const char *diag = getenv("RP412RENDERDIAG");
static int reported = 0;
if (diag != NULL && atoi(diag) != 0 &&
currentAppState == Application::EndingMission && reported < 4)
{
++reported;
int dynamic_count = 0;
SChainIteratorOf<HierarchicalDrawComponent*> diag_iter(&mRenderables);
while (diag_iter.ReadAndNext() != NULL)
{
++dynamic_count;
}
DEBUG_STREAM << "RenderDiag: ending frame " << reported
<< " dead=" << (l4_application->IsDead() ? 1 : 0)
<< " statics=" << (int) mConsolidatedStaticObjects.size()
<< " renderables=" << dynamic_count
<< " camera=" << (mPresentationCamera ? 1 : 0)
<< "\n" << std::flush;
}
}
gNumBatches = 0;
static Time lastFrameTime = mTargetRenderTime;
Scalar dT = mTargetRenderTime - lastFrameTime;
lastFrameTime = mTargetRenderTime;
currentFrameTime = Now();
//
// Fade the presentation up out of black, if one is running. Same lever
// the end-of-mission fade pulls: scale the fog colour and both fog
// distances, here from nothing up to what the podium asked for.
//
if (mPresentationFadeTime > 0.0f)
{
Scalar remaining = mPresentationFadeEnd - Now();
Scalar fade = 1.0f - (remaining / mPresentationFadeTime);
if (fade >= 1.0f)
{
fade = 1.0f;
mPresentationFadeTime = 0.0f; // done
}
if (fade < 0.0f)
{
fade = 0.0f;
}
fogRed = mPresentationFogRed * fade;
fogGreen = mPresentationFogGreen * fade;
fogBlue = mPresentationFogBlue * fade;
currentFogNear = mPresentationFogNear * fade;
currentFogFar = mPresentationFogFar * fade;
mDevice->SetRenderState(D3DRS_FOGCOLOR,
D3DCOLOR_XRGB((int)(255 * fogRed), (int)(255 * fogGreen),
(int)(255 * fogBlue)));
}
//
// Pillarbox: black the whole target first, then narrow the viewport so
// everything after this - clear, scene, reticle - lands inside the crop
// and the surround stays black. Restored after Present.
//
if (mPresentationAspect > 0.0f)
{
D3DVIEWPORT9 full;
full.X = 0;
full.Y = 0;
full.Width = mPresentParams.BackBufferWidth;
full.Height = mPresentParams.BackBufferHeight;
full.MinZ = 0.0f;
full.MaxZ = 1.0f;
mDevice->SetViewport(&full);
mDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);
DWORD cropped = (DWORD)
(mPresentParams.BackBufferHeight * mPresentationAspect);
if (cropped > mPresentParams.BackBufferWidth)
{
cropped = mPresentParams.BackBufferWidth;
}
D3DVIEWPORT9 crop = full;
crop.Width = cropped;
crop.X = (mPresentParams.BackBufferWidth - cropped) / 2;
mDevice->SetViewport(&crop);
}
DWORD currentFog;
mDevice->GetRenderState(D3DRS_FOGCOLOR, &currentFog);
hr = mDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, currentFog, 1.0f, 0);
@@ -6152,15 +6324,70 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
mDevice->SetRenderState(D3DRS_DIFFUSEMATERIALSOURCE, D3DMCS_COLOR1);
mDevice->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE);
if (mReticle && !l4_application->IsDead())
//
// No gunsight on the podium - the race is over and nothing is being
// aimed at. It is drawn in the 2D pass, so it survives everything else
// the presentation turns off.
//
if (mReticle && !l4_application->IsDead() && !InPresentation())
mReticle->Render(0, &viewTransform);
if (mCamShipHUD)
if (mCamShipHUD && !InPresentation())
mCamShipHUD->Render(0, &viewTransform);
hr = mDevice->EndScene();
hr = mDevice->Present(NULL, NULL, gMainPresentWindow, NULL);
// hand the whole target back
if (mPresentationAspect > 0.0f)
{
D3DVIEWPORT9 full;
full.X = 0;
full.Y = 0;
full.Width = mPresentParams.BackBufferWidth;
full.Height = mPresentParams.BackBufferHeight;
full.MinZ = 0.0f;
full.MaxZ = 1.0f;
mDevice->SetViewport(&full);
}
//
// RP412RENDERDIAG=1: did the ending frame actually reach the window, and
// what colour did it clear to? A black clear with a blue-violet fog set
// means something is overwriting the fog behind us.
//
{
static const char *diag = getenv("RP412RENDERDIAG");
static int presented = 0;
if (diag != NULL && atoi(diag) != 0 &&
application->GetApplicationState() == Application::EndingMission &&
presented < 4)
{
++presented;
DWORD fog_now = 0;
mDevice->GetRenderState(D3DRS_FOGCOLOR, &fog_now);
HWND present_window = (HWND) gMainPresentWindow;
RECT present_rect;
present_rect.left = present_rect.top = 0;
present_rect.right = present_rect.bottom = 0;
if (present_window != NULL)
{
GetClientRect(present_window, &present_rect);
}
DEBUG_STREAM << "RenderDiag: present " << presented
<< " hr=0x" << std::hex << (unsigned int) hr << std::dec
<< " fogcolor=0x" << std::hex << (unsigned int) fog_now << std::dec
<< " window=" << (void*) present_window
<< " visible=" << (present_window != NULL &&
IsWindowVisible(present_window) ? 1 : 0)
<< " rect=" << present_rect.right << "x" << present_rect.bottom
<< " backbuf=" << mPresentParams.BackBufferWidth << "x"
<< mPresentParams.BackBufferHeight
<< "\n" << std::flush;
}
}
if (hr == D3DERR_DEVICELOST)
{
int bbCount = mPresentParams.BackBufferCount;
@@ -6736,27 +6963,167 @@ void
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//
// Un-stubbed for the Winners Circle, which asks for 45 degrees to get all
// eight racers in frame. The DPL body below it built a dpl_View; the D3D9
// path builds the same two matrices DPLReadINIPage does and pushes them,
// because mProjectionMatrix only reaches the device on the LaunchingMission
// edge and on the sky-pass restore - rebuilding alone would not show until
// the next frame.
//
// Pass degrees, as the INI does. RPDPL.INI ships viewangle=40, which is what
// to hand back afterwards - not the 30 in the constructor, which the INI
// overwrites at load.
//
void DPLRenderer::SetViewAngle(Degree new_angle)
{
//STUBBED: DPL RB 1/14/07
//Check(this);
////
////~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//// Convert From Degree To Radian
////~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
////
//Radian view_angle;
//view_angle = new_angle;
//viewAngle = view_angle;
//viewRatio = tan(viewAngle/2.0f);
////
////~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//// Calc Aspect Ratio and Set View Projection
////~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
////
//aspectRatio = (float) y_size / (float) x_size;
//dpl_SetViewProjection ( dplMainView, -1.0f, -aspectRatio, 1.0f, aspectRatio, 1.0f/viewRatio);
//dpl_FlushView(dplMainView);
Check(this);
viewAngle = (float) new_angle.angle;
Radian view_angle;
view_angle = new_angle;
// tan(half angle): the culling helper GetViewRatio() reads this, and
// nothing has written it since the DPL body was stubbed out
viewRatio = (float) tan(view_angle / 2.0f);
aspectRatio = (float) y_size / (float) x_size;
// pillarboxed shots render into a narrower viewport, so the projection
// has to use that shape or the scene comes out squashed rather than
// cropped
float projection_aspect = (mPresentationAspect > 0.0f)
? mPresentationAspect
: ((float) x_size / (float) y_size);
D3DXMatrixIdentity(&mProjectionMatrix);
D3DXMatrixPerspectiveFovLH(
&mProjectionMatrix,
viewAngle * (PI / 180.0f),
projection_aspect,
clipNear,
clipFar);
mProjectionMatrix(0, 0) *= -1; // handedness flip - the view is RH
mDecalProjectionMatrix = mProjectionMatrix;
mDecalProjectionMatrix._33 -= mDecalEpsilon;
if (mDevice != NULL)
{
mDevice->SetTransform(D3DTS_PROJECTION, &mProjectionMatrix);
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// The presentation camera. DPLEyeRenderable writes D3DTS_VIEW from the
// viewpoint entity every frame; ExecuteImplementation reads it straight back
// out of the device and hands it to every draw call. Overriding it between
// those two points is enough to move the shot without touching the eye
// renderable or building a CameraShip.
//
void
DPLRenderer::SetPresentationCamera(const Point3D &eye, const Point3D &look_at)
{
Check(this);
D3DXVECTOR3 from((float) eye.x, (float) eye.y, (float) eye.z);
D3DXVECTOR3 at((float) look_at.x, (float) look_at.y, (float) look_at.z);
//
// LookAt*LH*, to match the LH projection. The two differ by exactly the
// sign of the view direction, and RH here points the camera the opposite
// way: ask to look down at the stand and you get sky behind you.
//
// The engine's own eye renderable does use RH, and is right to - its
// forward and up come out of the entity matrix already in that
// convention. A camera aimed with plain world coordinates does not, so
// it wants the handedness the projection was built with.
//
D3DXVECTOR3 up(0.0f, 1.0f, 0.0f);
D3DXMatrixLookAtLH(&mPresentationView, &from, &at, &up);
mPresentationCamera = True;
}
void
DPLRenderer::ClearPresentationCamera()
{
Check(this);
mPresentationCamera = False;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// Give the pilot a vehicle to look at. Their own is built insideEntity - a
// cockpit and no hull - so on the podium they would be the one empty spot.
// outsideEntity is what builds the exterior, and the renderables for it are
// added to what is already there.
//
void
DPLRenderer::ShowViewpointFromOutside()
{
Check(this);
Entity *viewpoint = GetLinkedEntity();
if (viewpoint == NULL)
{
DEBUG_STREAM << "WinnersCircle: no viewpoint entity to turn around\n"
<< std::flush;
return;
}
//
// Add the exterior alongside what is already there rather than tearing
// the entity down and rebuilding it. The teardown path is safe for
// scenery going out of range, but not for the viewpoint entity: it is
// never uninteresting, the eye renderable goes with it, and doing it
// mid-mission stops the scene rendering altogether.
//
// This is what NotifyOfNewInterestingEntity does, minus the teardown
// and with the view type forced.
//
Check(application);
ResourceFile *resource_file = application->GetResourceFile();
Check(resource_file);
ResourceDescription *video_resource = resource_file->SearchList(
viewpoint->GetResourceID(),
ResourceDescription::VideoModelResourceType);
if (video_resource == NULL)
{
DEBUG_STREAM << "WinnersCircle: own vehicle has no exterior model\n"
<< std::flush;
return;
}
video_resource->Lock();
MakeEntityRenderables(viewpoint, video_resource, outsideEntity);
video_resource->Unlock();
DEBUG_STREAM << "WinnersCircle: own vehicle given an exterior\n"
<< std::flush;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// Come up out of the black. SetFogStyle(winnersCircleFogStyle) has already
// put the target fog in place, so remember it and ramp toward it - the same
// multiply the end-of-mission fade uses, run the other way.
//
void
DPLRenderer::StartPresentationFadeIn(Scalar seconds)
{
Check(this);
mPresentationFogRed = fogRed;
mPresentationFogGreen = fogGreen;
mPresentationFogBlue = fogBlue;
mPresentationFogNear = currentFogNear;
mPresentationFogFar = currentFogFar;
mPresentationFadeTime = (seconds > 0.0f) ? seconds : 0.01f;
mPresentationFadeEnd = Now();
mPresentationFadeEnd += mPresentationFadeTime;
}
//
//#############################################################################
@@ -6800,8 +7167,56 @@ void DPLRenderer::SortAndReloadNameBitmaps()
LoadBitSliceTexture(name_bitmap, mNameTextures[index]);
}
}
LoadOrdinalBitmaps();
BindNamePlates();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// Put the callsigns on the Winners Circle plates.
//
// mNameTextures is indexed by finishing place (rank + 1), which is how the
// signs are numbered, so the plate beside each spot names whoever is standing
// on it. The plates are static geometry and have already been merged into the
// consolidated mesh by now, so it is the consolidated draw ops that have to be
// re-pointed - the objects they were built from are no longer drawn.
//
void DPLRenderer::BindNamePlates()
{
int name_count = (int) (sizeof(mNameTextures) / sizeof(mNameTextures[0]));
int bound = 0;
int found = 0;
std::list<d3d_OBJECT*>::const_iterator iter;
for (iter = mConsolidatedStaticObjects.begin();
iter != mConsolidatedStaticObjects.end(); ++iter)
{
d3d_OBJECT *object = *iter;
if (object == NULL)
{
continue;
}
for (int op = 0; op < object->GetDrawOpCount(); ++op)
{
L4DRAWOP *draw_op = object->GetDrawOp(op);
int plate = d3d_OBJECT::NamePlateFor(draw_op->texture.texture);
if (plate == 0)
{
continue;
}
++found;
if (plate < name_count && mNameTextures[plate] != NULL)
{
draw_op->texture.texture = mNameTextures[plate];
++bound;
}
}
}
DEBUG_STREAM << "NamePlates: " << bound << " of " << found
<< " plates given a callsign\n" << std::flush;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+63
View File
@@ -310,10 +310,57 @@ public:
void SetViewAngle(Degree new_angle);
//------------------------------------------------------------------
// Winners Circle presentation camera.
//
// The eye normally rides the viewpoint entity - your own vehicle -
// and DPLEyeRenderable writes D3DTS_VIEW from it each frame. For the
// podium the shot has to come from off the stand looking back, with
// every racer including you in frame, so this overrides that view
// for as long as it is set. Culling still runs from the viewpoint
// entity, which is standing on the podium, so the stand and everyone
// on it stay resident.
//
// Eye and target are world space. ClearPresentationCamera() hands
// the view back to the entity.
//------------------------------------------------------------------
void SetPresentationCamera(const Point3D &eye, const Point3D &look_at);
void ClearPresentationCamera();
//------------------------------------------------------------------
// True once the Winners Circle has taken the screen. The end-of-
// mission fade-to-black checks this and stands down: it multiplies
// the fog to black over FADE_OUT_TIME, which would otherwise black
// out the podium no matter what is drawn.
//------------------------------------------------------------------
Logical InPresentation() const { return mPresentationFog; }
//------------------------------------------------------------------
// Bring the presentation up out of black over the given seconds. The
// race's own fade-to-black has already run by this point; this is
// its mirror image, ramping the fog colour and both fog distances
// back up from nothing to what the Winners Circle asked for.
//------------------------------------------------------------------
void StartPresentationFadeIn(Scalar seconds);
//------------------------------------------------------------------
// Rebuild the viewpoint entity - the pilot's own vehicle - as an
// exterior. It is normally built insideEntity, which is a cockpit
// and no hull, so from any camera that is not in it you see nothing
// where your own vehicle should be. Disconnected_Eye is the engine's
// own switch for this: "so higher level renderers can fix the eye in
// one spot and watch the viewpoint entity drive around."
//------------------------------------------------------------------
void ShowViewpointFromOutside();
unsigned int* MakeBitSliceStorage();
void SortAndReloadNameBitmaps();
// re-point the Winners Circle sign faces at the current callsign
// textures; the plates live in the consolidated static mesh
void BindNamePlates();
void LoadNameBitmaps();
void LoadOrdinalBitmaps();
@@ -438,6 +485,22 @@ private:
D3DXMATRIX mDecalProjectionMatrix;
float mDecalEpsilon;
// Winners Circle: overrides D3DTS_VIEW while set (see
// SetPresentationCamera)
D3DXMATRIX mPresentationView;
Logical mPresentationCamera;
Logical mPresentationFog;
// >0 pillarboxes the scene to this aspect. The Winners Circle was
// built to be looked at from a 4:3 pod monitor and its platform runs
// out at the sides of a 16:9 canvas, so the shot is cropped to the
// shape it was composed for and the surround left black.
float mPresentationAspect;
// fade-in: end time, duration, and the fog it is ramping toward
Time mPresentationFadeEnd;
Scalar mPresentationFadeTime;
float mPresentationFogRed, mPresentationFogGreen, mPresentationFogBlue;
float mPresentationFogNear, mPresentationFogFar;
void FindBestAdapterIndices(bool isWindowed);
float mCloudRed, mCloudGreen, mCloudBlue;
+14
View File
@@ -2283,6 +2283,20 @@ void
//
case FadeOutState:
{
//
// The Winners Circle has taken the screen, so leave it alone.
// This fade multiplies the fog colour and both fog distances
// toward zero every frame, which blacks out the whole scene -
// correct when the race just ends, fatal to a podium shown
// afterwards. Stand down and stop running.
//
if (myRenderer->InPresentation())
{
myState = WaitForStartState;
myRenderer->RemoveDynamicRenderable(this);
break;
}
percent_time_left = (myStateTimer - current_time)/FADE_OUT_TIME;
if(percent_time_left <= 0.0f)
{
+11
View File
@@ -50,6 +50,17 @@ position. And a lobby holds **eight** players rather than four, a full grid as
the pod hall ran it, with the room sizing its roster to whatever space the
window gives it.
[v4.12.5](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.5)
restores the **Winners Circle**. The pod hall stood the finishers on a
numbered award platform when the race ended, and all of it was still in this
repo — the stand, eight ranked spots in every map, and the code to put racers
on them — wired only into the mission-review build and so never once run by a
pod. The race now fades out and fades back in on the platform: finishers in
finishing order, each pilot's callsign on the plate beside their spot, the
cockpit glass cleared away, held for a few seconds before the results screen.
Three pieces of it had been stubbed out in the D3D9 port and are working
again.
## Playing
Grab the release zip (or run `pack-dist.ps1` on a build). Single player:
+28
View File
@@ -11,6 +11,16 @@
#include "..\munga\hostmgr.h"
#include "..\munga\nttmgr.h"
//
// How long the mission stays up after the buzzer, for the Winners Circle.
// This timer is the only thing holding the simulation and the renderer
// open once the race is over - when it runs out the player dispatches the
// StopMission that retires the application. The stock 3 seconds is the
// fade; the rest is the podium. Kept under the +30s LightsOut post so that
// never fires while the stand is up.
//
const Scalar winnersCircleHoldTime = 11.0f;
//#############################################################################
//######################## RPPlayer__StatusMessage ######################
//#############################################################################
@@ -188,6 +198,24 @@ void
ForceUpdate();
}
}
//
//---------------------------------------------------------------------
// Hold the mission open for the Winners Circle.
//
// The base handler sets a 3 second fade, and when it runs out the
// player dispatches the StopMission that retires the application and
// takes the renderer with it. That is the only thing keeping the sim
// and the renderer alive after the race, so the podium gets exactly as
// long as this timer says. Sim and render both keep running throughout
// EndingMission - neither has a case for it that bails out.
//
// Only on a clean finish: an abort should still leave promptly.
//
if (application->GetApplicationState() == Application::EndingMission)
{
fadeTimeRemaining = winnersCircleHoldTime;
}
Check_Fpu();
}
+1 -1
View File
@@ -177,7 +177,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
fclose(file);
}
DEBUG_STREAM << "Red Planet 4.12.4" << std::endl << std::flush;
DEBUG_STREAM << "Red Planet 4.12.5" << std::endl << std::flush;
DEBUG_STREAM << "L4CONTROLS=" << getenv("L4CONTROLS") << std::endl << std::flush;
#ifdef RP412_STEAM
+371
View File
@@ -15,12 +15,56 @@
#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,
@@ -43,6 +87,333 @@ 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;
}
//
//---------------------------------------------------------------------
// Your own vehicle is built as a cockpit with no hull, so from any
// camera outside it there is nothing where you should be - on the stand
// you would be the one empty spot. Turn it inside out before the shot.
//---------------------------------------------------------------------
//
dpl_renderer->ShowViewpointFromOutside();
//
//---------------------------------------------------------------------
// 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
+34
View File
@@ -26,4 +26,38 @@ private:
Mission* MakeMission(NotationFile *notation_file, ResourceFile *resources);
Entity* MakeViewpointEntity(Entity__MakeMessage *);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// The Winners Circle.
//
// The podium is engine content that only the mission-review build ever
// ran: geometry, the eight ranked dropzones and the presentation code all
// ship, but the sequence lived on RPL4PlaybackApplication behind a spool
// file. This is the same sequence on the path the pods actually race.
//
public:
enum
{
// posted at the buzzer, fires once the race has faded out
WinnersCircleMessageID = L4Application::NextMessageID,
NextMessageID
};
static const HandlerEntry
MessageHandlerEntries[];
static MessageHandlerSet& GetMessageHandlers();
static Derivation *GetClassDerivations();
static SharedData DefaultData;
void
StopMissionMessageHandler(StopMissionMessage *message);
// the podium proper, once the screen is already black
void
WinnersCircleMessageHandler(Receiver::Message *message);
private:
// stand the finishers on their ranked spots and frame the shot
void
ShowWinnersCircle();
};
+13
View File
@@ -498,6 +498,19 @@ namespace
<< mission_seconds << "s\n" << std::flush;
}
//
// RP412MISSIONSECONDS overrides the menu's game length. The shortest
// the menu offers is 3:00, which is a long wait when what you are
// testing is what happens at the buzzer.
//
const char *seconds_override = getenv("RP412MISSIONSECONDS");
if (seconds_override != NULL && atoi(seconds_override) > 0)
{
mission_seconds = atoi(seconds_override);
DEBUG_STREAM << "LocalConsole: length overridden to "
<< mission_seconds << "s by RP412MISSIONSECONDS\n" << std::flush;
}
gMissionSeconds = mission_seconds;
InterlockedExchange(&gLengthMs, (LONG) mission_seconds * 1000);
gPhase = PhaseWaiting;
+32 -2
View File
@@ -216,6 +216,36 @@ L4RADARSCALE=100
# both of them, and it grows from the middle in both directions).
L4RADARPOS=CENTER
# The Winners Circle: at the end of a race the finishers are stood on
# the award platform in finishing order, with each pilot's callsign on
# the plate beside their spot, and held there for a few seconds before
# the results screen. 1 = show it, 0 = straight to the results.
RP412PODIUM=1
# The shot is framed for you, but these move the camera if you want it
# somewhere else. Distances are in game units, measured from the middle
# of the group of finishers.
# STANDOFF how far out in front of the stand the camera sits
# HEIGHT how far above the group
# AIM height of the point it looks at, relative to the group -
# negative tilts down, positive tilts up
# ASPECT the stand was composed for a 4:3 pod monitor, so the shot
# is cropped to that shape with black either side. 0 runs it
# full width instead.
# FADEIN seconds to come up out of the black after the race fades
# CAM 0 watches from your own cockpit rather than off the stand
#RP412PODIUMSTANDOFF=36
#RP412PODIUMHEIGHT=12
#RP412PODIUMAIM=2
#RP412PODIUMASPECT=1.333
#RP412PODIUMFADEIN=0.45
#RP412PODIUMCAM=1
# Override the game length the menu picked, in seconds. The shortest the
# menu offers is 3:00, which is a long wait when what you are testing is
# what happens at the buzzer. Unset = use the menu's choice.
#RP412MISSIONSECONDS=20
# Simulation/render frame rate, integer frames/second. The desktop
# default is 60; the arcade pods shipped at 25.
TARGETFPS=60
@@ -327,7 +357,7 @@ start rpl4opt.exe -fit
"@
Set-Content -Path "$dist\README.txt" -Encoding ascii -Value @"
Red Planet 4.12.4
Red Planet 4.12.5
=================
Run start-fullscreen.bat for borderless over the whole monitor, or
@@ -392,7 +422,7 @@ $size = (Get-ChildItem $dist -Recurse | Measure-Object Length -Sum).Sum
Write-Host ("dist ready: {0:N1} MB" -f ($size / 1MB))
if ($Zip) {
$zipPath = Join-Path $root 'RedPlanet-4.12.4.zip'
$zipPath = Join-Path $root 'RedPlanet-4.12.5.zip'
Write-Host "zipping to $zipPath..."
# Everything lives under a single RP412\ folder inside the zip, so