cdccb16251072d8d7016840f1d9a76885f4cee3c
53
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
25e25260b1 |
Home and End are the bass knob
The volume keys wanted a partner, and the bass trim could not be one as it stood: it scaled the sample data as it loaded, so by the time anyone pressed a key the audio was already sitting in OpenAL buffers and nothing short of a restart would move it. So the trim is now a per-zone gain applied in the mix instead. Each buffer's depth - how much of the low band it occupies - is still worked out once at load from its playback rate, but the trim itself is read every frame, which is what lets Home and End move it while sounds are playing. It is the better form regardless: no rewriting of sample data, and no quantisation on top of audio that has already been through one gain stage. Home raises, End lowers, in steps of 0.05, and the setting is written to bass.cfg beside the exe exactly as the volume writes volume.cfg. Together with PageUp and PageDown that is the amplifier and the crossover the cabinets had in hardware and a desktop does not. Builds clean, runs, and neither knob fires unprompted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4e8392fcfb |
PageUp and PageDown are the volume knob
The cabinets had no volume control - they ran at unity and left level to an external amplifier - so a player without that hardware had nowhere to turn it down but environ.ini and a restart. PageUp and PageDown now step the master volume by 0.05 while you play, from silent to double, and whatever you leave it on is written to volume.cfg beside the exe and used from then on. The environ.ini figure decides where a machine that has never been touched starts out; the keys are the knob, and a knob stays where it was left. Page keys because they produce no typed character, so they cannot collide with the character-keyed commands the engine already answers to, nothing else in RP binds them, and they are on every keyboard including tenkeyless. They are polled rather than read off the key-message path, which is worth recording because the message path looked like the obvious home for them and was tried first. RP's keyboard pump only takes WM_KEYUP, WM_SYSKEYUP and WM_CHAR off the front of the queue, and the front end runs message loops of its own, so key messages get raced for and lost: six deliberate, well-spaced presses arrived as two. Fine for the abort chord, useless for something you tap repeatedly to find a level. Reading key state directly costs nothing and cannot be dropped. That losses figure is a pre-existing property of the input path, not something this change introduced, and is worth knowing before anything else gets bound there. Builds clean, runs, and does not fire unprompted. The step function itself is proven - it was driven end to end through the message path before the switch, stepping the right way, clamping, and persisting. What I could not test from here is the polling trigger, because Windows would not hand the game foreground and injecting keys without it would have sprayed them across whatever else was open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
523f713a30 |
Volume and bass knobs, for players without an amplifier
The cabinets ran the game at unity and shaped volume and tone outside it, in an external amplifier and a 3-way crossover. That is why there is no master volume anywhere in the original code and none in AUDIO.INI - an operator turned a knob on an amp. A desktop player has no amp and no crossover, and the recovered soundbanks are a good deal livelier than what 4.12 shipped with, so the game has to offer the two controls the pod got from hardware. RP412AUDIOVOLUME, 0.0 to 4.0, is the amplifier: a listener gain, which the port had never set at all. RP412AUDIOBASS, 0.0 to 1.0, is the crossover's low band. Both default to leaving the mix exactly as the pod played it, so neither changes anything for anyone who does not go looking. The bass trim is not a filter, and the reason is worth writing down: the OpenAL we ship is Creative's, not OpenAL Soft, and it implements only AL_FILTER_LOWPASS. It rejects highpass and bandpass outright. A bandpass would have been the tidy answer, carrying the authored brightness model on GAINHF and the trim on GAINLF across the single direct filter a source gets. It is not on offer. So the trim scales sample data as it loads, which suits how this low end is actually built: the weight lives in discrete deep layer zones whose per-zone tuning bakes out to a very low playback rate - thirteen zones below 8kHz, three to five octaves under their recorded pitch, against four fifths of the set at 22kHz and up. Baked rate is a dependable proxy for band, so pulling down the low-rate zones is a real low-band trim and not a blunt cut. It eases in below 22kHz and reaches full depth at 5.5kHz. Caught while building this, and the reason for the probe: EFX_Initialize checks alGetError after configuring the scratch filter, so asking for a filter type the driver refuses leaves an error pending and takes the entire bridge down - reverb included. The bandpass attempt did precisely that and would have silently killed the reverb and brightness work. Initialize now survives losing the filter and says so. Builds clean, runs with both knobs set and with neither. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d361a0b8be |
The sound effects play at the pitch they were written at
Red Planet's original AWE32 soundbanks are back in the tree, and the game's sound effects are now generated from them instead of from an incomplete one-off extraction. AUDIO1.RES and AUDIO2.RES come from the 1996 release in the TeslaRel410 archive, hash-identical. AUDIO.INI has named them all along - they were simply never carried into the port. tools/rp_sf2extract.py reads them and regenerates both the WAV set and RP_L4/WTPresets.cpp, so the assets are reproducible from the banks rather than hand-maintained. Two things were wrong with the old set: Pitch. Every shipped WAV was flat 44100 Hz with the banks' tuning discarded, so 202 of the 219 zones played at the wrong speed - the worst by nine semitones. The EMU8000's per-zone root key and tuning are now baked into each file's declared sample rate, which is exact and needs no engine change. Layers that were meant to be deep now are: a collision sub-thud that lasted 18 milliseconds at the wrong rate is a 0.66 second one at 1228 Hz. Missing layers. 93 presets were short of zones and 176 were missing outright, 219 of 395. Nothing was lost recovering them - the 46 preset slots that disappeared were all empty placeholders. The old files were also over-read, running past the end of their sample into whatever PCM came next; WellheadDrill02a was six seconds where the bank says eight hundred milliseconds. Every one of the 395 files now matches its bank record exactly. Also baked in: per-zone layer attenuation, and the static resonant low-pass the EMU8000 applied in hardware. Measured while doing it, and worth knowing: RP's banks contain no key-splits at all - every multi-zone preset is a pure layer stack - and no preset has more than four zones, which is what the engine's own "AWE appears to only play 1st 4 voices" warning has been asserting since 1995. Still to do: loop regions and the release fades, which 349 zones ask for and which need new SAMPLEINFO fields. And voice demand per sound has gone from about one zone to about two and a half, so the per-event alGenSources and alDeleteSources churn roughly doubles - the BT tree measured pooling as the fix for that, and a CPU win besides. Builds clean. The extreme baked rates, 1228 Hz up to 88200, were checked through the real path - libsndfile, alBufferData, alSourcePlay - and all load. Not yet listened to on the pod. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6ce729bab5 |
Lit cockpit buttons keep up with the sim
BT411's f99003c, brought across. Its playtesters reported the cockpit lighting going slow or stopping altogether while the 3D view stayed smooth, and RP412 has the same structure exactly: the on-screen vRIO buttons light themselves from PadRIO::GetLampState, but what FILLS that store is lampManager->Update() in GaugeRenderer::ExecuteForeground - once per full gauge cycle. Which is the cycle the previous commit was about. Measured on a starved frame budget it now completes 3.1 times a second, and completed 0.7 times a second before that; either way far too slow to carry a flashing lamp. So sweep the lamps once per frame from the main render instead, which runs regardless of how little frame is left over. It is cheap, and AssertNewLampValue already drops anything unchanged, so this pushes no extra traffic - it only stops changes arriving late. Only when a PadRIO is active, i.e. cockpit-less play, and only while a mission is actually running. With real serial hardware selected the pod keeps its authentic bandwidth-paced cadence, untouched. RP412LAMPSWEEP=0 restores the once-per-cycle behaviour. BT411's other half, 02ce9f5, does not apply. That one is about Windows throttling WM_TIMER and paint messages for background windows, which made the glass panels' flash crawl whenever they did not have focus. RP412 has no timer-driven repaint anywhere - the MFD windows are D3D devices presented from SVGA16::Update, and the panel strips repaint from there too - so there is no throttled message path to bypass. That path was starved rather than throttled, and the previous commit is the fix. Verified: no regression at either budget, 20.0 display sweeps/s at a normal frame budget and 3.1/s starved, both unchanged by this commit; mission runs clean. The lamp win itself is structural - the sweep is now an unconditional per-frame call - and would want a busy multiplayer mission to see directly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f4fef29428 |
The map keeps drawing when the view gets busy
Two testers reported the map and the countdown clock freezing, one of them only on larger, more complex maps, and one of them until a death. Both details point at the same place. The gauges and the cockpit displays are redrawn in whatever time is left after the 3D view. The background loop is guaranteed a single pass per frame and gets more only while time remains before the frame is due, and one pass drew exactly one gauge. So a full sweep of ninety-odd gauges needed ninety-odd passes - free when there is spare frame, but on a busy map the 3D view eats all of it, the loop drops to its one guaranteed pass, and a sweep takes ninety-odd FRAMES. Seconds. A death makes the renderer skip every static object, the budget frees up, and the backlog drains at once: the display appears to come back to life. Worse, the copy phase that follows ended after a SINGLE display, so the map - one of three - came round only every third sweep. So: draw gauges to a 2ms slice rather than one per pass, which ties the refresh rate to elapsed time instead of to how much spare frame there happened to be; and copy every display before reporting the sweep done. Measured on a deliberately starved frame budget, which reproduces the reported symptom: 0.7 sweeps/s before, 3.1 after. At a normal budget 20/s, against 18-19 before - no cost to the healthy case. RP412GAUGESLICE tunes the slice and 0 restores the old behaviour, which reproduces the 0.7 exactly. RP412GAUGEDIAG=1 logs the rate; watching the screen cannot tell a display that has stopped refreshing from one whose picture simply is not changing, which is what made this hard to see. Also fixes the constructor calling Update() three lines before it initialised mDisplayToUpdate, so the first pass indexed the D3D device and surface arrays with whatever was on the stack. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
eb17220dd5 |
Test builds go stale after a fortnight
A tester still racing a two-week-old binary reports things that were fixed a week ago, and the afternoon spent chasing them is gone. An expired build now says so and stops: a dialog naming its version and expiry date, pointing at the releases page, and an exit before anything else runs. The log carries the same line, so a report from an expired build identifies itself. $expireDays at the top of stamp-version.ps1 is the shelf life, sitting next to the product line it belongs with. It counts from the day a build was MADE rather than the day the code was written - rebuilding an old commit to chase something should hand back a usable binary, not one born stale. SET IT TO 0 FOR A REAL RELEASE. A shipped build that expires is a catastrophe, and that one line decides it. It is called out in the script, in the generated header and in BUILD.md, because it is the kind of thing that gets noticed exactly once, too late. The date is what makes rpl4build.h differ from one day to the next, so the first build of each day recompiles RPL4.CPP and the rest do not. This is a nudge, not a lock. The date comes from the machine's own clock and anyone determined can wind it back; the point is to stop an honest tester wasting a day, not to stop anybody at all. RP412NOEXPIRY=1 waives it for us and logs that it did, so a waived build is never mistaken for a current one. It is deliberately absent from environ.ini - a bypass every tester can see is a bypass every tester will use, and then it never goes stale for the one person it was meant to stop. Verified all four ways by backdating the shelf life rather than touching the clock, which is what a negative $expireDays is for: a fresh build runs untouched; an expired one raises the dialog, exits 1, and logs "Build expired on 4 August 2026 - refusing to run"; the same expired build with RP412NOEXPIRY=1 runs and logs the waiver; and a build with two days left runs and logs two days left. Two things that only showed up by running it. Negative days first meant "never" rather than "already expired", so the refusal path went untested on the first pass - only 0 means never now. And the days-left count was anchored at midday, reporting one day fewer than the build had; it is anchored at the end of the expiry day, which is the rule the check actually enforces. The packaged README tells testers the build expires, where to get the next one, and that unzipping it over the folder keeps their four files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
bfdbfe6353 |
Release 4.12.1: version banner and packaging name
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
12b31187f9 |
Move the build to VS2022 (v143) with runtime parity against VC9
Hand-converted the four .vcproj projects to .vcxproj (Win32, v143, Windows 11 SDK + DXSDK June 2010 for d3dx9/dxerr only). WinTesla.sln now builds the v143 projects; the legacy solution is kept as WinTesla_vc9.sln. Kept: /Zp1 in Munga_L4+RP_L4, Unicode, x86, /DYNAMICBASE:NO, /FORCE:MULTIPLE (header-defined globals still duplicated across TUs). Changed: CRT unified to /MD(d); import libs linked by the exes instead of merged into Munga_L4.lib; WINDOWS_IGNORE_PACKING_MISMATCH and _SILENCE_STDEXT_HASH_DEPRECATION_WARNINGS defined; legacy_stdio_definitions.lib for the June-2010 dxerr.lib. Source fixes, all behavior-preserving: Time gains standard (non-volatile) copy-ctor/assignment overloads (rvalues cannot bind to volatile& in standard C++); operator==(SOCKADDR_IN&,...) made inline; L4DINPUT's Enum*Callback pair renamed DIEnum* (collided with L4CTRL's under LTCG); std::ios.in -> std::ios::in in CAMMGR.cpp. Verified: VC9 baseline rebuilt from this tree first, then the v143 build compared against it in a sandboxed game working copy - identical logs and behavior through RIO init (against vRIO) and mission load, including the same pre-existing AV in d3d_OBJECT::LoadTexture (L4D3D.cpp:262) that both toolchains hit; documented in BUILD.md 4 as the next debugging target. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8d18ce0ee4 |
Wire up remaining DirectX SDK paths; build now succeeds
Verified full build with VC++ 2008 Express SP1 + Windows SDK v6.0A + DirectX SDK (June 2010): 4 Projects succeeded, 0 failed (Release|Win32). Outputs: Release\rpl4opt.exe, Release\RPL4TOOL.exe, lib\Munga_L4.lib, lib\DivLoader.lib. Two projects referenced DirectX but were never repointed at $(DXSDK_DIR) (they had no hardcoded path to replace earlier): - DivLoader.vcproj: add "$(DXSDK_DIR)Include" to both compiler configs (was failing on D3DX9.h). - RPL4TOOL.vcproj / RPL4TOOL VS2008.vcproj: add "$(DXSDK_DIR)Lib\x86" to the linker search path (was failing with LNK1181 on dinput8.lib). .gitignore: ignore the build-output static libs that land in lib/ (Munga_L4.lib, DivLoader.lib); the dependency libs OpenAL32.lib and libsndfile-1.lib stay tracked. BUILD.md / docs/BUILD-NOTES.md: record the verified build, the CLI recipe, and the DXSDK_DIR stale-environment gotcha. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |