From 9389ec20031ac88a3dca530b76ad5222b17a11ab Mon Sep 17 00:00:00 2001 From: Cyd Date: Mon, 27 Jul 2026 09:46:38 -0500 Subject: [PATCH] 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 --- MUNGA_L4/L4VIDEO.cpp | 225 ++++++++++++++++++++++++++++++--- MUNGA_L4/L4VIDEO.h | 31 +++++ MUNGA_L4/L4VIDRND.cpp | 14 +++ RP/RPPLAYER.cpp | 28 +++++ RP_L4/RPL4APP.cpp | 284 ++++++++++++++++++++++++++++++++++++++++++ RP_L4/RPL4APP.h | 23 ++++ RP_L4/RPL4CONSOLE.cpp | 13 ++ 7 files changed, 599 insertions(+), 19 deletions(-) diff --git a/MUNGA_L4/L4VIDEO.cpp b/MUNGA_L4/L4VIDEO.cpp index de2eaeb..085972e 100644 --- a/MUNGA_L4/L4VIDEO.cpp +++ b/MUNGA_L4/L4VIDEO.cpp @@ -1797,6 +1797,12 @@ 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; + D3DXMatrixIdentity(&mPresentationView); dplMainView = NULL; dplDeathZone = NULL; dplMainZone = NULL; @@ -2343,10 +2349,39 @@ 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; + + // 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 +5643,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,6 +6050,44 @@ 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 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; @@ -6161,6 +6252,43 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte hr = mDevice->EndScene(); hr = mDevice->Present(NULL, NULL, gMainPresentWindow, NULL); + + // + // 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 +6864,86 @@ 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; + + D3DXMatrixIdentity(&mProjectionMatrix); + D3DXMatrixPerspectiveFovLH( + &mProjectionMatrix, + viewAngle * (PI / 180.0f), + (float) x_size / (float) y_size, + 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; } // //############################################################################# diff --git a/MUNGA_L4/L4VIDEO.h b/MUNGA_L4/L4VIDEO.h index 897f850..7ea6f04 100644 --- a/MUNGA_L4/L4VIDEO.h +++ b/MUNGA_L4/L4VIDEO.h @@ -310,6 +310,31 @@ 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; } + unsigned int* MakeBitSliceStorage(); void SortAndReloadNameBitmaps(); @@ -438,6 +463,12 @@ private: D3DXMATRIX mDecalProjectionMatrix; float mDecalEpsilon; + // Winners Circle: overrides D3DTS_VIEW while set (see + // SetPresentationCamera) + D3DXMATRIX mPresentationView; + Logical mPresentationCamera; + Logical mPresentationFog; + void FindBestAdapterIndices(bool isWindowed); float mCloudRed, mCloudGreen, mCloudBlue; diff --git a/MUNGA_L4/L4VIDRND.cpp b/MUNGA_L4/L4VIDRND.cpp index 0a47997..ffc8350 100644 --- a/MUNGA_L4/L4VIDRND.cpp +++ b/MUNGA_L4/L4VIDRND.cpp @@ -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) { diff --git a/RP/RPPLAYER.cpp b/RP/RPPLAYER.cpp index 56cdbaa..40f13f4 100644 --- a/RP/RPPLAYER.cpp +++ b/RP/RPPLAYER.cpp @@ -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(); } diff --git a/RP_L4/RPL4APP.cpp b/RP_L4/RPL4APP.cpp index 661c47a..c771da6 100644 --- a/RP_L4/RPL4APP.cpp +++ b/RP_L4/RPL4APP.cpp @@ -15,12 +15,44 @@ #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" // //############################################################################# // RPL4Application //############################################################################# // +const Receiver::HandlerEntry + RPL4Application::MessageHandlerEntries[]= +{ + MESSAGE_ENTRY(RPL4Application, StopMission) +}; + +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() + ); + RPL4Application::RPL4Application( HINSTANCE hInstance, HWND hWnd, @@ -43,6 +75,258 @@ 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 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: far enough back for all eight bays and + // the numbers above them, high enough to look down the tiers + Scalar standoff = 45.0f; + Scalar height = 18.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. + // + // + // 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; + } + + 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, so only the first stands anybody up. + // + if (GetApplicationState() != Application::EndingMission) + { + DEBUG_STREAM << "WinnersCircle: mission stopped, standing the finishers up\n" + << std::flush; + ShowWinnersCircle(); + } + + L4Application::StopMissionMessageHandler(message); + Check_Fpu(); +} + // //############################################################################# // MakeRegistry diff --git a/RP_L4/RPL4APP.h b/RP_L4/RPL4APP.h index a46e5f0..1615ea6 100644 --- a/RP_L4/RPL4APP.h +++ b/RP_L4/RPL4APP.h @@ -26,4 +26,27 @@ 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: + static const HandlerEntry + MessageHandlerEntries[]; + static MessageHandlerSet& GetMessageHandlers(); + static Derivation *GetClassDerivations(); + static SharedData DefaultData; + + void + StopMissionMessageHandler(StopMissionMessage *message); + +private: + // stand the finishers on their ranked spots and frame the shot + void + ShowWinnersCircle(); }; diff --git a/RP_L4/RPL4CONSOLE.cpp b/RP_L4/RPL4CONSOLE.cpp index 3de0906..e57f3d4 100644 --- a/RP_L4/RPL4CONSOLE.cpp +++ b/RP_L4/RPL4CONSOLE.cpp @@ -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;