Files
BT411/context/build-and-run.md
T
Joe DiPrimaandClaude Fable 5 4e89baeff3 bench parity: local nodes now launch exactly like a player's shortcut
Local benches had drifted from the shipped launchers and every divergence
cost a debugging session:

  * no BT_START_INSIDE -> benches opened in the EXTERNAL CHASE camera while
    every player starts in the COCKPIT.  The first first-person look at a
    bench window was misread as a broken HUD.
  * novice bench eggs  -> every SHIPPED egg is expert; novice gates off the
    entire heat model, crits and jams, so bench combat was not field combat.
    (Verified: the aligned node now boots experience=3 simLive=1
    heatModelOn=1, where it used to boot 0/0/0.)
  * 1-LP affinity pins -> starved the gauge executive and produced a false
    "the comms panel never counts deaths" reading.  Two LPs per node keeps
    the documented single-box jitter fix without the starvation.

scratchpad/night6/bench_common.sh now owns the contract (bt_player_env,
bt_launch, bt_expert_egg, bt_novice_egg) copied verbatim from play_solo.bat,
with bt_assert_player_env warning if the shipped bat ever drifts.  All three
4-node benches source it.  The limp bench keeps a novice egg -- expert crits
the aimed leg and the mech turns-but-never-moves -- and now says so loudly.

Also: BT_SHOT_EVERY=<n> headless backbuffer capture (btl4vid.cpp), the tool
this was diagnosed with.  It must sit above the warp phase-0 early-out since
that fn is the per-frame alpha-pass hook.  OS screen-capture is off-limits:
a foreground-lock failure photographed the user's browser instead of the game.

KB: build-and-run.md gains the bench-parity rule + the capture diag;
cockpit-view.md gains the measured aspect/FOV finding (the surround's 2.05:1
view collapses vertical FOV to 31.5 deg vs the pod's 46.8, so fixed canopy
geometry covers 68.6% of the lower half -- NOT a regression: the shipped 643
binary reports identical geometry).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 13:30:30 -05:00

184 lines
13 KiB
Markdown

---
id: build-and-run
title: "Build / Run / Debug — recipe, repo layout, env gates"
status: established
source_sections: "PROGRESS_LOG.md §10a, §10a-bis; README.md"
related_topics: [wintesla-port, decomp-reference, reconstruction-method]
key_terms: [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.
## 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 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 `SetCurrentDirectory`s 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 `cd`s 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 `CreateProcess`es 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)
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.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.