Files
BT411/context/open-questions.md
T
arcattackandClaude Opus 4.8 89b4f53046 Gauges: trigger-config joystick is LIVE -- @004c6ee0 is LinkToEntity, not SetColor
The weapon panel's btjoy trigger-config display (ConfigMapGauge) never rendered
because the reconstruction labelled @004c6ee0 as "SetColor(int)" and, finding no
caller, concluded the gauge was authentically dormant (task #6), gating it
behind a BT_CONFIGMAP dev env.

The user challenged this (the PROGRAM/TRIGGER CONFIG button implies the display
visualizes your config).  Re-verification found the old analysis failed twice:
the "no caller" search looked for direct calls to a VIRTUAL, and its slot math
used the wrong vtable copy.  The real ConfigMapGauge vtable @0051a1b8, matched
against the T0 GaugeBase virtual roster (GAUGE.h), pins @004c6ee0 at slot 9
(+0x24) == GaugeBase::LinkToEntity, and @0x94 is the linkedEntity -- the
Execute gate is "not yet linked", not "disabled".  The engine broadcasts
LinkToEntity(viewpointEntity) to every gauge at viewpoint bind (APP.cpp:1277 ->
GaugeRenderer::LinkToEntity GAUGREND.cpp:3011), arming the gate -- the joystick
DOES render in the shipped game, showing per-trigger mapping state (solid =
mapped, matching the DOSBox reference).

Port fix: SetColor(int color) -> LinkToEntity(Entity*), color -> linkedEntity,
BT_CONFIGMAP dev enable deleted (the authentic path lights it).  Render-verified
with no env override: the joystick + mapped-trigger lamp draw on weapon panels.

KB swept per the correction mandate: gauges-hud.md, decomp-reference.md,
open-questions.md, GAUGE_COMPOSITE.md, VEHICLE_SUBSYSTEMS.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 08:49:58 -05:00

811 lines
69 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: open-questions
title: "Open Questions — deferred systems, unknowns, get-from-Nick"
status: living
source_sections: "CLAUDE.md §9; docs/HARD_PROBLEMS.md; per-subsystem deferral notes"
related_topics: [source-completeness, subsystems, multiplayer, combat-damage, locomotion]
---
# Open Questions
Known unknowns + deliberately-deferred systems. For each: what's known, what's missing, what
would resolve it, status. Deferred ≠ broken — most are marked-and-guarded stand-ins with the
authentic path scoped.
## From Nick (external — gating for some paths)
- ~~**[GATING] The BT game SOURCE CODE**~~ **CLOSED 2026-07-19: per the user, Nick holds NO
assets beyond what has already been provided.** Route A (recompile original) is permanently
off; the decomp reconstruction ([[source-completeness]]) is THE route — behavioral in
`game/reconstructed/`, literal in `TeslaRel410/restoration/source410/`. Asset-type asks below
are likewise closed; knowledge/lore asks (throttle detents, pod procedures) remain valid.
- ~~Confirmation the PodPC image is the complete content master~~ **CLOSED 2026-07-19** (no
further assets exist; the runtime `BTL4.RES` with 8 maps + full anim set is the master we have).
- Pod specifics for Phase 8: the 7-monitor driver setup, the RIO cockpit I/O protocol, current
Win10+wrapper pod config. Status: OPEN.
- **Did the pod throttle quadrant have MECHANICAL detents** (how many / lever positions)? The
software path is notch-free [T1, [[pod-hardware]]], but the gait SM's walk/run dead band
([[locomotion]]) only makes sense if the hardware discouraged parking the lever mid-band —
"5 speeds" pod lore suggests physical notches. Status: OPEN.
- ~~Which mechs used rear fire~~ **ANSWERED from data (2026-07-16, task #68)**: the Blackhawk's
ERMLaser_2/3 (back gun ports) are the only rear weapons in the roster; see [[combat-damage]].
Remaining: bind the side/down look views to keys (only 'V' look-back is bound); pod look-button
mappings for Phase 8.
- ~~The `cameras\` directory (per-map camera-network files)~~ **RESOLVED from our own data
(2026-07-18, same day)**: the `cameras\<mapname>` FILE path (CAMMGR.cpp:390-415, RP-style
loose `.CAM` notation — present for RP maps in the nick-games 410SRC archive) is only the DEV
OVERRIDE; the shipped binary falls back to **BTL4.RES resource TYPE 27 (0x1b) named after the
map** (`FUN_0042be3c` probe → `FUN_0042c078(res, mapname, 0x1b)`; binary records
`{count; per-cam {size; type 1|2; pos+quat+...}}`, parsers @0042ad54/@0042b9f8) — and our
BTL4.RES has ALL 8 maps' networks (arena1 = 62 cameras; cavern/dbase/grass ~57KB each).
[T1] **CAMERA SEAT LIVE (2026-07-18, same day) [T2]**: the type-27 loader ALREADY existed in
the engine (`CreateStreamedCameraInstances`, CAMMGR.cpp:530 — the file probe falls back to it;
62 arena1 cameras load, `[cam]` diag under BT_CAM_LOG). The REAL missing piece was
**`Mech::PlayerLinkMessageHandler` (@0049f624)** — the BT override the reconstruction never
filled: base resolves mech→player; BT adds the REVERSE link `player->playerVehicle = this` on
EVERY node (replicants included — how the director/scoreboard find a remote player's mech),
clears `NonScoringPlayerFlag` (0x4000 = bit 14: a pilot with a vehicle SCORES/ranks — this
also makes cross-node RANKINGS work), and on the master seeds the heat bank's
`ambientTemperature` from the mission `temperature=` (mech+0x7dc bank +0x1d4 ← BTMission+0xf4
— the heat family's "frozen 300" note stands CORRECTED). Verified live: mech+camera 2-seat
relay session — replicated player links (`bmp=1+veh`), director locks the goal (30s timer),
camera ship tracks the moving mech (`sees=1`) and CUTS between authored cameras
(screenshots). Egg: pilot page `hostType=1` + `vehicle=camera`. **RANKING WINDOW LIVE (same day)**: the stubbed
1995 overlay reimplemented as screen-space quads in `CameraShipHUDRenderable::Render` — the
followed player's callsign banner (bottom center, the Execute index is ALREADY 0-based) + a
rank-ordered row per scoring player ([ordinal][callsign], right of center), visibility = the
Director's flash logic (10s on/15s off, solid final 30s), live `playerRank` pointers re-sort
rows as scores change. **Each 128×32 ordinal bitmap packs TWO ordinals side by side**
("1st|2nd", "3rd|4th" — why 4 bitmaps serve 8 players): texture rank/2, u-half by parity.
BT_SHOT now captures AFTER the 2D pass (it used to dump pre-HUD — overlays were on screen
but invisible to screenshots). Screenshot-verified: "1st MAVERICK" + banner over live
coverage. Remaining: operator-app camera-seat row helper (hostType not in the roster UI),
shot-composition polish. Status: broadcast display COMPLETE.
## Pod-hardware readiness gaps (Phase 8 assessment, 2026-07-17) [[pod-hardware]]
Everything below EXISTS in the tree but has NEVER run against real pod hardware. Mechanism
status vs the pod's 7-display / RGB-splitter / dual-sound-card rig:
- **CARRIED + plausibly functional (untested on hardware):**
- RIO serial controls: real Win32 COM (`L4SERIAL` CreateFileA/SetCommState), the full chain
RIO AnalogReply → Ranger → ControlsManager → streamed `.CTL` mappings → mapper traced [T1];
keyboard bridge disable = `BT_KEY_BRIDGE=0`. Pod look/config buttons are `.CTL` data-driven.
- RGB-splitter channel packing: `L4GraphicsPort::ChannelEnableID` (Red/Green/Blue/All +
transparent-zero variants) — each gauge draws into ONE color channel of the shared
640x480x16 surface, feeding one monochrome CRT per channel through the splitter. The gauge
CONTENT is complete (same `L4GaugeRenderer` verified daily in the `BT_DEV_GAUGES` window).
- Multi-output selection: `SVGA16::BuildWindows` (a fullscreen D3D9 device per gauge/MFD
surface), adapter env `PRIMGAUGE`/`SECGAUGE`/`MFDGAUGE`/`MFDGAUGE2`/`SPANDISABLE`, MFD span
= width x2 across two displays; `content/SETENV.BAT` = the authentic pod env preset.
- The `L4PLASMA.cpp` serial plasma-display driver (`L4PLASMA=com2`).
- **EXPECTED TO NEED REAL WORK on modern hardware:**
1. **Multi-head display management**: the original used TWO video cards; simultaneous
exclusive-fullscreen D3D9 devices on multiple heads of one modern GPU is fragile — likely
needs a borderless-window-per-head modernization of `BuildWindows`.
2. **The REAR sound card is folded away**: the pod ran `AWE_FRONT`/`AWE_REAR` as two physical
cards for the 4-speaker front/rear split; our OpenAL opens ONE device (front/rear collapses
to stereo). Feasible fix: OpenAL Soft quad output (surround config) or a second device.
3. Serial protocol timing/handshake vs a real RIO board (and the intercom path) — unverified.
4. `-platform pod` deliberately does NOT auto-enable `L4GAUGE` (each surface needs its own
device); the pod bring-up procedure itself is undocumented → the Nick items above.
## BINARY-COVERAGE AUDIT discoveries (2026-07-13, 6-agent decomp census) [T1 verified-uncited]
New unaccounted functionality no prior list knew (addresses verified absent from game/+context/+docs):
- **✅ `BTL4VideoRenderer::StartEntityEffectImplementation` @004d097c — DONE (2026-07-13) [T2].**
The per-zone EFFECT DISPATCHER (+helpers @4d0c14 StopAll / @4d0c5c) is reconstructed
(btl4vid.cpp:889) + WIRED: `MechDamageZone` change → `BTStartZoneEffect`
RendererManager::StartEntityEffect → this virtual → the band effect ATTACHED to the struck
zone's segment (smoke/fire follows the mech); the Destroyed descriptor fires on kill
(mechdmg.cpp:1120). Band smoke/fire (psfx ≥1000) render through the live PFX layer
(BTStartPfxAttached); the 3-15 specialfx band + scaling booms/flames render through the
reconstructed L4VIDEO specialfx engine + `ScalingExplosionRenderable` (see next bullet).
**LIVENESS-VERIFIED (2026-07-14):** `BT_CRIT_PROBE=8` on solo DEV.EGG (hammers a local zone) drove
the full chain — `[zonefx] entity 1:321 seg 12 psfx 12/13 at(527,5.8,-669)`: TakeDamage → the
mechdmg damage-effect loop → BTStartZoneEffect → StartEntityEffectImplementation resolves the
struck segment's world pos → BTStartPfxAttached. (The 2-node harness `scratchpad/fx_liveness.sh`
did NOT engage — nodes spawned but never targeted/drove into combat, `mechPicks=0`; an MP-harness
spawn/connect issue, orthogonal to FX. `BT_CRIT_PROBE` is the reliable solo FX-liveness path.)
- **✅ The impact/destruction FX visual chain — RECONSTRUCTED (Fire VISUALS wave 48c9c84 +
Impact-FX FORENSICS wave 065c114, 2026-07-12/13) [T2].** The i860-firmware-decoded specialfx
engine (L4VIDEO.cpp: `specialfxN` descriptor pages, per-round detonations, authored firesmoke
sheet, vertex-alpha effect cards), the case-4 wreck dressing, and `ScalingExplosionRenderable`
(L4VIDRND.cpp:3413 — real ctor/Execute, the scaling-boom motion) are all live. The audit's six
renderable-body addresses (@4540ac/45447c/455eb8/456cf0/4589e0/458e5c) map to this reconstructed
set; a byte-level coverage cross-check of each vs the raw bodies is a minor open audit, NOT a
dead chain. (NOTE: the `//STUBBED: DPL RB 1/14/07` markers scattered in L4VIDRND are benign 2007
empty ctor/dtor notes on UNRELATED renderables — InnerProjectile/DPLObjectWrapper/ChildLight —
not the explosion FX; do not mistake them for a gap.)
- **FUN_00472480** (273 ln) — unnamed analog→discrete stepper Execute in the gauge band
(hysteresis + key-repeat), no caller found; the one hole in "gauge system complete".
Settle via vtable lookup (plausibly a config/roster scroller).
- **FUN_00454a70** (193 ln) — the PNAME1-8/PLACE1-8.bgf MP name-billboard loader (the known
"MP target identification" gap's exact function).
-**Explosion::SplashDamage @0042fad0 — RECONSTRUCTED (task #62, 2026-07-13) [T1/T2].** ONE call
site, **Missile::Perform (FUN_004bef78, part_013.c:10097)**, in the collision branch (fires on ANY
missile impact — mover OR world), gated on `missile+0x360`, radius = `missile+0x364`. **Radius source
corrected:** it is the ROUND's OWN GameModel (type-0xf) `+0x50`, seeded from the launcher's linked
**AmmoBin `ammoModelFile @0x1e8`** (part_013.c:8778 → missile ctor SearchList @10184) — NOT the
launcher's `ExplosionModelFile`. The MissileThruster parser `FUN_004bf8ec` writes `"SplashRadius"` to
+0x50 (size-0x54 record). Burst falloff `baseBurst / dist^exp` floored at 1 (arcade 1.25 / WinTesla
`EXPLODE.cpp:209` 1.2f). Only missiles splash (AC tracer is not a Missile). The engine `EXPLODE.cpp`
diff is DONE — the T0 source confirmed the model; the only 1995-vs-WinTesla drift is the exponent.
Port: `BTResolveSplashRadius`/`BTApplySplashDamage` (mech4.cpp), see [[combat-damage]]. **Remaining
T3:** the per-player enable sub-gate `missile+0x360 = BTPlayer+0x264` (its writers read as a per-frame
toggle, not a config flag; the `showDamageInflicted@0x264` label is a guess) — port treats authored
`SplashRadius>0` as the enable. The mech's `DeathSplashDamage`/`DeathSplashRadius` (mech+0x520/0x524)
stay **VESTIGIAL** (written, never read; no mech-death splash mechanic — death is visual only).
- **The binary's own VelociRender BGF/BMF/BSL/VTX loader (~4,550 ln, dead code)** — a free
ground-truth cross-check for [[bgf-format]]/[[asset-formats]] claims.
Full ranked audit: the workflow output (2026-07-13); top-10 ranking cross-checked against this
register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTORY in the KB
(task #8 "landed" vs the kShotDamage=12 bring-up residue) — audit before reworking.
## Deferred subsystems / feeds (authentic path scoped, marked in code)
- **Glass panel dead-button backlog (input-coverage audit 2026-07-20) [T1 census].** 8 of the 72
panel buttons dispatch a streamed message with NO reconstructed handler (silently ignored):
0x13 → Mech `DuckRequest` @0049fa00 (CROUCH — `duckState` attr + SQUAT clips exist!), 0x28 →
Mech `BalanceCoolant` @0049f728, 0x1A-0x1D → Generator A-D `ToggleGeneratorOnOff` @004b1ed0,
0x12 → ThermalSight toggle (pvision unported), 0x14 → Searchlight `ToggleLamp` (body
reconstructed, handler-set unwired — note the 1995 latent bug below: even wired, the light
never turns on in the original). Plus page-gated Eng-page meanings: `ToggleCooling` @004ad6f8
(msg 3), `ToggleSeekVoltage` @004ba478/@004b8a48 (msg 0xb energy / 9 Myomers), `EjectAmmo`
@004bb9b8 (msg 0xb ammo weapons), and MechRIOMapper's own Keypress @004d2514 (id 0x19).
Filed as the Gitea "glass panel: unreconstructed button handlers backlog" issue; tables in
[[decomp-reference]] §2026-07-20; census in `docs/GLASS_COCKPIT.md`.
- **Searchlight-driven fog swap — DEFERRED, and it is an ORIGINAL 1995 LATENT BUG (task #63,
2026-07-13) [T1 decomp].** The arcade swaps fog between `fog=` (lights on) and `nosearchlightfog=`
(off) via `PullFogRenderable` watching the Searchlight's `lightState`. It never fires — in the PORT
because `PullFogRenderable` is never constructed, AND in the BINARY ITSELF because the Searchlight
never lights: `ToggleLamp` (@004b860c) toggles `commandedOn` @0x1DC but `SearchlightSimulation`
(@004b841c) reads `requestedOn` @0x1E0, which is zeroed by the ctor and never written — no
0x1DC→0x1E0 bridge exists (self-consistent sibling ThermalSight reads the field it toggles, 0x1DC).
So `lightState` is perpetually 0. The port reproduces this faithfully (searchlight.cpp:189).
**DECISION: left as-is + documented (faithful to the buggy original); port keeps the static
lights-ON `fog=`.** A working swap is a DESIGNER-INTENT deviation, fully scoped if ever wanted:
(1) sim reads `commandedOn` (mirror ThermalSight); (2) construct `PullFogRenderable` at
btl4vid.cpp `MakeMechRenderables` reticle-build/inside pass (== arcade part_014.c:5173, Dynamic,
bound per Searchlight `lightState` via a new `LightStatePtr()` accessor); (3) a toggle input (the
authentic cockpit button-5→`ToggleLamp` dispatch is a controls-family reconstruction; a dev key
stands in). `ControlsAllowLights()` (searchlight.hpp) is WIRED since issue #2 to the
`player+0x25c` not-novice experience flag via the BTPlayerExperienceSimLive bridge
([[experience-levels]]); the consumer (ToggleLamp) still needs the cockpit button dispatch.
See [[rendering]] fog section. Verified inert live: BT_FOG_LOG shows zero `SetFogStyle(2/3)`.
- **Factory capability-roster loops 2-4 are STILL DEAD (task #57 discovery).** mech.cpp's
post-roster loops add to `heatableSubsystems`(0x51155c)/`weaponRoster`(0x511830)/
`damageableSubsystems`(0x50e4fc) through the local `SubProxy` stub whose `IsDerivedFrom`
RETURNS 0 — so those three mech-level rosters stay EMPTY (whatever consumes them sees
nothing; weapons demonstrably work through other paths, so audit consumers before assuming
impact). Loop 1 (the watcher CONNECT, 0x50e604) was reconstructed for real in task #57
use the same bridge pattern (family-side derivation test + typed body) for the other three. [T2]
- **Initial-snapshot torso record carries no twist extras**: the replicant's blind +0x10 read
can latch 0xCDCDCDCD ONCE at spawn until the first real twist record; clamps contain it
(invisible on fixed-torso mechs). Binary-authentic blind read — fix only if a visibly wrong
spawn pose ever surfaces. See [[multiplayer]]. [T2]
- **✅ 0xBD3 SubsystemMessageManager — UNTANGLED + LIVE (task #7, 2026-07-11) [T1/T2].**
Both halves landed: (1) the factory case builds the REAL messmgr (ctor @0049bca4, 0x130,
static reconstruction messmgr.cpp) cached at `mech+0x434` — the binary-wide census found
exactly ONE reader (@0x4b984b in `MechWeapon::SendDamageMessage` @004b9728, which was ALSO
mislabeled, as "DrawWeaponPip"); every mapper consumer re-pointed to roster slot 0
(`MappingMapper()`; `SetMappingSubsystem` @0049fe40 touches only slot 0); non-viewpoint mechs
carry a slot-0 demand LATCH ([T3] accommodation for the port's wider drive/gait reachability).
(2) The authentic CONSOLIDATED damage delivery is live: the beam path submits TakeDamage into
`AddDamageMessage` → per-frame `ConsolidateAndSendDamage` (@0049b784) builds ONE
`Entity::TakeDamageStreamMessage` (id 0x13, wire-verified 0x34+4+N×12) dispatched at the
victim, whose T0 handler (ENTITY.cpp:817) re-splits it; replicant victims reroute cross-pod.
Explosion bundling (weapon+0x3E4 per record, unique-queued, Explosion::Make at the impact)
implemented; TWO latent chain-purge bugs fixed (records re-applied every tick). RESIDUE
([T3], next task): the weapon-side submission with per-weapon `damageData` +
`inflictingSubsystemID` awaits the DAMAGE-ECONOMY reconciliation (authored 0.25-scale
amounts vs the bring-up kShotDamage=12); the 0.1s explosion stagger + the terrain-hit
derivation check are noted in messmgr.cpp.
⚠ The earlier "0xBD3 gates the valve/Myomers" claim remains WRONG (those read the owning
BTPlayer at mech+0x190 — see the next entry); the old wiring notes are superseded by this
landing.
- **✅ `mech+0x190` IDENTIFIED (2026-07): it is the owning `BTPlayer`** (`Mech::GetPlayerLink()`,
`MECH_OWNING_PLAYER`; ENTITY.h:430). Set by `FUN_0049f624` (the mech↔player bind: `mech+0x190 = player`
AND `player->playerVehicle(+0x1fc) = mech`), which resolves the player from the mission player registry
(`app+0x2c+0x54`, `FUN_0041fd18`) by the pilot key.
**✅ FLAG SEMANTICS PINNED (2026-07-18) [T1]: the `player+0x25c..0x274` block = the EXPERIENCE
LEVEL (egg `experience` novice/standard/veteran/expert), written by the BTPlayer ctor @4c0bc8
from `btMission(+0x1f8)->experienceLevel(+0xe4)` — full mapping, consumers, and the 4.0-manual
behavior in [[experience-levels]] (+ [[decomp-reference]] §3 flag table).** Resolved en route: the
old "resolve first" items — `FUN_004ad7d4` (labeled both `HeatModelActive` and `OwnerAdvancedDamage`)
is ONE body reading `player+0x260` = the heat-model master switch (veteran+expert); `FUN_004ac9c8`
reads `player+0x274` = the novice lockout.
**✅ THE WIRING LANDED (issue #2, 2026-07-20) [T2]:** the ctor now seeds from
`BTMission::ExperienceLevel()`/`AdvancedDamageOn()` via the inherited `Player::playerMission`
(the binary's +0x1f8; the old `btMission` phantom member removed), the members are renamed to
the true semantics (`simLive`/`heatModelOn`/`experienceLevel`/…), and all five gate stubs +
the FUN_004ac9c8 family (`MechSubsystem::IsDamaged`, `BT_IsBusLive`, powersub call sites) route
through the three complete-type bridges (`BTPlayerExperienceSimLive`/`…HeatModelOn`/
`BTPlayerRoleLocksAdvanced`). Every shipped egg says `experience=expert`, so default behavior is
unchanged. Detail: [[experience-levels]] §Port state.
- **✅ Authentic target acquisition RECONSTRUCTED (tasks #36/#39, 2026-07-08)** — the `Reticle`
pick-ray chain is LIVE (see [[combat-damage]] Targeting for the full port map): the crosshair =
**torso boresight** (NO free-aim mouse — the pod stick twisted the torso; you steer to aim) →
pick ray → per-frame lock (boresight ON the mech = locked; off = none) → aimed zone damage +
designator hotbox; `BT_FIRE_ARC` is opt-in-only now. Firing needs a target but NOT a manual
lock/pinpoint — the target is AUTO-acquired (task #40, binary-verified: FireWeapon doubly-gated
on mech+0x388, but 0x388 has 11 reads / 0 direct stores across CODE → set indirectly). Residue:
(a) the binary's own per-frame reticle→mech copy + the AUTO-TARGET SELECTOR are still in an
un-exported gap (a message/selector; our port auto-targets the single living enemy — for MP,
reconstruct the real multi-target selection: nearest/most-aligned/cycle); (b) the eyepoint now
rides the parent DCS (task #55 authentic eye), so torso pose reaches the view; on a
TWIST-CAPABLE mech (selectable via `BT_FORCE_MODEL`) the crosshair should deflect with the
torso twist (`BTTwistToReticleX` is wired for it) AND the torso should visibly lead the legs —
verify the reticle deflection on a twist mech (the BLH is fixed-torso, boresight always
centred); (c) pre-burial, the pick still
tests the sinking wreck's collision box at its parked position (pre-existing wreck-targeting
behavior).
- **✅ WORLD STRUCTURES (garages/walls) now targetable (task #50, 2026-07-15)** — the world-pick's
non-mech tier previously only sampled the visual heightfield (`BTGroundRayHit`), which on arena1
is a single flat `'sky'` ground mesh → shots passed THROUGH the garages and only mechs moved the
range axis (user-reported regression). Resolved by ray-testing the **zone's static collision
solid tree** (the geometry that already blocks the walk) via the authentic engine query
`Mover::FindBoxedSolidHitBy`'s static-world tail — factored into `Mover::FindStaticSolidHitBy` +
`Mech::WorldStructurePick`. See [[combat-damage]] "WORLD STRUCTURES ARE TARGETABLE". This also
closes the old "0x388 writer is un-exported / unknown world-pick mechanism" residue: the
authentic non-mech world pick is the static-solid-tree ray query (`BoxedSolidTree::FindBoundingBoxHitBy`),
the same one `FindBoxedSolidHitBy` uses for mover-vs-world collision. Interactive aim is
user-verified (headless has no `BTGetAimRay`); the `BT_WSWEEP` ray-fan is the headless proof.
- **Cockpit HUD leftovers (tasks #35-#38 residue, 2026-07-08)** — the reticle + 7 weapon pips +
ALL the instrument dynamics are LIVE (the Execute @004cdcf0 gap was RECOVERED via capstone
disasm, task #37; the authentic Lock producer + the simple-X mode landed in #38 — see
[[gauges-hud]] for the full instrument map). Remaining: (a) the **`*_cop` canopy shell** —
SOLVED for all 8 (task #55): authentic eye + the PUNCH stencil-cut kit + double-sided cop
geometry + unlit constant frame colour (`BT_COP_FRAME`) — see [[cockpit-view]] Rendering/FINAL;
shows by DEFAULT now (`BT_HIDE_COCKPIT=1` hides). NOTE the old claim here ("canopy windows are
punch texels") was WRONG — the shell has no texture; the openings are geometry. ✅ RESOLVED
(2026-07-11): PUNCH = a 3-chunk STENCIL-CUT kit (mask/hull/twin, i860-firmware-decoded)
executed as a D24S8 stencil cut; visible canopy = hull-minus-apertures ([[cockpit-view]]
§FINAL). All 8 mechs render connected dark frames with clear viewports (the intermediate
"frameless bhk1/loki/vulture/avatar" reading was wrong); thor matches the pod footage exactly.
Eye placement verified exact per-mech incl. Thor's +1.13 offset cockpit ([[cockpit-view]]). (b) the **3D marker
chain** ([0x31c]/[0x320] dpl objects + the PNAME1-8.bgf player-name meshes over the locked
target — matters for MP). (c) the compass rotation SIGN is derived-not-verified [T3] (turn
left → the stem should swing right; flip the sin sign in Draw if footage disagrees).
- **✅ StatusMessagePool / MessageBoard — LIVE (2026-07-12) [T1/T2].** Decoded: FUN_0042e580 =
the engine `Player::AddStatusMessage` (chain add at +0x1e4); FUN_0042e5a0 = `StatusMessageUpdate`
(+0x1dc = the displayed message; +0x14 displayTime countdown, expiry-deleted) — ALL already in
the engine [T0 PLAYER.cpp]. The ONE binary producer = BTPlayer::ScoreMessageHandler's KILL
branch (@004c02e4 type-2): pool-alloc @00512f6c + ctor(victim's player, strip 0, 6.0s) +
AddStatusMessage. Port: engine `new Player__StatusMessage` + Register_Object (the engine
reclaims with delete — the binary pool is an allocation detail); consumer =
BTResolveMessageBoard reads the mission player's statusMessagePointer + resolves the name via
Mission::GetSmallNameBitmap; the board's SetSource binder (no recovered caller) is replaced by
a lazy viewpoint-entity bind in Execute. Ticker pops on BOTH nodes (each node's death
transition posts its local KillScore).
- **Gyroscope (0xBC4) — RE-ENABLED LIVE (task #56, 2026-07-10).** The NaN revert's root causes
are fixed byte-exact (ctor field map @004b3778 — springConstant@0x1E8/dampingConstant@0x1F4
were mislabelled, the 0x254-0x2B3 block was missing, clamps/accumulators uninitialised; the
integrators' state-minus-target/componentwise-damping-overwrite/X-Z-crossing semantics; the
writers' one-node-two-channels form; dispatch from the MECH performance tail, not the gyro
Performance). Layout locked (sizeof==0x3D0); runtime clean (joints type 5, finite state, no
NaN). Empirical: spring targets are SYMMETRIC → eye equilibrium (0,0,0) → the gyro is a
±0.1-0.15u BOUNCE mechanism, NOT a steady offset (the earlier hypothesis is disproven).
**FAN-OUT LANDED (2026-07-11):** FUN_004b2980 re-disassembled from raw bytes and reconstructed
(`Gyroscope::ApplyDamageResponse`) + wired at the take-damage hub, the crushable crunch and the
firing recoil — the hit-BOUNCE is live and verified (damped eye oscillation, no NaN).
REMAINING (task #56 tail): (a) the alternate-gait engage jolt + engaged-gait rumble
(@4aa158-4aa365 — [T3] gate naming, mutates the #49/#50-stabilized gait machine; byte recipe in
the wf_6880e605 synthesis); (b) the mech+0x3F0 overspeed sway model (swayBias fed 0); (c) the
per-frame `EyepointRotation.pitch = torso pitch` writer (FUN_004b66b4) + mapper glance-look
states (FUN_004afd10 — yaw/pitch from mech+0x564..0x570); (d) senders filling
`Damage::damageForce` for directional bounce (random-fallback is binary-legal meanwhile).
Also: `deathAnimationLatched`/`legResetLatch` were never ctor-initialised (0xCDCDCDCD gate bug,
fixed in mech.cpp ctor). See [[cockpit-view]].
- **MechControlsMapper look/eyepoint commit** — reconstructed but its offsets collide with declared
members; arbitrate before enabling (no port consumer yet). (The fire-trigger-through-mapper is NO
LONGER the bring-up: the `gBT*Trigger` globals are retired / never-read — the authentic LBE4
buttonGroup path replaced them, task #5 `8ed6184`.)
- **✅ Weapon groups — the AUTHENTIC defaults + fire path LIVE (task #5, 2026-07-11) [T1/T2].**
The system decoded end-to-end: a fire "channel" = a physical `buttonGroup` slot in the global
`LBE4ControlsManager` (fire buttons 0x40 Trigger / 0x45 Pinky / 0x46 ThumbLow / 0x47 ThumbHigh);
binding = a permanent mode-0x10000 DIRECT mapping button → the weapon's **TriggerState** attr
(id **0x13**, = `fireImpulse@0x31C`; the binary MechWeapon table's ONLY attribute @0x511890);
the weapon self-edge-detects (CheckFireEdge @4b9608). **The default grouping is NOT messages —
it is the per-mech type-6 controls-map resource in BTL4.RES** ("Thrustmaster"/"L4" variants;
e.g. madcat: Trigger={4 weapons}, ThumbHigh={2}), installed at `MakeViewpointEntity` by the
ENGINE's `CreateStreamedMappings` (T0, L4CTRL.cpp) — which the port already called; it needed
only the TriggerState attribute published (id PINNED to 0x13 — our attr-id chain is 6 short of
the binary's; full binary attr-table dump in the task #5 scan) + an input feed. Keyboard now
pushes press/release edges into the buttonGroups (SPACE→Trigger, '2'→ThumbLow, '3'/CTRL→
ThumbHigh; BT_AUTOFIRE pulses the Trigger); the gBT*Trigger bypasses + pulse hack are retired.
VERIFIED 2-node at the BINDING level (⚠ honest correction, task #8: those kills still flowed
through the retired mech4 bring-up block -- the emitter DISCHARGE chain was latched shut by
the NaN edge bug below until 2026-07-11; the full weapon-fire chain is now verified solo).
- **✅ THE CONFIG-MODE SESSION — RECONSTRUCTED (task #6, 2026-07-11) [T1 anchors].** The
pod's in-cockpit weapon-REGROUPING UI, fully decoded from the binary + T0 engine:
* **The flow**: the pilot HOLDS a weapon's configure button (a per-weapon type-6
EventMapping, id 9, on the MFD-quadrant aux panels; e.g. BTL4.RES @0xAA3DC) →
MechWeapon handler id 9 "ConfigureMappables" @004b9550 (table @0x511860; the old
"Myomers fns" label swept) → mapper(slot 0)->EnterConfiguration @004d1840 (vtbl+0x38):
StartMappableButtonsConfigure (T0 CONTROLS.cpp:291 -- latches the active weapon for the
gauge, flips the GLOBAL mode NonMapping 0x10000 → Mapping 0x8000, so every normal fire
mapping goes dormant), re-arms the held button (+0x8000, id 9), temp-maps the four fire
buttons 0x40/0x45/0x46/0x47 → id 10. Tapping a fire button → handler id 10
"ChooseButton" @004b95b8 → RIO AddOrErase(direct) @004d262c: buttonGroup[v-1].AddOrErase
(0x10000, &fireImpulse) — a TOGGLE; the permanent 0x10000 instances ARE the commit.
Release → ExitConfiguration @004d18dc: StopMappableButtonsConfigure + strip the 0x8000
layer. Only the RIO (pod) mapper implements AddOrErase; L4/Thrustmaster carry RET
no-ops @004d195c/@004d1964 (a joystick cockpit cannot regroup).
* **Vtable truth (swept)**: base @0050F45C +0x38 Enter(Fail @004b0280) / +0x3C Exit(Fail
@004b029c) / +0x40 AddOrErase-event(Fail @004b02b8) / +0x44 AddOrErase-direct(Fail
@004b02d4) / +0x48,+0x4C Notify pair. There is NO "secondary vtable @0050f498" and NO
"CreateTemporaryEventMappings" virtual (RP-name drift). @004afbc4 (the mapper's shared
aux/zoom handler) is a pure Fail thunk ("Unhandled button mapping!", line 0x7a), not an
AddOrErase router.
* **Port landed**: MechWeapon handlers 9/10 + the GetMessageHandlers() accessor chain
(MechWeapon → Emitter/ProjectileWeapon → MissileLauncher, + GAUSS/PPC.CPP; dispatch
resolves through the CONCRETE class's SharedData — the empty per-class sets silently
swallowed the messages); the 6-arg Enter/1-arg Exit reshape; the L4 Enter/Exit full
bodies; MechSubsystem +0xE8/+0xEC corrected to controlDestination/controlMessageID (the
old hostEntity/subsystemId2 labels were wrong; MechWeapon ctor defaults the destination
to &fireImpulse per @004b99a8); dev harness = HOLD 'G' (BT_CONFIG_SLOT selects the
weapon; BT_CONFIG_TEST scripts a headless session).
* **ConfigMapGauge finding [T1] — CORRECTED 2026-07-21**: the old "authentically DORMANT /
no SetColor caller" claim was WRONG. @004c6ee0 is the virtual GaugeBase::LinkToEntity
override (vtbl @0051a1b8 slot 9 +0x24; @0x94 = linkedEntity, the Execute gate), and the
engine broadcasts LinkToEntity(viewpointEntity) at viewpoint bind (APP.cpp:1277 →
GAUGREND.cpp:3011) — the gauge IS live in the shipped game. The state loop
(@004c6f1c: 4 {y,button} pairs from DAT_00518eb4 = {13,Pinky}{37,ThumbLow}{61,Trigger}
{85,ThumbHigh}, x=0xc; buttonGroup[btn].GetMapState(controlDestination, subsys,
controlMessageID, 0x10000) → cm_off/cm_other/cm_only/cm_both) now runs on the authentic
LinkToEntity path; the BT_CONFIGMAP dev enable was deleted.
* **Bonus [T1]**: the binary MechWeapon ATTRIBUTE table @0x511890 has ELEVEN entries
(0x12 PercentDone .. 0x1C WeaponState) — future gauge-wave feeds; the "TriggerState is
the only MechWeapon attribute" claim was wrong (fixed in mechweap.hpp).
* Tails: weapon+0x110 (write 0/-1 on session open/close; port name vitalSubsystemIndex)
has NO confirmed reader yet; @004d1acc (+0x48 L4 Notify override) is unexported —
re-export from Ghidra.
* **Task #12 (2026-07-11) [T2]: the PoweredSubsystem POWER-ROUTING handlers landed** --
ids 4-8 registered (table @0x50F4EC): SelectGeneratorA-D = FindGeneratorByNumber
(@004b0b18 ROSTER walk matching generatorNumber@0x1E0) -> AttachToVoltageSource ->
modeAlarm=Connected; ToggleGeneratorMode (@004b0abc) = <2 -> AutoConnect, ==2 ->
Detach + ManualConnect. Weapons inherit via the MechWeapon handler chain (the old
qualified GetMessageHandlers() calls silently resolved to the engine Receiver ROOT
set -- same trap as the task #6 weapon handlers). The AutoConnect hunt's
GetSegment() scan corrected to the subsystem ROSTER (the +0x128 gotcha). Desktop:
F5-F8 assign the selected weapon (BT_CONFIG_SLOT) to Generator A-D, F9 toggles
Manual/Auto; BT_GENSEL_TEST scripts a headless re-tap. Note: Myomers/Sensor still
chain the root set (their binary tables unverified) -- extend if aux maps prove
they receive these ids. TWO MORE TRAPS found landing this: (a) MESSAGE_ENTRY
tables must be function-local statics INSIDE the accessor (cross-TU static-init
order emptied the table -- see [[reconstruction-gotchas]] #9 last bullet); (b)
**FUN_004ac9c8 is NOT IsDamaged** -- raw body: `owner(+0xD0) -> mech+0x190 player
-> +0x274 == 0` = the NOVICE-experience lockout for advanced cockpit systems
(2026-07-18 correction: +0x274 = the egg experience level, NOT a role class --
see [[experience-levels]]; port bridge BTPlayerRoleLocksAdvanced, btplayer.cpp;
NULL player = unlocked [T3]; seeded from the egg experience since issue #2).
✅ AUDIT TAIL CLOSED (issue #2, 2026-07-20): every powersub.cpp site annotated
FUN_004ac9c8 (PoweredSubsystem::HandleMessage @4b0efc, ForceShortRecovery
@4b11bc, Generator::HandleMessage @4b21d0, PowerWatcher::HandleMessage @4b179c)
plus MechSubsystem::IsDamaged and BT_IsBusLive now call the bridge; the
state-based HeatSink::IsDamaged shadow remains only for the gauge tint.
- **Myomers authentic coupling** — the structural un-stub is INERT (mover feed + heat-gen no-op).
The heat-model gate (`OwnerAdvancedDamage`/`FUN_004ad7d4` → the owning **BTPlayer**
`mech+0x190`+0x260 = veteran/expert experience — see [[experience-levels]]) is WIRED since
issue #2, but the sim stays arithmetically inert (the Owner* motion accessors return neutral
0s). Real coupling still needs those accessors + `MoverAttach` routing into the LIVE
JointedMover (must be reconciled with the gait cutover first).
- **SeekVoltageGraph** — 4 Seek* attrs unpublished (a cluster-child, not config-called; non-blocking).
- **✅ DAMAGE ECONOMY — AUTHENTIC (task #8, 2026-07-11) [T1/T2].** The whole economy closes:
`_DAT_004bafbc` is an x87 float80 = **1e-7** (the port's kDamageScale=1.0 was the mystery) --
it cancels the ctor's ×1e7 energy bookkeeping, so **damagePortion = authored DamageAmount ×
(charge/seekV[rec])²** (port: the closed form at fire time; the bring-up charge cycle keeps
the degenerate EC=1 electrical model). Authored values (BTL4.RES type-0x11, +0x19C; scanner
scratchpad/scan_weapsub.py): madcat AC=25/LRM15 salvo=50/ERLL=6/ERSL=2; bhk1 PPC=12/SRM6=35/
ERML=3.5. Zone model already byte-exact (Δlevel = amount/armorPoints, legs ×0.5; armor dump
in the task #8 synthesis) → the pod's 4-8-hit heavy-weapon pacing. The weapon-side
SendDamageMessage (@004b9728) is LIVE from Emitter::FireWeapon (damageData filled incl.
**damageForce = targetmuzzle → the gyro's directional hit-bounce feed**); the mech4
bring-up damage block + flat kShotDamage are retired to diag hooks **(CORRECTED, task #60:
`kShotDamage=12` was only HALF-retired — still LIVE at the kill-score `mech4.cpp:1551` until
task #60 replaced it with `lastInflictingDamage`, the real killing-blow magnitude; only the
cross-pod force hook `mech4.cpp:3255` was env-gated. See the task #60 reconciliation below).**
**ROOT-CAUSE find: the
CheckFireEdge NaN latch** -- TriggerState carries ControlsButton INTS (release 65 = a
negative NaN); the binary's x87 unordered-compare treated it as "released" but an
IEEE-correct float compare latched the detector shut after the first release (why the
emitters never discharged in-game until now); fixed with bit-pattern sign compares
(@004b9608). Residues [T3]: heat stays on the bring-up scale (authentic = heatCost×1e7
energy units, the missile path already feeds it -- the HEAT-CALIBRATION audit);
LODReuseHysteresis corrected 0.82→0.33 (a double); the MP beam-kill live-verify awaits a
clear-sightline spawn (mechanism identical to solo; force-dmg cross-pod cycles verified).
- **✅ DAMAGE-ECONOMY RECONCILIATION (task #60, 2026-07-13) [T1/T2].** Full 5-path audit vs the
decomp (5-finder + adversarial-verify workflow). **Clean as-is:** energy beam (`emitter.cpp`,
closed form byte-exact; `_DAT_004bafbc` dumped = x87 1e-7), autocannon (`projweap.cpp`, full
authored DamageAmount from resource +0x19C; the 0.0625 at :667 is the shooter's own gyro recoil,
not the round), zone-armor BASE model (`damageLevel += amount·damageScale[type]`, engine
DAMAGE.cpp:379, legs ×0.5, 1.0=destroyed). **3 stand-ins FIXED:** (A) `mechdmg.cpp:451`
`mech->stance` (phantom, perma-0 → the leg-shot-out fall/death branch was DEAD) → `MovementMode()`
(mech+0x40, @part_012.c:6910); (B) `mechdmg.cpp:458` `IsAirborne()` (always-0 stub) → `IsDisabled()`
(@0049fb54) — the task-#52 wreck-graphic corruption fixed AT SOURCE (was only masked by the
IsMechDestroyed latch); (C) `mech4.cpp:1551` flat `kShotDamage=12` kill-score → `lastInflictingDamage`
(real killing-blow, latched in TakeDamageMessageHandler; the score handler @0x4c02e4 derives the
whole kill award from it, so a flat 12 scored every kill identically). Phantom `int stance` slot
reused for `lastInflictingDamage`. **KB corrected:** the "damageScale[5] EVEN=1/armorPoints"
prose is a mild inaccuracy — the array is 5 cells indexed by damageType directly
(Collision/Ballistic/Explosive/Laser/Energy); the per-type value IS ≈1/armorPoints for the
destructive types (net model unchanged). See [[combat-damage]].
- **DEFERRED (task #60-D): the missile CLUSTER model.** The port fires N flying rounds each
carrying `authored/N` damage (net salvo total = authored, so per-armor damage is correct), but
the binary (@004bcc60) fires ONE missile whose `burstCount` is a RANDOM cluster-hit roll
(`min(rand(0..N-1)+N/4, N)`, FUN_004bef78) applied to ONE cylinder-resolved zone. The port thus
loses (i) cluster-hit VARIANCE (always full N) and (ii) single-zone CONCENTRATION (N rounds
scatter across zones). Net-neutral on total armor, so LOW priority. **OPEN semantic gating the
severity:** does `burstCount` multiply ARMOR damage or only the gyro kick? DAMAGE.h:49-54 [T0]
says "times to apply the damage" (→ apply-count → port total authentic); settle by reading the
consumer chain FUN_004bef78 → FUN_004be078 → EXPLODE.cpp:209-210 (does it loop burstCount over
DamageZone::TakeDamage or apply once?). Fix only if made byte-faithful (roll clusterHits, thread
`burst` through BTPushProjectile, one damage-bearing round + rest visual-only).
- Also open (bring-up, not stand-ins): `scoreAward=0` (btplayer.cpp:1526 — the authentic kill
bonus lives in the un-exported master-perf writer 0x4a9770-0x4ab188, T4); the exact authentic
kill `damageAmount` from that same writer (msg+0x24) is unrecovered — the killing-blow magnitude
is the faithful stand-in until it is.
- **HEAT (task #9, 2026-07-11): authentic 1e7-unit heat LIVE; conduction ROUTING defect
remains [T2/T3].** Landed + byte-anchored: emitters emit `heatCostToFire x 1e7 x
(charge/seekV)^2` (closed form; PPC = 1.1e8 -> +632 K on its 174000-mass sink); the missing
projectile/missile heat adds (raw pre-scaled resources); the bank's AMBIENT RADIATOR @4ae73c
(the system's only heat exit: relax toward 300 K, conductance x 0.1 x HeatSinkCount
[_DAT_004ae974 float80 = 0.1 -- the audit's "1.7375" was a misread]); the link-attach guard
corrected (skip = the 0xBBE bank, NOT Condenser -- the inversion had closed the system);
three coolant epsilon corrections (0.0025/0.003/1e-4 for the old single 1e-4) +
CoolantCapacityScale = 0.05 float80 (both prior readings wrong). Verified: heat flows,
exits (~2/3 of a 1e10 spam run drained), weapons overheat under max-rate autofire exactly
as the authored-constant simulation predicts (thermal spam is unsustainable BY DESIGN; the
authentic overheat cutout is the FailureHeat CHARGE-HOLD -- `ResetFiringState` + currentLevel=0
until it cools, @004baa88 -- NOT a range collapse. ⚠ CORRECTION (`aab7a8a`, task #50):
effectiveRange = (1 - HOST-ZONE damage) x weaponRange reads `Subsystem::damageZone->damageLevel`
(weapon+0xE0), NOT heatLoad; the old "heatLoad -> effectiveRange 0" mechanism here was the same
@0xE0-DamageZone-vs-heat misattribution fixed in heat.cpp:803).
- **Task #10 -- the "scrambled routing" RESOLVED (2026-07-11) [T2].** The routing was NEVER
scrambled: a `[heat-link]` attach log proved every subsystem links its authored sink
exactly (bhk1: PPC_1->C4, PPC_2->C6, ERM_1/2/3->C1/C6/C4, SRM6s->C2, generators->C1/C2/C3/C5,
condensers+reservoir->bank@slot2). The "pools in Condenser1" observation was the
DIAGNOSTIC-SAMPLER ALIASING trap (one shared static 1-Hz timer, pendingHeat-gated); the
census log is now per-instance. The REAL defect was the fire RATE: emitter.cpp's local
`FUN_00417ab4` stub returned NULL, so although the PoweredSubsystem ctor attaches the
authored generator to `voltageSource@0x1D0`, the Emitter TU could never see it -- the whole
electrical model was inert and the E7 force-charge recharged every emitter in ONE frame
(~0.3 s cycle, 1501 fires/90 s, ~1.7e9 heat/s input). Fixed authentically [T1, disasm +
byte-verified constants]:
* Emitter ctor @004bb120: seekVoltage[i] = authored fraction x generator ratedVoltage
(authored 10000); EC = energyTotal/(seekV[rec]^2 x 0.5); **voltageScale@0x310 =
(RechargeRate / -ln(1 - 0.0001 x seekV)) / EC** -- the exponential charge reaches
seekV[rec] in EXACTLY the authored RechargeRate on a cold generator (PPC 5 s, ERLLaser
4 s, SRM6 3 s, ERMLaser 2 s, LRM15 6 s). The gate reads the OWNER's simulationFlags
(the usual ctor-gate gotcha; the old this-flags read never armed).
* `PoweredSubsystem::ChargeTimeScale()` (@004b0d50, was a =1.0 stub): voltageScale x
(1 + thermalResistivityCoefficient x max(0, srcT - srcT0)) -- a HOT generator charges
slower ("voltageScale is never read back" was wrong -- this reads it every tick).
* TrackSeekVoltage @004ba838: charging I^2R (`seekRate^2 x dtScale x dt`) lands in the
GENERATOR's pendingHeat@0x1c8 (~3.5e8 per full PPC charge -> generators self-heat ->
conduct to their condensers -> slow further charging). This closes the heat/firepower
feedback economy.
* **FailureHeat consumers found [T1, disasm]:** `this+0x184` (the weapon's own heatAlarm
status level) == 2 gates BOTH families -- @004baa88 (emitter: ResetFiringState +
currentLevel=0 until it cools) and @004bbd36 (ballistic: recoil pinned to rechargeRate +
alarm state 7). Emitter::GetFaultState() now returns heatAlarm.GetLevel() (was a 0 stub).
* ProjectileWeaponSimulation @004bbd04 opens with `call 0x4b0bd0` (disasm-verified) -- the
launchers now run the powered/heat step (their task-#9 firing heat previously accumulated
in pendingHeat forever; SRM6s now census at ~950 under sustained fire).
Verified live (120 s max-rate autofire): PPC ~470-500 (was 55,000), bank plateaus ~600 and
sheds, generators 1100-1400, ERMLaser self-regulates at the 2000 failure threshold
(shutdown -> cool -> resume). Remaining tails: the HeatWatcher/AmmoBin cook-off
watchedLink; the mech-level heatAlarm@0x450 producer; the cockpit power-routing message
handlers (@004b099c..@004b0abc: assign weapon to Generator 1-4 + auto/manual toggle via
modeAlarm@0x2CC -- wired to buttons, not yet reachable).
- **Task #11 -- the live-play regressions (2026-07-11) [T2].** User-reported after the
electrical model landed. THREE distinct causes, all resolved:
(1) *Phantom fire on the enemy*: the mech4 bring-up shot block painted an explosion at the
victim on its OWN 0.3s cadence whenever fire was held -- desynced once real recharges landed.
RETIRED (the authentic impact visual flows from each real discharge via the messmgr's
SubmitExplosion). (2) *Weapons bricking one by one* ("fire ~1s then cuts out"): the
FRAME-PACING TRAP -- see [[reconstruction-gotchas]] §12: the Loading->Loaded snap window
(+-0.01 around seekV) assumes the pod's locked 60 fps; a dt spike jumps it, the overshoot
clamp zeroes rechargeLevel, and the weapon sticks in Loading at level ~10000 forever.
Fixed with pod-frame (1/60s) sub-stepping of the binary's own Loading tick (also fixes the
I^2R integral overfeeding generator heat on big steps). (3) *Slow kills / short fire
windows*: AUTHENTIC -- beams last the authored DischargeTime, recharges take the authored
2-5s stretched by hot generators (ChargeTimeScale), and max-rate autofire equilibrates
generators in the degradation band (1100-1500 K; the FailureHeat breaker never trips in
solo max-abuse -- duty-cycle measured). The pilot's authentic counters are the systems
under reconstruction: config-mode regrouping (task #6), generator reassignment (ids 4-8),
coolant valves.
- **✅ AUTOCANNON JAM / coolant-priority — FAITHFUL + CONFIRMED against QA ground-truth, SOLVED
purely from the decomp (2026-07-14) [T1/T2].** An original QA dev reported: "all AFCs jam on their
3rd shot if coolant priority isn't boosted; boosted → almost never jam." VERDICT: the whole chain
is reconstructed end-to-end, FAITHFUL, and reproduces the reported behavior — no bug, no stand-in.
**DO NOT "fix" it by driving a weapon's `coolantFlowScale` from the valve; that field being a
constant 1.0 for weapons is AUTHENTIC** (the ONLY writer of any HeatSink `+0x15C` from a valve value
is `RecomputeCondenserValves`/FUN_0049f788, which iterates ONLY the condenser chain `mech+0x7cc`).
Mechanism: a weapon dumps `heatCostToFire` (AFC100=6.5e7 raw) into its OWN linked condenser (AFC100→
Condenser4); the condenser sheds to the shared bank at a rate scaled by its valve SHARE
(`coolantFlowScale = valveState_i / Σ valveState`, MoveValve msg id 4, detents 1/5/50/0). The
weapon's OWN thermal conductance (35500) caps how fast it sheds into its condenser, so coolant
priority shifts the weapon's equilibrium only ~100 K — but that is EXACTLY enough to straddle the
`degradationTemperature`(1000) jam line at combat fire rates. Cross `1000``heatAlarm`≥1 → the
jam roll arms (`p=clamp(0.41·T/failT, 0.05, 1)`); on a hit → sticky `JammedState`(5), clears only on
ResetToInitialState. (Distinct: `failureTemperature`(2000) → self-clearing overheat lockout,
recoil-pinned + weaponAlarm 7.) **ALL constants VERIFIED AUTHENTIC vs the raw BTL4.RES bytes**
(`scratchpad/afc_dump.py`): AFC100 startT=77, degradT=1000, failT=2000, thermalConductance=35500,
thermalMass=94700, heatCostToFire(+0x1A4)=6.5e7, recharge(+0x190)=8, damage=25, minJamChance=0.05 —
no mis-scaling. **THE ANSWER IS FIRE-RATE-DEPENDENT** (measured, BT_JAM_LOG/BT_VALVE_BOOST/BT_AF_PERIOD,
madcat AFC100 → Condenser4): at MAX spam (8s recharge) the AFC overheats regardless (~1460 boosted
~1360 — both jam); at a SLOW cadence (16s) it plateaus ~770 and never jams; at the ~11s COMBAT cadence
it is DECISIVE — **not boosted → 1058 K (jams), boosted (Condenser4 valve→50, others→1) → 953 K (never
jams)**. So the QA is right and the reconstruction is faithful; the earlier "boost is weak" read was
an artifact of testing at max spam (above the window). Diag probes committed: `BT_JAM_LOG`
(projweap.cpp), `BT_VALVE`/`BT_VALVE_BOOST=<condenserNum>` (heatfamily_reslice.cpp), `BT_AF_PERIOD=<sec>`
(mech4.cpp). See [[combat-damage]], [[subsystems]] heat.
- **Subsystem-panel online/offline gate — fields unidentified [T4] (2026-07-12).** The binary's
SubsystemCluster draw-state reads `*(subsystem+0x40)==1` and `*(subsystem+0x278)!=4`; neither
1995 field is identified (weapon simulationState is 0/2/3/4 -- +0x40 isn't it). The port pins
the panel ONLINE (GetDrawState()==1) because the old raw reads were layout NOISE that flapped
the state and black-filled the panel over the recharge arc (the frozen-PPC-dial bug; the
WeaponCluster repaint-heal also landed). Identify both fields to restore the authentic
destroyed-subsystem dark-panel look.
## Locomotion / combat polish (non-gating)
- **✅ Interior vs exterior gait CLIP SET — DONE 2026-07-13 (task #59) [T2].** The local cockpit
mech was leaning 8° into every walk because the authentic ctor clip-set gate
(@part_012.c:10308-10320) was a no-op (`LoadLowDetailBody`/`LoadHighDetailBody` mislabeled the
FUN_004a80d4/86c8 clip-loader addresses) and `LoadLocomotionClipsExt` was a stub aliased to the
exterior loader. Reconstructed the real 4-char INTERIOR (`'i'`-suffix) loader (omits `jointhip`,
shakes `jointshakey`). Because the port never sets the replicant copy bit the ctor gate lands
everyone on interior, so `Mech::MaintainViewClipSet()` picks the set by VIEWPOINT each frame
(your mech → interior/level; every other mech → exterior/lean). Both verified live: cockpit
level, peer replica leans 8/11°. See [[locomotion]] "The walk lean IS authored".
- FOLLOW-UP (non-blocking): if the copy bit ever gets set at construction for other reasons,
the ctor gate already handles it and MaintainViewClipSet becomes a no-op agreement — no
conflict, but revisit whether the per-frame maintenance is still needed.
-**Authentic per-mech TURN-RATE — DONE (task #64b, 2026-07-14) [T1/T2].** The master-perf
disasm (0x4aa3d3-0x4aa4ff) turn-rate lerp (walkingTurnRate@0x574 → runningTurnRate@0x578 by
ground speed, run-speed refinement) is wired into the drive, replacing the `kDriveTurnRate`
bring-up constant; the rates (which the `Wword(0x15d/0x15e)` no-op was discarding) are real
members now. Also the trn-in-place DISPATCHER (entry/exit) reconstructed from the same disasm,
replacing the task-#64 `0.25×`/fast-forward stand-ins. See [[locomotion]].
- Body-callback gimp handlers (states 16-19, targets 0x70b2/0x7161 undecoded → fall back to stand);
airborne callbacks (`FUN_004a6344`/`FUN_004a7970`).
- Wall-block-vs-climb tuning. **✅ Collision DAMAGE application — DONE 2026-07-08 [T2].** The two deferred
dispatches in `Mech::ProcessCollision` (mech4.cpp: mech-vs-mech `:15324-15358` + icon-crunch `:15369-15401`)
now fire: on a collision with another Mech or a CulturalIcon, an `Entity::TakeDamageMessage{zone==1}` is
dispatched to the victim (via `BTDispatchCollisionDamage`, the engine ctor — same as the weapon path), and
STEP 6's cylinder table resolves the impact point → a zone on the receiver. Terrain (walls/hills) matches
neither branch → blocks without damage (faithful). Reachability is guaranteed (`Mover::ProcessCollisionList`
calls the **virtual** `ProcessCollision``Mech::ProcessCollision`); built + stable. The live dispatch
wasn't captured headlessly (the solo auto-walker never rammed a tree/mech), but the path is proven reachable
and composed of runtime-verified pieces. **Note:** validated STEP 6's height ref along the way —
`collisionTemplate->maxY ≈ 7.1` (a real mech height), confirming `CylinderReferenceHeight` reads a height,
not the heat value the `mech+0x2ec` dual-labeling (heat-gauge sink vs groundRef, mech4.cpp:2697) hinted at.
- **✅ Cylinder hit-location (STEP 6) — DONE + LIVE 2026-07-08 [T2].** Built + runtime-verified: the `dmgtable.cpp`
classes now have real storage + a working `ResolveHit→zone`, the mech ctor loads the type-0x1d table by the
DamageZoneStream name (`[cyl] table 'bhk1' layers=7`), and `Mech::TakeDamageMessageHandler` resolves
unaimed (zone==1) hits before base-routing. ⚠ The FIRST "verified" claim was PARTIAL: the table was
cached via `Wword(0x111)` (the ABSORBER — stores nothing) so every unaimed hit silently no-op'd; fixed
2026-07-08 by promoting the cache to the named member `Mech::damageLookupTable`
([[reconstruction-gotchas]] §2). Now verified end-to-end (19 zones resolving, `BT_AUTOFIRE` harness).
Full change list in [[combat-damage]] "STEP 6 COMPLETE". Historical investigation notes retained
below for provenance.
**Re-investigated 2026-07 (see [[combat-damage]] for the full algorithm):** the table (height×angle
grid, resource type 0x1d) IS built (recon `StandingAnimation` @Mech[0x111], but with an EMPTY name → 0 rows,
because `ResourceFindByName` is a **no-op template stub**). **CORRECTION to the earlier note:** the LOOKUP is
**exported** [T1] — `FUN_0049eb54` (table height→row) + `FUN_0049e678` (row angle→cell), NOT unexported.
Only the thin `Mech::TakeDamageMessageHandler` glue (zone==-1 → call the lookup → `damageZones[zone]->TakeDamage`)
is unexported, and it's fully implied by pieces already reconstructed → no disassembly needed. Remaining:
6b real `FindByName` (name = the mech's type-0x14 DamageZoneStream name @ResourceDescription+0xc); 6c the 3
container ctors + the lookup as a `BTCylinderResolveZone` bridge (ground the cell zone-dict stream format vs
the real .RES bytes); 6d register the handler + verify. Feasible; the only open risk is the cell stream format.
The earlier-cited `FUN_004a0230`/`FUN_0049ed0c` don't exist.
- **Death sequence — CORE DONE 2026-07-08 [T2]; effects+anim deferred.** `Mech::UpdateDeathState()`
(mech4.cpp) reconstructs the death STATE machine (collapse `movementMode` → RP `VTV::DeathShutdown`
subsystem loop → settle to disabled/frozen; wreck stays, no crash) — runtime-verified (`BT_DEATH_LOG`).
Full map + citations in [[combat-damage]] "Death SEQUENCE". ✅ (b) `MechDeathHandler` DONE (mechdmg.cpp) +
✅ the render-side **RemakeEntity** destroyed-mesh swap DONE 2026-07-08 (btl4vid.cpp
`RemakeEntityRenderables` + `BTRemakeMechModel` bridge; in-place `SetDrawObj` swap — the tree dtor does
NOT cascade so never rebuild). ✅ (a) **RESOLVED — no collapse animation exists in BT 4.11** [T1]: the
fall latch (modes 5-8 → table slots 0x1c-0x1f) is a vestige — no loader fills the slots, no fall clip
ships, and firing it would bind resource 0 (an AUDIO stream) as keyframes. ✅ The authentic death
VISUAL is the **WRECK-HULK SWAP** (death ModelList `blhdead` → effect 104 → the victim becomes its
`<mech>dbr.bgf` burning hulk) — RECONSTRUCTED + verified (`SwapToWreck`, btl4vid.cpp; see
[[combat-damage]] "Death SEQUENCE" for the full chain + proofs).
✅ The wreck **sinks** (the 1996 quadratic burial, `offsetY = -0.025·t²`, gone ~17s) with the `ldbr`
debris field; at burial it goes **INERT** (collisionVolumeCount=0 + target lock dropped — no phantom
blocking/hits). **Remaining**: (c) the whole-mech **DeathSplash** radius damage; wreck mesh FLAMES
(flamesml/flamebig + sweep flicker, 1996 script case 4); the **full entity teardown at burial**
(the authentic death-row removal — needs the mech render tree unhooked from the renderer first;
the inert wreck is the safe stand-in until then).
Do NOT issue DestroyEntityMessage on death (before the render-tree teardown lands).
-**Critical-subsystem plugs BOUND — damage propagates (task #2, 2026-07-11) [T1/T2].**
The "binding write is elsewhere" reading was WRONG: the binding is IN the zone ctor loop
(@0049d0e1-0049d10d) — **Ghidra dropped the two argument pushes** ("type propagation not
settling"), making `Slot::AddImplementation(roster[streamedIndex])` read as a bare no-arg
call (mislabeled Resolve). The stream's third per-entry dword IS the roster index into
`subsystemArray` (mech+0x128). Port: the `DZSlot` stand-in (always-null Resolve) replaced
with the engine `SlotOf<T>` (binary vtables 0050bb84/0050bb7c ARE its instantiations); bind
gated MasterInstance+DynamicFlag (`simulationFlags@+0x28`); replicant plugs stay unbound
(master-authoritative). `SendSubsystemDamage` rewritten to the recovered @0049c9a8 logic:
the unused crit allotment lands in the subsystem's OWN private 0x160 DamageZone (the old
`SubProxy2` cast to a mech zone was wrong), ≥1.0 → statusAlarm Destroyed + gated PrintState
+ zone valve, vital → `graphicAlarm 9` (mech kill); `CriticalHit` now calls the real
`ApplyDamageAndMeasure` (@4ac07c); the artifact `parentArtifactZone.Add` back-pointer bind
also revived (LOD damage averaging). `videoObjectFlag` was `vitalSubsystem` (+0xE4,
res+0x48 "VitalSubsystem"). VERIFIED live (`BT_CRIT_PROBE=<zone>` diag): 66 plugs bound at
ctor per mech, zone destroyed → crits damaged/DESTROYED with authentic accumulation.
Residue: roster idx 0/1 (mapper installed post-ctor / voltage-bus stub) stay unbound with a
logged skip — none of the 8 mechs' streams crit-list them in practice.
- **✅ Two dead Wword comparison branches — DONE (task #1, `c9f0c2a`).** `Mech::ReadUpdateRecord`
case 3 now reads real named fields off `Mech__StateUpdateRecord` (legState / stability / legResetLatch /
speedDemand); the always-false Wword/BTVal branches are gone (the old `mech.cpp:1511`/`:1613` line
numbers no longer point at that code).
## Multiplayer (Phase 7 / P6)
- **Gitea #12 MP incident (2026-07-19) — root causes found; BOTH FIXES LANDED 2026-07-19
(awaiting the human MP death-and-survive verification).** Findings [T1/T2 — see the #12
comments + `scratchpad/incident_2157/`]:
(1) *Gauge-window freeze* = lobby-built gauge tree dangling over a mission-launch mech
RE-STREAM after a pre-launch host drop/rejoin; SEH guard disables each faulted gauge
permanently — see [[gauges-hud]] §MP gauge-window FREEZE. ✅ FIX LANDED:
`BTL4GaugeRenderer::TearDownForViewpointRestream()` (alarms + Remove(0) + entity grids — the
entity-bound half of the engine `ShutdownImplementation`), called from
`MakeViewpointEntity` before `ConfigureForModel` on a viewpoint RE-make; the tree rebuilds
bound to the new mech. Sentinel: `[gauge] viewpoint re-stream: tearing the gauge tree down`.
(2) *Missiles cycle-but-never-launch*: ✅ FIX LANDED — the port's RivetGun-approximation
ProjectileWeaponSimulation replaced with the disasm-exact @004bbd04 machine
([[decomp-reference]] §5; projweap.cpp): fault gate reads `simulationState@0x40==1`; the
`simulationFlags|=0x1` DelayWatchersFlag poisoning removed everywhere
([[reconstruction-gotchas]] §20 — emitter SetDirty split into ForceUpdate/ExecuteOnUpdate per
site); view/target gating + the `bin->FeedAmmo` pull + recoil set moved into the caller's
Loaded case with the authentic 4-blip stay-Loaded denial; recoil bleeds ONLY in Loading at
electrical-Ready and clamps at 0; slot-17 @004b9c9c called from Loading/7 (the launcher
recharge dial now ANIMATES). State 7 kept binary-faithful (re-asserted unconditionally —
permanent once entered; recovery only via ResetToInitialState/mission reset). Solo-verified
on 4.11.388+: 54-60 `[projectile] PUSH` + impact smoke; denial window (BT_LOOK_TEST
look-back) = trig pulses, state STAYS 2, zero launches, resumes on release; disabled-mech
gate pins enemy launchers at 7+full recoil; STREAK-6 dial sweeps across screenshots; lasers
fire throughout. Open fidelity question REMAINS: is launcher FailureHeat/jam-rate at
sustained autofire authentic-intensity? (both SRM6s jam within ~2 min of held autofire via
the bring-up CheckForJam heat gate; emitters never trip it — task #11 duty-cycle
measurement.)
(3) *J/K/L preset hypothesis REFUTED for solo* [T2]: BT_PRESET_TEST cycling on 387 — missiles
kept launching after presets (36 PUSH before / 24 after), pip count unchanged, gauges live,
[mode] masks touch ONLY ModeMFD bits 0-14 (`reticleElementMask` is a different object).
Corroborating: a 2-player LAN session on the same build (no J/K/L) had working missiles +
normal pips. UNTESTED: J/K/L during a live RELAY MP session.
(4) *"WAY MORE pips" (BLH, symptom 4)* — UNRESOLVED: the incident registered 10 pips
(8×3016 @750/500×5/225×2 + 2×3024 @800) which matches NO stock arsenal (solo dumps: bhk1=7,
madcat=6, thor=7); the reticle built identically TWICE (lobby + launch re-stream). Needs the
incident egg / a live 3-seat repro to identify the mech that owned those 10 weapons
(custom operator-egg loadout? viewpoint mis-assignment at build time?).
- **✅ Console-death peer-replication freeze — NOT REPRODUCIBLE + the one real bug FIXED
(2026-07-15) [T2].** The 2026-07-14 report (btconsole.py died mid-match → peer↔peer replication
froze both ways) did NOT reproduce in the current build under EITHER failure mode, tested directly
(BT_REPL_LOG on a circling peer, affinity-pinned): (1) a clean relay kill — the replicant kept
circling smoothly through the kill, NO disconnect logged, node survived; (2) a deliberately STUCK
console (`scratchpad/btconsole_stuck.py` — connects, starts the mission, then stops recv()'ing while
holding the socket OPEN = the exact "receive pad full → close never seen" mode hypothesized) — the
replicant kept moving the entire run. So **peer replication is INDEPENDENT of the console**: the
pods replicate peer-to-peer over the GAME socket; the console is a separate egg/mission/status
channel. The original freeze was a transient (plausibly the same single-box packet-jitter /
CPU-contention artifact root-caused 2026-07-15, `49d73dc`) or was fixed by later MP work.
**The flagged latent bug IS real and is now FIXED:** `HostDisconnectedMessageHandler`'s
ConsoleHostType branch closed **`gameListenerSocket`** (the GAME listener) on a CONSOLE disconnect —
a naming bug; the comment + the commented-out `OpenConnection` intended a CONSOLE re-listen, which
`CreateConsoleHost()` already does. Removed (L4NET.CPP:969). Harmless to established peer sockets
(why a live match keeps replicating through console loss), but it would have blocked a NEW peer
from joining after a console cycle. Verified: 2-node still connects (`All connections completed!`)
+ the peer circles. Diag harness retained: `scratchpad/btconsole_stuck.py`.
- ✅ Cross-pod COMBAT — DONE (tasks #46/#47: replicant targeting + damage rerouted to the owning
master, full cross-pod kill verified). ✅ Replicant GAIT animation — DONE (task #50: replicant
derives speedDemand from the replicated velocity, full stand→walk→run lifecycle verified).
See [[multiplayer]] — the entries here were stale (swept 2026-07-11).
-**Mech-level update records (@0x4a0c2c) — DONE (task #1, 2026-07-11):** all 9 types
transcribed + 2-node verified; the peer's death SINK/burial, knockdown, heat and state now
replicate (the observer's replicant runs its own wreck loop off the replicated
simulationState). Full mechanics in [[multiplayer]] "Mech-level update records". Residue
[T3]: stream the model's UpdatePositionDiffrence/TurnVelocity/TurnDegree deadband constants
to the senders (stand-in thresholds meanwhile).
- The WinSock2 stack (L4NET.CPP) is 2-node verified end-to-end (cross-pod kills, beams,
replicant gait — tasks #46-#51). Real-IP pod-LAN config VALIDATED 2026-07-15; **D1 relay mode
+ UDP channel + LAN discovery + relay-assigned seats + operator console shipped 2026-07-18**
(see [[multiplayer]] D1). Remaining (config, not code): an actual two-physical-machine
internet session through the relay (ports verified open; `players/join.bat` ready).
- **✅ Subsystem-record replication IS wired — the AC just wasn't using it (task #61,
2026-07-13) [T2, CORRECTED].** (An earlier draft of this note WRONGLY claimed "no
subsystem-record channel" — that was concluded from an AC-only test before the AC's
ForceUpdate gap was found. The channel exists and works.) MECHANISM: the mech's per-frame
tick calls `subsystem->PerformAndWatch(till, update_stream)` for every subsystem
(mech4.cpp:3893) → base `Simulation::WriteSimulationUpdate` walks the subsystem's `updateModel`
and serializes any set record; `Entity::UpdateMessageHandler` (ENTITY.cpp:387) routes each
incoming record to `GetSimulation(subsystemID-1)->ReadUpdateRecord` on the peer. A subsystem
replicates its fire IFF its `FireWeapon` calls **`ForceUpdate()`** (sets the updateModel bit).
The EMITTER (beam) and MISSILELAUNCHER (salvo mirror, mislanch.cpp:326) both do → enemy lasers
+ missiles ARE visible on the peer. The AUTOCANNON set only `simulationFlags |= 0x1` (the
+0x28 instance flag, NOT updateModel) and had no fire record → its shot never serialized → the
enemy's cannon (tracer + DAFC muzzle flash) was invisible. FIX: added
`ProjectileWeapon::WriteUpdateRecord/ReadUpdateRecord` (fire counter + aim, the AC twin of the
salvo mirror) + `ForceUpdate()` in FireWeapon. Verified live: watching node now logs
REPLICANT AC shots + muzzle flashes. **LESSON (methodology):** the binary-coverage audit is a
function-PRESENCE census — it can't see "reconstructed but inert" code (a function that exists
but is never CALLED, like the salvo mirror was thought to be, or a missing ForceUpdate). A
LIVENESS audit (does a live call path reach each reconstructed fn?) would catch this class.
## Rendering follow-ups (non-blocking)
- ~~Per-pilot mech PAINT (color/badge/patch)~~ — **DONE 2026-07-17, verified live** (crimson MadCat +
yellow VGL emblems + hip hazard stripes). Mechanics + the vehicletable color/badge/patch name
tables in [[rendering]] §Per-pilot mech PAINT. Residual nit: eggs saying `color=Red` mean
**Crimson** (table has no Red; the binary Fail()ed on it, the port warns + drops).
- **.PFX effect-layer polish (deferred 2026-07-08; the layer itself is LIVE + verified).** The
reconstructed BT particle layer (L4VIDEO.cpp, see its banner) renders the authentic .PFX content
(fire + smoke, premultiplied blend, impact-frame oriented — weapon hits AND damage bands via
`lastInflictingID`, now written by `Mech::TakeDamageMessageHandler`). Remaining nuances, all
cosmetic: (a) `atten/attenv` distance attenuation not honoured; (b) the per-file texture name
(`btfx:firesmoke1_scr_tex`) — all effects share the radial-masked grit sheet; decoding FIRE.BSL
would give the authored sheet; (c) beams draw before particles, so a beam through thick smoke
reads too bright (pass-ordering); (d) `colorWarp/alphaWarp` applied as `t^(1/warp)` [T4 convention];
(e) emission uses rate×releasePeriod batching [T4 convention vs the exact 1995 emitter].
- Replacement-LOD selection for NON-additive multi-LOD models (needs the board's hot-spot/
reference-point semantics; leads: `s_dplobject.lod_ranges[16]/lod_hot_spot`, 0x2047/0x2048).
- Day/night MATERIAL path priority (BMF/IMG) — infrastructure done, gated `BT_MATPRI` (over-applies
to shared terrain mats); needs the terrain-material model understood.
- RGBA4444 alpha (TREE.BSL cutout) decoded but not alpha-tested outside punch batches; VTX/TGA paths.
## Content build sub-project (low priority)
- Lab/other-build maps (`des`/`burnt`/`frstrm`) are source-only (`.map` in CONTENT/BT/MAPS/); would
need compiling into a RES via the DOS `btl4tool.exe`. The 8 RES maps cover testing.
## Key Relationships
- Feeds from: every subsystem/render topic (their deferral notes collect here).
- Gating master: [[source-completeness]] (the missing BT source).
## Peer coupled-motion architecture — ✅ DONE (task #50, 2026-07-14/15) [T1/T2]
RESOLVED. (This block was the old COMPLETION PLAN from `ea39af1`; it was left stale after
the work actually landed — reconciled 2026-07-15.) The coupled single-source gait pipeline
is reconstructed, DEFAULT ON, and the residual was root-caused. See [[multiplayer]] two
sections: **"Authentic coupled peer motion — DONE"** (linear position = BODY gait channel
integrated into `projectedOrigin@0x260`; angular = replicated-velocity slerp; master
SEND-mirror runs the SAME `IntegrateMotion` predictor fed the last-sent `bodyTargetSpeed`)
and **"Peer motion: the 'random shakiness' is single-box packet jitter, NOT the game"**.
- Landed: coupled path promoted to default (`c52a1ad` + `b013742`); peer poses from the
BODY channel (`96a896a`); turn-step on the body channel (`f094d78`); true-mirror cadence
driving the clip from actual replicated motion (`23f1532`). Groundwork: exact quaternion
integration (`a8eb8a4`), incremental peer heading (`d78e77b`), clock guard (`93456be`),
commanded-speed cadence feed (`6374efc`), Abs-macro fix (`7615ecd`).
- **The old "needs the MASTER HALF" conclusion was SUPERSEDED:** the leftover accel/decel
shakiness was ROOT-CAUSED as single-box **packet jitter** (`49d73dc`) — two Debug nodes on
one box batch TCP delivery, so records arrive bursty (BT_RXJIT burstiness 3-7x) and the peer
dead-reckons across the gaps then snaps. Pinning each node to a disjoint core set
(`ProcessorAffinity 0x00F/0x3C0`, baked into `tools/mp_launch.sh`) restored even ~17ms
delivery and the shakiness vanished (user-confirmed). Real pods are dedicated machines and
never see it. **DO NOT add an interpolation/jitter buffer** to mask a rig artifact.
- Minor non-gating follow-ups (tracked in [[multiplayer]]): the peer accel/decel drift
(flagged ~2.9u, but **MEASURED ~0.64u max** on the affinity rig with the peer on the body
channel -- `BT_SNAPLOG` + `BT_DRIVE_SWEEP0`, 2026-07-15; sub-unit, effectively resolved,
no fix warranted); the exact `_DAT_004ab9cc` offset-decay constant; whether
`IntegrateMotion`'s 2-stage angular integrate is intended.
- Speed model decomp-settled (`2c6db6a`): analog throttle, '5 speeds' false, `throttleState@0x4a4`
is actually the fall-surface material (RENAME PENDING).
## Missile launcher trips FailureHeat -> permanent NoAmmo brick (REPRODUCED 2026-07-20)
Playtest: "lost the ability to fire missiles mid-fight, no pips, sad noise -- did I run out?"
Added the `[ammo]` fire trace (env `BT_AMMO_LOG`, projweap.cpp) and reproduced it: the SRM6 is
NOT out of ammo -- after ~5 volleys it trips **gate 1 FailureHeat** (`heatAlarm==2`) and latches
into the NoAmmo roach-motel (weaponAlarm 7, never released) with **19 rounds still in the bin**:
[ammo] SRM6_2 FIRED, rounds left=19
[ammo] SRM6_2 -> NoAmmo (gate1): destroyed=0 failHeat=1 heatLvl=2 mechDisabled=0
This is the heat-economy open question made concrete (gauges-hud.md "degenerate bring-up heat
scale"; projweap.cpp:730 "whether FailureHeat is reachable at authentic play intensity"): our
autofire test fires 60/s (extreme), but the user hit it in MANUAL play too -> either the per-shot
heat / FailureTemperature scaling is too aggressive, or the roach-motel (no recovery when heat
falls) is the wrong reading. NEXT: audit the SRM6 heatPerShot vs FailureTemperature vs the mech's
dissipation, and whether the binary's gate-1 FailureHeat is a LATCH or clears when heat drops.
Trace it live with `BT_AMMO_LOG=1` (+ `BT_PROJ_LOG=1` for per-tick heat/recoil).