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>
167 lines
8.5 KiB
Markdown
167 lines
8.5 KiB
Markdown
# RP412 Front End & Steam Multiplayer — Design Notes
|
|
|
|
Findings from reading TeslaConsole's Red Planet control code
|
|
(`TeslaSuite/Console/TeslaConsole.RedPlanet/`) against the game side
|
|
(`MUNGA_L4/L4NET.CPP`, `MUNGA/NETWORK.cpp`), and how a Steam front end maps
|
|
onto it. Written 2026-07-12 for the implement-decision.
|
|
|
|
## 1. What the console actually does
|
|
|
|
TeslaConsole is four things, and RP412's front end must absorb all four:
|
|
|
|
### a) Pod control channel (`Munga.Net`, TCP console port)
|
|
|
|
Per-pod state machine (`RPGame.cs`): take **ownership** of a pod, watch its
|
|
app state, and drive the lifecycle:
|
|
|
|
```
|
|
WaitingForEgg --egg chunks--> (ACK, 5s retry)
|
|
WaitingForLaunch --RunMissionMessage-->
|
|
RunningMission --telemetry--> EndMission (final scores)
|
|
--AbortMissionMessage--> reset
|
|
```
|
|
|
|
The egg is sent as `EggFileMessage` chunks of 1000 bytes with an
|
|
`AcknowledgeEggFileMessage` reply from the pod.
|
|
|
|
### b) The mission egg (the ENTIRE mission definition)
|
|
|
|
A NotationFile-format text file (`RPMission.ToEggString()`), newline→NUL:
|
|
|
|
```ini
|
|
[mission] adventure=Red Planet, scenario, map, time, weather,
|
|
temperature, compression, length (seconds)
|
|
[pilots] ordered pilot=<host address> list (players, then cameras)
|
|
[<address>] per participant: hostType, dropzone, name, bitmapindex,
|
|
loadzones, vehicle, color, badge (+ team/position in football)
|
|
[largebitmap] player names PRE-RENDERED as 1bpp bitmaps for the plasma
|
|
[smallbitmap] glass (128x32 and 64x16), generated by the console
|
|
[ordinals] 1st-4th place plasma graphics (hardcoded art)
|
|
```
|
|
|
|
The game already loads a local egg with `-egg <file>` (standalone mode) —
|
|
the same text format. **Building an egg locally = the whole single-player
|
|
front end problem.**
|
|
|
|
### c) The catalog (`RedPlanet/RPConfig.xml`)
|
|
|
|
Two scenarios — **Martian Death Race** and **Martian Football** (2-color
|
|
teams, crusher/blocker/runner positions) — 11 maps, ~30 vehicles (sharing a
|
|
handful of models), 9 colors, 11 badges, day/night, 3 weather levels, with
|
|
per-scenario exclusion lists. Clean data file; reusable as-is.
|
|
|
|
### d) Results & telemetry
|
|
|
|
During the mission the pods stream `VTVBooster/Damaged/Killed/Scored/
|
|
ScoreUpdate` and `EndMission(finalScore)` to the console host, which records
|
|
them (`RPMissionRecorder`) into `RPMissionResults` and prints score sheets.
|
|
The consumer version wants this as a post-race results screen (and, later,
|
|
Steam leaderboards/achievements fed from the same events).
|
|
|
|
## 2. How the pods network with each other (the key finding)
|
|
|
|
The console does NOT relay gameplay. Every pod receives the *same* egg, and
|
|
`L4NetworkManager::StartConnecting` builds a **deterministic full TCP mesh**
|
|
from the ordered `[pilots]` list:
|
|
|
|
- Resolve each address (`ip[:port]`, default = game port).
|
|
- Entries **before your own**: open a TCP connection to them.
|
|
- Entries **after your own**: listen for their incoming connection.
|
|
- No entry matching a local address → single-user mode, immediate load.
|
|
- Egg-from-network (`SlaveMode`) → ACK the egg, wait for mesh completion.
|
|
|
|
So the "lobby protocol" is simply: *agree on an ordered participant list,
|
|
then everyone meshes*. This is an exceptionally good fit for Steam.
|
|
|
|
## 3. Steam mapping (plan)
|
|
|
|
| Arcade concept | Steam concept |
|
|
|---|---|
|
|
| TeslaConsole operator | **Lobby owner** (host player) |
|
|
| Pod list / Site Management | `ISteamMatchmaking` lobby members |
|
|
| Pilot config dialog | In-lobby cockpit UI; member data (name/vehicle/color/badge) via lobby member data |
|
|
| Egg delivery + ACK | Owner builds the canonical egg, distributes via lobby data or reliable P2P channel; same ACK semantics |
|
|
| `[pilots]` host addresses | **Ordered SteamID list** — same list, different address type |
|
|
| TCP mesh (`StartConnecting`) | `ISteamNetworkingSockets` P2P mesh over SDR: ConnectP2P to earlier entries, accept later ones — the connect/listen ordering ports 1:1 |
|
|
| Console telemetry sink | Lobby owner doubles as the console host (results collection); results screen replaces the printout |
|
|
| Site network / fixed IPs | NAT traversal + IP privacy for free via Steam Datagram Relay |
|
|
|
|
Transport seam: mirror the `RIOBase` pattern at the L4NET layer — a
|
|
`NetTransport` interface with the existing WinSock TCP implementation
|
|
(LAN/dev, keeps working today) and a Steam-sockets implementation (retail).
|
|
`L4Host` keeps its identity/queue role; only connect/listen/send/recv move
|
|
behind the interface.
|
|
|
|
**Status (2026-07-12): the seam is IN.** `MUNGA_L4/L4NETTRANSPORT.h/.cpp`
|
|
defines `NetTransport` (startup/cleanup, local-address list, ip[:port]
|
|
resolve, connect/listen/accept/close, send/receive, remote-address) with
|
|
`WinsockNetTransport` as the process default; L4NET.CPP contains no raw
|
|
Winsock calls anymore. `MUNGA_L4/L4STEAMTRANSPORT.h` documents the
|
|
per-method ISteamNetworkingSockets mapping (FakeIP keeps the `[pilots]`
|
|
list as IPv4 strings) and stays behind `RP412_STEAM` until the Steamworks
|
|
SDK is dropped at `extern\steamworks` (partner-login download). Remaining
|
|
Steam work: SDK drop → implement `SteamNetTransport` → lobby UI in the
|
|
front end (owner collects FakeIPs/loadouts via lobby data, builds and
|
|
distributes the canonical egg, runs the RPL4CONSOLE marshal) → install
|
|
with `NetTransport_Set` at WinMain when launched under Steam.
|
|
|
|
**Multi-pod verified (2026-07-12):** `tools/two-pod-test.ps1` runs two
|
|
`-net` pods on loopback (console ports 1501/1601 → game ports 1502/1602)
|
|
marshaled by a minimal console feeder built on TeslaSuite's vendored
|
|
`Munga Net.dll`. Confirmed end to end through the seam: egg chunks +
|
|
per-pod ACK after "All connections completed!" (pod A listened, pod B
|
|
connected from its bound game port — the deterministic mesh), both pods
|
|
raced the same 60s mission, StopMission(0) ended it, and both pods
|
|
returned EndMission final scores. The feeder is exactly the marshal the
|
|
Steam lobby owner will run in-process.
|
|
|
|
**SteamNetTransport implemented (2026-07-12, SDK 1.64 vendored at
|
|
`extern/steamworks_sdk_164`):** `L4STEAMTRANSPORT.cpp` is the full
|
|
FakeIP implementation — `SteamNetTransport_Install()` (SteamAPI init,
|
|
relay access, `BeginAsyncRequestFakeIP(2)`: fake port 0 = console
|
|
channel, 1 = game mesh) swaps the process wire when `RP412STEAM=1`;
|
|
any failure logs and stays on TCP. Addressing convention: all pods
|
|
launch with the same `-net` port, eggs carry `<fakeip>:<engine port>`,
|
|
and only the transport maps engine ports ↔ Steam fake ports (peers fed
|
|
by `SteamNetTransport_RegisterPeer`). Runtime-verified on this box:
|
|
`SteamNetTransport: up as 169.254.59.52 (fake ports 32256 console,
|
|
32257 game)` under AppID 480, graceful TCP fallback without Steam,
|
|
default boot untouched. `steam_api.dll` ships in the dist.
|
|
|
|
Remaining for Steam multiplayer: the lobby (front-end UI +
|
|
`ISteamMatchmaking`): owner collects each member's FakeIP + fake game
|
|
port + loadout via lobby member data, feeds `RegisterPeer` on every
|
|
pod, builds/distributes the canonical egg over a reliable channel, and
|
|
runs the RPL4CONSOLE marshal. Needs a second Steam account/machine to
|
|
test end to end.
|
|
|
|
## 4. Implementation options (decide here)
|
|
|
|
**A. In-engine front end (recommended).** Port the egg builder (~300 lines:
|
|
`RPMission/RPPlayer.ToEggString` + the plasma name-bitmap generator) to C++
|
|
inside RP_L4. New application state before `WaitingForEgg`: a menu that
|
|
renders on the cockpit displays we just built — scenario/map/weather/length
|
|
on the viewscreen, selections driven by the MFD buttons and map presets
|
|
(the cockpit IS the menu — maximally pod-authentic). Single player works
|
|
immediately: build egg → hand to the existing egg-load path. Multiplayer
|
|
then layers the Steam lobby under the same UI (owner builds the egg,
|
|
distributes, mesh over Steam sockets).
|
|
|
|
**B. Standalone launcher app** (C#, reuse TeslaConsole code nearly verbatim,
|
|
drive the game via `-net` + console port like the arcade). Fastest to stand
|
|
up, but a second process, not cockpit-feel, and awkward under Steam (overlay,
|
|
launch flow, no in-game rejoin). Useful as a dev tool, not the product.
|
|
|
|
**C. Hybrid:** in-engine UI, but mission/egg logic in a small shared C++
|
|
library so a dev console tool and the game share one egg builder.
|
|
|
|
### Open decisions
|
|
|
|
1. Front end location — Option A (in-engine, cockpit-rendered) confirmed?
|
|
2. v1 scenario scope — Death Race only, or Football (needs teams UI) too?
|
|
3. Results screen — post-race on the cockpit displays (replaces printout)?
|
|
4. Steam model — lobby-owner-as-console confirmed? (Implies owner migration
|
|
handling later; the egg re-issue path makes host migration plausible.)
|
|
5. Pilot identity — Steam persona as pilot name; vehicle/color/badge
|
|
persisted per player (Steam Cloud later)?
|