Commit Graph
100 Commits
Author SHA1 Message Date
CydandClaude Opus 5 82e733c1a6 Replicants reckon from when an update was sent
Simulation::ReadUpdateRecord threw away the sender's timestamp and
stamped lastUpdate with its own arrival time. The line carried the
original authors' own note: "HACK - should be based upon
message->timeStamp".

The dead reckoner extrapolates a replicant over
(lastPerformance - lastUpdate), so starting that clock at ARRIVAL rather
than at SEND leaves every remote vehicle exactly one network latency
behind where it should be. On the 1 ms LAN inside an arcade that is
nothing. Over Steam Datagram Relay it is 50-150 ms of positional lag on
every other player - a constant bias, not jitter, and the information
needed to remove it was already in the packet.

The timestamp cannot be used as it stands: both machines run
QueryPerformanceCounter since their own boot, so the two clocks share no
epoch. The offset is estimated per peer instead. Each record gives

    sample = ourNow - theirStamp = trueOffset + oneWayLatency

and latency is never negative, so the smallest sample seen is the
closest to the truth. A rolling minimum over 128 samples follows crystal
drift and re-adapts when a route gets slower, rather than being pinned
forever by one lucky packet; a shorter path is believed immediately.

Applied with two clamps: never ahead of our own clock, and never further
back than 500 ms. Past that the packet is stale or the estimate is
wrong, and throwing a vehicle half a second forward does more damage
than the lag being corrected.

Entity::UpdateMessageHandler is the only point on the receive path that
knows whose update this is - records carry a timestamp but not an owner -
so it publishes the sender around the loop, and only for entities
somebody else owns. Offsets are forgotten in CreateMission: the hosts in
the next race are not the hosts in the last one and a HostID gets reused.

RP412NETCLOCK=0 restores the arrival-time behaviour, documented in
environ.ini, so a test machine can compare the two without a rebuild.
The estimate is logged per host when it first settles and whenever it
moves more than 50 ms, which is what a three-machine session should be
read against.

WHAT IS AND IS NOT VERIFIED. A full single-player race runs unchanged -
the path is never entered without replicants, which is the regression
risk that reaches everybody. The behaviour this exists for needs real
latency between real machines and is therefore untested: a two-instance
loopback race would only have exercised the zero-latency case, where the
correction is a no-op by construction. Expect remote vehicles to sit
further forward than before, and watch for overshoot when somebody
changes direction sharply - that is the tradeoff this makes, and the
clamp above is what bounds it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 15:50:28 -05:00
CydandClaude Opus 5 68f5780efa The cockpit clock counts the console's clock
A race ends when the console says so, but the countdown on the map
display was computed from the engine clock and its own idea of when the
race started - QueryPerformanceCounter from Application::gameStarted,
against the console's GetTickCount from gRunStartTick. Two clocks, two
epochs, two threads. They agreed to within a frame in the ordinary case,
which is why nobody noticed.

They do not agree at all when RP412MISSIONSECONDS is set: the override
shortens the CONSOLE's length and leaves the egg's alone, so a 25-second
test race displayed a clock counting down from 5:00 and was stopped with
4:35 still showing.

gMissionClockHook (APPMGR.h, alongside the gPerFrameHook it mirrors) lets
the console answer for the countdown when it is marshalling. NULL, or a
console that has no answer yet, falls back to exactly the old
computation - which is what the arcade -net pods, lobby members and
mission review all take, none of them running a console locally. A
member's clock is anchored by the console's RunMission arriving over the
wire anyway, so it starts within one latency of correct and only drifts
at the rate the two crystals differ.

Two things come out of it beyond the clock itself. The camera directors
switch behaviour at "30 seconds left" (DIRECTOR.cpp, RPDIRECT.cpp) and
were reading the same free-running number, so the dramatic end-of-race
camera and the actual buzzer were on different clocks too; they now
share one. And the countdown holds at 00:00 instead of going negative -
the console polls at 250 ms, so zero always arrives slightly before the
stop is dispatched.

The hook is guarded on gWatchedApp == application. Nothing ever
uninstalls it, so a player who hosts a race and then joins somebody
else's lobby still has it wired up, and in that race the console is a
bystander holding the previous mission's gLengthMs and gRunStartTick.

Verified by running a 25-second race with the menu still set to 5:00 and
photographing the map display: 00:17, 00:01, then 00:00 held while
"time expired - stopping mission" went to the log. Captures use
PrintWindow rather than CopyFromScreen - the first attempt grabbed the
desktop sitting in front of the Map window, which is somebody's screen
contents written to disk, and those files were deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 15:39:19 -05:00
CydandClaude Opus 5 4f34684b16 environ.ini is written on first run, not shipped
Packing one into every zip meant a tester who unzipped a new build over
their folder got their configuration replaced. bindings.txt has never had
that problem, because the exe carries the template and writes the file
only when it is absent. environ.ini now works the same way, so a new
build can land on an existing folder and every setting survives.

The 245-line template moves out of pack-dist.ps1 and into RPL4ENVIRON.cpp
as the exe's own literal, which also means the exe alone can produce a
working install. It was lifted mechanically rather than retyped, and the
file it writes is line-for-line identical to the one we have been
shipping - only the line endings changed, from a mongrel 243 LF plus one
stray CRLF that PowerShell's Set-Content left on the end, to the uniform
LF the game already writes bindings.txt with.

It cannot simply become optional. Without environ.ini, L4GAUGE is unset -
which disables the gauge renderer and takes every MFD with it - and
L4MFDSPLIT is unset, which is the packed-window arcade layout rather than
the glass cockpit. The shipped values ARE the desktop game; the built-in
getenv fallbacks are the 1995 pod. So the game writes the file rather
than tolerating its absence.

The cost of a file that is never overwritten is that a tester carrying
one across many builds stops being offered new options. Nothing breaks -
an option added later defaults to "behave as before" - but it goes
unnoticed, and "the podium does not work" is a confusing bug report when
the real answer is that their environ.ini predates RP412PODIUM. So the
load names every template key the player's file has never mentioned, and
says they are at built-in defaults and that deleting the file brings the
documented one back. A stale seven-line file lists all 40.

The file is read, never rewritten. The mention test is deliberately
generous - a key counts as known if it appears in any form, commented or
not - because the failure it guards against is worse than a missed
notice: environ.ini is applied line by line, so a second copy of a key
appearing later in the file would silently override the player's own.

The version line also moves to the top of WinMain. It used to print after
the environment was loaded, so the first thing in rpl4.log was a message
about environ.ini rather than which build wrote it.

Verified: the written file matches the old shipped one line for line; an
edited file with a hand-added comment survives another run untouched; a
seven-line file from an older build boots and names all 40 options it has
never heard of; and a full mission on a self-written file brings up the
glass cockpit at 125% with the virtual RIO active and nothing alarming in
the log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 14:46:18 -05:00
CydandClaude Opus 5 72bb3b394f The controls map becomes the handbook
It stopped being a controls map somewhere around the display arrangement
and the joystick wizard, and a page called CONTROLS.html is the wrong
place to look for what a file in the game folder does. So:
docs/rp412-controls.html is now docs/rp412-handbook.html and ships as
HANDBOOK.html, titled to match.

The new section answers the question the page could not: what is in the
game folder and which of it is yours. Four files are - environ.ini,
bindings.txt, pilot.cfg, mfd_layout.cfg - and only the first ships, so a
fresh unzip has none of the others and deleting one simply starts that
part over. A second table covers the shipped engine data, which nobody
should edit but everybody eventually wonders about: which INI the gauge
canvas comes from, why there are audio mixer tables for hardware that has
not existed since 1995, and that JOYSTICK.INI is the legacy path rather
than anything the new joystick support reads.

The callout carries the two that actually catch people, both of which
have caught us during this work: environ.ini is applied OVER the
environment, so a variable set in a shell loses to an uncommented line in
the file; and bindings.txt is never overwritten once it exists, which is
what protects a player's edits and also why an update's new defaults do
not appear until it is deleted.

CONTROLS.txt keeps its name. It is the controls half in plain text for
Notepad, which is still exactly what it is, and the README now says so
rather than describing the two as the same thing.

Verified by rendering the packed HANDBOOK.html headless: both tables and
the callout sit in the page's own components, the footer names the new
file, and the version stamp still flows through - the shipped copy reads
4.12.96 from the build it was packed with.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 14:25:34 -05:00
CydandClaude Opus 5 a1d2de591c The patch number is the commit count
A hand-maintained version says what somebody remembered to type. Pinning
it to the repository means a binary always names the commit it came from,
so a log from a test machine settles which changes are in it.

stamp-version.ps1 runs as RP_L4's pre-build step and writes the generated
RP_L4\rpl4build.h:

  #define RP412_VERSION       "4.12.96"
  #define RP412_VERSION_LONG  "4.12.96 (a1b2c3d)"

The hash beside the number names the commit exactly; a trailing '+' means
the tree had uncommitted changes to TRACKED files when it was built, which
is the state a puzzling bug report usually comes from. Untracked files do
not count - one scratch document in the tree would otherwise mark every
build dirty and the marker would stop meaning anything.

Generated rather than committed, and gitignored, because a hardcoded
number cannot work: the commit that records "4.12.96" is itself commit 96,
so the file is stale the moment it lands. The header is rewritten only
when the stamp changes, so ordinary rebuilds do not drag RPL4.CPP through
a recompile.

pack-dist.ps1 reads that header instead of asking git again - a commit
between building and packing would otherwise have the zip claiming a
version the binary inside it does not report - and warns when the build
it is packing came from a modified tree. The README banner, the zip name
and the shipped CONTROLS.html all take the same number.

Numbering stays ordered: 95 commits so far, so 4.12.95 follows 4.12.7 and
every future build sorts after it. Only the "4.12" line is set by hand,
at the top of the script.

Two things the wiring turned up:

  Windows PowerShell turns a native command's stderr into ErrorRecords,
  so with $ErrorActionPreference = 'Stop' git's routine "LF will be
  replaced by CRLF" warning threw straight past the dirty check and
  stamped a modified tree as clean. Every git call now goes through cmd,
  which keeps stderr out of PowerShell's error stream entirely.

  The script ended on "git diff --quiet", which exits 1 to mean "there
  are changes" - as a pre-build step that failed the build on exactly
  the tree a developer builds in. It exits 0 explicitly now.

Verified: deleting the header and building recreates it; a second build
reports "(unchanged)" and leaves the timestamp alone; a build on a
modified tree succeeds and stamps 4.12.95 (c1729e4+); and the packed game
logs "Red Planet 4.12.95 (c1729e4+)" on its first line while README.txt
and CONTROLS.html in the same package both read 4.12.95.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 13:38:38 -05:00
CydandClaude Opus 5 c1729e40c7 The callsign and loadout outlive the session
The loadout has always survived a race - gPersistSelection is why the
setup screen reopens the way you left it - but only for as long as the
process lived. Closing the game was a reset, and the callsign is the one
thing on that screen a player types rather than picks, so it was the one
they had to type again every launch.

pilot.cfg beside bindings.txt now holds both, KEY=VALUE like environ.ini,
one line per group.

BT411 solved this first, in fe_last.ini, and its own comment says why
RP412 never grew the file: BT411 relaunches the process between missions
and would otherwise forget the loadout mid-evening, while RP412 stays in
one process. That made the gap invisible from inside a session and total
across two. Same idea, two differences worth naming:

  BT411 saves only on a launch - it returns before SavePersisted when
  the player quits. That loses a callsign typed by somebody who then
  changed their mind, which is exactly the moment this feature exists
  for, so this writes on the way out however the menu is left:
  launching, stepping into a lobby, or EXIT GAME.

  BT411 takes the stored name as-is. A callsign here is quoted into
  frontend.egg, joined into a comma-separated list for the results
  screen, and published as Steam lobby member data, so a comma alone
  would split one pilot into two on the score sheet. SanitizeCallsign
  drops what could end a token early and is applied to what is typed as
  well as to what is read, so the file cannot hold what the game will
  not accept.

Every index is range-checked on the way in, against the group's real
size rather than a constant - the track list is the one that moves,
since football and the death race carry different maps, so it answers
for whichever scenario is selected. The track is re-checked after the
whole file is read as well, because the file is parsed in the order it
happens to be written and the scenario may arrive second.

Written unconditionally rather than only on a change: it is a few
hundred bytes, and writing every time means a value hand-edited out of
range comes back corrected instead of being quietly re-rejected on every
launch forever.

Verified by round trip. A callsign typed and then abandoned via EXIT
GAME is in the file and back in the box next launch. A file carrying
   Ba"d,Na#me   loads as BadName; an empty one falls back to Pilot. A
full loadout round-trips value for value; vehicle=999 and color=-3 come
back 0 with the rest untouched; and track=9 under football falls back to
0 both when the scenario is read first and when it is read second, which
is the case the second check exists for.

One correction to my own test rig on the way: cross-process
SetWindowText on an EDIT updates the cached caption, which an external
GetWindowText then reads back happily, while leaving the control's own
buffer alone - so the harness looked right and the game correctly saw
the old name. WM_SETTEXT is marshalled properly and shows the truth.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 12:56:15 -05:00
CydandClaude Opus 5 f20547cb25 Controls page covers the joystick setup
CONTROLS.md gained this in the port; the page that ships beside it did
not, and the page is the one people actually look at.

A "Bring your own stick" section before Rebinding: what DirectInput is
and why it needs telling what its axes are for, joyconfig.bat as the
answer, and the four beats of running it. The callout carries the reason
the wizard reads direction rather than asking you to know it, since that
is the part that looks like a quirk until it is explained. Then what it
writes beside the grammar that produced it, and a table for the two
rules the pod's shape asks for - the signed Pedals axis working the
pedal pair, and a real lever owning the throttle channel.

The reference tables were left incomplete by the port and are now
whole: Pedals joins the axis list, the joy rows join the grammar block,
and DirectInput's own axis names get a row of their own.

Written in the page's existing components - glance, callout, two-col,
tbl-scroll - rather than new ones. The two <kbd> elements I reached for
first are not styled anywhere on this page and would have rendered as
browser defaults, so they are <code> like every other inline literal
here.

No version change: 4.12.7 is republished with the page in it.

Verified by rendering the packed CONTROLS.html headless at 1280 wide and
reading the section back - heading, lede, the four-panel strip, callout,
both code blocks, both tables, and the reference rows all sit in the
page's own idiom, and the tag balance is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 10:27:19 -05:00
CydandClaude Opus 5 52da65bc0d Release 4.12.7
Flight sticks, HOTAS throttles, twist grips and rudder pedals, none of
which the game could see before: they arrive through DirectInput rather
than XInput, and PadRIO only read XInput.

joyconfig.bat is the setup: the wizard asks you to move each control in
turn and derives the sign convention from the direction of the move,
then writes the joystick rows of bindings.txt between marker lines,
leaving anything you have edited yourself alone.

Confirmed on the Logitech Extreme 3D: a full pass wrote all four axes,
six buttons and the hat, with X and the throttle lever inverted to match
the pod's convention and Y left alone - and the deadzone on the twist
grip was then hand-tuned from 0.08 to 0.18 in the file, which is the
workflow the marker section exists for.

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

Built clean, packed, zipped (1004 entries, nothing loose at the root)
and smoke-tested from the dist: boots reporting 4.12.7, virtual RIO up,
and no DirectInput enumeration at all on a default bindings.txt - the
joystick layer only opens when the profile asks for it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
v4.12.7
2026-08-05 10:18:43 -05:00
CydandClaude Opus 5 91420b5cb2 Flight sticks, HOTAS and pedals, with a setup wizard
Ported from BT411, which needed the same thing for its glass cockpit.

PadRIO reads XInput, which covers Xbox-class pads and nothing else. A
flight stick, a HOTAS throttle, a twist grip, rudder pedals or a wheel
arrive through DirectInput instead, and until now the game could not see
any of them - the only generic-joystick path left was the 1995 single-
device DIJoystick behind L4CONTROLS=DIJOYSTICK, which is untouched here.

L4JOY is the reader: up to four devices as normalized state blocks, hot-
plug re-enumeration on the same ~3 s cadence PadRIO uses to look for a
pad, and a device lost mid-race zeroed rather than left holding whatever
was pressed when it went. XInput-class devices are excluded by VID/PID
against the RawInput paths carrying the "IG_" marker - without that an
Xbox pad arrives through both APIs and every button counts twice.

bindings.txt gains four rows in the grammar it already had, using its own
vocabulary (deadzone/rate) rather than BT411's:

  joydev <slot> [product-name substring]
  joyaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n>]
  joybutton <n> button <addr> [toggle]
  joyhat <n> <up|down|left|right> button <addr>

Slots resolve to a live device every poll, by name substring or ordinal,
so unplugging and replugging does not rewrite anyone's file.

Two things the pod's shape forced that BT411 solved differently:

  Pedals - a signed composite axis that decomposes into the pod's two
  pedals, positive right and negative left. The pod has a pedal each
  side; a twist grip or rudder bar is one signed control, and pressing
  one or the other but never both is exactly what it wants to say. It
  is a channel name like any other, so a pad stick can drive the turn
  too.

  A joyaxis on Throttle with no rate is a real lever and OWNS the
  channel - full travel maps onto the 0..1 the pod runs on, instead of
  nudging the accumulator that a spring-centred pad stick has to use.

RP412JOYCONFIG=1 (joyconfig.bat) runs the capture wizard before the
console screen: it asks the player to move each control, and derives the
sign convention from the DIRECTION of the move. That is the point of it -
a stick that reads positive pushed right and one that reads negative are
equally common, and no amount of documentation gets a player to work out
which they own. It writes only its own section, between marker lines, so
hand-edited keyboard and pad rows survive re-running it.

The wizard also prints every axis at rest before it starts. A driver that
refuses the +-32767 range we ask for reports its own, and an axis then
sits hard over instead of near zero; seeing "X +1.00" on an untouched
stick is the difference between a five-minute fix and a bug report that
says it configured itself. Each capture reports the move it saw for the
same reason.

Verified on the Logitech Extreme 3D on this machine. Enumeration finds
it and excludes the Xbox pad, which still arrives separately through
XInput. Every row shape parses - 7 axes, 2 buttons, 4 hat directions -
and three deliberately malformed rows (a bad axis name, button 99, a
"sideways" hat) are each rejected by line number rather than silently
dropped. The wizard lists the device with its axes at rest reading
X +0.00 Y -0.01 RZ -0.04 SL0 +1.00, waits on the first prompt without
self-triggering, and with a hand on the stick captures X to steering,
Y to pitch, RZ to the pedals and SL0 to the throttle, inverting the ones
that read backwards.

Running the captures through to a written file needs a hand on the
stick, so that part is the machine's to confirm, not this build's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 10:06:30 -05:00
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>
v4.12.6
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
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>
v4.12.5
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
CydandClaude Fable 5 3a24de03e6 Release 4.12.4
Version string, zip name and README. Cut as its own release rather than
replacing 4.12.3's asset again: the three test machines need to be able
to tell builds apart from what the log and the README say, and a binary
that reports 4.12.3 while carrying the lobby work defeats exactly that.

This one is about the lobby. Both rooms show the host's mission setup,
the football team sheet names each player's VTV, and a lobby holds eight
players rather than four.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v4.12.4
2026-07-25 15:38:24 -05:00
CydandClaude Fable 5 e4025c2aec Lobbies hold eight, and the room lays itself out to fit them
The lobby capped at four, which is half a grid. The pod hall raced eight
and the egg builder already carries the owner plus maxExtraPilots, so
eight members fit with room to spare. CreateLobby now asks Steam for
eight and the roster arrays follow.

Checked what eight has to travel through before raising it: the go
roster is 43 bytes a member against an 800 byte buffer, the score sheet
41 against 600, and the pods list 21 against 512, so all three hold
eight with margin. The member side parses the go string with a cursor
rather than a fixed array, and RegisterRoster walks the count it is
given, so neither needed touching.

The layout is the part that could not just be renumbered. Rows were a
fixed sixteenth of the window, and eight of those ran off the bottom of
every window we support - the buttons are laid out upward from the
bottom edge while the roster runs down from the top, so the two meet in
the middle. The room now measures the band between the title and the
topmost button and sizes its rows to it, counted in half rows as

  setup lines + gap + eight rows + gap + hint

so the row height falls out of the space actually available. It never
grows past the old sixteenth, so a two player lobby does not get circus
sized rows, and when the band cannot give eight rows the room the font
needs, the block is drawn in a font that does fit rather than letting
rows overlap.

The minimum row is the font cell plus two, not the cell plus an eighth.
That sounds like a detail and is not: at 1920x1080 the generous version
missed by one pixel and paid for it by dropping the conditions line out
of every football lobby, which is the one thing in the room nobody can
see for themselves.

Verified live on hosted lobbies at 1904x1041, and modelled across every
window size from 3440x1440 down to 800x480 in race and football, host
and member. Race keeps both setup lines everywhere; football, which
carries four stacked buttons, keeps both down to 1080 and drops to one
below that.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 15:17:33 -05:00
CydandClaude Fable 5 43cf086ea4 Show the host mission setup in both lobbies, and the VTV in football
The room told you who was in it and nothing about what you were about to
fly into. Only the owner's menu decides the mission, so nobody else could
see the track or the conditions until the race had already started.

The owner now publishes its picks as lobby data (track, time of day,
weather, game length) and the room draws them under the title: the track
on its own line, the conditions under it. Published as display names
rather than catalog keys, because the map list differs between race and
football and resolving it at the source saves the room from knowing
which table a key came from.

Football also names the VTV now. The team sheet was team and position
only, but the vehicle still decides how someone plays the position, and
it was already travelling as member data, just never drawn. Both roster
rows now run keys back through the catalogs, so nobody reads bttlbrg
where Battle Barge belongs, and the rows widened from the middle half to
two thirds to carry three fields.

The setup lines are only taken if they fit. Buttons are laid out upward
from the bottom edge while the roster runs down from the top, so on a
short window the two extra lines would have pushed the roster into MY
TEAM. The room measures the gap first and takes two lines, one, or none.
Checked against every window size from 3440x1440 down to 640x400 in both
scenarios.

Verified live on a hosted lobby. Race shows the track, the conditions
line, and Battle Barge / Red. Football shows the same setup with Battle
Barge, Blue / Aqua, Crusher, tracking the MY TEAM and MY POSITION
buttons as they cycle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 15:08:10 -05:00
CydandClaude Fable 5 abd720dd6e Ship the controls map as a page in the dist
The published artifact source now lives in docs/, and pack-dist emits it
as CONTROLS.html beside the plain-text CONTROLS.txt so players get the
diagrams without the repo.

The artifact source is a fragment - the publisher supplies the document
shell - so it is wrapped here with a doctype, charset and viewport.
Without those a browser opens it in quirks mode and renders the
typography as mojibake. Written without a BOM so the charset declaration
is the only thing speaking.

The map itself gains a section for the mouse: the button banks reach
under the glass, and the six displays are the players to size and the
radar to place. Verified the five radar-placement diagrams against the
layout they describe - box 16:9, radar 16.7x39.3% of it, centred flush
to the bottom edge, flush in either bottom corner, and 30/30 vertically
when halfway up a side.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 14:39:17 -05:00
CydandClaude Fable 5 0c696c9952 Release 4.12.3
Version string, zip name and README. This one is about the screen:
the cockpit scales both ways and re-fits on resize keeping 16:9, -fit
runs it borderless over the monitor at a matched render size, the six
secondary displays are the players to scale and the radar to place,
and the button banks reach under the glass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v4.12.3
2026-07-25 14:19:10 -05:00
CydandClaude Fable 5 ea9491d2d5 Add the cockpit glass-sizing declarations missed by the last commit
L4VB16.h carries GlassSize and CockpitGlassSizes, which L4VB16.cpp uses
in both the constructor and LayoutCockpit - 8dc6605 left it behind (the
add named the path in the wrong case), so that commit does not build on
its own.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 14:05:24 -05:00
CydandClaude Fable 5 8dc6605a07 Cockpit: buttons under the glass, -fit, and player display layout
Button banks
  The exploded diagnostic view was still display-only - it predates the
  button work - so it now builds the same banks as the cockpit, with
  the pod arrangement laid out from the panes measured sizes rather
  than a hardcoded 640/480 grid (the banks make each window bigger than
  its glass, and the bottom row hung off the work area otherwise).

  The map side columns were spread height/6 from the top, but the maps
  own legend grid is not sixths: measured off the bitmap it starts 13
  rows down with six 102-tall cells on a 105 pitch. Every button sat
  high of its label, worst at the bottom. Each buttons top and bottom
  now come off that grid separately and are subtracted - scaling a
  height directly would let rounding drift them back out of step on a
  resized cockpit.

  Depth 100 to 240: against the 480 glass the two banks meet in the
  middle bar the strips, so practically the whole display is a press
  target. This mattered most in the cockpit, where the panes are small
  enough that the halfway clamp governs - at 100 the MFDs had a 110px
  dead band straight through the middle of the glass.

-fit (also spelled -windowed-fullscreen)
  Borderless over the whole monitor, with the render size chosen to
  match. The cockpit presents the 3D into a viewscreen that fills its
  canvas, so the right -res is that canvas at the scale the cockpit
  will settle on; computing it with identical arithmetic makes the
  stretch a copy. On the 3440x1440 panel that is 133% and -res 2553
  1436, against 125% for the windowed path that pays for the taskbar.

  The pick runs after the whole command line, so an explicit -res wins
  from either side of -fit. Capped at 3840x2160. Cockpit mode only -
  mode 0 has to stay playable on real pod hardware and mode 2 is a dev
  view - so those get the resolution and keep their windows.

Display layout, in environ.ini
  L4MFDSCALE sizes all five MFDs, L4MFDSCALE_UL and friends override
  any one of them, L4RADARSCALE the radar, and L4RADARPOS puts the
  radar bottom centre, in either bottom corner, or halfway up either
  side. Scaling is applied in canvas units before the canvas is fitted
  to the window, so a number means the same thing on every monitor.

  Sizing each display separately let the clamps become exact rather
  than one conservative rule for all five: what limits a display is its
  actual neighbour. Which neighbour that is depends on the radar, so
  the clamps follow it - on the bottom edge it clears the one MFD above
  its column, but centred on a side it has one above AND below and
  grows from the middle both ways, so it must clear the taller twice
  over. Clamping shrinks uniformly; these are photographs of real
  instruments and a one-axis clamp would squash them.

Verified on the ultrawide: all five radar positions, per-display and
group scaling with the clamps biting, -fit with and without an explicit
-res, and the button geometry measured back off the screen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 14:05:02 -05:00
CydandClaude Fable 5 6b43971d2d Cockpit scales both ways and re-fits on resize, aspect locked
The canvas was capped at 100%, so a bigger monitor got a 1920x1080
cockpit in the corner of the screen and maximising did nothing - the
layout only ran once at startup.

Now the fit is one uniform scale with no ceiling, recomputed whenever
the window changes size (maximise, restore, drag), and the canvas is
centred in whatever client area it gets. Uniform scale is what locks
the aspect: a wider-than-16:9 desktop letterboxes with even black bars
instead of stretching the cockpit. Panes gained Resize() so the glass
and its button banks re-scale with the canvas; the pixel buffers keep
their native source resolution.

Scaling up is free quality on the MFDs - their glass is a downscale of
a native 640x480 channel until about 200%.

Verified on the 3440x1392 ultrawide: opens at 125% (2400x1350) instead
of 100%, maximises to 126% centred with even bars, a 1884x661 window
fits 61% letterboxed left/right, 1084x961 fits 56% letterboxed top/
bottom, and a 2584x1461 client scales up to 134%.

The 3D still renders at -res and D3D stretches it to the canvas, so
raise -res to match a large screen for 1:1 pixels; start-windowed.bat
and the README say so.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 12:41:47 -05:00
CydandClaude Fable 5 b6045f3c94 Glass cockpit: radar columns reach under the map too
The amber Secondary/Screen columns get the same treatment as the red
MFD banks, turned on its side: each button reaches 100px in behind the
map with a 10px indicator clearing the edge, so the lamp reads as a
slim column and the radar picture is the click target. The shared
buttonDepth/indicatorStrip constants now drive both banks, and the
columns are clamped so they can never meet behind a narrow map.

The map pane narrows from 404 to 344 for a 324-wide glass, handing the
difference back to the viewscreen.

Verified live: clicking 60px inside the radar from either edge presses
the column button behind it, with feedback in that side's strip (720
and 1350 pixels).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 12:31:10 -05:00
CydandClaude Fable 5 0bec0f9640 Glass cockpit: MFD buttons reach under the display
The red banks were thin strips sitting outside the glass, so the click
target was only as tall as the strip. They are now 100px buttons that
extend BEHIND the MFD picture with just a 10px indicator clearing the
edge: the lamp still reads as a slim strip along the bezel, but the
region of the display above or below it is what you press. Paint order
flipped to match - buttons first, glass over them.

The banks are clamped so they can never meet in the middle on a
scaled-down cockpit, and the panes lost 40px of height each (260 vs
300 for a 240-tall glass), which hands that space back to the
viewscreen.

Verified live: clicking 50px inside the picture presses the button
behind it and the press shows in the indicator strip (770 pixels
changed, all of them within the strip).

Amber map columns are untouched for now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 12:25:23 -05:00
CydandClaude Fable 5 8fb4b72f7a Pad Start and Back ship unbound
They were mapped to the config buttons (0x37/0x36), which the 9 and 0
keys already reach from the Upper Right MFD bank. Freeing them gives
players two pad buttons to assign; bindings.txt carries the mapping as
commented example lines so the way to re-enable it is in the file.
Docs and the dist README follow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 21:08:05 -05:00
CydandClaude Fable 5 00cb87907c Controls map: docs/CONTROLS.md, shipped with the dist
Written from the default bindings.txt so it matches what the game
actually writes. Covers the Xbox pad (ASCII diagram plus a table of
every button and its RIO address), the keyboard - numpad flight
cluster diagram, the Shift/Ctrl/Alt throttle and reverse modifiers,
Alt+Q abort - and the panel button banks, showing how the number and
letter rows map onto the five MFD clusters exactly as printed on the
pod board, with the G/B gap keys and the F-key Secondary/Screen
columns. Also notes the keypads being deliberately unbound, the RGB
lamp mirror, and how to rebind with worked examples.

pack-dist copies it to the dist as CONTROLS.txt, flattened to ASCII so
it reads correctly in Notepad while the markdown source keeps its
typography. README links it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 20:24:43 -05:00
CydandClaude Fable 5 53228686b4 pack-dist: release zips nest everything under an RP412 folder
Unpacking the zip anywhere now yields one self-contained RP412\
directory instead of scattering ~1000 files into the extraction
folder. The -Zip step stages a copy under the temp dir as RP412\ and
compresses that folder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 20:12:53 -05:00
CydandClaude Fable 5 1bd6dd83e2 MFD rendering: HALFTONE downscale, plus the exploded diagnostic view
Two pieces of MFD work that had been sitting in the tree (already in
every build and dist since).

The compact cockpit glass shows the full 640x480 gauge canvas at about
half size, and COLORONCOLOR did that by dropping every other row and
column - which shredded the 1-bit vector strokes and small text.
HALFTONE area-averages instead (with the brush origin set, as MSDN
requires) so the downscaled MFDs stay legible.

L4MFDSPLIT=2 adds an exploded diagnostic view: every display in its
own full-size desktop window at native resolution, decoded exactly as
the pod VDB split them from the single gauge canvas, with no cockpit
compositing and no downscale. It makes an individual MFD readable and
screenshottable at full resolution for comparison against the
emulator per-channel reference windows. L4MFDSPLIT=1 keeps the
composited glass cockpit and stays the default.

Also: .gitignore now covers the packaged RedPlanet-*.zip releases,
which live on the Gitea release page rather than in the tree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 20:10:14 -05:00
CydandClaude Fable 5 1ba3bde36a pack-dist: ship steam_appid.txt so a fresh dist is test-ready
The packer wipes and rebuilds dist\, which silently discarded the
hand-made steam_appid.txt every repack - and without it SteamAPI_Init
fails and the game quietly falls back to TCP, which is the confusing
symptom testers hit. The dist now ships it (480, Spacewar) alongside
the RP412STEAM=1 environ.ini, so copying the folder to a machine with
Steam running is the whole setup. Test doc updated to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 20:08:25 -05:00
CydandClaude Fable 5 4b25f89e6f Football: every player picks their own team and position
Closes the gap left by the football commit. Members publish their
picks as lobby member data (tm/ps) alongside their loadout, and the
owner publishes the scenario (sc) so members know when the picks
matter. The lobby room turns into a team sheet: each member row shows
their team and position, and MY TEAM / MY POSITION buttons cycle the
local pick and republish it live, so everyone watches the sides fill
out before the host launches.

The egg builder honors explicit picks and only fills gaps:
- pilots who chose a team get it; the rest are spread across the
  host team and one other so the sides stay balanced
- a team with no volunteer runner promotes its first UNPICKED member,
  never overriding someone who chose crusher or blocker
- if two pilots on a team both claim runner, the first keeps it and
  the second lines up
- colors stay derived: runner in the team runner color, everyone else
  in the team color

Verified: a three-pilot egg puts the host on crusher in team color
with an unpicked teammate promoted to runner in runner color, the
other side gets its own runner, and the teams/pilots blocks group
correctly; the lobby room cycles picks and repaints the roster; both
scenarios still pass their single-player regressions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 19:59:42 -05:00
CydandClaude Fable 5 3194d3f973 Martian Football: the second scenario is playable
The setup menu gains a SCENARIO group. Picking Football relayouts the
menu live: the track list swaps to the football-legal set (drops
Paingod Passage and the race build of Freezemoon Freeway, adds the
football build headmf - RPConfig per-scenario invalid lists), the
COLOR and BADGE columns become TEAM and POSITION, and the title
reads MARTIAN FOOTBALL.

The egg builder was restructured around one flat pilot table so both
scenarios share it. Football emits scenario=football, per-pilot team=
and position= entries, and the [teams] / [team::X] / [pilots::X] /
[teambitmap] blocks in RPFootballMission layout (team name bitmaps
generated by the same GDI plasma renderer as pilot names). Colors are
derived rather than chosen, as the arcade did it: the runner wears the
team runner color, crushers and blockers the team color. Multi-pod
games alternate pilots across two teams and give each team exactly one
runner, honoring the host own position pick.

Verified end to end: Football launches from the menu, the engine loads
the football mission (the cockpit shows the RUNNER - GO FOR POINTS
panel), the console marshals it to a scored finish, and the Death Race
path still passes its regression unchanged.

Known gap: lobby members cannot pick their own team or position yet -
the host pick seeds a deterministic assignment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 19:42:33 -05:00
CydandClaude Fable 5 0b84b32486 README: corrections from review
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 13:07:35 -05:00
CydandClaude Fable 5 0cf5a23d2e Release 4.12.2: version bump
Carries the worldwide lobby-search filter for cross-region internet
testing (the only change since 4.12.1 besides the README rewrite).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v4.12.2
2026-07-13 11:30:36 -05:00
CydandClaude Fable 5 13424cce50 README: the Steamification story; lobby search goes worldwide
README.md rewritten around the lineage: Red Planet 4.10 on the
Tesla 1 pod platform, 4.11 the Win32 port, 4.12 the Steamification
of 4.11 - with the arcade-to-consumer replacement table, current
status (v4.12.1 verified on three machines), playing/building/docs
sections brought up to date.

And ahead of internet-wide beta testing: lobby search now applies the
worldwide distance filter - Steam defaults to roughly same-region
results, which would have hidden lobbies from cross-region testers
even though joining them works fine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 11:27:45 -05:00
CydandClaude Fable 5 bfdbfe6353 Release 4.12.1: version banner and packaging name
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v4.12.1
2026-07-13 11:10:34 -05:00
CydandClaude Fable 5 f1604213ad Cockpit: radar trimmed to 1.35x (1.5x was a touch too large)
324x432 glass on the 1080p canvas, per playtest feedback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 11:09:05 -05:00
CydandClaude Fable 5 d6f4f16a24 Cockpit: radar 50 percent larger
The map/radar glass grows from 240x320 to 360x480 on the 1920x1080
canvas (user request - the radar reads best big). The flanking
Secondary/Screen columns, bottom-center placement, and canvas scaling
all follow from the measured size automatically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 11:05:02 -05:00
CydandClaude Fable 5 29f8572d5c environ.ini is self-documenting: every option shipped with comments
The environ.ini reader now skips comments (# or ;), blank lines, and
anything that is not KEY=VALUE, so the shipped file documents the
whole configuration surface: the core settings as-shipped (controls,
renderer, gauge canvas, plasma, single-window cockpit, frame rate,
Steam networking), the optional toggles (keyboard lighting, stick
flip, AA, particles, plasma scale/position, fixed seed), LAN hosting
without Steam, the developer/testing switches (RP412DEVKEYS,
L4CONSOLELEN, the Steam self-test), and the arcade multi-monitor
heritage variables. Stale L4MFDSCALE reference dropped from the
README.

Verified: the game boots on the commented file with values applied
(controls line honored, Steam transport up from the in-file switch).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 11:00:19 -05:00
CydandClaude Fable 5 0ca6b5b01f RGB keyboard lamp mirror: vRIO Dynamic Lighting port, live in-game
The polish-backlog item, implemented from vRIO KeyboardLampMirror:
game-commanded RIO lamp states paint per-key RGB keyboards through
Windows Dynamic Lighting (WinRT LampArray). Keys bound to lamp
addresses in the active bindings profile glow with the panel palette
(red banks, yellow Secondary/Screen columns), flash modes use the
exact L4MFDVIEW formula so keyboard and on-screen buttons blink in
step, unbound keys are blacked out so the board reads as the button
field, and zone-lit keyboards fall back to a board-wide mirror of the
strongest lamp. Advantage over vRIO: Dynamic Lighting grants LEDs to
the FOREGROUND app - which is the game - so no Windows settings
dance.

Isolation: L4KEYLIGHT.cpp compiles /std:c++17 + DEFAULT packing +
conformance (per-file vcxproj settings; the engine /Zp1 would break
the WinRT ABI) with a scalars-only interface, and all WinRT work runs
on a private worker thread (watcher, claiming, 100ms paint loop).
On by default with a bindings map present; RP412KEYLIGHT=0 opts out;
missing Dynamic Lighting logs once and stays dormant.

Verified live on the dev laptop: claimed its 24-zone keyboard
(board-wide mirror) during a race; race cycling with per-race
start/stop of the mirror thread stays green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 10:49:35 -05:00