Files
RP412/README.md
T
CydandClaude Fable 5 7d485c9672 The wire keeps what it could not send
Cyd asked for an analysis of the networking stack and what would make the
simulation feel better over the internet. The analysis found something
more urgent than latency: the transport has been losing data silently
since the arcade, and nothing in the game could see it happen.

Every send result was discarded - L4NET, the console, all of it. On the
1ms arcade LAN the socket buffer never filled, so it never mattered. Over
the internet it matters twice. A peer stalled in its own 10-30 second
mission load stops reading, its window closes, and our nonblocking send
starts answering would-block, which threw the message away; or worse,
answering a PARTIAL count, and since framing on that stream is recovered
purely from each message's length prefix, the bytes that never followed
sheared it for good. Both are reachable in an ordinary race, because
every race has a load in it.

So sends go through a bounded per-connection queue now. What the wire
will not take is kept, byte-exact, and retried at three flush points -
before the render (the present blocks on vsync, and this frame's state
should be travelling while it does), at the top of the receive pump, and
before a connect sequence. Nothing is ever dropped from the middle: these
are reliable ordered messages carrying entity creation, damage and race
control, so a queue that overflows its 256K declares the connection dead
and lets the disconnect path run rather than quietly desyncing the
stream. RP412NETSENDQ=0 restores the old behaviour and still logs what it
would have lost, which is the honest way to A/B it. On Steam the same
queue finally surfaces k_EResultLimitExceeded, which the old code
collapsed into -1 and discarded - that was backpressure, unlogged.

The receive side gained the check the release build never had. The length
prefix is untrusted input; Verify() compiles away in release, so a
corrupt one went to memmove as a negative, or copied 4096 bytes of
assembled packet into a 1600-byte stack buffer, or named a size the pad
could never complete and wedged the connection forever. It is now
validated against the same bounds the sender works to, and a stream that
fails them is dropped like any other lost peer.

And a fry that never ends: drop zones are map entities dealt round-robin
at load, ownership transfer is not implemented, so a leaver's pads stay
in the DropZones group. The respawn request dispatched to one goes to a
host that is gone - dropped at the send, the 'no host N in the table'
path - and the two-second retry re-dispatches to the same dead owner
forever. The pad scan now skips zones whose owner has left, and
re-validates one assigned earlier before reusing it.

The rest is measurement, because the symptoms this work exists to chase
are all reported in prose and none of them are in any log. Sixteen logs
from the six-player night contain zero player-facing latency lines. A
race now ends with a NetLog summary: per remote pod, how many updates
arrived and how evenly (median and p95 out of a log2 histogram), the
widest gap, how many gaps were long enough to mean a quiet sender versus
short enough to mean OUR loop stalled, how often its motion snapped
instead of blending, and how far arriving updates moved it. Per peer,
whether the clock alignment ever had to step mid-race - which is the
input for deciding if it needs slewing, rather than guessing. The mission
t0 tick goes in the log too, alongside the console's per-pod RunMission
send ticks, because nothing has ever measured how far apart the machines
actually start; the clockwork doors inherit that skew directly.

RP412NETSTATS adds the transport's own view - per connection: messages,
bytes, wire writes, partials, refusals, how much sat queued - and on
Steam the first read this codebase has ever taken of GetConnectionRealTime
Status. Ping, quality, pending and unacked bytes, and one route
description per connection at teardown. The API was vendored and never
called; there was no RTT number anywhere in the game.

Finally, rpl4opt -spoolstats reads any recording offline. The data was
already in every spool ever made and nothing read it that way: the
recorder restamps each packet with local arrival time while the update
records inside keep the sender's sim-grid stamp, so the difference is
clock offset plus one-way delay, and the same running-minimum estimator
the game runs live separates them. It prints delay above the per-host
minimum, and decomposes each entity's gaps into sender pacing versus
delivery jitter - which no live counter can do. It lives in the game exe
rather than RPL4TOOL because the tool is deliberately not /Zp1 and would
misread every struct in the file.

Verified on the two-pod loopback harness: mesh up, egg fed, 60s raced,
stopped on command, scores collected, and both summaries reading exactly
what a pair of PARKED pods should read - heartbeat cadence, one snap per
heartbeat, sub-quarter-metre corrections, no clock steps. The t0 ticks
and the netclock offsets agree with each other to the two seconds the
pods launched apart.

The latency tier is deliberately NOT here. TCP_NODELAY, the Steam
NoNagle flag, per-frame coalescing and the pre-sim receive drain are all
scoped and all wait on this build's numbers, because the point of
shipping measurement first is to find out whether the thing we would fix
is the thing that hurts. Nagle is still on. Interest management is still
inert. The wire format is untouched, so this build and the last one still
race each other.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:59:25 -05:00

141 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Red Planet 4.12 — the Steamification
**Red Planet** is VWE's pod-racing game: eight-player VTV contests on Mars
**Martian Death Race** and **Martian Football** — originally played from
inside VWE Tesla cockpit pods. This repo is the third life of that code:
| Generation | What it was |
|------------|-------------|
| **Red Planet 4.10** | The original game, running on the **Tesla 1** pod platform (DOS-era MUNGA engine, serial RIO cockpit hardware, operator console, batch-file relaunch between missions) |
| **Red Planet 4.11** | The **Win32 port** of 4.10 ([RP411](https://gitea.mysticmachines.com/VWE/RP411.git)) — DirectX 9, WinSock TCP, TeslaConsole/TeslaLauncher session control; still runs in pods, still a LAN |
| **Red Planet 4.12** | **This repo: the Steamification of 4.11** — the same engine, the same wire protocol, the same missions, made distributable on Steam and playable over the internet with no cockpit hardware |
The architecture is deliberately conservative: the VWE's design survives
intact, with each pod-era dependency replaced by a consumer equivalent
behind the engine's existing seams.
| Arcade (4.10/4.11) | 4.12 replacement |
|--------------------|------------------|
| RIO cockpit board (serial) | **PadRIO** — virtual RIO from XInput pad + keyboard, fully rebindable (`bindings.txt`, vRIO profile format) |
| Seven physical displays | **Single-window cockpit** — all displays composed on a locked 1920×1080 canvas around the viewscreen, with the real button banks lamp-lit and clickable |
| Pod button lamps | On-screen lamps, plus an **RGB keyboard mirror** (Windows Dynamic Lighting) |
| TeslaConsole operator | **In-game front end** — race setup menu builds the mission egg locally; an in-process console marshals every race (missions still only end on a console stop, exactly as designed in 1994) |
| TeslaLauncher relaunch-per-mission | **Single binary** — menu → race → results → menu in one process |
| WinSock TCP LAN mesh | **NetTransport seam** — the deterministic pod mesh unchanged, running over plain TCP (LAN/dev) or **Steam Networking Sockets** (FakeIP + Steam Datagram Relay) |
| Site network / fixed IPs | **Steam lobbies** — lobby owner is the console; members exchange FakeIPs and loadouts as lobby data |
**Status: it works.**
[v4.12.2](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.2)
carried the first verified end-to-end internet build: three machines, three
Steam accounts, lobby → mesh → marshaled five-minute race → deaths and
respawns → timed stop → results on every machine → rematch from the same
lobby.
[v4.12.3](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.3)
is about the screen. The cockpit now scales up as well as down and re-fits
whenever the window changes, keeping 16:9 so a wide panel letterboxes
instead of stretching; `-fit` runs it borderless over the whole monitor and
picks the render size to land on the viewscreen 1:1. The displays are the
player's to arrange — each of the six can be scaled on its own and the
radar moved out of the middle of the road (`environ.ini`). Each display's
button bank now reaches under its glass, so the picture itself is the press
target rather than a sliver at the edge.
[v4.12.4](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.4)
is about the lobby. Both rooms now show the host's mission setup — the track,
then time of day, weather and game length — since everyone flies the owner's
picks and it was the one thing in the room nobody could see for themselves.
The football team sheet names each player's VTV beside their team colours and
position. And a lobby holds **eight** players rather than four, a full grid as
the pod hall ran it, with the room sizing its roster to whatever space the
window gives it.
[v4.12.5](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.5)
restores the **Winners Circle**. The pod hall stood the finishers on a
numbered award platform when the race ended, and all of it was still in this
repo — the stand, eight ranked spots in every map, and the code to put racers
on them — wired only into the mission-review build and so never once run by a
pod. The race now fades out and fades back in on the platform: finishers in
finishing order, each pilot's callsign on the plate beside their spot, the
cockpit glass cleared away, held for a few seconds before the results screen.
Three pieces of it had been stubbed out in the D3D9 port and are working
again.
[v4.12.6](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.6)
is about the windows. Where you put them now survives the
menu-race-menu loop: `RP412MFDLAYOUT` remembers the game window, the
exploded view's display panes and the plasma glass in `mfd_layout.cfg`
beside `bindings.txt`. Append `,noframe` to a line to take that
window's title bar and border off — a cockpit filling the monitor edge
to edge at a rect you chose, rather than `-fit` taking the whole
screen — and the setup screen carries its own **EXIT GAME** button,
since a window with no title bar needs a way out. The Steam buttons
now dim and say *STEAM NOT RUNNING* instead of disappearing. And the
Winners Circle camera is framed off the award stand itself rather than
off whoever is standing on it, so the shot is the same one for every
player at every head count.
[v4.12.7](https://gitea.mysticmachines.com/VWE/RP412/releases/tag/v4.12.7)
is about sticks. Anything that is not an Xbox-class pad — a flight stick,
a HOTAS throttle, a twist grip, rudder pedals, a wheel — comes in through
DirectInput rather than XInput, and the game could not see any of it.
Now it can, and `joyconfig.bat` sets it up: the wizard asks you to move
each control in turn, works out which device and axis answered and which
way round it reads, and writes the joystick rows of `bindings.txt`,
leaving anything you have edited yourself alone. A twist grip or rudder
bar drives both pedals through one signed `Pedals` axis, and a real
throttle lever owns its channel outright rather than nudging a position
the way a spring-centred stick has to. Ported from the sibling BT411.
## Playing
Grab the release zip (or run `pack-dist.ps1` on a build). Single player:
run `start-windowed.bat` — the game boots into the setup menu, where the
SCENARIO group picks Death Race or Football (Football swaps in the
team/position columns and its own track list). Steam multiplayer: see
[docs/STEAM-3-MACHINE-TEST.md](docs/STEAM-3-MACHINE-TEST.md) (until RP412
has its own AppID it runs under Spacewar, 480).
The config files beside the exe are self-documenting and none of them
ship: the game writes each one the first time it needs it and then leaves
it alone, so a new build dropped over an existing folder keeps every
setting. **environ.ini** is every engine option, commented; **bindings.txt**
every key, pad button and axis; **pilot.cfg** your callsign and loadout;
**mfd_layout.cfg** where you dragged the windows. Delete any of them to
start that part over with the current defaults.
Default controls: numpad flies (8/2/4/6 stick, 7/9 pedals, 0 trigger),
Shift/Ctrl throttle, Alt reverse, arrows look, Space fires, letter rows
are the MFD button banks as printed on the panel. **Alt+Q** aborts a
mission. Full map with pad and keyboard diagrams:
[docs/CONTROLS.md](docs/CONTROLS.md).
## Building
**VS 2022 (v143)** + DirectX SDK June 2010 + the vendored Steamworks SDK
(`extern/steamworks_sdk_164`) — see [BUILD.md](BUILD.md). Solution
`WinTesla.sln`, configuration `Release|Win32`, output
`Release\rpl4opt.exe`.
## Documentation
| Doc | Contents |
|-----|----------|
| [docs/CONTROLS.md](docs/CONTROLS.md) | Controls map — pad and keyboard diagrams, the panel button banks, rebinding |
| [docs/RP412-ROADMAP.md](docs/RP412-ROADMAP.md) | The original plan and workstreams |
| [docs/RP412-FRONTEND-DESIGN.md](docs/RP412-FRONTEND-DESIGN.md) | TeslaConsole analysis, the egg format, the console protocol, and the Steam mapping — with status notes as each layer landed |
| [docs/STEAM-3-MACHINE-TEST.md](docs/STEAM-3-MACHINE-TEST.md) | Multiplayer test procedure, Steam Input notes, the abort key |
| [docs/RP412-LINUX.md](docs/RP412-LINUX.md) | Linux compatibility assessment — Proton path vs native port; parked until late playtesting |
| [docs/NET-TEST.md](docs/NET-TEST.md) | Network test protocol — WAN emulation profiles, the NetLog/NetStats telemetry, the stall-recovery test, `-spoolstats` |
| [BUILD.md](BUILD.md) | Toolchain and build steps |
Dev tooling: `tools/two-pod-test.ps1` races two pods on loopback,
marshaled by a console feeder speaking the arcade Munga protocol.
## Related repositories
| Repo | Role |
|------|------|
| [RP411](https://gitea.mysticmachines.com/VWE/RP411.git) | Upstream: the Win32 arcade port this repo Steamifies (full history preserved; remote `rp411` for cross-pulling fixes) |
| [VRIO](https://gitea.mysticmachines.com/VWE/VRIO.git) | Virtual RIO panel + vPLASMA — source of the bindings format, the cockpit layout, and the keyboard lamp mirror |
| [TeslaSuite](https://gitea.mysticmachines.com/VWE/TeslaSuite.git) | TeslaConsole / Launcher / vPOD — the arcade session-control stack 4.12 absorbed (and the reference implementation of the Munga control protocol, TCP 1501) |