58 Commits
Author SHA1 Message Date
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
CydandClaude Fable 5 97adff22b3 Default layout: flight on the numpad, modifiers for throttle/reverse
Per the user: the game never reads the pilot keypad, so the numpad
becomes the flight cluster - 8/2/4/6 stick, 7/9 pedals, 0 trigger -
with Shift/Ctrl as throttle up/down and Alt as reverse thrust. That
frees the entire letter board: W/A/S/D/Q/E return to their printed
MFD bank positions and B goes back to being the gap key, so the
default profile now carries the complete unmodified vRIO bank layout.
The keypad addresses stay bindable (arcade key events) but ship
unbound.

Alt as a held flight control meant every release popped the window
menu and stole focus - WndProc now eats SC_KEYMENU. Shift/Ctrl/Alt
key-name aliases added to the parser alongside the .NET names.

Profile parses clean (59 key buttons, 8 key axes); single-player
cycle and key-bomb tests green (Alt+Q abort unaffected).

Machines with an existing bindings.txt keep their old map - delete
the file to take the new defaults.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 10:31:33 -05:00
CydandClaude Fable 5 b0def79de2 Rebindable input: vRIO bindings profile ported, full board coverage
PadRIO now loads bindings.txt (vRIO profile grammar: key/pad/padaxis
lines with toggle, deflect/rate, invert/deadzone options) written
self-documenting with the full default layout on first run. The
default is vRIO board-complete map - number and letter rows are the
MFD banks as printed on the panel, F-keys the secondary/screen
columns, numpad the pilot keypad (0x50-0x5F delivered as arcade RIO
KeyEvents, a new PadRIO capability), Space/arrows the joystick
column - with the desktop driving keys carved out: WASD stick, Q/E
pedals, PgUp/PgDn throttle, B reverse (vRIO gap key; R returns to
its bank). Pad bindings unchanged in spirit, plus Panic on LB and
config on Start/Back; axis signs are encoded in the profile now, so
L4PADFLIP flips on top of it.

Default profile parses with zero rejected lines (68 key buttons, 8
key axes, 12 pad buttons, 5 pad axes); single-player cycle and the
key-bomb tests stay green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 10:18:36 -05:00
CydandClaude Fable 5 fbd23ff1ca Key-command audit: the up arrow was the abort key; bombs disarmed
Full input-surface audit of the PC keyboard channel. The arcade abort
was the typed ampersand character (0x26), but the Win32 port feeds
WM_KEYUP VIRTUAL-KEY codes into the same channel - and VK_UP is also
0x26. The hat look-up key aborted the mission: that was every mystery
abort across the multiplayer test rounds. Likewise the E key (0x45,
the right pedal!) dumped the event queue on every release.

Disarmed: plain 0x26 and E are swallowed at the L4 layer; the abort
answers only to the deliberate Alt+Q chord (translated to the legacy
engine code, so APP.cpp is untouched); the debug toggles (wireframe
Alt+W, predator vision Alt+V, frame dump Alt+F, perf stats, event
queue on Alt+E now) arm only with RP412DEVKEYS=1. The cheat-string
manager has no PC-keyboard strings and the trace-log keys are
compiled out of release - both inert.

Verified live: a volley of VK_UP releases mid-race leaves the mission
running; Alt+Q aborts on demand. Docs and the dist README updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 09:47:04 -05:00
CydandClaude Fable 5 18c6f05cf1 Members get the score sheet: owner publishes results through the lobby
Round seven was the first complete Steam race: three machines, five
minutes, wall deaths respawning, the console ending the race on time,
and rematches launched from the same lobby. The one gap the run showed:
only the owner saw scores - members went straight back to the room.

The owner now publishes its console results (host, score, name, keyed
by the race nonce) into lobby data right after teardown; members pull
the sheet (waiting up to 8s for it to land), inject it into the local
results intake, and the same RACE RESULTS screen shows on every
machine before the room.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 00:34:34 -05:00
CydandClaude Fable 5 a1b524da29 Test doc: Steam Input steals the pad on 480; abort key; steam env baked in
Reproduced A/B on the dev box: with RP412STEAM=1 the wired pad
disappears from XInput because Steam Input intercepts Xbox controllers
for AppID 480. One-time client setting fixes it (disable Steam Input
for Spacewar, or globally for Xbox controllers); PadRIO hot-connects
within 3s once released. Our own AppID will configure this
server-side.

environ.ini in the dist now ships RP412STEAM=1 (the packer regenerates
the file, which was wiping the hand edit each round); machines without
Steam credentials fall back to TCP cleanly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 00:10:26 -05:00
CydandClaude Fable 5 3f691cacb3 Departed-pod resilience: collision guard + console loss ends the race
Round six raced all three machines (staging fix confirmed) and then
exposed what happens when a pod leaves mid-mission - which arcade pods
never did.

The B crash dump named it exactly: VTV::TakeDamageMessageHandler
resolved message->inflictingEntity to NULL (the entity belonged to the
departed owner) and dereferenced it - Verify is compiled out in
release. Collision damage from an entity that no longer exists is now
ignored.

And the race B and C were left in was a zombie: the owner (console)
had aborted, so the mission clock would count up forever and the
death/respawn flow hung with nobody to arbitrate. Lobby-member races
now set gConsoleLossEndsMission: losing the console mid-mission posts
StopMission locally, the pod tears down, and lands back in the lobby
room. Arcade -net pods keep the re-listen-and-wait behavior.

Loopback hosted race still green.

For the drivers: the ampersand key is the arcade mission-abort - that
was every crash-on-keypress so far; and a sleeping Bluetooth pad wakes
on the Xbox button and hot-connects within 3 seconds (PadRIO
re-probes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 00:02:54 -05:00
CydandClaude Fable 5 2d5057c528 Hosted races stage properly: the owner no longer launches itself
Round five reached the race - full mesh on all three machines, eggs,
ACKs, mission running - but the owner raced ALONE. Cause: the engine
self-runs a pod at WaitingForLaunch when its console host is not
online (the arcade no-console fallback), and the owner''s in-process
console never connects to its own pod. On fast-loading owners the
self-run beat the console''s staging gate, so RunMission was never
sent and the members sat staged at black screens until someone hit
the & emergency-abort key.

gConsoleMarshalsLaunch (APPMGR) now tells the engine an in-process
console owns the launch: the network-race install sets it and the
owner holds at WaitingForLaunch with everyone else; plain single
player leaves it False and auto-runs as always. Verified on loopback:
all pods staged - RUN is back in the hosted-race log and both the
hosted race and the single-player cycle pass.

Also: unhandled-exception minidumps (rpl4crash.dmp beside the exe,
dbghelp loaded lazily) so test-machine crashes hand back stacks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 23:40:04 -05:00
CydandClaude Fable 5 7ad53e03b5 Steam transport: identity-resolved accepts + load-proof timeout
Round four reached one step from the race: eggs delivered, both
members ACKed with complete meshes, one member entered LoadingMission.
Two failures remained, both now fixed.

One: Steam reports incoming callers under locally-allocated ALIAS
FakeIPs, not their global ones - the owner accepted both mesh legs
but its identity check compared the alias against the egg address and
never counted the connections (no Connected to GameMachineHost on the
owner). The peer table now carries each member SteamID (lobby go
roster gained a field) and Accept resolves the caller identity back
to the global FakeIP the egg promised.

Two: mission load stalls the game thread for 10-30s with nothing
pumping, and Steam default 10s connected-timeout sheared every
connection mid-load (end reason 4001, rx ages 11.5-20.5s - right at
load duration). Connected timeout is now 90s; TCP never timed out an
idle arcade link and races pump every frame once running.

Self-test still green (ping, survives listener close).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 23:19:03 -05:00
CydandClaude Fable 5 974cc6e120 Steam transport: accepted connections survive listener close
Round-three diagnosis from the three-machine logs: the console
channel connected first try on SDR both ways - then died seconds
later with end reason 5010 (PeerSentNoConnection). Cause: the arcade
engine closes its console LISTENER the moment the console connects
(correct under TCP, where accepted sockets outlive the listener), but
Steam''s CloseListenSocket closes all accepted connections
ungracefully. The members silently killed their console connection at
accept; the owner''s next packet got no-connection back, no eggs were
ever fed, and all three pods sat waiting.

The transport now bridges the semantics: an engine close only marks
the listener (new callers are rejected, pending queue dropped); the
Steam socket is destroyed in Cleanup at mission teardown. Re-listening
on the same engine port reopens the marked listener.

The loopback self-test now covers exactly this: accept, close the
listener, then push data both ways over the accepted connection -
PASSED (ping 1, survives listener close 1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 23:03:22 -05:00
CydandClaude Fable 5 7eb765696d Steam transport: status callbacks now dispatch (CCallback, not config ptr)
Root cause of the three-machine failure found and fixed. The
connection-status callback was registered as a per-connection config
value pointer - which the steam_api flavor of the library never
dispatches (only the standalone lib does). So members never saw the
incoming connection request, never accepted, and the owner timed out
after 120s of state-5 retries. Valve''s own SpaceWar example registers
SteamNetConnectionStatusChangedCallback_t with STEAM_CALLBACK; the
transport now does the same through a CCallback listener constructed
after SteamAPI_Init.

Proven with the new RP412STEAMSELFTEST=1 loopback: one machine
listens on its fake console port and dials its own FakeIP - the log
now shows incoming/accept/connected on both ends and a verified data
round trip, connect succeeded on attempt 1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:45:45 -05:00
CydandClaude Fable 5 d2836d195f Steam transport: full connection diagnostics + polled connect state
The three-machine test failed at exactly one step: the owner''s
ConnectByIPAddress to a member''s fake console port retried silently
for 120s while the member WAS listening on that port. The transport
threw away Steam''s reason for killing each attempt.

Now every connection state change logs the connection description,
and drops log the end reason plus Steam''s debug string - the next
run will say exactly which subsystem refused (cert, FakeIP directory,
routing, accept). Connect also polls GetConnectionInfo directly
instead of trusting only the callback flags, logs each attempt''s
final state, no longer holds a pointer into the connection table
across mutations, and paces retries at 1s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:19:34 -05:00
CydandClaude Fable 5 0deae09386 Three-machine Steam test procedure
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:27:12 -05:00
CydandClaude Fable 5 5892af318e Steam lobby: host, join, room, and launch into a marshaled race
RPL4LOBBY implements the multiplayer front door on ISteamMatchmaking.
The setup menu grows HOST STEAM RACE / JOIN STEAM RACE buttons when
the Steam wire is live; hosting creates a tagged public lobby, joining
finds one. Every member publishes FakeIP + fake ports + persona +
loadout as member data; the room screen lists members (host marked)
and gives the owner a launch button.

Launching writes a nonced go-roster into lobby data. Each pod
registers every peer with the Steam transport (two-port peer table:
engine console/game ports map to Steam fake ports on connect) and
enters the race: the owner through the hosted-race path - it builds
the multi-pilot egg from real personas and loadouts and its console
marshals everyone - and members as network pods that boot straight
into WaitingForEgg for the owner to feed over the wire.

The lobby outlives races: members loop back through WinMain into the
room (no local console needed - MissionCompleted is waived for member
races), and the owner returns to the room after its results screen.
Leaving the lobby clears the hosted-race priming.

Verified on this box: menu buttons appear under RP412STEAM=1, hosting
creates a lobby on the Steam backend, the room runs and leaves back to
the menu; single-player cycling and the LAN hosted race both still
pass. Full three-account mesh test is next, on real hardware.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:26:31 -05:00
CydandClaude Fable 5 c66cb00a7e Network races: the local console marshals remote pods over the wire
The lobby-owner-as-console architecture, in-engine. RPL4CONSOLE gains
RPL4LocalConsole_InstallNetworkRace: the owner pod switches to network
mode and meshes like any pod (egg fed locally via FeedLocalEgg, which
now opens the ConsoleOnly state gate), while the console tick also
marshals REMOTE pods over NetTransport speaking the exact arcade
protocol - egg chunks with 5s-retry-until-ACK, 1Hz state polling,
RunMission once every pod stages at WaitingForLaunch, StopMission at
expiry (remotes first, local pod holds until their EndMission scores
land), score intake labeled with [pilots]-order names on the results
screen.

The front end builds the multi-pilot egg: RP412HOSTPODS lists member
console channels (lobby stand-in; the Steam lobby feeds the same
path), RP412HOSTPORT/RP412HOSTADDR set the owner side.

Winsock Connect now redials with a fresh socket per attempt (a refused
TCP socket is dead; the old loop reused it) bounded at 120s - needed
whenever a peer boots after the caller, which is the normal Steam
lobby launch order.

Verified on loopback: member pod in -net, owner hosting from its menu;
mesh completed both sides, 30s race, remote score collected over the
wire (host 3), local stop after the drain, results screen shows both
pilots by name in one process that returns to the menu.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:08:18 -05:00
CydandClaude Fable 5 a8053ad784 Track steam_api.dll (root .gitignore filtered it)
The exe imports it; fresh clones need it beside rpl4opt.exe and in
the dist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 20:34:47 -05:00
CydandClaude Fable 5 ff6ec8c56a SteamNetTransport: the Steam wire is implemented and live
Steamworks SDK 1.64 vendored at extern/steamworks_sdk_164 (headers +
win32 redistributables only; .gitignore trims the rest). Both projects
build with RP412_STEAM; activation stays behind the RP412STEAM=1
environment switch, so plain desktop runs never touch Steam.

L4STEAMTRANSPORT.cpp implements NetTransport on ISteamNetworkingSockets
with FakeIP: SteamNetTransport_Install brings up SteamAPI, relay
network access, and a two-port FakeIP identity (fake port 0 = console
channel, 1 = game mesh), then swaps the process wire; any failure logs
the reason and the game carries on over TCP. Addressing keeps the
engine untouched: all pods share the -net port convention, eggs carry
fakeip:engineport, and the transport alone translates engine ports to
Steam fake ports via the lobby-fed peer table (RegisterPeer). Connect
mirrors the TCP retry-while-refused loop; Receive normalizes message
lanes back into the stream semantics CheckBuffers expects.

Runtime verified on this box: RP412STEAM=1 under AppID 480 came up as
169.254.59.52 (fake ports 32256/32257); without Steam credentials it
falls back to TCP cleanly; default boot logs no Steam lines at all.
steam_api.dll ships in the dist.

Next: the lobby layer (ISteamMatchmaking member data -> RegisterPeer +
egg build + RPL4CONSOLE marshal), which needs a second account to test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 20:34:07 -05:00
CydandClaude Fable 5 7315f3488d Two-pod LAN mesh test: multiplayer verified through the seam
tools/two-pod-test.ps1 stands up two -net pods on loopback and
marshals them with a minimal console feeder speaking the Munga
protocol (TeslaSuite vendored Munga Net.dll): state polling, egg
chunk delivery with ACK-after-mesh, RunMission when both pods reach
WaitingForLaunch, StopMission(0) at time expiry, EndMission score
intake.

First run passed clean: pod A listened on its game port, pod B
connected from its bound port, both ACKed after the mesh completed,
raced the same 60s mission on Wiseguy's Wake, stopped on command,
and reported final scores. The deterministic TCP mesh works end to
end through the NetTransport seam - the feeder logic is exactly the
marshal the Steam lobby owner will run in-process.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 20:14:47 -05:00
CydandClaude Fable 5 22421fb418 NetTransport seam: the wire moves behind an interface
The Steam-multiplayer prerequisite from the design doc: L4NET keeps
hosts, message queues, and the deterministic mesh ordering, while
connect/listen/accept/close, send/receive, startup/cleanup, the local
interface list, and ip[:port] parsing move behind NetTransport
(L4NETTRANSPORT.h). WinsockNetTransport carries the existing TCP
behavior over verbatim - including the connect retry-while-refused
loop the egg-ACK ordering relies on - and is the process default;
NetTransport_Set installs a replacement before the network manager
comes up.

L4STEAMTRANSPORT.h documents the ISteamNetworkingSockets mapping
per method (FakeIP keeps [pilots] entries as IPv4 strings) behind
RP412_STEAM until the Steamworks SDK lands.

Also fixed in passing: the ExclusiveBroadcast path sent
sizeof(network_packet) - four bytes of pointer - instead of the
message size, which would have sheared the stream framing had it
ever fired.

Verified: single-player race cycle (menu, race, results, menu, race)
green; -net 8000 boots, listens for a console through the transport,
and idles stable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:38:37 -05:00
CydandClaude Fable 5 8d7373974f Results screen between race and menu
After a console-marshaled race ends, the race loop now shows a RACE
RESULTS screen (place / pilot / final score, sorted descending, with a
CONTINUE button) before returning to the setup menu. Scores come from
the local console's intake; single-player rows carry the pilot's own
name, additional pods show their host number until the Steam roster
maps IDs to personas.

The setup menu also keeps the player's selections and pilot name across
races now instead of resetting to defaults each cycle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:05:55 -05:00
CydandClaude Fable 5 570eb3aceb Single-binary race loop: menu -> race -> menu in one process
WinMain now wraps the engine block in a loop: when a front-end-launched
mission ends under the local console, the setup screen comes back in the
same process instead of exiting (the arcade relaunch-per-mission model).
Replaces the CreateProcess self-respawn - required for Steam, where the
lobby and sockets must survive across races.

Second-cycle re-init crash fixed: d3d_OBJECT kept a static texture cache
keyed by filename, so race 2 got IDirect3DTexture9 pointers created on
race 1 destroyed device and died at first draw (DrawMesh AV). The cache
is now flushed in ~DPLRenderer before the device is released, and
ParticleEngine::Initialize drops particles left over from the previous
mission. Verified: three consecutive 30s races in one PID, each stopped
on time by the console with final scores collected.

Also: L4CONSOLELEN env override for test-length races, and the console
exposes MissionCompleted() for the loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 18:48:56 -05:00
CydandClaude Fable 5 9f79508257 LocalConsole: the in-process marshal that ends missions
Domain correction from playtest: hand-fed eggs are a developer shortcut
- a mission only ends on a console command, so the clock hits 00:00 and
counts up forever. Even single-player games need a console marshal.

RPL4CONSOLE is that console. Like the real one it lives on its own
thread: it owns the mission clock and raises the stop request at the
selected length; the app-manager per-frame hook (new gPerFrameHook seam
in APPMGR, called while the application global is live - the loop
condition NULLs it on exit, which ate the first attempt) executes the
engine-safe part, dispatching the same StopMissionMessage TeslaConsole
sent. Final scores flow in through a new RP-layer sink
(gConsoleScoreSink in RPCNSL): RPPlayer feeds it the same score it
sends a real console at mission end.

It also inherits the launcher role: the application tears down after a
stop (arcade pods were relaunched per mission by TeslaLauncher), so
WinMain respawns the process when the console ended the mission,
landing back on the race-setup screen. L4NetworkManager grows
FeedLocalEgg (the single-user egg-inject path, callable mid-session)
for the future in-process loop.

Verified end to end: menu -> 3:00 race -> stop dispatched exactly on
time -> final score collected (host 1 = 4113) -> process respawned with
the front end up. -egg runs stay unmarshaled (the dev shortcut).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 18:10:02 -05:00
CydandClaude Fable 5 58eb25a792 In-game front end: race setup menu builds the mission egg locally
Starting without -egg, -net, or -mr now boots a race-setup screen
(RP_L4/RPL4FE.cpp) instead of aborting: track / vehicle / color / badge
/ time-of-day / weather / race length plus pilot name, populated from
TeslaConsole''s RPConfig.xml catalog (Death Race scenario). LAUNCH
builds the egg exactly as the console did - the RPMission.ToEggString
port, including the pilot name pre-rendered to 1bpp plasma bitmaps
(128x32 + 64x16) via GDI with the console''s auto-shrink font logic and
the verbatim ordinal graphics - writes frontend.egg, and injects it
into the standard egg-load path (new L4Application::
SetEggNotationFileName).

The menu is a GDI child of the main window (pod green-on-black, double
buffered, mouse driven, EDIT control for the name) running a modal loop
before engine init; closing the window exits cleanly. Found and fixed
along the way: the empty egg CString holds a NULL representation
(operator! is the safe emptiness test), and the modal loop needed a
queue nudge for launch clicks delivered via SendMessage.

Verified end to end: boot -> menu -> LAUNCH -> generated egg (7.5KB) ->
racing in the 1080p cockpit with score and mission clock running.
start-windowed.bat now boots into the front end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 16:20:05 -05:00
CydandClaude Fable 5 f842425452 Front-end design notes: TeslaConsole control code analysis + Steam plan
Findings from TeslaConsole.RedPlanet and L4NET: the pod lifecycle
(egg chunks/ACK, RunMission, telemetry, results), the egg as the entire
mission definition (NotationFile text incl. pre-rendered plasma name
bitmaps), the RPConfig.xml catalog, and the key topology finding -
every pod gets the same egg and builds a deterministic full TCP mesh
from the ordered pilots list (connect to earlier entries, listen for
later). Maps 1:1 onto Steam: lobby owner = console, SteamIDs = pilot
addresses, ISteamNetworkingSockets P2P = the mesh, with a NetTransport
seam at L4NET mirroring the RIOBase pattern. Implementation options and
open decisions listed for signoff.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 15:53:34 -05:00
CydandClaude Fable 5 4f93bbc843 Panes flush to their edges - no black border against the viewscreen
The 4-8px black padding framing every MFD and the map read as a jarring
border over the 3D. Glass and button strips now run to the pane edges
(last button in each strip/column absorbs the rounding remainder);
only the thin separations between buttons within a strip remain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 15:47:08 -05:00
CydandClaude Fable 5 2a6398702c Black cockpit canvas + flicker-free pane painting
SS_BLACKRECT paints the system window-frame color - gray on modern
Windows - so the viewscreen child is now a plain STATIC subclassed to
erase true black. The MFD/map panes flickered because Paint() cleared
the on-screen surface before redrawing at the fill cadence; the pane
now composes off screen and lands in a single BitBlt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 15:41:49 -05:00
CydandClaude Fable 5 293e369656 Restore full-height MFD buttons
Playtest: the half-height red buttons did not read well; back to
display_height/8 (18..40px). The contiguous amber map columns stay.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 15:33:54 -05:00
CydandClaude Fable 5 04b968da70 Full-canvas 1080p viewscreen, compact MFDs in the corners
Playtest direction on the canvas layout: all five MFDs at the compact
320x240 glass size - upper pair pushed to the top corners, score glass
top-center, lower pair in the bottom corners, map bottom-center - and
the viewscreen now fills the entire 1920x1080 canvas. Launched with
-res 1920 1080 the 3D renders native 1:1 (the 2007 D3D9 path takes the
1080p backbuffer and 16:9 aspect without complaint). start-windowed.bat
updated accordingly.

Verified live: full-screen native 3D with the cockpit floating over its
edges, mission running, preset lamp lit on the map column.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 15:32:44 -05:00
CydandClaude Fable 5 1e15519e90 1920x1080 internal cockpit canvas with pod-bezel occlusion
The cockpit is now a fixed 1920x1080 canvas (uniformly scaled down on
smaller monitors, min 25%):

  [ MFD UL 640 ][ MFD UC 640 ][ MFD UR 640 ]  <- native glass, 1920 wide
             [ viewscreen 800x600 centered ]
  [ MFD LL ]       [ Map ]        [ MFD LR ]  <- bottom edge

Playtest direction: the original pods MFDs partially occluded the main
display, and that is used here on purpose - the panes overlap the
viewscreen and render over it (viewscreen pinned to the bottom of the
sibling z-order, WS_CLIPSIBLINGS everywhere, so the 3D present clips
around every pane). Top-row glasses are gauge-canvas native 640x480 -
three across is exactly 1920. Red MFD buttons at half height; the amber
map columns stack contiguous like the pod strips. The auto-fit
scale/L4MFDSCALE machinery is gone - the canvas is the design space.

Verified live at 100% scale: contiguous native top row over the
centered viewscreen, bottom cluster overlapping its lower edge, map and
gauges updating (BOOST values moving, presets lit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 15:26:22 -05:00
CydandClaude Fable 5 6a06187d38 pack-dist: refuse to repack while the game runs from dist
Remove-Item raced a live playtest session and gutted the folder around
the locked files. Detect a running rpl4opt.exe under dist and abort
before deleting anything.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 15:13:12 -05:00
CydandClaude Fable 5 4037e39810 Paint the cockpit panes immediately - queued WM_PAINTs starve
The game loop pumps ONE message per frame (APPMGR.cpp PeekMessage), and
WM_PAINT is synthesized only when the queue is otherwise empty - so the
child panes queued invalidations that never delivered: the map stayed
black and button lamps froze at their first-paint state on the live
screen. (PrintWindow-based captures forced paints and the synthetic
SendMessage click bypassed the queue, which is why every automated
verification looked fine while the live window was frozen.)

MFDSplitView::Repaint and PlasmaScreen::Update now paint synchronously
via RedrawWindow(RDW_INVALIDATE|RDW_UPDATENOW) at fill time, and the
button press/release feedback goes through the same path. Verified with
a true screen capture (CopyFromScreen): map drawing live, timer
counting, the active preset lamp bright on the map right column.

Lamp anchor sanity-check against the game code: mode switch NOV/STD/
VET/EXP = ButtonAuxUpperRight5-8 = 0x33..0x30 = the upper-right MFD
bottom strip left-to-right; presets = Secondary7-12 = 0x18-0x1D = the
map right column - both match the placed buttons.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 15:12:17 -05:00
CydandClaude Fable 5 3b775e19d9 Auto-fit the cockpit to the monitor; hide the plasma glass for now
The assembled shell could exceed the work area (1291px tall at default
scale) - on a 1080p monitor the whole lower row, map included, hung off
the bottom of the screen, which read as the map not rendering, and the
lamp-reactive preset buttons were unreachable down there. (The render
and click paths themselves were verified fine: a synthetic press on a
map preset button lights it through the game lamp command.)

The layout now measures SPI_GETWORKAREA and fits: the pane scale steps
down (to 20% minimum) and the viewscreen takes the leftover height at
the 3D aspect, shrinking below native when needed - Present stretches,
so the scene scales cleanly. Rows and viewscreen center in the shell,
which is sized to the work area. Verified: at 70% panes in a 1392-high
work area the viewscreen reduced to 609x457 with everything on screen.

The plasma glass sits out of the cockpit for the moment
(PlasmaScreen::Hide) per playtest direction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 15:01:19 -05:00
CydandClaude Fable 5 aa24968c3d Assemble the whole cockpit in a single window
The main game window becomes the cockpit shell (enlarged, clipping
children); every display folds in as a chrome-less child pane in the
pod interior arrangement:

  [ MFD UL ] [ MFD UC ] [ MFD UR ]
  [ plasma (reduced) ][ viewscreen (centered) ]
  [ MFD LL ] [   Map  ] [ MFD LR ]

The 3D scene presents into a black STATIC viewscreen child via
Present's hDestWindowOverride (new gMainPresentWindow global) - no
swap-chain changes, and STATIC's transparent hit-testing keeps mouse
input over the 3D view flowing to the game window. MFDSplitView gains a
parent/child mode; PlasmaScreen::Position reparents the glass into the
shell. Main window class background goes black for the cockpit gaps.

Verified by screenshot: live green gauges (LIFT CUT / BOOST / CHUTE /
trigger-program screens) with their red button strips, the 3D canyon in
the centered viewscreen, plasma score glass at its left, map with lit
amber preset lamps - one window, 976x1132 client at 50% scale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 14:52:26 -05:00
CydandClaude Fable 5 775ee130a7 Default layout now emulates the pod interior
[ MFD UL ] [ MFD UC ] [ MFD UR ]
  [ plasma (reduced) ][ main screen (centered) ]
  [ MFD LL ] [   Map  ] [ MFD LR ]

SVGA16 moves the main game window into the middle band (centered under
the MFD columns) and shrinks the plasma glass to fill the space at its
left via a new PlasmaScreen::Position hook (the glass paint stretches to
the client area, so resizing rescales it). Lower row sits below the
main screen band.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 14:40:32 -05:00
CydandClaude Fable 5 d0553648eb Drop the lower display row below the main screen
The pod stacks its displays vertically - upper MFDs above the
viewscreen, weapon MFDs + secondary below. Default grid now places the
lower row (MFD LL / Map / MFD LR) at the main screen's bottom edge
instead of directly under the upper row, so the main screen reads as
the middle band, with the plasma glass at its lower-left.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 14:33:51 -05:00
CydandClaude Fable 5 ec4d1d7fa1 Recenter the map glass under the upper-center MFD
The side-button columns widened the map window, drifting the glass right
of center; shift the default position left by one column width.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 14:27:14 -05:00
CydandClaude Fable 5 c1990d0ffa Hide the mouse cursor only in fullscreen
The arcade startup hid the cursor unconditionally in release builds -
correct for a pod, but desktop windowed play needs the mouse for the
on-screen cockpit buttons and the cursor vanished over every display
window. Hide (and restore) it only when running fullscreen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 14:26:14 -05:00
CydandClaude Fable 5 1058de326d Cockpit buttons on the split displays, lamp-lit and clickable
Each MFDSplitView window now carries its display's physical button bank,
placed as in the pod (addresses per vRIO CockpitLayout): a 4x2 red
cluster around each MFD glass (anchors 0x2F/0x27/0x37 upper, 0x0F/0x07
lower, addresses descending row-major) and 6 amber buttons down each
side of the map - Secondary 0x10-0x15 left, Screen 0x18-0x1D right; the
remaining column addresses are Tesla relays, per the pod wiring, so they
get no buttons.

Buttons light from the lamp state the game commands: PadRIO grows a
static active-instance hook (SetScreenButton/GetLampState); mouse
press/release feeds PadRIO's desired-state sampling alongside pad and
keyboard, and paint decodes the lamp byte (state1/state2 brightness,
solid/slow/med/fast flash animated by tick). With real serial hardware
(no PadRIO) the buttons draw dark and inert.

Verified: map flank buttons light per the game's preset lamps, aligned
with the labels the glass draws at its edges; MFD clusters render 4+4.
Roadmap: queued the vRIO Dynamic Lighting RGB-keyboard lamp mirror as a
polish-pass item. dist repacked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 14:22:38 -05:00
CydandClaude Fable 5 214a8e079c Un-pack the 7-display cockpit in-engine (L4MFDSPLIT=1)
The pod drove five monochrome MFDs from the color channels of two video
outputs - SVGA16 packs bit-slices of the shared gauge canvas into R/G/B
of gauge window 3 (upper MFDs) and R/G of window 4 (lower MFDs), with the
map palettized on the secondary and physically mounted portrait. The
desktop reconstruction previously required an external BitBlt-mirror
wrapper.

With L4MFDSPLIT=1, SVGA16 renders each display into its own window
(MFDSplitView, plain GDI) straight from the canvas + port bit-masks:
five green-screen MFD windows and the 90CW-rotated Map, tiled in the pod
grid to the right of the main view (L4MFDSCALE percent, default 50). The
packed D3D windows stay hidden but keep presenting off-screen, leaving
the original path untouched. Handles spanning mode (2-window setups).

Also: the plasma glass now opens directly below the main view (clamped
to the work area; L4PLASMAPOS=x,y overrides) per playtest feedback.

Verified: window grid comes up as main + 5 MFDs + Map + plasma with the
packed windows hidden; screenshots confirm a green MFD score readout and
the portrait tactical map rendering correctly. dist packer and BUILD.md
updated; the launcher wrapper is obsolete for split-mode use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 14:10:52 -05:00
CydandClaude Fable 5 7362fd0811 PadRIO: pod stick convention is opposite XInput on both axes
Playtest feedback: X and Y from the Xbox controller were both inverted.
Negate both by default (pad and WASD go through the same assignment, so
they stay consistent); L4PADFLIP now flips back toward raw XInput
orientation per axis.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 13:55:56 -05:00
CydandClaude Fable 5 a2843fe0ea pack-dist.ps1: assemble a runnable game package into dist\
Collects the Release exe (+pdb), the AUDIO/GAUGE/VIDEO data and INIs from
assets\RP411 (leaving the arcade launch scripts and old 4.10 exe behind),
libsndfile + the system OpenAL runtime (oalinst.exe as fallback), a
desktop environ.ini (PAD;KEYBOARD + on-screen plasma), start-windowed.bat
and a controls README. -Zip additionally produces a handoff zip. dist\ is
gitignored; verified by running the game from the packaged folder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 13:51:31 -05:00
CydandClaude Fable 5 de5a97d37d Workstream A prototype: play without the cockpit
Splits the control surface the game consumes from the RIO board into
RIOBase (8 virtuals + the five analog scalars); the serial RIO is now one
implementation of it. Adds two new ones:

- PadRIO (L4CONTROLS=PAD): in-process RIO speaking the full surface from
  an XInput controller + PC keyboard using vRIO's default profile (left
  stick/WASD = stick, triggers/Q,E = pedals, right stick Y/PgUp,PgDn =
  rate throttle that holds position, A/Space = trigger, B/R = reverse,
  dpad/arrows = hat, Start,Back/F1,F2 = config). Samples in GetNextEvent
  so button latency does not depend on the 15 s menu-time analog cadence;
  hot-plugs pads; L4PADFLIP=XY inverts stick axes; lamp commands land in
  lampState[] for the planned on-screen cockpit panel. The stock
  VTVRIOMapper/lamp/button path runs unchanged.

- PlasmaScreen (L4PLASMA=SCREEN): the 128x32 plasma glass as a desktop
  window in plasma orange (L4PLASMASCALE, default x4), rendering the same
  Video8BitBuffered surface the gauge system always drew; no COM port.

Verified in the sandbox with vRIO off and no serial devices: boots to a
running mission, controller hot-detected, plasma window drawing live game
content (score readout). BUILD.md 4 documents the desktop environ.ini and
bindings; roadmap updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 13:13:37 -05:00
CydandClaude Fable 5 e4afc5ca19 Untrack the local cockpit-launcher wrappers
startrp-800x600.bat/.ps1 rode into the previous commit unintentionally;
remove them from tracking (local copies untouched) and gitignore them so
a bulk add cannot sweep them back in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 12:53:34 -05:00