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>
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 |
|
|
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.
Build (Win32 / VS2019 BuildTools — the DXSDK link libs are Lib/x86)
# 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 atC:\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::CreateSubsystemStreamreferencesvoid*-signatureCreateStreamedSubsystemfor every class; never called at runtime). ⚠/FORCEalso 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 aDPLRenderer/d3d_OBJECTclass 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 resolvesBTL4.RES,VIDEO\,BTDPL.INI, eggs relative to cwd (theloadTablesgotcha:L4VIDEO.cpp:849fopen("VIDEO\\REPLACEMATS.tbl")is relative + unchecked → fread on NULL if cwd is wrong). Logs tocontent\<stem>_YYYYMMDD.log(orBT_LOG=<file>). [T2] Log naming (2026-07-28): stem = the launcher (solo/steam/join/joyconfig, elsebtl4), chosen fromBT_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 andlastrun_<stem>.txteach callGetLocalTimeseparately and carry true local time. ⚠BT_LOG=<file>is used VERBATIM and truncates unlessBT_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 noBTL4.RESit probes from the exe's own directory (..\..\contentfor the shipped layout, plus flattened / in-content cases) andSetCurrentDirectorys there, before the log file opens. The boot line says so when it had to look. ⚠ The landmine it defuses (field report): a barebtl4.exelaunch frombuild\Releasefound no resources ("Resource file btl4.res v1.0.0.0 is obsolete!"), wrote a straybindings.txt/environ.ini/btl4_*.logNEXT TO THE EXE — so the player's real ones incontent\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 launchercds tocontent\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:btwaitloop that waits for the handed-off process before printing its sign-off. That loop used to polltasklist /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 theif /I "%%P"=="btl4.exe"guard — parsing tasklist withtokens=2alone 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.EXEsits next toBTL4.RES/VIDEO\/GAUGE\/AUDIO\(it is still there incontent\) — and RP411/RP412 keep that shape (pack-dist.ps1copies 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.pyzips 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 indocs/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'svehicle=— 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:
- No Release PDB is produced. Only
build/Debug/btl4.pdbexists and the shipped exe embeds no PDB path, sobtl4+0xNNNNfrom a tester cannot be resolved to a function. - Frame pointers are omitted. Release flags are
/O2 /Ob2 /DNDEBUGwith no/Oy-anywhere, and/O2implies/Oyon x86 — so the EBP walk is unreliable and the[crash] stack:chain is often shallow or wrong. The first line is still exact (addrcomes from the exception record, not the walk). FIX when field stacks are wanted: add/Oy-to the Release flags and/Zi+ linker/DEBUGso a PDB is generated, then ARCHIVE that PDB per build (never ship it in the zip). The session header already stampsbuild=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.cmake →
build/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.py → dist/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
- Base: wintesla-port (the engine build recipe).
- Verify loop: reconstruction-method; env gates: decomp-reference §6.