Port RP412's RPL4LOBBY to BT: an ISteamMatchmaking room stands in for the arcade Site-Management screen. Compiled both gates -- stubs without BT412_STEAM, the full room under it (both configs build + link clean). Lobby (game/reconstructed/btl4lobby.*): - Room screen (green-on-black, like the menu), owner = console. - Member data: FakeIP + fake console/game ports + persona + mech/color/badge (ip/cp/gp/nm/vh/cl/bd keys). - Nonced "go" launch roster -> SteamNetTransport_RegisterPeer for every peer. - Push/PullRaceResults over lobby data rebuild the shared score sheet. Front end (btl4fe.*): HOST/JOIN buttons when BTLobby_Available() (Steam transport up); the menu loop exits on a steam action and BTFrontEnd_Run routes the lobby outcome -- host builds the egg with every member as a [pilots] mesh entry (BTFrontEnd_SetHostedPilots), member returns launch mode 2. Results screen prefers the marshal's collated result names. Marshal (btl4console.*): add GetResultName / ClearResults / InjectResult so the lobby owner can refill the sheet from the collated wire scores; Result gains a name field. WinMain (btl4main.cpp): install the Steam transport on BT412STEAM env (before the front end, so the lobby is offered); branch on BTFrontEnd_LastLaunchMode() -- host owns the marshal clock + joins the mesh, member enters as a network pod on :1501 (SetNetworkCommonFlatAddress + gConsoleLossEndsMission); Push (host) / Pull (member) results, then relaunch. CMake: btl4lobby.cpp joins bt410_l4; that lib gets BT412_STEAM + the Steamworks include under the gate. build-steam/ gitignored. Deferred (untestable here -- needs Steam + multiple machines): the host->member wire egg-feed marshal (InstallNetworkRace); a member currently waits for a console connection nothing supplies, so a live host+member race is blocked on it. The lobby object does not survive the per-mission relaunch. docs/STEAM-3-MACHINE-TEST.md for BT not yet authored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
224 lines
14 KiB
Markdown
224 lines
14 KiB
Markdown
# BT 4.12 Roadmap — Steam + Internet Multiplayer
|
||
|
||
Working plan for turning the 4.11 pod port into a consumer Steam title, following the
|
||
recipe proven end-to-end by **RP412** (Red Planet 4.11 → 4.12; see
|
||
`C:\vwe\RP412\docs\RP412-ROADMAP.md` and its git history — the reference implementation
|
||
for every workstream here). Status: plan authored 2026-07-14. Items marked
|
||
**[investigate]** need archaeology before they become concrete tasks.
|
||
|
||
## Where 4.11 stands
|
||
|
||
- **Game**: `btl4.exe`, 32-bit Win32, DirectX 9, CMake + VS2019 BuildTools (v142),
|
||
DXSDK June 2010, `/FORCE:MULTIPLE`. Full single-player combat loop
|
||
(drive → animate → target → fire → damage → destroy → respawn) on all 8 maps.
|
||
- **Engine**: the same WinTesla MUNGA/MUNGA_L4 engine as Red Planet — the MUNGA core is
|
||
file-identical to RP's; BT's L4 layer adds bgfload/image + BT render work
|
||
(L4D3D/L4VIDEO) and dev-gauge docking in L4VB16.
|
||
- **Networking**: same arcade stack — `MUNGA/NETWORK|HOSTMGR|INTEREST`, WinSock2 TCP in
|
||
`MUNGA_L4/L4NET.CPP`. Two-instance loopback MP verified including cross-pod combat,
|
||
kills, respawn, gait, and weapon visuals. Console feeder: `tools/btconsole.py`
|
||
(egg msgID 3 in 1040-byte chunks, RunMission msgID 5 ×2, must stay connected).
|
||
- **Input/output**: pod hardware is a serial **RIO** board (stick X/Y, throttle, pedals,
|
||
fire/config buttons 0x40/0x45/0x46/0x47 + MFD aux buttons) — `L4RIO`/`L4SERIAL`. Dev
|
||
box uses keyboard debug controls (WASD etc.). Displays: **7 monitors** (main 800×600
|
||
3D view, radar 640×480, five mono MFDs as one spanned 1280×480 surface) + the
|
||
**128×32 plasma** (identical hardware to RP's; `L4PLASMA`). `BT_DEV_GAUGES` docks the
|
||
gauge strip into the main window (dev mode).
|
||
- **Session control**: missions are **egg** files (NotationFile); the operator console
|
||
configures and launches over TCP 1501. No in-game menu; no mission-end flow (mission
|
||
review msgID 0x18 is a stub, `scoreAward=0`); live kill/death tally exists on the
|
||
Comm MFD `pilotList`.
|
||
|
||
## Defaults (decided at planning, change only deliberately)
|
||
|
||
- Gates mirror RP412 with a BT412 prefix: `BT412_STEAM`, `BT412KEYLIGHT`, env
|
||
`BT412STEAM=1`. Exe stays `btl4.exe`; CMake project is `bt412`.
|
||
- AppID: Spacewar **480** via hand-created `steam_appid.txt` (gitignored, never packed)
|
||
until a real AppID exists.
|
||
- Mech catalog: base 8 (`avatar bhk1 loki madcat owens sunder thor vulture`) + variants
|
||
(`ava1 lok1 lok2 mad1 mad2 own1 snd1 thr1 vul1 blkhawk`); trim any that fail to load.
|
||
- Results v1: read the live Comm-MFD `pilotList` tally (kills + observed deaths) at
|
||
stop; EndMission wire scores when remote-pod marshaling needs them.
|
||
- `L4VB16.cpp` end-state: `MFDSplitView` is the successor to the dev-gauge dock; keep
|
||
`BT_DEV_GAUGES` gated until parity, then retire.
|
||
- **This repo never touches the BT411 remote.** `origin` = `VWE/BT412.git` only.
|
||
|
||
## Phase 1 — Toolchain: VS2022 (v143)
|
||
|
||
RP412's step 1: unblocks everything, no design risk.
|
||
|
||
- Configure `-G "Visual Studio 17 2022" -A Win32` (drop the 2019
|
||
`CMAKE_GENERATOR_INSTANCE`). **Stay Win32** — DXSDK June 2010 x86 libs are the hard
|
||
constraint. BT's CMake has no `/Zp1`, so RP's packing-mismatch defines don't apply.
|
||
- Apply RP412 BUILD.md §3 source fixes as v143 errors demand: `MUNGA/TIME.h` Time
|
||
copy-ctor; `MUNGA/NETWORK.h` inline `operator==`; `L4DINPUT.cpp` DIEnum callback
|
||
renames; `CAMMGR.cpp` `std::ios::in`; `legacy_stdio_definitions.lib` for dxerr.
|
||
- [investigate] RP412's `L4D3D.cpp` NULL-texture AddRef guard — BT's L4D3D is heavily
|
||
rebuilt; port the guard if the crash pattern survives.
|
||
- **Verify**: v143 build boots DEV.EGG; two-instance loopback + btconsole.py — drive,
|
||
fire, cross-pod kill, respawn.
|
||
|
||
## Phase 2 — Input layer: RIOBase / PadRIO (Workstream A.1)
|
||
|
||
Rule for every touched engine file: 3-way check (BT copy vs RP411 baseline vs RP412);
|
||
identical-to-baseline → take RP412's file verbatim, diverged → hand-apply the diff.
|
||
|
||
- Port `L4RIO.h/.cpp` (RIOBase split), new `L4PADRIO.*`, `L4PADBINDINGS.*`; the
|
||
`L4CTRL.cpp` hook (`new PadRIO()` for token `PAD`). BT never customized these —
|
||
expected clean.
|
||
- CMake: explicit source lines (no globs); link `xinput9_1_0.lib`.
|
||
- Confirm `L4CONTROLS=PAD` yields `primaryControlType=PrimaryRIO` so the stock
|
||
`MechRIOMapper` engages unchanged (`btl4app.cpp MakeViewpointEntity`) — RP precedent:
|
||
stock VTVRIOMapper ran against PadRIO unmodified.
|
||
- Author the default BT `bindings.txt` (vRIO grammar): stick/throttle/pedal axes, fire
|
||
buttons, MFD aux banks.
|
||
- Fix desktop-unreachable controls: secondary-MFD view cycling (mapper cases
|
||
0x13d/0x13e are dead DOS F3/F4 codes). Weapon regrouping comes alive via the RIO
|
||
path for free.
|
||
- **Verify**: full mech drive+fight from pad+keyboard, no COM ports; pad hot-plug;
|
||
`L4CONTROLS=RIO` pod regression still initializes.
|
||
|
||
## Phase 3 — Cockpit on the desktop (Workstream A.2)
|
||
|
||
- **3a PlasmaScreen** (low risk, land first): `L4PLASMASCREEN.*` verbatim + the
|
||
`L4GREND.cpp` selector diff. `L4PLASMA=SCREEN`.
|
||
- **3b Single-window MFD split** (`L4MFDSPLIT=1`) — **the HIGH-risk hand-merge**:
|
||
RP412 rebuilt `L4VB16.cpp` ~3846–4046 (splitViews, fixed 1920×1080 canvas,
|
||
3D Present into the viewscreen pane via `hDestWindowOverride`); BT rebuilt the same
|
||
region for dev-gauge docking (`BT_DEV_GAUGES*`, `BT_GAUGE_*`, `BT_ALL` — 15 sites).
|
||
Write 3-way diff notes into `context/steamification.md` **before** editing.
|
||
`L4MFDVIEW.*` copied as skeleton; replace RP's pod tables with BT's display set
|
||
(viewscreen, radar, five mono MFDs sliced from the 1280×480 span, plasma glass).
|
||
- **[investigate]** BT cockpit button-bank geometry: no vRIO `CockpitLayout` exists for
|
||
the BT pod. Sources: `content/GAUGE/l4gauge.cfg`, pod photos, ask Nick. Land
|
||
panes-without-buttons first; clicks inject `PadRIO::SetScreenButton`, lamps light
|
||
from `GetLampState` once geometry lands.
|
||
- **Verify**: one cockpit window, 3D presents into its pane (smoke BT's rebuilt L4D3D
|
||
present path EARLY), live MFDs/radar/plasma; pod multi-surface path (`L4GAUGE`)
|
||
unbroken.
|
||
|
||
> **Status (2026-07-14):** 3a landed. **3b DEFERRED** — blocked on (1) the missing BT
|
||
> cockpit geometry and (2) BT's own MFD dev-composite being an unfinished upstream
|
||
> beachhead (`docs/GAUGE_COMPOSITE.md`: only radar/secondary composites; the five mono
|
||
> MFDs don't render yet). Prereq order when resumed: finish the MFD dev-composite →
|
||
> obtain BT cockpit geometry → hand-merge `MFDSplitView`. Details in
|
||
> `context/steamification.md`. Phase 4 does not depend on 3b.
|
||
|
||
## Phase 4 — NetTransport seam + Steam transport (Workstream C.1)
|
||
|
||
- `L4NETTRANSPORT.*` verbatim (abstract seam + `WinsockNetTransport` default;
|
||
SOCKADDR_IN-shaped addresses keep egg `[pilots] ip[:port]` working in both worlds).
|
||
- `L4NET.CPP`: apply RP412's refactor (~22–26 Winsock sites → `NetTransport_Get()`);
|
||
**re-site the 5 `BT_NET_TRACE` blocks** onto the seam calls.
|
||
- `L4STEAMTRANSPORT.*` near-verbatim, gate `BT412_STEAM`; vendor
|
||
`extern/steamworks_sdk_164/` (copy from RP412).
|
||
- CMake: seam always compiled; `option(BT412_STEAM)` gates the transport TU + define +
|
||
SDK include + `steam_api.lib` + post-build dll copy.
|
||
- **Verify**: (1) loopback + btconsole.py full MP parity through the seam (TCP);
|
||
(2) Steam smoke — transport installs, FakeIP allocated, graceful TCP degrade when
|
||
Steam is absent; (3) real Steam-sockets session (may defer to Phase 6).
|
||
|
||
> **Status (2026-07-14): DONE.** Seam landed; `L4NET.CPP` from RP412 post-seam with
|
||
> BT's 3 `BT_NET_TRACE` blocks re-sited; `gConsoleLossEndsMission` engine global
|
||
> ported. `option(BT412_STEAM)` gates the Steam TU + SDK. Verified: default TCP build
|
||
> passes full loopback MP through the seam (console → egg → mesh → both tick, traces
|
||
> fire through `NetTransport_Get()`); `BT412_STEAM=ON` compiles + links against the
|
||
> vendored SDK + boots solo. Live Steam session deferred to Phase 6 (needs the lobby +
|
||
> 3 machines). Details in `context/steamification.md`.
|
||
|
||
## Phase 5 — Front end + LocalConsole marshal (Workstream B — bulk of new code)
|
||
|
||
- **5a `game/reconstructed/btl4fe.cpp`** (skeleton: RP412 `RP_L4/RPL4FE.cpp`): no
|
||
`-egg`/`-net` → setup menu. Catalog: 8 maps
|
||
(`cavern grass rav polar3 polar4 arena1 arena2 dbase`), mechs per defaults,
|
||
scenario=freeforall, time/weather/temperature/length, pilot name.
|
||
`BTMission::ToEggString` against the verified egg format (`content/MP.EGG`,
|
||
`DEV.EGG`) incl. GDI-rendered 128×32 `[ordinals]`/name plasma bitmaps → the standard
|
||
egg-load path.
|
||
- **5b `game/reconstructed/btl4console.cpp`** (skeleton: `RPL4CONSOLE.cpp`): in-process
|
||
console on its own thread over NetTransport — egg chunks + ACK, RunMission ×2
|
||
(mirror btconsole.py), stays connected (fixes the console-loss listener bug for
|
||
solo), owns the mission clock, StopMission at `length=` expiry, launcher-role loop,
|
||
`InstallNetworkRace` marshal for remote pods.
|
||
- **5c Score intake — genuinely new for BT** (no mission-end flow exists): a
|
||
`gConsoleScoreSink` equivalent through `BTPlayer`; v1 results screen = kills/deaths
|
||
per pilot from the Comm-MFD `pilotList` tally at stop.
|
||
- **5d `game/btl4main.cpp` WinMain wiring** (skeleton: RP412 `RPL4.CPP` diff):
|
||
front-end-mode detection, single-binary `for(;;)` loop (results → menu → launch),
|
||
host/member branch, self-documenting `environ.ini` parsing, minidump crash filter,
|
||
`SC_KEYMENU` swallow, `BT412STEAM` env → `SteamNetTransport_Install()`.
|
||
- **Verify**: no-args boot → menu → LAUNCH → mission → stop at set length → results →
|
||
fresh menu; `-egg DEV.EGG` stays unmarshaled (dev shortcut); btconsole.py external
|
||
feed still works; marshaled 2-instance loopback collects both score lines.
|
||
|
||
> **Status (2026-07-16): DONE (5a–5d).** The zero-argument launch shows the interactive
|
||
> green-on-black catalog menu (`game/reconstructed/btl4fe.*`: map/mech/color/time/weather/
|
||
> length + pilot name), builds `frontend.egg` (verified structurally identical to MP.EGG,
|
||
> GDI-rendered plasma name bitmaps), and boots a playable mission. The in-process
|
||
> LocalConsole marshal (`game/reconstructed/btl4console.*`) owns the mission clock off the
|
||
> engine per-frame hook (`gPerFrameHook`, added to `APPMGR`) and dispatches
|
||
> `Application::StopMissionMessage` at the chosen length; it snapshots per-pilot
|
||
> kills/deaths at the stop (BT's own score model — msgID 0x18 stays a stub). The
|
||
> single-binary loop is a RELAUNCH per mission (`CreateProcessW`; avoids the stale
|
||
> `gBT*`-global re-init crash), showing a GDI "MISSION COMPLETE" scoreboard between races.
|
||
> WinMain (`game/btl4main.cpp`) wires front-end-mode detection, the marshal arm, the
|
||
> results screen + relaunch, and the launch-mode branch (solo/host/member). Details in
|
||
> `context/steamification.md`.
|
||
|
||
## Phase 6 — Steam lobby (Workstream C.2)
|
||
|
||
- **`game/reconstructed/btl4lobby.cpp`** (skeleton: `RPL4LOBBY.cpp` — mechanics
|
||
generic): lobby owner = console; member data = FakeIP + fake console/game ports +
|
||
persona + loadout (**mech**/color/badge); nonced launch roster → `RegisterPeer` all →
|
||
owner hosts via egg+marshal, members join as network pods; Push/PullRaceResults
|
||
distribute the score sheet. FE gains HOST/JOIN screens.
|
||
- Author `docs/STEAM-3-MACHINE-TEST.md` for BT (adapt RP412's; keep its gotchas:
|
||
disable Steam Input on 480, one Steam account per machine, abort key).
|
||
- **Verify**: 3-machine Steam test — lobby → mesh → marshaled timed mission →
|
||
kills/deaths → stop → results everywhere → rematch. Known MP fidelity gaps (peer
|
||
warp spheres, missile clusters, PNAME/PLACE billboards) do not block.
|
||
|
||
> **Status (2026-07-16): CODE LANDED; live 3-machine test DEFERRED (no Steam + multi-machine
|
||
> here).** `game/reconstructed/btl4lobby.*` ships (compiled both gates: stubs without
|
||
> `BT412_STEAM`, full ISteamMatchmaking room under it). Built: the room screen (green-on-black,
|
||
> like the menu), member data exchange (FakeIP + fake console/game ports + persona +
|
||
> mech/color/badge), the nonced "go" launch roster → `SteamNetTransport_RegisterPeer` all peers,
|
||
> and Push/PullRaceResults over lobby data. The menu shows HOST/JOIN when `BTLobby_Available()`
|
||
> (Steam transport up); a launch routes through `BTFrontEnd_LastLaunchMode()`: **host** builds the
|
||
> egg with every member as a `[pilots]` entry + owns the marshal clock, **member** enters as a
|
||
> network pod on `:1501` (`SetNetworkCommonFlatAddress` + `gConsoleLossEndsMission`). WinMain
|
||
> installs the Steam transport on `BT412STEAM` env. **Remaining / deferred:** the host→member
|
||
> **wire egg-feed marshal** (`InstallNetworkRace` — the owner streaming eggs to remote pods) is
|
||
> NOT built; a member currently waits for a console connection that only that marshal provides, so
|
||
> a live host+member race needs it. The lobby OBJECT does not survive the per-mission relaunch
|
||
> (documented single-binary limitation — everyone lands on a fresh menu, not back in the room).
|
||
> `docs/STEAM-3-MACHINE-TEST.md` for BT is not yet authored. Details in `context/steamification.md`.
|
||
|
||
## Phase 7 — Polish, packaging, release
|
||
|
||
- KeyLight RGB mirror: `L4KEYLIGHT.*` verbatim (C++/WinRT TU — per-file C++17 props),
|
||
gate `BT412KEYLIGHT`.
|
||
- `pack-dist.ps1` adapted: `btl4.exe`+pdb, `BTL4.RES`, `BTDPL.INI`,
|
||
`VIDEO/ GAUGE/ AUDIO/`, eggs, `steam_api.dll`, OpenAL runtime, generated commented
|
||
`environ.ini`, `start-windowed.bat`, README.txt, refuse-while-running.
|
||
- `tools/two-pod-test.ps1` adapted (feeder = btconsole.py). Version banner `4.12.x`;
|
||
tag the release on gitea.
|
||
- **Verify**: dist zip runs on a clean machine (no DXSDK); solo loop; packaged
|
||
2-machine Steam session.
|
||
|
||
## Copy matrix (from RP412)
|
||
|
||
| Treatment | Files |
|
||
|-----------|-------|
|
||
| **Verbatim** (after 3-way check) | `L4PADRIO.*`, `L4PADBINDINGS.*`, `L4PLASMASCREEN.*`, `L4NETTRANSPORT.*`, `L4STEAMTRANSPORT.*` (gate rename), `L4KEYLIGHT.*` (gate rename), `extern/steamworks_sdk_164/` |
|
||
| **Diff-apply / hand-merge** | `L4RIO.*`, `L4CTRL.cpp`, `L4GREND.cpp` (expected clean); `L4NET.CPP` (medium — BT_NET_TRACE); `L4VB16.cpp` (**high** — dev-gauge collision); `L4VIDEO.*`; v143 source fixes |
|
||
| **Adapt** (RP skeleton, BT tables/fields) | `L4MFDVIEW.*`, `btl4lobby`, `pack-dist.ps1`, `two-pod-test.ps1`, `environ.ini` template, STEAM-3-MACHINE-TEST doc |
|
||
| **New** | `btl4fe`, `btl4console`, score sink + results screen, BT `bindings.txt`, CMake Steam gating, `context/steamification.md` |
|
||
|
||
## Standing regressions (after every engine-file merge)
|
||
|
||
1. Solo: `run\run.cmd` boots DEV.EGG; drive → fire → kill → respawn works.
|
||
2. Loopback MP via btconsole.py keeps entity/combat replication.
|
||
3. Pod paths (`L4CONTROLS=RIO`, `L4GAUGE`, `L4PLASMA=com2`) still compile and
|
||
initialize.
|