Files
RP412/docs/RP412-FRONTEND-DESIGN.md
T
CydandClaude Opus 5 0f4592c39a The wireframe key works and Steam is optional
Alt+W had been a no-op since the 2007 DPL->Direct3D port, which stubbed
DPLToggleWireframe along with every other dpl_ call. D3D9 has no global
wireframe property, so the toggle now records the intent in gWireframe
and ExecuteImplementation applies D3DRS_FILLMODE once per frame - which
also means it re-asserts itself after a device Reset. The sky dome and
the 2D pass are held solid: a wireframed dome buries the geometry you
turned the key on to look at, and the gunsight would otherwise come out
as bare diagonals.

Proving it needed a working copy, and that turned up a separate problem.
steam_api.dll was a hard import, so a machine without it died at load
time with 0xC0000135 - before a window, before a log line. It is now
delay-loaded, and because delay loading only moves that failure to the
first call, every Steam path is gated on
SteamNetTransport_ClientLibraryPresent(): Install and the two lobby
entries, with everything else downstream of one of them. Absent DLL
boots and races; absent DLL with RP412STEAM=1 logs the reason and stays
on TCP; DLL present brings the transport up exactly as before.

The documentation now says which debug keys are real. Five of the seven
are still 2007 stubs and always have been, so they are named as inert in
environ.ini's template and at the dispatch site - that beats letting the
next person debug a dead key, which is how this started. BUILD.md gains
a debug-key table, the delay-load contract for anyone adding a Steam
call site, and the environ.ini BOM trap that silently reverts
L4CONTROLS to KEYBOARD and then fail-fasts for want of a pod mapper.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 11:15:11 -05:00

10 KiB

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:

[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.

The DLL is optional (2026-08-10): it is delay-loaded, and every Steam call site is gated on SteamNetTransport_ClientLibraryPresent(), so a machine with no steam_api.dll boots and races instead of dying at load time with 0xC0000135. Verified all three ways on this box: absent DLL boots, absent DLL with RP412STEAM=1 logs Steam: steam_api.dll not found beside the exe and stays on TCP, and DLL present still brings the transport up as before. See BUILD.md §4 for which gate covers what — a new Steam call site has to sit behind one of them.

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).