Renders BT's 128x32 plasma display into a desktop window instead of streaming to serial hardware. L4PLASMASCREEN.* copied verbatim from RP412; the L4GREND.cpp selector diff applied (BT's only divergence there is a comment -- no collision). Verified: a 'Plasma Display' window opens and the game ticks normally beside it (L4GAUGE=640x480x16 L4PLASMA=SCREEN). Phase 3b (single-window MFD-split cockpit) is DEFERRED -- blocked on the missing BT cockpit-layout geometry and BT's unfinished upstream MFD dev-composite; the L4VB16 hand-merge is HIGH-risk (BT's dev-gauge docking overlaps RP412's split rewrite). See context/steamification.md and docs/BT412-ROADMAP.md. (Phase 3 of the roadmap) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
12 KiB
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 inMUNGA_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_GAUGESdocks 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 MFDpilotList.
Defaults (decided at planning, change only deliberately)
- Gates mirror RP412 with a BT412 prefix:
BT412_STEAM,BT412KEYLIGHT, envBT412STEAM=1. Exe staysbtl4.exe; CMake project isbt412. - 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
pilotListtally (kills + observed deaths) at stop; EndMission wire scores when remote-pod marshaling needs them. L4VB16.cppend-state:MFDSplitViewis the successor to the dev-gauge dock; keepBT_DEV_GAUGESgated until parity, then retire.- This repo never touches the BT411 remote.
origin=VWE/BT412.gitonly.
Phase 1 — Toolchain: VS2022 (v143)
RP412's step 1: unblocks everything, no design risk.
- Configure
-G "Visual Studio 17 2022" -A Win32(drop the 2019CMAKE_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.hTime copy-ctor;MUNGA/NETWORK.hinlineoperator==;L4DINPUT.cppDIEnum callback renames;CAMMGR.cppstd::ios::in;legacy_stdio_definitions.libfor dxerr. - [investigate] RP412's
L4D3D.cppNULL-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), newL4PADRIO.*,L4PADBINDINGS.*; theL4CTRL.cpphook (new PadRIO()for tokenPAD). BT never customized these — expected clean. - CMake: explicit source lines (no globs); link
xinput9_1_0.lib. - Confirm
L4CONTROLS=PADyieldsprimaryControlType=PrimaryRIOso the stockMechRIOMapperengages 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=RIOpod regression still initializes.
Phase 3 — Cockpit on the desktop (Workstream A.2)
- 3a PlasmaScreen (low risk, land first):
L4PLASMASCREEN.*verbatim + theL4GREND.cppselector diff.L4PLASMA=SCREEN. - 3b Single-window MFD split (
L4MFDSPLIT=1) — the HIGH-risk hand-merge: RP412 rebuiltL4VB16.cpp~3846–4046 (splitViews, fixed 1920×1080 canvas, 3D Present into the viewscreen pane viahDestWindowOverride); BT rebuilt the same region for dev-gauge docking (BT_DEV_GAUGES*,BT_GAUGE_*,BT_ALL— 15 sites). Write 3-way diff notes intocontext/steamification.mdbefore 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
CockpitLayoutexists for the BT pod. Sources:content/GAUGE/l4gauge.cfg, pod photos, ask Nick. Land panes-without-buttons first; clicks injectPadRIO::SetScreenButton, lamps light fromGetLampStateonce 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-mergeMFDSplitView. Details incontext/steamification.md. Phase 4 does not depend on 3b.
Phase 4 — NetTransport seam + Steam transport (Workstream C.1)
L4NETTRANSPORT.*verbatim (abstract seam +WinsockNetTransportdefault; 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 5BT_NET_TRACEblocks onto the seam calls.L4STEAMTRANSPORT.*near-verbatim, gateBT412_STEAM; vendorextern/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).
Phase 5 — Front end + LocalConsole marshal (Workstream B — bulk of new code)
- 5a
game/reconstructed/btl4fe.cpp(skeleton: RP412RP_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::ToEggStringagainst 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 atlength=expiry, launcher-role loop,InstallNetworkRacemarshal for remote pods. - 5c Score intake — genuinely new for BT (no mission-end flow exists): a
gConsoleScoreSinkequivalent throughBTPlayer; v1 results screen = kills/deaths per pilot from the Comm-MFDpilotListtally at stop. - 5d
game/btl4main.cppWinMain wiring (skeleton: RP412RPL4.CPPdiff): front-end-mode detection, single-binaryfor(;;)loop (results → menu → launch), host/member branch, self-documentingenviron.iniparsing, minidump crash filter,SC_KEYMENUswallow,BT412STEAMenv →SteamNetTransport_Install(). - Verify: no-args boot → menu → LAUNCH → mission → stop at set length → results →
fresh menu;
-egg DEV.EGGstays unmarshaled (dev shortcut); btconsole.py external feed still works; marshaled 2-instance loopback collects both score lines.
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 →RegisterPeerall → 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.mdfor 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.
Phase 7 — Polish, packaging, release
- KeyLight RGB mirror:
L4KEYLIGHT.*verbatim (C++/WinRT TU — per-file C++17 props), gateBT412KEYLIGHT. pack-dist.ps1adapted:btl4.exe+pdb,BTL4.RES,BTDPL.INI,VIDEO/ GAUGE/ AUDIO/, eggs,steam_api.dll, OpenAL runtime, generated commentedenviron.ini,start-windowed.bat, README.txt, refuse-while-running.tools/two-pod-test.ps1adapted (feeder = btconsole.py). Version banner4.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)
- Solo:
run\run.cmdboots DEV.EGG; drive → fire → kill → respawn works. - Loopback MP via btconsole.py keeps entity/combat replication.
- Pod paths (
L4CONTROLS=RIO,L4GAUGE,L4PLASMA=com2) still compile and initialize.