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>
185 lines
9.7 KiB
Markdown
185 lines
9.7 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 — all resolved
|
|
|
|
1. Front end location — Option A (in-engine) **confirmed and shipped**.
|
|
2. v1 scenario scope — **both scenarios shipped** (2026-07-24). The setup
|
|
menu has a SCENARIO group; picking Football swaps the map list to the
|
|
football-legal set (no `pain`/`headoff`, adds `headmf`), replaces the
|
|
COLOR/BADGE columns with TEAM/POSITION, and the egg builder emits
|
|
`scenario=football`, per-pilot `team=`/`position=`, and the
|
|
`[teams]` / `[team::X]` / `[pilots::X]` / `[teambitmap]` blocks in
|
|
RPFootballMission's layout. Colors are derived, not chosen: the
|
|
runner wears the team's runner color, everyone else the team color.
|
|
**Every player picks their own team and position** (2026-07-24):
|
|
members publish their picks as lobby member data (`tm`/`ps`), the
|
|
room shows the roster as a team sheet and gives each member MY TEAM
|
|
/ MY POSITION buttons that cycle and republish live, and the owner
|
|
publishes the scenario (`sc`) so members know when those picks
|
|
matter. The egg builder honors every explicit pick and only fills
|
|
gaps: unpicked pilots are spread across the sides, a team with no
|
|
volunteer runner promotes its first *unpicked* member (explicit
|
|
picks are never overridden), and a second pilot claiming runner on
|
|
one team is moved to the line.
|
|
3. Results screen — **shipped**, on the cockpit displays, with the owner
|
|
publishing the score sheet to lobby members.
|
|
4. Steam model — lobby-owner-as-console **confirmed and verified** on
|
|
three machines. (Owner migration still unhandled.)
|
|
5. Pilot identity — Steam persona as pilot name **shipped**; loadout
|
|
persists in-session (Steam Cloud later).
|