8 Commits
Author SHA1 Message Date
CydandClaude Opus 5 fd61316a40 Release 4.12.6
Seven commits since 4.12.5, all about where things sit on screen.

RP412MFDLAYOUT remembers window placement across the menu-race-menu
loop, in mfd_layout.cfg beside bindings.txt: the game window, the
exploded view's display panes, and the plasma glass. Append ,noframe to
a line to take that window's title bar and border off - a cockpit
filling a monitor edge to edge at a rect you chose, where -fit could
only do it by taking the whole screen.

That needed a way out of a window with no title bar, so the setup screen
carries an EXIT GAME button, bottom left and diagonally opposite LAUNCH.

The Steam host/join buttons dim and say STEAM NOT RUNNING rather than
disappearing - two buttons quietly missing reads as a broken build.

And the Winners Circle camera is framed off the award stand rather than
off whoever is standing on it. It had been averaging the filled spots,
which moved the shot with the head count: eight finishers put the eye 24
units closer to the stand and aimed it at the middle of the tiers
instead of at the winner. The framing constants are untouched, so the
shot everybody gets now is the one that was dialled in.

Version strings bumped in RPL4.CPP, pack-dist.ps1 and the controls page
that ships in the zip.

Built clean, packed, and smoke-tested from the dist: boots to the
console screen reporting 4.12.6, virtual RIO up, Steam transport up,
nothing alarming in the log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 09:15:41 -05:00
CydandClaude Opus 5 dce8818273 The plasma glass is placeable too
It is a draggable top-level window whenever L4PLASMA=SCREEN, which makes
it the last one still being placed fresh every launch. It joins
mfd_layout.cfg under "Plasma Display", the caption it already carries -
the same key-is-the-title rule the display panes follow.

Position only, like the panes: the glass is 128x32 at L4PLASMASCALE, so
its size is a setting rather than something to drag.

It registers and loads at the point it creates its window rather than
leaving that to SVGA16. The glass comes from the gauge renderer and the
panes from the video mode, and nothing guarantees which is built first;
loading in both places means whichever runs second simply re-applies a
placement the first already has. Its window procedure picks up the same
WM_EXITSIZEMOVE save the panes have, so a drag writes the file straight
away, and the destructor forgets the window before destroying it.

Verified by round trip in the exploded view: dragged to 640,880, the
file took "Plasma Display=640,880,528,167" alongside the panes and the
game window, and a fresh launch in load mode put it physically back at
640,880. The three load lines in the log - one window, then two, then
eight - are the ordering doing its job.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 08:05:50 -05:00
CydandClaude Opus 5 a1f9c0e3c0 Winners Circle framed off the stand, not off who is on it
The camera was built from the spots that actually got filled: the first
filled one for the front, the average of the filled ones for the centre.
So it moved with the finishing order and the head count, and would move
between machines if a remote player's vehicle was not there to place.

Measured on Wiseguy's Wake, where win1 sits at (1199.84, 3, 2.92) and
the eight spots run back to z~38 on the high tier:

  one finisher    eye 1199.84,15,-33.08   aim z 2.92
  eight finishers eye 1199.82,18.44,-9.03 aim z 26.97

Twenty-four units closer to the stand and looking at the middle of the
tiers instead of at the winner - a different photograph of the same
podium depending on how many people showed up.

The stand is fixed furniture on every map, so the shot comes off the
geometry now. The eight dropzones are read once, up front, before
anybody is placed; win1 anchors the framing and the axis from the back
rows out through win1 gives the facing, so a map that mounts its stand
at another angle is still photographed from the front rather than
relying on the old (0,0,-1) fallback. Placement then runs as its own
pass and is the only thing that cares who finished - the log reports
"N placed on M spots" precisely so a changing N beside unchanged camera
numbers is visible.

The framing constants are untouched and so is the shot they produce:
the new numbers for a single finisher are eye 1199.77,15,-33.08 aim
1199.84,5,2.92, which is the old single-finisher shot to within 0.07 in
x. That is the case the standoff/height/aim defaults were dialled in
against, so the approved photograph is what everybody gets now instead
of what one person got.

Verified by running a race to the podium: the eight spot positions log
as expected, the camera numbers match the calculation, and the frame is
the same one as before - rank 1 centred with its callsign, 2 and 3
flanking on the low tier, 4-8 across the high tier behind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 08:05:30 -05:00
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
13 changed files with 768 additions and 50 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
+356
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.
+83
View File
@@ -77,6 +77,14 @@ public:
void void
Hide(); Hide();
// caption and HWND, for the sticky-placement file
const char *
Title() const
{ return paneTitle; }
void *
Window()
{ return window; }
~MFDSplitView(); ~MFDSplitView();
Logical Logical
@@ -147,4 +155,79 @@ 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 plasma glass registers
// the same way - its size is L4PLASMASCALE's business.
// 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();
+27
View File
@@ -3,6 +3,7 @@
#include "l4plasmascreen.h" #include "l4plasmascreen.h"
#include "l4app.h" #include "l4app.h"
#include "l4mfdview.h" // RPWindowLayout_*
namespace namespace
{ {
@@ -45,6 +46,15 @@ namespace
} }
break; break;
case WM_EXITSIZEMOVE:
//
// Dragged to a new spot. Write the whole arrangement now rather
// than trusting teardown, so a hard kill still leaves what was
// on screen. No-op unless RP412MFDLAYOUT is save.
//
RPWindowLayout_Save();
return 0;
case WM_CLOSE: case WM_CLOSE:
// The glass is part of the cockpit; just hide it. // The glass is part of the cockpit; just hide it.
ShowWindow(hwnd, SW_HIDE); ShowWindow(hwnd, SW_HIDE);
@@ -176,6 +186,7 @@ PlasmaScreen::~PlasmaScreen()
} }
if (window != NULL) if (window != NULL)
{ {
RPWindowLayout_Forget(window);
RemovePropA((HWND) window, "PlasmaBitmapInfo"); RemovePropA((HWND) window, "PlasmaBitmapInfo");
RemovePropA((HWND) window, "PlasmaPixels"); RemovePropA((HWND) window, "PlasmaPixels");
DestroyWindow((HWND) window); DestroyWindow((HWND) window);
@@ -275,6 +286,22 @@ void
SetPropA((HWND) window, "PlasmaPixels", pixelBuffer->Data.MapPointer); SetPropA((HWND) window, "PlasmaPixels", pixelBuffer->Data.MapPointer);
ShowWindow((HWND) window, SW_SHOWNOACTIVATE); ShowWindow((HWND) window, SW_SHOWNOACTIVATE);
activeInstance = this; activeInstance = this;
//
//-----------------------------------------------------------
// Join the saved layout. Position only, like the display
// panes: the glass is 128x32 at L4PLASMASCALE, so its size
// is a setting rather than something to drag.
//
// Loading here rather than leaving it to SVGA16 keeps this
// independent of which is built first - the glass comes from
// the gauge renderer, the panes from the video mode, and
// whichever runs second re-applies a placement the first
// already has.
//-----------------------------------------------------------
//
RPWindowLayout_Register(window, "Plasma Display", False);
RPWindowLayout_Load();
} }
else else
{ {
+39
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);
} }
@@ -4457,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)
{ {
@@ -4668,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
{ {
@@ -4863,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];
+14
View File
@@ -61,6 +61,20 @@ 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 Three pieces of it had been stubbed out in the D3D9 port and are working
again. again.
[v4.12.6](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.6)
is about the windows. Where you put them now survives the
menu-race-menu loop: `RP412MFDLAYOUT` remembers the game window, the
exploded view's display panes and the plasma glass in `mfd_layout.cfg`
beside `bindings.txt`. Append `,noframe` to a line to take that
window's title bar and border off — a cockpit filling the monitor edge
to edge at a rect you chose, rather than `-fit` taking the whole
screen — and the setup screen carries its own **EXIT GAME** button,
since a window with no title bar needs a way out. The Steam buttons
now dim and say *STEAM NOT RUNNING* instead of disappearing. And the
Winners Circle camera is framed off the award stand itself rather than
off whoever is standing on it, so the shot is the same one for every
player at every head count.
## 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:
+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.5" << std::endl << std::flush; DEBUG_STREAM << "Red Planet 4.12.6" << 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())
+95 -39
View File
@@ -133,20 +133,27 @@ void
// meaningful in both. // meaningful in both.
//--------------------------------------------------------------------- //---------------------------------------------------------------------
// //
//
//---------------------------------------------------------------------
// The stand's own geometry first, before anybody stands on it.
//
// The eight spots are fixed furniture on every map, so reading them up
// front gives one set of numbers to both jobs below: where to stand
// the finishers, and where to put the camera. Only the first is
// allowed to care who finished.
//---------------------------------------------------------------------
//
enum { winnersSpotCount = 8 };
DropZone *spot[winnersSpotCount];
int spots_found = 0;
char winners_spot[] = "win?"; char winners_spot[] = "win?";
char *place = winners_spot + 3; 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 i = 0; i < winnersSpotCount; ++i)
for (int rank = 0; (p = CameraDirector::FindPlayerByRank(rank)) != NULL; ++rank)
{ {
if (rank > 7) *place = (char) ('1' + i);
{ spot[i] = NULL;
break; // only eight spots exist on the stand
}
*place = (char) ('1' + rank);
ChainIteratorOf<Node*> iterator(dropzones->groupMembers); ChainIteratorOf<Node*> iterator(dropzones->groupMembers);
DropZone *dropzone; DropZone *dropzone;
@@ -154,10 +161,36 @@ void
{ {
if (!strcmp(dropzone->GetDropZoneName(), winners_spot)) if (!strcmp(dropzone->GetDropZoneName(), winners_spot))
{ {
spot[i] = dropzone;
++spots_found;
Point3D at = dropzone->localOrigin.linearPosition;
DEBUG_STREAM << "WinnersCircle: " << winners_spot << " at "
<< at.x << "," << at.y << "," << at.z << "\n" << std::flush;
break; break;
} }
} }
if (dropzone == NULL) }
if (spot[0] == NULL)
{
DEBUG_STREAM << "WinnersCircle: no win1 spot on this map\n" << std::flush;
return;
}
//
//---------------------------------------------------------------------
// Stand the finishers up, in finishing order.
//---------------------------------------------------------------------
//
int placed = 0;
Player *p;
for (int rank = 0; (p = CameraDirector::FindPlayerByRank(rank)) != NULL; ++rank)
{
if (rank >= winnersSpotCount)
{
break; // only eight spots exist on the stand
}
if (spot[rank] == NULL)
{ {
continue; continue;
} }
@@ -168,20 +201,9 @@ void
continue; continue;
} }
VTV *vtv = (VTV*) vehicle; VTV *vtv = (VTV*) vehicle;
vtv->Reset(dropzone->localOrigin, VTV::MissionReviewReset); vtv->Reset(spot[rank]->localOrigin, VTV::MissionReviewReset);
vtv->SetPerformance(&VTV::DoNothing); vtv->SetPerformance(&VTV::DoNothing);
vtv->FlushEvents(); 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; ++placed;
} }
@@ -239,25 +261,52 @@ void
// line-up frames up. The stand runs from z~3 (rank 1, low) to z~39 // 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 // (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 // 30 units of standoff. Eye is above the top tier looking slightly
// down at the middle of the group. // down at the winner's spot.
//
// Framed off the stand, NOT off who is standing on it. This used to
// average the filled spots and anchor on the first one filled, which
// meant the camera moved with the head count: eight finishers put the
// anchor in the middle of the tiers and the eye about 58 units out,
// one finisher collapsed it onto the winner's spot and brought the eye
// in to 36. Same stand, different shot per race - and per machine, if
// a remote player's vehicle was not there to be placed. The furniture
// does not move, so the camera does not either.
//--------------------------------------------------------------------- //---------------------------------------------------------------------
// //
standCentre.x /= (Scalar) placed; Point3D standFront = spot[0]->localOrigin.linearPosition;
standCentre.y /= (Scalar) placed;
standCentre.z /= (Scalar) placed;
// //
// Stand off along the line from the middle of the group out through the // The stand's own axis: back rows to the winner's spot, which points
// front spot, so the shot faces the stand however the map has it turned, // out of the front however the map has the stand turned. Taken from
// and lift the eye above the top tier to look down on the line-up. // the geometry rather than assumed, so a map that mounts its stand at
// another angle still gets photographed from the front.
// //
Point3D standBack(0.0f, 0.0f, 0.0f);
int back_count = 0;
for (int i = 1; i < winnersSpotCount; ++i)
{
if (spot[i] != NULL)
{
standBack += spot[i]->localOrigin.linearPosition;
++back_count;
}
}
if (back_count > 0)
{
standBack.x /= (Scalar) back_count;
standBack.y /= (Scalar) back_count;
standBack.z /= (Scalar) back_count;
}
Vector3D facing; Vector3D facing;
facing.x = standFront.x - standCentre.x; facing.x = standFront.x - standBack.x;
facing.y = 0.0f; facing.y = 0.0f;
facing.z = standFront.z - standCentre.z; facing.z = standFront.z - standBack.z;
Scalar reach = (Scalar) sqrt(facing.x * facing.x + facing.z * facing.z); Scalar reach = (Scalar) sqrt(facing.x * facing.x + facing.z * facing.z);
if (reach < 0.01f) if (reach < 0.01f)
{ {
// a one-spot stand, or all eight stacked: nothing to take a
// direction from, so fall back to the way the shipped stands face
facing.x = 0.0f; facing.z = -1.0f; reach = 1.0f; facing.x = 0.0f; facing.z = -1.0f; reach = 1.0f;
} }
// //
@@ -285,10 +334,12 @@ void
if (tune != NULL) aim_lift = (Scalar) atof(tune); if (tune != NULL) aim_lift = (Scalar) atof(tune);
Point3D eye; Point3D eye;
eye.x = standCentre.x + (facing.x / reach) * standoff; eye.x = standFront.x + (facing.x / reach) * standoff;
eye.y = standCentre.y + height; eye.y = standFront.y + height;
eye.z = standCentre.z + (facing.z / reach) * standoff; eye.z = standFront.z + (facing.z / reach) * standoff;
standCentre.y += aim_lift;
Point3D aim = standFront;
aim.y += aim_lift;
// //
// RP412PODIUMCAM=0 leaves the view in the cockpit, which is also the // RP412PODIUMCAM=0 leaves the view in the cockpit, which is also the
@@ -315,7 +366,7 @@ void
if (camera_mode == NULL || atoi(camera_mode) != 0) if (camera_mode == NULL || atoi(camera_mode) != 0)
{ {
dpl_renderer->SetViewAngle(Degree(45.0f)); dpl_renderer->SetViewAngle(Degree(45.0f));
dpl_renderer->SetPresentationCamera(eye, standCentre); dpl_renderer->SetPresentationCamera(eye, aim);
} }
else else
{ {
@@ -340,8 +391,13 @@ void
} }
dpl_renderer->StartPresentationFadeIn(fade_in); dpl_renderer->StartPresentationFadeIn(fade_in);
DEBUG_STREAM << "WinnersCircle: " << placed << " placed; centre " //
<< standCentre.x << "," << standCentre.y << "," << standCentre.z // placed is reported for diagnosis only - the camera numbers beside it
// must not move when it does.
//
DEBUG_STREAM << "WinnersCircle: " << placed << " placed on "
<< spots_found << " spots; aim "
<< aim.x << "," << aim.y << "," << aim.z
<< " eye " << eye.x << "," << eye.y << "," << eye.z << " eye " << eye.x << "," << eye.y << "," << eye.z
<< "\n" << std::flush; << "\n" << std::flush;
} }
+60 -4
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;
} }
@@ -612,10 +658,20 @@ namespace
PostMessageA(fe->menuWindow, WM_NULL, 0, 0); PostMessageA(fe->menuWindow, WM_NULL, 0, 0);
} }
else if (item->group == GroupSteamHost || item->group == GroupSteamJoin) else if (item->group == GroupSteamHost || item->group == GroupSteamJoin)
{
// dead until the Steam client is there; the button says so
if (RPL4Lobby_Available())
{ {
fe->steamAction = (item->group == GroupSteamHost) ? 1 : 2; fe->steamAction = (item->group == GroupSteamHost) ? 1 : 2;
PostMessageA(fe->menuWindow, WM_NULL, 0, 0); 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);
}
else else
{ {
fe->selection[item->group] = item->index; fe->selection[item->group] = item->index;
+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();
+3 -3
View File
@@ -1,4 +1,4 @@
<title>Red Planet 4.12.3 — Controls</title> <title>Red Planet 4.12.6 — Controls</title>
<style> <style>
:root { :root {
@@ -754,7 +754,7 @@
<div class="wrap"> <div class="wrap">
<header class="masthead"> <header class="masthead">
<p class="eyebrow">Virtual World Entertainment · Tesla pod · RP 4.12.3</p> <p class="eyebrow">Virtual World Entertainment · Tesla pod · RP 4.12.6</p>
<h1>Red Planet<br>Controls Map</h1> <h1>Red Planet<br>Controls Map</h1>
<p class="lede"> <p class="lede">
Every input the pod answers to, on the gamepad and the keyboard. The Every input the pod answers to, on the gamepad and the keyboard. The
@@ -1197,7 +1197,7 @@
</section> </section>
<footer> <footer>
<span>Red Planet 4.12.3</span> <span>Red Planet 4.12.6</span>
<span>gitea.mysticmachines.com/VWE/RP412</span> <span>gitea.mysticmachines.com/VWE/RP412</span>
<span>docs/CONTROLS.md · CONTROLS.txt ships with the game</span> <span>docs/CONTROLS.md · CONTROLS.txt ships with the game</span>
<span>RGB keyboards mirror the pod lamps · RP412KEYLIGHT=0 to disable</span> <span>RGB keyboards mirror the pod lamps · RP412KEYLIGHT=0 to disable</span>
+24 -2
View File
@@ -166,6 +166,28 @@ 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.
#
# The plasma display window takes part too, under "Plasma Display".
#
# 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
@@ -357,7 +379,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.5 Red Planet 4.12.6
================= =================
Run start-fullscreen.bat for borderless over the whole monitor, or Run start-fullscreen.bat for borderless over the whole monitor, or
@@ -422,7 +444,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.5.zip' $zipPath = Join-Path $root 'RedPlanet-4.12.6.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