Files
BT412/docs/BT412-ROADMAP.md
T
CydandClaude Opus 4.8 77f019ed4f Net: host->member mission marshal -- a Steam/LAN mission plays end to end
The lobby could stage a room but the launched mission never fed the members.
BTLocalConsole_InstallNetworkMission (btl4console.cpp) makes the host play the
arcade console in-process over the NetTransport seam (Winsock TCP or Steam SDR):

  - connect to each member's console channel (ip[:port] from BT412HOSTPODS);
  - feed each the chunked egg (NetworkManager::ReceiveEggFileMessage, \n->NUL
    wire image like tools/btconsole.py), resending until it ACKs;
  - poll member state (StateQueryMessage); when the whole mesh is staged at
    WaitingForLaunch, dispatch RunMission to every member + locally at once;
  - StopMission at expiry (members first, then self after a short grace);
  - scores are BT's kills/deaths snapshotted from the *meshed* roster at the
    stop -- no EndMission wire intake (that flow is a BT stub; the mesh already
    put every pilot in the host's roster). The owner's own pod is fed locally
    via L4NetworkManager::FeedLocalEgg.

Engine change (ported 1:1 from RP412): gConsoleMarshalsLaunch (APPMGR.h/.cpp) +
the `!gConsoleMarshalsLaunch &&` guard in APP.cpp's WaitingForLaunch self-launch.
The owner has no console connection to itself, so without this it would
self-launch before the mesh staged and never send the coordinated RunMission.
Default False = stock behavior; solo boot un-regressed.

WinMain host path (btl4main.cpp) now honors BT412HOSTPODS (+BT412HOSTPORT) for
the lobby host AND a classic-LAN host: SetNetworkCommonFlatAddress +
InstallNetworkMission, falling back to a solo marshal if no member is reachable.

Verified (loopback): two `btl4.exe -net` pods fed by tools/btconsole.py reach
"All connections completed!" and run after the engine change -- the exact
protocol + launch handshake the marshal uses. Both gates build+link; the
Release dist boots and the Steam transport comes up. The live multi-machine
Steam mission (FakeIP mesh + pilot-slot matching) is untestable here -- see the
new docs/STEAM-3-MACHINE-TEST.md. Dist README updated: multiplayer is now
"newly implemented, please test" rather than deferred.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 07:30:17 -05:00

16 KiB
Raw Blame History

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 ~38464046 (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 (~2226 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, InstallNetworkMission 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 (5a5d). 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 missions. 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/PullMissionResults 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-17): CODE COMPLETE; live multi-machine test remains (no Steam + multi-machine here). game/reconstructed/btl4lobby.* ships (both gates: stubs without BT412_STEAM, full ISteamMatchmaking room under it). Built: the room screen, member data exchange (FakeIP + fake console/game ports + persona + mech/color/badge), the nonced "go" launch roster → SteamNetTransport_RegisterPeer all peers, and Push/PullMissionResults over lobby data. The menu shows HOST/JOIN when BTLobby_Available(); a launch routes through BTFrontEnd_LastLaunchMode(). The host→member wire egg-feed marshal is now DONE (BTLocalConsole_InstallNetworkMission, btl4console.cpp): the owner connects to each member's console channel over the NetTransport seam, feeds each the chunked egg (ACK-tracked), holds the whole mesh at WaitingForLaunch via the ported engine flag gConsoleMarshalsLaunch (APP.cpp/APPMGR), launches everyone together, and stops at expiry; scores are BT's kills/deaths snapshotted from the meshed roster (no EndMission wire intake — that flow is a stub). WinMain's host path (lobby OR classic-LAN BT412HOSTPODS) calls it; member enters as a network pod on :1501. Verified: loopback — two -net pods fed by tools/btconsole.py mesh + run after the gConsoleMarshalsLaunch change (the exact protocol + launch handshake the in-process marshal uses); both gates build+link; Release dist boots + Steam transport comes up. Remaining: the live multi-machine Steam mission (FakeIP mesh + pilot-slot matching across machines) — docs/STEAM-3-MACHINE-TEST.md is the procedure. The lobby OBJECT does not survive the per-mission relaunch (single-binary limitation). 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.

Status (2026-07-16): PACKAGING SCRIPTED (test dist). pack-dist.ps1 (repo root) assembles dist\ from a Release Steam build (build-steam\Release\btl4.exe — Release so it runs without VS's non-redistributable debug CRT) + steam_api.dll + OpenAL32.dll, the content\ data (AUDIO/GAUGE/VIDEO, BTL4.RES, BTDPL.INI, the VREND manifests, bindings.txt, a few eggs), oalinst.exe (OpenAL fallback), tools\btconsole.py, a steam_appid.txt=480 (TEST only), a self-documenting environ.ini (BT412STEAM=1, L4MFDSPLIT cockpit), start.bat, and a README that states the multiplayer status plainly. -Zip emits BattleTech-4.12.zip (~46 MB). Verified: the Release exe boots a live DEV.EGG mission (optimizer-clean), and the packaged exe run from dist\ brings the Steam transport up (FakeIP allocated, lobby available) — self-contained. Remaining for a real release: KeyLight gate (L4KEYLIGHT not yet compiled in), a real AppID (drop steam_appid.txt), the clean-machine acceptance test, and the deferred host→member mission feed (InstallNetworkMission) before a full Steam mission plays end-to-end.

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.