Files
BT411/context/build-and-run.md
Joe DiPrimaandClaude Opus 5 c791fae0f8 KB: new topic test-harness.md -- the bench machinery + the verification doctrine
User mandate (2026-08-02): "do tests like this from now on" -- after the #110
grind bench, where the field composition (peer fire destroying an arm over the
wire) replaced the constructed proxy that had let #86 be called fixed while
players kept hitting it.

The topic carries two halves on purpose:

DOCTRINE -- what counts as VERIFIED:
  1. reproduce the REPORTED scenario, not a convenient adjacent one
  2. scalpel hooks (BT_SELF_DAMAGE_ZONE / BT_KILL_SUBSYS / BT_FORCE_*) locate
     defects; they support a "fixed" claim only with proven path-identity to
     the field composition -- and the field composition still gets run
  3. MP symptom -> two-node proof (master-side correctness says nothing about
     what a peer sees)
  4. visual symptom -> pixel proof (gotcha 23)
  5. coverage claims need the axis enumerated and measured (all gates, all
     chassis), because per-chassis behaviour lives in authored data
  6. an unexplained extra effect in a passing run means the run has not passed

MACHINERY -- the bench_common.sh contract (summarized, file = source of truth),
single-node and two-node skeletons (relay, ports, affinity, fire cadence,
GOTO_STOP standoff), process hygiene (stale-node taskkill first, never
double-background, teardown kill order, stale-exe tells), and log-reading rules
(capped diagnostics are not evidence of absence; alarm lines are not trends;
name the actor at every refusal; field logs have no gates set -- spawn-time
summaries ungated, per-frame traces gated).

Routed: Quick Lookup row, CLAUDE.md reasoning step 4, build-and-run parity
section, reconstruction-method Key Relationships.  checkctx CLEAN.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 09:17:01 -05:00

14 KiB

id, title, status, source_sections, related_topics, key_terms
id title status source_sections related_topics key_terms
build-and-run Build / Run / Debug — recipe, repo layout, env gates established PROGRESS_LOG.md §10a, §10a-bis; README.md
wintesla-port
decomp-reference
reconstruction-method
BTL4OPT
cdb
BTL4.RES
EGG
WinTesla

Build / Run / Debug

The one top-level CMakeLists.txt builds munga_engine (engine) + bt410_l4 (reconstructed BT game lib) + btl4.exe. Full recipe + repo-layout map: docs/PROGRESS_LOG.md §10a / §10a-bis + README.md. Env-gate table: decomp-reference §6.

# configure once:
cmake -S C:\git\bt411 -B C:\git\bt411\build -G "Visual Studio 16 2019" -A Win32 \
  -DCMAKE_GENERATOR_INSTANCE="C:/Program Files (x86)/Microsoft Visual Studio/2019/BuildTools"
# build:
cmake --build C:\git\bt411\build --config Debug
  • Links DXSDK d3d9/d3dx9/dinput8 + OpenAL/libsndfile (engine/lib/). DXSDK June 2010 at C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\ (overridable -DDXSDK). [T2]
  • Linker uses /FORCE — tolerates header-defined globals + the dead offline-factory unresolved externals in mech3.cpp (Mech::CreateSubsystemStream references void*-signature CreateStreamedSubsystem for every class; never called at runtime). ⚠ /FORCE also HIDES real unresolved symbols as runtime AVs — see reconstruction-gotchas §6. [T2]
  • Editing an engine/ file rebuilds the engine lib automatically (one project). A NEW member on a DPLRenderer/d3d_OBJECT class needs the game objs that embed its layout recompiled — delete stale objs if layout-mismatch corruption appears. [T2]
  • ⚠ Kill the running exe before rebuilding (taskkill //F //IM btl4.exe) or you get LNK1104.

Run

run\run.cmd [EGG]      # default DEV.EGG; cd's to content\ and runs btl4.exe -egg <EGG>
  • cwd MUST be content\ — the engine resolves BTL4.RES, VIDEO\, BTDPL.INI, eggs relative to cwd (the loadTables gotcha: L4VIDEO.cpp:849 fopen("VIDEO\\REPLACEMATS.tbl") is relative + unchecked → fread on NULL if cwd is wrong). Logs to content\<stem>_YYYYMMDD.log (or BT_LOG=<file>). [T2] Log naming (2026-07-28): stem = the launcher (solo/steam/join/joyconfig, else btl4), chosen from BT_FE_SOLO/BT_STEAM_NET/BT_FE_JOIN/BT_RELAY/BT_JOYCONFIG. A self-named day file always appends (the default stream mode truncates, so a second launch would erase the morning — that is how the 2026-07-27 Owens crash stacks were lost under the old rotation). Nothing is ever deleted; past 8 MB the NEXT launch opens <stem>_YYYYMMDD.1.log (checked only at open, so a live session is never split). ⚠ The log DAY runs 06:00 → 06:00 local, not midnight — a late session stays filed under the evening it started, so one playtest night is one file. Only the FILENAME is shifted; the session header and lastrun_<stem>.txt each call GetLocalTime separately and carry true local time. ⚠ BT_LOG=<file> is used VERBATIM and truncates unless BT_LOG_APPEND=1 — the player bats clear it for exactly this reason. Since 2026-07-26 the exe enforces this itself (BTEnsureContentDirectory, btl4main.cpp): if cwd has no BTL4.RES it probes from the exe's own directory (..\..\content for the shipped layout, plus flattened / in-content cases) and SetCurrentDirectorys there, before the log file opens. The boot line says so when it had to look. ⚠ The landmine it defuses (field report): a bare btl4.exe launch from build\Release found no resources ("Resource file btl4.res v1.0.0.0 is obsolete!"), wrote a stray bindings.txt/environ.ini/btl4_*.log NEXT TO THE EXE — so the player's real ones in content\ looked like they were never created — and killed the mission generation the menu launched (the child inherits the parent's cwd). Invisible for years because every launcher cds to content\ first; it became reachable once glass became the desktop default and a zero-arg launch started opening the menu.
  • The launcher's handoff wait must track ITS OWN generation [T2, 2026-07-27]. The front end does not stay resident — it CreateProcesses the mission generation and exits — so every launcher ends in a :btwait loop that waits for the handed-off process before printing its sign-off. That loop used to poll tasklist /FI "IMAGENAME eq btl4.exe", which is machine-wide: a second client, the operator's own pod, or an orphan from a crash kept the window spinning forever, which is the field report "closing the game leaves the terminal open". Fixed by snapshotting the PIDs alive before the launch and waiting only on PIDs absent from that snapshot. ⚠ Keep the if /I "%%P"=="btl4.exe" guard — parsing tasklist with tokens=2 alone reads its "INFO: No tasks are running" line as a PID and spins forever with nothing running. Full investigation, including which windows exit vs merely hide: phases/phase-12-orphan-processes.md.
  • Why this is a BT411-only hazard [T1]. The 1995 pod shipped ONE folder — BTL4OPT.EXE sits next to BTL4.RES/VIDEO\/GAUGE\/AUDIO\ (it is still there in content\) — and RP411/RP412 keep that shape (pack-dist.ps1 copies the exe and every asset dir into one dist root). BT411's two-folder split (build\Release\btl4.exe + content\) is not a design decision: mkdist.py zips TRACKED REPO PATHS verbatim, so the dist inherited the repo's developer layout. In the authentic single-folder shape cwd is right by construction and a double-click just works, which is why the other games never hit this. A proposal to ship the authentic shape is written up in docs/DIST_LAYOUT_PLAN.md — PROPOSED, not implemented, awaiting a decision between the authors. The cwd guard stays either way (it protects the developer tree, where the split is real and permanent).
  • Interactive: ⚠ under the GLASS profile (content\bindings.txt, the default since the cyd merge) drive is the 1995 throttle-lever scheme, NOT WASD: SHIFT = throttle up (lever STICKS where you leave it), CTRL = throttle down, ALT = reverse thrust, X = all-stop, BACKTICK = view toggle. ("WASD drive" was the pre-glass dev profile — telling a glass user W is the confirmed way to watch a mech stand perfectly still, 2026-07-29.) Weapon groups (keyboard, task #43) 1/Space = lasers, 2 = PPCs, 3/Ctrl = missiles; X all-stop; V cockpit/chase view. Default egg = DEV.EGG (map=grass, time=day). Swap mech via the egg's vehicle=ALL 18 ModelList names CERTIFIED playable (2026-07-18 vehicle sweep): the canonical 8 (avatar, bhk1/blkhawk, loki, madcat, owens, sunder, thor, vulture) + the short variants (ava1, lok1/lok2, mad1/mad2, own1, snd1, thr1, vul1); each boots a solo mission, spawns, animates, no crash. The code path is mech-agnostic. [T2]

Local benches MUST launch like a player (bench parity, 2026-07-30)

Parity is one rule of the harness. The full harness contract, the two-node pattern, and the VERIFICATION DOCTRINE (field composition over constructed proxies) live in test-harness — read that before writing any bench.

Every shipped launcher (play_solo.bat, join*.bat, play_steam.bat) sets the SAME three things, and a local bench that omits them is not testing the game the field plays:

set BT_PLATFORM=glass      set BT_START_INSIDE=1      set BT_DEV_GAUGES=1

plus a front-end flag (BT_FE_SOLO / BT_FE_JOIN), cd content, and NO -res. BT_START_INSIDE=1 is the big one — without it a bench opens in the EXTERNAL CHASE camera while every player starts in the COCKPIT. Benches ran chase-only for weeks; the first first-person look at one was misread as a broken HUD [T2]. Experience: every SHIPPED egg is experience=expert. A novice bench egg silences the whole heat model, crits and jams (see experience-levels) — bench combat then is not field combat. Only the aimed-leg gimp bench may use novice (expert crits the aimed leg and the mech turns-but-never-moves); it must say so out loud. Affinity: pin each node to TWO logical processors (0x03/0x0C/0x30/0xC0), not one. Disjoint core sets keep single-box packet delivery even (the peer-shakiness fix, multiplayer); ONE LP per node starves the gauge executive and fakes a "the comms panel never counts" reading [T2].

scratchpad/night6/bench_common.sh owns this contract (bt_player_env / bt_launch / bt_expert_egg / bt_novice_egg, and bt_assert_player_env warns if the shipped bat drifts). The 4-node benches (mp4_panel.sh respawn+scoreboard, mp4_cross.sh real cross-fire kills, mp4_limp.sh gait replication) all source it. A -net node renders NOTHING until tools/btconsole.py starts the mission — a solo launch is the quick visual check.

Headless frame capture — BT_SHOT_EVERY (2026-07-30)

BT_SHOT_EVERY=<n> dumps the game's own backbuffer every n frames to <BT_SHOT_PREFIX|shot>_NNN.png (btl4vid.cpp, top of BTDrawTranslocationSpheres — the per-frame alpha-pass hook; it MUST stay above that function's phase-0 early-out). This is how rendering is diagnosed without foregrounding a window, and it is the ONLY capture method to use: OS screen-capture grabs whatever is actually on the user's screen (a foreground-lock failure once photographed the user's browser instead of the game) and is off-limits. Frames land mid-alpha-pass, so a process killed during a write leaves ONE truncated PNG — skip it.

Debug (cdb x86)

"C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe". Pattern for a faulting stack (cwd = content): -g -c ".lines;sxe av;g;kp 24;q" with BT_ASSERT_TO_DEBUGGER=1. Debug CRT fills fresh heap 0xCDCDCDCD (uninit) + freed 0xFEEEFEEE — invaluable for "was this ever constructed?". BT_HEAPCHECK=1 = whole-heap validation every alloc/free (O(n²) at mission load — SLOW). To attach to a frozen abort dialog: cdb -p <pid> -c ".lines;~*kp 30;q". [T2]

A FIELD crash stack is currently NOT symbolizable — two gaps, 2026-07-28 [T1]. BTCrashFilter (btl4main.cpp:193) catches unhandled exceptions and writes [crash] UNHANDLED EXCEPTION code=… addr=… (btl4+0xNNNN) + an EBP-chain walk straight into the day log — no minidump, no separate file. Its comment claims "we hold the PDB". We do not:

  1. No Release PDB is produced. Only build/Debug/btl4.pdb exists and the shipped exe embeds no PDB path, so btl4+0xNNNN from a tester cannot be resolved to a function.
  2. Frame pointers are omitted. Release flags are /O2 /Ob2 /DNDEBUG with no /Oy- anywhere, and /O2 implies /Oy on x86 — so the EBP walk is unreliable and the [crash] stack: chain is often shallow or wrong. The first line is still exact (addr comes from the exception record, not the walk). FIX when field stacks are wanted: add /Oy- to the Release flags and /Zi + linker /DEBUG so a PDB is generated, then ARCHIVE that PDB per build (never ship it in the zip). The session header already stamps build=4.11.<n> (<hash>), so an archived PDB matches a tester's log exactly.

Repo layout (bt411)

  • engine/MUNGA/ + engine/MUNGA_L4/ — the shared 2007 MUNGA engine + Win32/D3D9 HAL (carries our BT render/loader work: bgfload/L4D3D/L4VIDEO + the image codec). engine/shim/ (ATL), engine/lib/ (OpenAL/libsndfile), engine/rp/ (RP headers the audio HAL includes).
  • game/reconstructed/ — the reconstructed BT source (the bulk).
  • game/original/BT/ + BT_L4/ — surviving original BT .cpp + all BT headers (the include path).
  • game/fwd/ — ~186 forwarding shims (#include <EXPLODE.hpp>../../engine/MUNGA/<NAME>.h).
  • game/btl4main.cpp — the WinMain launcher.
  • content/ — the runtime tree (BTL4.RES, VIDEO/, GAUGE/, AUDIO/, eggs, BTDPL.INI). Run cwd.
  • reference/decomp/ — the raw Ghidra pseudocode (all/part_*.c) — the source-of-truth.
  • docs/ — the detailed ledgers (incl. PROGRESS_LOG.md, the full pre-restructure CLAUDE.md).
  • tools/ — btconsole.py, disas2.py, map/res scanners. context/ — this knowledge base.

Versioning (2026-07-18)

4.10 = the 1995 arcade release; 4.11 = this win32 reconstruction; dev builds = 4.11.<git commit count> + short hash, + suffix = built from an uncommitted tree (e.g. 4.11.311 (980c9cd+)). Stamped every build by tools/btversion.cmakebuild/btversion.h (BT_VERSION_* macros); shown in the boot banner (day-log head) and the window title. Ask a tester for their title bar, or read the ===== BT411 SESSION … build=… ===== header that starts every session inside content\<stem>_YYYYMMDD.log, to identify a build.

Cutting a tester zip: python tools/mkdist.pydist/BT411_4.11.<n>.zip (name from build/btversion.h; ships git-TRACKED content/ only, so working-tree junk stays out). ⚠ It reads build/CMakeCache.txt for the flavor, so configure BEFORE building: with BT_STEAM=OFF the zip is renamed -nosteam and carries neither play_steam.bat nor steam_api.dll — useless for a Steam playtest, and easy to ship by accident since OFF is the CMake default (-DBT_STEAM=ON is the documented dev-checkout state). BT_EXPIRE=ON (default) gives the 14-day tester window; an expire-OFF zip is renamed -noexpire and warns. Verify by extracting the zip somewhere clean and booting it with no repo present — that is what catches a missing runtime DLL.

Key Relationships