13 Commits
Author SHA1 Message Date
CydandClaude Opus 5 bfd5fa163e EXIT GAME on the console screen
,noframe takes the title bar away, and with it the only way out of the
game. The console screen now offers its own, bottom left: half width and
diagonally opposite LAUNCH GAME, because it is the one button on that
screen you cannot undo and it should not sit next to the one everybody
is aiming for.

It goes through the same door as closing the window - fe.closed, so
RPL4FrontEnd_Run returns False and the race loop breaks - rather than
opening a second shutdown path.

Two things had to move for it to make sense.

The saved placement now loads in RPL4.CPP, right after the main window
is shown, instead of only when SVGA16 builds the cockpit. That was not
until a mission started, so the console screen came up at the default
rect with its title bar still on and the window only jumped to the saved
placement once a race began - which, for a flag whose whole purpose is
to take the title bar off, meant it did nothing on the screen you land
on. RPL4.CPP now owns the main window's registration outright and
SVGA16's branches only reload; the reload after CockpitShellProc goes on
still matters, since its WM_SIZE is what re-fits the canvas.

That in turn made the exploded view's position-only registration
incoherent - the startup load had already applied the size - so the
game window is simply position and size everywhere now. The earlier
reasoning that its exploded size IS the -res render size does not hold:
the back buffer stretches to the window in either view, exactly as it
does for the cockpit.

WM_EXITSIZEMOVE moves from the cockpit subclass to RPL4.CPP's own
WndProc, which the subclass chains to anyway. In its old home it only
existed once a cockpit had been built, so dragging the window on the
console screen - the obvious moment to put it where you want it - saved
nothing. There is also a save on the way out of WinMain, for a session
that never started a race and so never ran SVGA16's teardown save.

Verified: on a bare-framed window the console screen comes up at the
saved 1280x760 with client == window rect, EXIT GAME ends the process
with code 0, and a screenshot shows it clear of the column content. A
console-only session dragged to 333,222 900x640 wrote that on the drag,
kept it through the exit, and came back to exactly it on relaunch -
without a race anywhere in the round trip. The noframe and cockpit
round trips still pass unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 10:01:16 -05:00
CydandClaude Opus 5 a52fec80a9 mfd_layout.cfg: ,noframe takes a window's title bar off
Append it to any line - "RPL4=240,120,1000,620,noframe" - and that
window comes up with no caption and no border. For the game window that
is a cockpit filling a monitor edge to edge at a rect you chose, which
-fit could only do by taking the whole screen; for an exploded pane it
is a display photographed without chrome.

Per line rather than global, so the shell can go bare while the panes
keep their captions, or the other way round.

The flag is an instruction rather than something measured off the
window, so Save carries it back out - otherwise the first finished drag
would rewrite the file and quietly drop it. Windows are always built
framed and Load only ever strips, so deleting the flag is all it takes
to get the frame back; there is no un-strip path to get wrong.

A bare window's rect IS its client rect, so the client area is what
survives: a window that had a size in the file keeps it as the client,
and a position-only pane keeps whatever client it had. That also makes
the round trip stable - once bare, what Save records is already the
client, so load-save-load does not creep.

WS_SYSMENU stays on. It draws nothing without a caption, but without it
DefWindowProc will not honour Alt+F4, and a window with no title bar and
no way to close it is a trap. Nothing else can be dragged either, hence
the note in the file header and environ.ini: place it first, add the
flag after.

Verified in both views. Cockpit: the same 240,120 1000x620 line with and
without the flag, CAPTION|THICKFRAME and a 984x581 client becoming POPUP
with a 1000x620 one, and a screenshot showing the displays hard against
all four edges where the framed shot had them inside a letterbox. Save
mode with the flag set, nudged with WM_EXITSIZEMOVE, rewrote the line
with ",noframe" intact. Exploded: Map bare at 777,333 with its 500x640
client preserved while the shell beside it kept its caption.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 09:21:54 -05:00
CydandClaude Opus 5 40b00ddde1 The game window remembers where you put it too
RP412MFDLAYOUT already kept the exploded view's display panes where they
were dragged. The main window is the one people move most, and it was
still being placed fresh every launch, so it joins them.

MFDSplitView_LoadLayout/SaveLayout become RPWindowLayout_Load/Save, with
Register/Forget taking any HWND rather than the module reaching into a
pane registry. Same file, same format, one more line in it.

What comes back depends on the window, so Register takes it as a flag:

  display panes    position only, as before. A pane's size follows its
                   content and its button banks, so an old size from a
                   different build must not distort it.
  the game window  position and size in the cockpit view. Nothing
                   derives that size - the cockpit fits itself to
                   whatever client area it is given - so a window sized
                   to suit a monitor should come back that way, and
                   half-restoring it would be the strange behaviour. In
                   the exploded view its size IS the render resolution
                   -res asked for, so there only the position returns.

Registered after the CockpitShellProc subclass is installed, on purpose:
the restore's WM_SIZE then runs LayoutCockpit again and the canvas
re-fits the restored client area. -fit does not register at all - it
owns the whole monitor, so there is no placement of the player's to
keep.

CockpitShellProc gained the WM_EXITSIZEMOVE hook the panes already had,
so dragging or resizing the shell writes the file immediately rather
than waiting for teardown.

Two hazards the panes were small enough to get away with and the game
window is not:

  - Save reads rcNormalPosition rather than GetWindowRect. A minimised
    window reports a nonsense rect and a maximised one reports the
    screen; since the file is rewritten whole, either would have
    replaced a good line with a useless one. rcNormalPosition is the
    restored placement whatever state the window is in.
  - Load drops any placement that intersects none of the monitors
    currently plugged in. Restoring the game window onto a display that
    is no longer there would leave nothing to drag back.

Verified by round trip in both views. Cockpit: dragged and resized to
240,120 1000x620, the file took it, a fresh launch in load mode came up
exactly there with a 984x581 client - and a screenshot confirms the
canvas re-fit it, displays at the corners and the map centred at the
bottom, nothing spilling. Exploded: the shell came back at 60,60 still
1280x720 from -res while Map came back at 777,333, which is the
size-flag split doing its job.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 09:00:33 -05:00
CydandClaude Fable 5 16ce4dfbea Exploded view remembers where you drag its windows
Ported from BT411's BT_GLASS_LAYOUT (29c502d).

The exploded view's panes are draggable desktop windows, but the
arrangement is recomputed on every launch, so dragging one somewhere
useful never survived the menu-race-menu loop.

RP412MFDLAYOUT persists it to mfd_layout.cfg beside bindings.txt:

  off / 0 / unset   computed arrangement only, no file (default)
  load / restore    restore saved positions at startup, never write
  save / adjust     restore, then rewrite on each finished drag
                    (WM_EXITSIZEMOVE) and on teardown

One "<title>=x,y,w,h" line per pane. Position is restored and the size
read and discarded: a pane's size follows its content and its button
banks, so letting an old size back in would misshape it after any
geometry change - and this port has changed that geometry twice already.

Load runs after the computed arrangement rather than instead of it, so a
pane the file does not mention simply keeps its computed spot. Only the
exploded view registers: the composited cockpit's panes are chrome-less
children with nothing to drag, so they have no position worth keeping.

RP412 needs no equivalent of BT411's "restored" flag. Its re-snap is
LayoutCockpit on WM_SIZE, which only runs in cockpit mode, so nothing
comes back later to overwrite a hand-placed window.

Verified by round trip: dragged Map to 777,333 in save mode, the file
took all six panes, and a fresh launch in load mode put it physically
back at 777,333. The harness also resized the window while moving it,
which incidentally proved the saved size really is ignored - the pane
came back correctly sized from a cfg that recorded 136x39.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 08:28:44 -05:00
CydandClaude Fable 5 2a23ec0923 Say when the Steam buttons are off rather than removing them
With RP412STEAM=1 but no Steam client, the lobby buttons were not drawn
at all - the menu simply had two fewer buttons than last time, with the
only explanation a line near the top of rpl4.log that scrolls past during
a normal startup. It reads as a broken build, and it cost someone a
puzzled look today.

The buttons are now laid out whenever environ.ini asks for Steam, and
greyed out when the wire did not come up, with STEAM NOT RUNNING above
them. Clicks on a greyed button do nothing - a dead control that still
fires would be worse than the silence it replaces.

Two conditions rather than one, so RPL4Lobby_Configured joins Available:
configured is the ini switch, available is whether the transport got a
FakeIP. Configured keeps the #ifdef in the lobby module, so a build
without the Steam SDK still shows no buttons at all rather than a pair
that can never work.

The notice does not fit inside a button - they are about sixteen
characters wide and "HOST STEAM GAME - STEAM NOT RUNNING" is more than
twice that, so appending it clipped mid-word. It goes on its own line
above the pair instead.

Verified both ways, with the Steam client up and with it unavailable:
bright and clickable, dim and inert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 08:12:23 -05:00
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
20 changed files with 1784 additions and 31 deletions
+5
View File
@@ -40,6 +40,11 @@ ipch/
*.log *.log
rpl4.log rpl4.log
# Where the game window and the exploded view's panes were last dragged
# to (RP412MFDLAYOUT). Per-machine by nature - it is screen coordinates
# on somebody's desk.
mfd_layout.cfg
# Build-output static libs that land in lib/ (the two committed dependency # Build-output static libs that land in lib/ (the two committed dependency
# libs, OpenAL32.lib and libsndfile-1.lib, stay tracked). # libs, OpenAL32.lib and libsndfile-1.lib, stay tracked).
/lib/Munga_L4.lib /lib/Munga_L4.lib
+78 -1
View File
@@ -13,6 +13,7 @@ L4TEXOP::WrapType d3d_OBJECT::mLastWrapV = L4TEXOP::WrapType::REPEAT;
bool d3d_OBJECT::mLastTexturingState = true; bool d3d_OBJECT::mLastTexturingState = true;
long d3d_OBJECT::mNextID = 1; long d3d_OBJECT::mNextID = 1;
stdext::hash_map<std::string, L4TEXOP> d3d_OBJECT::mTextureCache; 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) 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); 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) if (nextDetailOp < numDetailOps && detailOps[nextDetailOp] == i)
@@ -286,6 +316,53 @@ void d3d_OBJECT::FlushTextureCache()
(*iter).second.texture->Release(); (*iter).second.texture->Release();
} }
mTextureCache.clear(); 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) d3d_OBJECT::d3d_OBJECT(LPDIRECT3DDEVICE9 device, int vertCount)
+24
View File
@@ -111,6 +111,27 @@ public:
// race loop tears the renderer down between missions). // race loop tears the renderer down between missions).
static void FlushTextureCache(); 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: private:
static d3d_OBJECT* LoadSpheres(LPDIRECT3DDEVICE9 device, char *fileName); static d3d_OBJECT* LoadSpheres(LPDIRECT3DDEVICE9 device, char *fileName);
@@ -150,6 +171,9 @@ private:
static long mNextID; static long mNextID;
static stdext::hash_map< std::string , L4TEXOP > mTextureCache; 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; extern int gNumBatches;
+370
View File
@@ -10,6 +10,74 @@ namespace
const int buttonGap = 4; const int buttonGap = 4;
//---------------------------------------------------------------
// Sticky placement for the exploded view. See L4MFDVIEW.h; ported
// from BT411's BT_GLASS_LAYOUT.
//---------------------------------------------------------------
enum { LayoutOff = 0, LayoutLoad, LayoutSave };
const char layoutFileName[] = "mfd_layout.cfg";
// every window taking part, so Save can walk them
enum { maxLayoutWindows = 12 };
struct LayoutWindow
{
void *window;
const char *title;
Logical withSize;
Logical bare; // frame stripped, per the file
};
LayoutWindow layoutWindows[maxLayoutWindows];
int layoutWindowCount = 0;
//---------------------------------------------------------------
// Take a window's frame off: no title bar, no border, nothing to
// drag it by. WS_SYSMENU stays - it draws nothing once the caption
// is gone, but without one DefWindowProc will not honour Alt+F4,
// and a bare window with no way to close it is a trap.
//---------------------------------------------------------------
void StripWindowFrame(HWND window)
{
LONG_PTR style = GetWindowLongPtrA(window, GWL_STYLE);
style &= ~(WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX |
WS_MAXIMIZEBOX | WS_BORDER | WS_DLGFRAME);
style |= WS_POPUP;
SetWindowLongPtrA(window, GWL_STYLE, style);
}
int LayoutMode()
{
static int mode = -1;
if (mode < 0)
{
const char *setting = getenv("RP412MFDLAYOUT");
if (setting == NULL || setting[0] == '\0')
{
mode = LayoutOff;
}
else if (!stricmp(setting, "off") || !stricmp(setting, "0"))
{
mode = LayoutOff;
}
else if (!stricmp(setting, "load") || !stricmp(setting, "restore"))
{
mode = LayoutLoad;
}
else // save / adjust / on / 1
{
mode = LayoutSave;
}
if (mode != LayoutOff)
{
DEBUG_STREAM << "MFDLayout: RP412MFDLAYOUT="
<< ((mode == LayoutSave) ? "save" : "load")
<< " (" << layoutFileName << ")\n" << std::flush;
}
}
return mode;
}
//--------------------------------------------------------------- //---------------------------------------------------------------
// Button banks run UNDER the glass: a button reaches this far in // Button banks run UNDER the glass: a button reaches this far in
// from the display edge, but only the indicator strip clears that // from the display edge, but only the indicator strip clears that
@@ -108,6 +176,15 @@ namespace
} }
break; break;
case WM_EXITSIZEMOVE:
//
// A drag just finished. Write the whole arrangement now rather
// than trusting teardown, so a hard kill still leaves the
// layout that was on screen. No-op unless the mode is save.
//
RPWindowLayout_Save();
return 0;
case WM_CLOSE: case WM_CLOSE:
// Part of the cockpit; just hide it. // Part of the cockpit; just hide it.
ShowWindow(hwnd, SW_HIDE); ShowWindow(hwnd, SW_HIDE);
@@ -148,6 +225,8 @@ MFDSplitView::MFDSplitView(
sourceHeight = source_height; sourceHeight = source_height;
buttonCount = 0; buttonCount = 0;
pressedIndex = -1; pressedIndex = -1;
paneTitle = title;
ownWindow = (parent == NULL) ? True : False;
pixels = new unsigned long[source_width * source_height]; pixels = new unsigned long[source_width * source_height];
memset(pixels, 0, source_width * source_height * sizeof(unsigned long)); memset(pixels, 0, source_width * source_height * sizeof(unsigned long));
@@ -230,6 +309,14 @@ MFDSplitView::MFDSplitView(
{ {
SetWindowLongPtrA((HWND) window, GWLP_USERDATA, (LONG_PTR) this); SetWindowLongPtrA((HWND) window, GWLP_USERDATA, (LONG_PTR) this);
ShowWindow((HWND) window, SW_SHOWNOACTIVATE); ShowWindow((HWND) window, SW_SHOWNOACTIVATE);
// only the exploded view's own windows can be dragged, so only
// those have a placement worth remembering. Position only - see
// RPWindowLayout_Register.
if (ownWindow)
{
RPWindowLayout_Register(window, paneTitle, False);
}
} }
else else
{ {
@@ -241,6 +328,9 @@ MFDSplitView::~MFDSplitView()
{ {
Check_Pointer(this); Check_Pointer(this);
// out of the layout registry before the window goes
RPWindowLayout_Forget(window);
if (window != NULL) if (window != NULL)
{ {
SetWindowLongPtrA((HWND) window, GWLP_USERDATA, 0); SetWindowLongPtrA((HWND) window, GWLP_USERDATA, 0);
@@ -259,6 +349,272 @@ Logical
return pixels != NULL; return pixels != NULL;
} }
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Join the saved layout. Registering twice just refreshes the entry, so
// a window re-registering after being rebuilt is harmless.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void
RPWindowLayout_Register(void *window, const char *title, Logical with_size)
{
if (window == NULL || title == NULL)
{
return;
}
for (int i = 0; i < layoutWindowCount; ++i)
{
if (layoutWindows[i].window == window)
{
layoutWindows[i].title = title;
layoutWindows[i].withSize = with_size;
return;
}
}
if (layoutWindowCount >= maxLayoutWindows)
{
return;
}
layoutWindows[layoutWindowCount].window = window;
layoutWindows[layoutWindowCount].title = title;
layoutWindows[layoutWindowCount].withSize = with_size;
layoutWindows[layoutWindowCount].bare = False;
++layoutWindowCount;
}
void
RPWindowLayout_Forget(void *window)
{
for (int i = 0; i < layoutWindowCount; ++i)
{
if (layoutWindows[i].window == window)
{
layoutWindows[i] = layoutWindows[--layoutWindowCount];
layoutWindows[layoutWindowCount].window = NULL;
layoutWindows[layoutWindowCount].title = NULL;
return;
}
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Sticky placement: restore the saved placement over the computed one.
//
// Called once everything has been built and placed, so a window the file
// does not mention simply keeps where it was put. Size comes back only
// for windows registered with it - see the header.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void
RPWindowLayout_Load()
{
if (LayoutMode() == LayoutOff || layoutWindowCount == 0)
{
return;
}
FILE *file = fopen(layoutFileName, "rt");
if (file == NULL)
{
DEBUG_STREAM << "MFDLayout: no " << layoutFileName
<< " yet (using the computed arrangement)\n" << std::flush;
return;
}
int restored = 0;
char line[256];
while (fgets(line, sizeof(line), file) != NULL)
{
char *cursor = line;
while (*cursor == ' ' || *cursor == '\t')
{
++cursor;
}
if (*cursor == '#' || *cursor == '\r' || *cursor == '\n' || *cursor == '\0')
{
continue;
}
// titles carry no '=', so the first one splits the line
char *equals = strchr(cursor, '=');
if (equals == NULL)
{
continue;
}
*equals = '\0';
char *end = equals;
while (end > cursor && (end[-1] == ' ' || end[-1] == '\t'))
{
--end;
}
*end = '\0';
int x = 0, y = 0, w = 0, h = 0;
if (sscanf(equals + 1, "%d,%d,%d,%d", &x, &y, &w, &h) < 2)
{
continue;
}
//
// Optional trailing keyword, "x,y,w,h,noframe". Searched for
// rather than parsed in place so a hand-edited line survives a
// stray space or a missing comma.
//
Logical bare = (strstr(equals + 1, "noframe") != NULL) ? True : False;
for (int i = 0; i < layoutWindowCount; ++i)
{
LayoutWindow &entry = layoutWindows[i];
if (entry.title == NULL || strcmp(entry.title, cursor) != 0)
{
continue;
}
HWND target = (HWND) entry.window;
if (target == NULL || !IsWindow(target))
{
break;
}
// a saved size of nothing is a corrupt line, not an instruction
Logical size_it = (entry.withSize && w > 0 && h > 0) ? True : False;
//
// Saved coordinates belong to whatever monitors were plugged
// in that day. If the placement lands on none of today's, drop
// it and keep the computed one - restoring the game window
// somewhere invisible would leave nothing to drag back.
//
RECT landing;
landing.left = x;
landing.top = y;
if (size_it)
{
landing.right = x + w;
landing.bottom = y + h;
}
else
{
RECT current;
if (!GetWindowRect(target, &current))
{
break;
}
landing.right = x + (current.right - current.left);
landing.bottom = y + (current.bottom - current.top);
}
if (MonitorFromRect(&landing, MONITOR_DEFAULTTONULL) == NULL)
{
DEBUG_STREAM << "MFDLayout: " << cursor << " was saved at "
<< x << "," << y << " - off every monitor now, ignoring\n"
<< std::flush;
break;
}
//
// Frame off, if the line asked. A bare window's rect IS its
// client rect, so whatever client area it has now is the size
// to hand back - which also makes the round trip stable: once
// bare, what Save records is already the client.
//
entry.bare = bare;
if (bare)
{
if (!size_it)
{
RECT client;
if (GetClientRect(target, &client))
{
w = client.right;
h = client.bottom;
size_it = True;
}
}
StripWindowFrame(target);
}
SetWindowPos(target, NULL, x, y, w, h,
SWP_NOZORDER | SWP_NOACTIVATE |
(size_it ? 0 : SWP_NOSIZE) | (bare ? SWP_FRAMECHANGED : 0));
++restored;
break;
}
}
fclose(file);
DEBUG_STREAM << "MFDLayout: restored " << restored << " window placement(s) from "
<< layoutFileName << "\n" << std::flush;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Write where every registered window currently is. The whole file each
// time - it is a handful of lines, and a rewrite leaves nothing
// half-written for a hard kill to catch.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void
RPWindowLayout_Save()
{
if (LayoutMode() != LayoutSave || layoutWindowCount == 0)
{
return;
}
FILE *file = fopen(layoutFileName, "wt");
if (file == NULL)
{
DEBUG_STREAM << "MFDLayout: could not write " << layoutFileName
<< "\n" << std::flush;
return;
}
fputs("# RP412 window placement. RP412MFDLAYOUT=save writes this on each\n"
"# finished drag and on exit; =load restores it.\n"
"# <title>=<x>,<y>,<w>,<h>. The game window gets its size back too;\n"
"# for the display panes the size is reference only, since theirs\n"
"# follows their content.\n"
"# Append ,noframe to take that window's title bar and border off.\n"
"# Put it where you want it first - a bare window has nothing to\n"
"# drag by, so its line stops changing once the frame is gone.\n", file);
int wrote = 0;
for (int i = 0; i < layoutWindowCount; ++i)
{
LayoutWindow &entry = layoutWindows[i];
if (entry.title == NULL)
{
continue;
}
HWND entry_window = (HWND) entry.window;
if (entry_window == NULL || !IsWindow(entry_window))
{
continue;
}
//
// rcNormalPosition rather than GetWindowRect: a minimised window
// reports a nonsense rect and a maximised one reports the screen,
// and neither is what to come back to. This is the restored
// placement whatever state the window is in, so quitting from
// maximised still records where the window will reappear.
//
WINDOWPLACEMENT placement;
memset(&placement, 0, sizeof(placement));
placement.length = sizeof(placement);
if (!GetWindowPlacement(entry_window, &placement))
{
continue;
}
RECT bounds = placement.rcNormalPosition;
// the flag is the player's instruction, not something measured off
// the window, so a rewrite has to carry it back out
fprintf(file, "%s=%ld,%ld,%ld,%ld%s\n", entry.title,
(long) bounds.left, (long) bounds.top,
(long) (bounds.right - bounds.left),
(long) (bounds.bottom - bounds.top),
entry.bare ? ",noframe" : "");
++wrote;
}
fclose(file);
DEBUG_STREAM << "MFDLayout: saved " << wrote << " window placement(s) to "
<< layoutFileName << "\n" << std::flush;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Compute the display rectangle and the button rectangles, growing the // Compute the display rectangle and the button rectangles, growing the
// client area to make room for the strips. // client area to make room for the strips.
@@ -490,6 +846,20 @@ void
// one message per frame, and queued WM_PAINTs (lowest priority) starve // one message per frame, and queued WM_PAINTs (lowest priority) starve
// behind it - panes would freeze on their first frame. // 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 void
MFDSplitView::SetPosition(int x, int y) MFDSplitView::SetPosition(int x, int y)
{ {
+88
View File
@@ -71,6 +71,20 @@ public:
void void
Resize(int display_width, int display_height); 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();
// caption and HWND, for the sticky-placement file
const char *
Title() const
{ return paneTitle; }
void *
Window()
{ return window; }
~MFDSplitView(); ~MFDSplitView();
Logical Logical
@@ -141,4 +155,78 @@ protected:
int int
buttonAnchorA, buttonAnchorA,
buttonAnchorB; buttonAnchorB;
// the window caption, and the key this pane is saved under
const char
*paneTitle;
// True for a pane of its own on the desktop (the exploded view); the
// composited cockpit's panes are chrome-less children and cannot be
// dragged, so there is nothing to remember for them.
Logical
ownWindow;
}; };
//########################################################################
// Sticky window placement.
//
// The game window and, in the exploded view, each display pane are all
// draggable, but their placement is recomputed on every launch - so
// putting a window somewhere useful never survived the menu-race-menu
// loop. RP412MFDLAYOUT remembers it, in mfd_layout.cfg beside
// bindings.txt:
//
// off / 0 / unset computed placement only, no file (default)
// load / restore restore saved placement at startup, never write
// save / adjust restore, then rewrite on each finished drag and
// on teardown - the round trip
//
// One "<title>=x,y,w,h" line per window, plus an optional ",noframe"
// that takes that window's title bar and border off - for a cockpit that
// fills a monitor edge to edge without -fit's all-or-nothing, or an
// exploded pane photographed without chrome. A bare window has nothing
// to drag by, so place it first and add the flag after; Save carries the
// flag back out, since it is an instruction rather than something
// measured off the window. Windows are always built framed, so deleting
// the flag is all it takes to get the frame back.
//
// Whether the size comes back depends on the window, which is why
// Register takes it as a flag:
//
// display panes position only. A pane's size follows its content and
// its button banks, so an old size from a different
// build must not distort it.
// the game window position and size. Nothing derives that size - -res
// only decides how sharp the scene is, and the cockpit
// fits itself to whatever client area it is given - so
// a window sized to suit a monitor should come back
// that way, and half-restoring it would be the strange
// behaviour. RPL4.CPP registers it before the console
// screen, so the saved placement is there from the
// first frame rather than arriving when a race starts.
//
// A placement that lands on none of the monitors currently plugged in is
// ignored rather than applied - restoring the game window off screen
// would leave nothing to drag back.
//
// Lives here because the display panes were the first to need it; the
// game window joined later rather than grow a second copy of the same
// file format.
//
// Ported from BT411's BT_GLASS_LAYOUT.
//########################################################################
// Take part in the saved layout. title is the key in the file and must
// outlive the window (the callers pass string literals).
void
RPWindowLayout_Register(void *window, const char *title, Logical with_size);
void
RPWindowLayout_Forget(void *window);
// Apply the saved placement over the computed one. Call once everything
// has been built and placed.
void
RPWindowLayout_Load();
// Write every registered window's placement. No-op unless mode is save.
void
RPWindowLayout_Save();
+62
View File
@@ -3839,6 +3839,11 @@ static LRESULT CALLBACK
cockpit->LayoutCockpit(LOWORD(lParam), HIWORD(lParam)); cockpit->LayoutCockpit(LOWORD(lParam), HIWORD(lParam));
} }
} }
//
// WM_EXITSIZEMOVE is not handled here: RPL4.CPP's own WndProc takes
// it, and this chains straight through to that, so the shell saves on
// every finished drag whether or not a cockpit has been built.
//
return CallWindowProcA(gCockpitBaseProc, hwnd, message, wParam, lParam); return CallWindowProcA(gCockpitBaseProc, hwnd, message, wParam, lParam);
} }
@@ -4161,6 +4166,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 // Fit the 1920x1080 cockpit canvas into the given client area: one
// uniform scale (so the canvas never stretches - a wider desktop just // uniform scale (so the canvas never stretches - a wider desktop just
@@ -4434,6 +4462,15 @@ SVGA16::SVGA16(
splitView[SplitMFDLowerRight]->SetPosition( splitView[SplitMFDLowerRight]->SetPosition(
work.left + right_x, work.top + bottom_y); work.left + right_x, work.top + bottom_y);
splitView[SplitMap]->SetPosition(work.left + map_x, work.top + map_y); splitView[SplitMap]->SetPosition(work.left + map_x, work.top + map_y);
//
// ...and then let RP412MFDLAYOUT put everything back where it was
// dragged to last time. Windows the file does not mention keep the
// arrangement just computed above. The game window is registered
// already - RPL4.CPP does that before the console screen ever
// shows - so this picks it up alongside the panes.
//
RPWindowLayout_Load();
} }
else if (splitViews) else if (splitViews)
{ {
@@ -4645,6 +4682,23 @@ SVGA16::SVGA16(
// catch maximise / restore / drag-resize and re-fit // catch maximise / restore / drag-resize and re-fit
gCockpitBaseProc = (WNDPROC) SetWindowLongPtrA( gCockpitBaseProc = (WNDPROC) SetWindowLongPtrA(
cockpit, GWLP_WNDPROC, (LONG_PTR) CockpitShellProc); cockpit, GWLP_WNDPROC, (LONG_PTR) CockpitShellProc);
//
// Sticky placement for the shell. Position AND size: nothing
// derives that size here - -res only decides how sharp the
// scene is - so a window sized to suit a monitor should come
// back that way. RPL4.CPP registered it before the console
// screen; this reload undoes the sizing just done above, and
// runs after the subclass on purpose so its WM_SIZE puts
// LayoutCockpit over the restored client area.
//
// -fit sits it borderless over the whole monitor, so there is
// no placement of the player's to remember.
//
if (!fit_display)
{
RPWindowLayout_Load();
}
} }
else else
{ {
@@ -4840,6 +4894,14 @@ SVGA16::~SVGA16()
//--------------------------------------------------------- //---------------------------------------------------------
//SVGASetSplitterClock(False); //SVGASetSplitterClock(False);
//
// Backstop for the sticky placement: every finished drag has already
// been written, but a window moved and then closed straight away would
// otherwise be missed. Must run before the panes go - it reads their
// live window rects, and the game window's.
//
RPWindowLayout_Save();
for (int view = 0; view < SplitViewCount; ++view) for (int view = 0; view < SplitViewCount; ++view)
{ {
delete splitView[view]; delete splitView[view];
+12
View File
@@ -349,6 +349,18 @@ public:
GetCockpit() GetCockpit()
{ return activeCockpit; } { 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: protected:
Logical Logical
splitViews; splitViews;
+436 -21
View File
@@ -1797,6 +1797,19 @@ DPLRenderer::DPLRenderer(
backgroundGreen = 0.0f; backgroundGreen = 0.0f;
backgroundBlue = 0.0f; backgroundBlue = 0.0f;
viewAngle = 30.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; dplMainView = NULL;
dplDeathZone = NULL; dplDeathZone = NULL;
dplMainZone = NULL; dplMainZone = NULL;
@@ -2343,10 +2356,53 @@ void
} }
break; break;
case winnersCircleFogStyle: 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 // 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. // 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_SetViewClipPlanes ( dplMainView, 0.25f, 1100.0f );
// dpl_SetViewFog( // dpl_SetViewFog(
// dplMainView, // dplMainView,
@@ -5608,6 +5664,24 @@ void
intersect_mask = INTERSECT_ALL; 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; Logical first_object = True;
video_iterator.First(); video_iterator.First();
@@ -5997,12 +6071,110 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
if (mCamShipHUD) if (mCamShipHUD)
mCamShipHUD->Execute(); 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; gNumBatches = 0;
static Time lastFrameTime = mTargetRenderTime; static Time lastFrameTime = mTargetRenderTime;
Scalar dT = mTargetRenderTime - lastFrameTime; Scalar dT = mTargetRenderTime - lastFrameTime;
lastFrameTime = mTargetRenderTime; lastFrameTime = mTargetRenderTime;
currentFrameTime = Now(); 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; DWORD currentFog;
mDevice->GetRenderState(D3DRS_FOGCOLOR, &currentFog); mDevice->GetRenderState(D3DRS_FOGCOLOR, &currentFog);
hr = mDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, currentFog, 1.0f, 0); 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->SetRenderState(D3DRS_DIFFUSEMATERIALSOURCE, D3DMCS_COLOR1);
mDevice->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE); 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); mReticle->Render(0, &viewTransform);
if (mCamShipHUD) if (mCamShipHUD && !InPresentation())
mCamShipHUD->Render(0, &viewTransform); mCamShipHUD->Render(0, &viewTransform);
hr = mDevice->EndScene(); hr = mDevice->EndScene();
hr = mDevice->Present(NULL, NULL, gMainPresentWindow, NULL); 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) if (hr == D3DERR_DEVICELOST)
{ {
int bbCount = mPresentParams.BackBufferCount; 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) void DPLRenderer::SetViewAngle(Degree new_angle)
{ {
//STUBBED: DPL RB 1/14/07 Check(this);
//Check(this);
//// viewAngle = (float) new_angle.angle;
////~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//// Convert From Degree To Radian Radian view_angle;
////~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ view_angle = new_angle;
//// // tan(half angle): the culling helper GetViewRatio() reads this, and
//Radian view_angle; // nothing has written it since the DPL body was stubbed out
//view_angle = new_angle; viewRatio = (float) tan(view_angle / 2.0f);
//viewAngle = view_angle; aspectRatio = (float) y_size / (float) x_size;
//viewRatio = tan(viewAngle/2.0f);
//// // pillarboxed shots render into a narrower viewport, so the projection
////~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // has to use that shape or the scene comes out squashed rather than
//// Calc Aspect Ratio and Set View Projection // cropped
////~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ float projection_aspect = (mPresentationAspect > 0.0f)
//// ? mPresentationAspect
//aspectRatio = (float) y_size / (float) x_size; : ((float) x_size / (float) y_size);
//dpl_SetViewProjection ( dplMainView, -1.0f, -aspectRatio, 1.0f, aspectRatio, 1.0f/viewRatio);
//dpl_FlushView(dplMainView); 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;
} }
// //
//############################################################################# //#############################################################################
@@ -6802,6 +7169,54 @@ void DPLRenderer::SortAndReloadNameBitmaps()
} }
LoadOrdinalBitmaps(); 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); 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(); unsigned int* MakeBitSliceStorage();
void SortAndReloadNameBitmaps(); 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 LoadNameBitmaps();
void LoadOrdinalBitmaps(); void LoadOrdinalBitmaps();
@@ -438,6 +485,22 @@ private:
D3DXMATRIX mDecalProjectionMatrix; D3DXMATRIX mDecalProjectionMatrix;
float mDecalEpsilon; 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); void FindBestAdapterIndices(bool isWindowed);
float mCloudRed, mCloudGreen, mCloudBlue; float mCloudRed, mCloudGreen, mCloudBlue;
+14
View File
@@ -2283,6 +2283,20 @@ void
// //
case FadeOutState: 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; percent_time_left = (myStateTimer - current_time)/FADE_OUT_TIME;
if(percent_time_left <= 0.0f) 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 the pod hall ran it, with the room sizing its roster to whatever space the
window gives it. 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 ## Playing
Grab the release zip (or run `pack-dist.ps1` on a build). Single player: 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\hostmgr.h"
#include "..\munga\nttmgr.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 ###################### //######################## RPPlayer__StatusMessage ######################
//############################################################################# //#############################################################################
@@ -188,6 +198,24 @@ void
ForceUpdate(); 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(); Check_Fpu();
} }
+39 -1
View File
@@ -26,6 +26,7 @@
#include "rpl4lobby.h" #include "rpl4lobby.h"
#include "..\munga_l4\l4steamtransport.h" #include "..\munga_l4\l4steamtransport.h"
#include "..\munga_l4\l4splr.h" #include "..\munga_l4\l4splr.h"
#include "..\munga_l4\l4mfdview.h" // RPWindowLayout_*
#include "rpl4ver.h" #include "rpl4ver.h"
#include "..\munga\resver.h" #include "..\munga\resver.h"
#include "..\munga\resource.h" #include "..\munga\resource.h"
@@ -123,6 +124,15 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
return 0; return 0;
} }
break; break;
case WM_EXITSIZEMOVE:
//
// A drag or resize just finished. Here rather than in the
// cockpit's subclass so it also covers the console screen, which
// is where the window gets moved before there is any cockpit to
// subclass. No-op unless RP412MFDLAYOUT is save.
//
RPWindowLayout_Save();
return 0;
} }
return DefWindowProc(hWnd, uMsg, wParam, lParam); return DefWindowProc(hWnd, uMsg, wParam, lParam);
} }
@@ -177,7 +187,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
fclose(file); 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; DEBUG_STREAM << "L4CONTROLS=" << getenv("L4CONTROLS") << std::endl << std::flush;
#ifdef RP412_STEAM #ifdef RP412_STEAM
@@ -254,6 +264,21 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
return FALSE; return FALSE;
ShowWindow(hWnd, nShowCmd); ShowWindow(hWnd, nShowCmd);
//
// Sticky placement, from the first frame the player sees. SVGA16 also
// registers and reloads when it builds the cockpit, but that is not
// until a mission starts - without this the console screen would come
// up at the default rect with its title bar still on, and only jump
// to the saved placement once a race began. -fit takes the whole
// monitor and has no placement of the player's to restore.
//
if (!L4Application::GetFitDisplay() && !L4Application::GetFullscreen())
{
RPWindowLayout_Register(hWnd, "RPL4", True);
RPWindowLayout_Load();
}
#if !_DEBUG #if !_DEBUG
// Arcade pods have no mouse - but desktop/windowed play needs the // Arcade pods have no mouse - but desktop/windowed play needs the
// cursor for the on-screen cockpit buttons, so hide it only when // cursor for the on-screen cockpit buttons, so hide it only when
@@ -475,6 +500,19 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
} }
} }
//
// Last word on the window's placement. Every finished drag has already
// written it and SVGA16 writes again as the cockpit comes down, but a
// session that never started a race has neither - and quitting from
// the console screen is exactly how somebody would leave after moving
// the window to where they want it. No-op unless the mode is save.
//
if (IsWindow(hWnd))
{
RPWindowLayout_Save();
}
RPWindowLayout_Forget(hWnd);
#if !_DEBUG #if !_DEBUG
// symmetric with the fullscreen-only hide at startup // symmetric with the fullscreen-only hide at startup
if (L4Application::GetFullscreen()) if (L4Application::GetFullscreen())
+371
View File
@@ -15,12 +15,56 @@
#include "..\rp\vtv.h" #include "..\rp\vtv.h"
#include "rpl4mppr.h" #include "rpl4mppr.h"
#include "..\rp\rpplayer.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 // 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( RPL4Application::RPL4Application(
HINSTANCE hInstance, HINSTANCE hInstance,
HWND hWnd, HWND hWnd,
@@ -43,6 +87,333 @@ RPL4Application::~RPL4Application()
Check_Fpu(); 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 // MakeRegistry
+34
View File
@@ -26,4 +26,38 @@ private:
Mission* MakeMission(NotationFile *notation_file, ResourceFile *resources); Mission* MakeMission(NotationFile *notation_file, ResourceFile *resources);
Entity* MakeViewpointEntity(Entity__MakeMessage *); 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; << 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; gMissionSeconds = mission_seconds;
InterlockedExchange(&gLengthMs, (LONG) mission_seconds * 1000); InterlockedExchange(&gLengthMs, (LONG) mission_seconds * 1000);
gPhase = PhaseWaiting; gPhase = PhaseWaiting;
+61 -5
View File
@@ -177,6 +177,7 @@ namespace
GroupLaunch, GroupLaunch,
GroupSteamHost, // buttons, not selections (Steam builds only) GroupSteamHost, // buttons, not selections (Steam builds only)
GroupSteamJoin, GroupSteamJoin,
GroupExit,
GroupCount GroupCount
}; };
@@ -413,8 +414,9 @@ namespace
launch->rect.right = col3 + col_w; launch->rect.right = col3 + col_w;
launch->rect.bottom = client_h - row_h; launch->rect.bottom = client_h - row_h;
// Steam lobby buttons (only when the Steam wire is live) // Steam lobby buttons, offered whenever environ.ini asked for Steam.
if (RPL4Lobby_Available()) // Painting greys them out and says so if the wire never came up.
if (RPL4Lobby_Configured())
{ {
FEItem *host = &fe->items[fe->itemCount++]; FEItem *host = &fe->items[fe->itemCount++];
host->group = GroupSteamHost; host->group = GroupSteamHost;
@@ -433,6 +435,23 @@ namespace
join->rect.bottom = client_h - (7 * row_h) / 2; join->rect.bottom = client_h - (7 * row_h) / 2;
} }
//
// Exit, bottom left. Diagonally opposite LAUNCH on purpose: it is
// the one button on this screen you cannot undo, so it does not go
// next to the one people are aiming for. Half width for the same
// reason - it is a way out, not a peer of LAUNCH.
//
// The window frame used to be the way out, and mfd_layout.cfg's
// ,noframe takes it away, so the screen has to offer its own.
//
FEItem *quit = &fe->items[fe->itemCount++];
quit->group = GroupExit;
quit->index = 0;
quit->rect.left = col1;
quit->rect.top = client_h - 2 * row_h;
quit->rect.right = col1 + col_w / 2;
quit->rect.bottom = client_h - row_h;
// pilot name edit sits at the top of column 3 // pilot name edit sits at the top of column 3
if (fe->nameEdit != NULL) if (fe->nameEdit != NULL)
{ {
@@ -484,6 +503,7 @@ namespace
case GroupLaunch: return "L A U N C H G A M E"; case GroupLaunch: return "L A U N C H G A M E";
case GroupSteamHost: return "HOST STEAM GAME"; case GroupSteamHost: return "HOST STEAM GAME";
case GroupSteamJoin: return "JOIN STEAM GAME"; case GroupSteamJoin: return "JOIN STEAM GAME";
case GroupExit: return "EXIT GAME";
} }
return ""; return "";
} }
@@ -544,12 +564,38 @@ namespace
RECT row = item->rect; RECT row = item->rect;
if (item->group >= GroupLaunch) if (item->group >= GroupLaunch)
{ {
HBRUSH launch_brush = CreateSolidBrush(kGreenBright); //
// The lobby buttons are offered whenever environ.ini asked
// for Steam, but they only work once the client is actually
// there. Dim them and say why rather than leaving them out -
// two buttons quietly missing looks like a broken build.
//
Logical steam_button =
(item->group == GroupSteamHost || item->group == GroupSteamJoin);
Logical dead = steam_button && !RPL4Lobby_Available();
COLORREF ink = dead ? kGreenDim : kGreenBright;
HBRUSH launch_brush = CreateSolidBrush(ink);
FrameRect(mem, &row, launch_brush); FrameRect(mem, &row, launch_brush);
DeleteObject(launch_brush); DeleteObject(launch_brush);
SetTextColor(mem, kGreenBright); SetTextColor(mem, ink);
DrawTextA(mem, ItemName(item->group, item->index), -1, &row, DrawTextA(mem, ItemName(item->group, item->index), -1, &row,
DT_CENTER | DT_VCENTER | DT_SINGLELINE); DT_CENTER | DT_VCENTER | DT_SINGLELINE);
//
// The reason, on its own line above the pair. It does not
// fit inside a button - they are about sixteen characters
// wide and this is more than twice that.
//
if (dead && item->group == GroupSteamHost)
{
RECT notice = row;
notice.bottom = row.top;
notice.top = row.top - (row.bottom - row.top);
SetTextColor(mem, kGreenDim);
DrawTextA(mem, "STEAM NOT RUNNING", -1, &notice,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
}
continue; continue;
} }
@@ -613,7 +659,17 @@ namespace
} }
else if (item->group == GroupSteamHost || item->group == GroupSteamJoin) else if (item->group == GroupSteamHost || item->group == GroupSteamJoin)
{ {
fe->steamAction = (item->group == GroupSteamHost) ? 1 : 2; // dead until the Steam client is there; the button says so
if (RPL4Lobby_Available())
{
fe->steamAction = (item->group == GroupSteamHost) ? 1 : 2;
PostMessageA(fe->menuWindow, WM_NULL, 0, 0);
}
}
else if (item->group == GroupExit)
{
// the same door closing the window goes through
fe->closed = True;
PostMessageA(fe->menuWindow, WM_NULL, 0, 0); PostMessageA(fe->menuWindow, WM_NULL, 0, 0);
} }
else else
+13
View File
@@ -11,6 +11,7 @@
//######################################################################## //########################################################################
Logical RPL4Lobby_Available() { return False; } Logical RPL4Lobby_Available() { return False; }
Logical RPL4Lobby_Configured() { return False; }
Logical RPL4Lobby_InRoom() { return False; } Logical RPL4Lobby_InRoom() { return False; }
int RPL4Lobby_Host(HINSTANCE, HWND) { return LobbyRoomLeft; } int RPL4Lobby_Host(HINSTANCE, HWND) { return LobbyRoomLeft; }
int RPL4Lobby_Join(HINSTANCE, HWND) { return LobbyRoomLeft; } int RPL4Lobby_Join(HINSTANCE, HWND) { return LobbyRoomLeft; }
@@ -919,6 +920,18 @@ Logical
return SteamNetTransport_GetFakeAddressString()[0] != '\0'; return SteamNetTransport_GetFakeAddressString()[0] != '\0';
} }
//
// The environ.ini switch, read the same way RPL4.CPP reads it to decide
// whether to install the transport at all. Says nothing about whether the
// Steam client was actually there.
//
Logical
RPL4Lobby_Configured()
{
const char *steam_switch = getenv("RP412STEAM");
return (steam_switch != NULL && atoi(steam_switch) != 0) ? True : False;
}
Logical Logical
RPL4Lobby_InRoom() RPL4Lobby_InRoom()
{ {
+9
View File
@@ -33,6 +33,15 @@ enum RPL4LobbyOutcome
Logical Logical
RPL4Lobby_Available(); RPL4Lobby_Available();
// True when this build has Steam and environ.ini asked for it, whether
// or not it actually came up. The menu offers the lobby buttons on this
// and greys them out on Available() - a player who turned Steam on and
// then launched without the client running should be told so, not left
// looking at a menu that quietly has two fewer buttons than the last
// time they saw it.
Logical
RPL4Lobby_Configured();
// True while we sit in a lobby (races return to the room). // True while we sit in a lobby (races return to the room).
Logical Logical
RPL4Lobby_InRoom(); RPL4Lobby_InRoom();
+52 -2
View File
@@ -166,6 +166,26 @@ L4PLASMA=SCREEN
# 480x640 - decoded exactly as the pod's VDB split them, no downscale). # 480x640 - decoded exactly as the pod's VDB split them, no downscale).
L4MFDSPLIT=1 L4MFDSPLIT=1
# The game window - and in the exploded view (L4MFDSPLIT=2) each display
# window - is placed fresh every launch, so moving one somewhere useful
# never survived the menu-race-menu loop. This remembers where you put
# them, in mfd_layout.cfg beside this file:
# off / 0 / unset computed placement only, no file (default)
# load put the windows back where they were saved
# save the same, and re-save on every finished drag
# The game window gets its size back too, so you can size the cockpit to
# suit your monitor once and keep it. The display windows get position
# only: their size follows their content and their button banks, so an
# old one is never restored over them. Arrange everything once with
# save, then leave it on load.
#
# Each line in mfd_layout.cfg reads <title>=<x>,<y>,<w>,<h>, and you can
# append ,noframe to take that window's title bar and border off - a
# cockpit that fills the monitor edge to edge without -fit taking the
# whole screen. Put the window where you want it first: a bare window
# has nothing to drag by. Delete the flag to get the frame back.
#RP412MFDLAYOUT=off
# Size of the six secondary displays in the glass cockpit, as a # Size of the six secondary displays in the glass cockpit, as a
# percentage of their pod size. The pod bolted them down at one size; # percentage of their pod size. The pod bolted them down at one size;
# on a big panel there is room to trade viewscreen for instrument, so # on a big panel there is room to trade viewscreen for instrument, so
@@ -216,6 +236,36 @@ L4RADARSCALE=100
# both of them, and it grows from the middle in both directions). # both of them, and it grows from the middle in both directions).
L4RADARPOS=CENTER 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 # Simulation/render frame rate, integer frames/second. The desktop
# default is 60; the arcade pods shipped at 25. # default is 60; the arcade pods shipped at 25.
TARGETFPS=60 TARGETFPS=60
@@ -327,7 +377,7 @@ start rpl4opt.exe -fit
"@ "@
Set-Content -Path "$dist\README.txt" -Encoding ascii -Value @" 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 Run start-fullscreen.bat for borderless over the whole monitor, or
@@ -392,7 +442,7 @@ $size = (Get-ChildItem $dist -Recurse | Measure-Object Length -Sum).Sum
Write-Host ("dist ready: {0:N1} MB" -f ($size / 1MB)) Write-Host ("dist ready: {0:N1} MB" -f ($size / 1MB))
if ($Zip) { 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..." Write-Host "zipping to $zipPath..."
# Everything lives under a single RP412\ folder inside the zip, so # Everything lives under a single RP412\ folder inside the zip, so