Files
BT412/context/open-questions.md
arcattackandClaude Fable 5 21abe2212e KB: autocannon jam/coolant-priority -- mechanism is FAITHFUL + COMPLETE (decomp hunt)
Two-agent decomp hunt on the QA report ('AFCs jam on 3rd shot if coolant priority
not boosted'). Conclusion: the whole coolant->jam chain is reconstructed end-to-end
and FAITHFUL -- the weapon's constant coolantFlowScale=1.0 is authentic (only
condensers' 0x15C is written from the valve, via RecomputeCondenserValves over the
condenser chain mech+0x7cc). Authentic mechanism is second-order-in-equation but
first-order-in-outcome: weapon -> its condenser -> bank; boosting a condenser's valve
share (~5x conductance swing) keeps its weapon below degradationTemp -> no jam.
Distinguished the sticky probabilistic jam (degrad 1000, JammedState 5) from the
self-clearing overheat lockout (failure 2000, weaponAlarm 7). Empirically the port is
directionally correct (~4th-shot probabilistic) but not the QA's deterministic 3rd
shot -- a calibration/scenario question pending QA clarification, NOT a stubbed path.
Do not tune the heat economy to force it without ground-truth.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 06:56:34 -05:00

600 lines
51 KiB
Markdown
Raw Permalink 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** — the missing implementation `.cpp` (mech, subsystems,
mapper, HUD, app; see [[source-completeness]]). Without it, Route A (recompile original) is
impossible; we reconstruct from the binary instead. Likely on a backup/dev drive. Status: OPEN.
- Confirmation the PodPC image is the complete content master. (Low priority — the runtime
`BTL4.RES` already has 8 maps + the full mech anim set.) Status: OPEN.
- Pod specifics for Phase 8: the 7-monitor driver setup, the RIO cockpit I/O protocol, current
Win10+wrapper pod config. Status: OPEN.
## 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)
- **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). Also flags a deferred **`Mech::ControlsAllowLights()`** (searchlight.hpp:158 stub).
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. The valve/Myomers "gates" read the owning player's
fields at `player+0x260` and `player+0x274` (the reconstruction maps this block as `showDamageReceived@0x25c
/ showKills@0x260 / showDamageInflicted@0x264 / roleClassIndex@0x274` in btplayer.hpp — from the SCORING
work, so the names may NOT match the gate semantics; the true meaning + the WRITER of `player+0x260/0x274`
are not yet pinned — part_013.c:4553-4660 is the TorsoSimulation, a different object, not the setter). So
there is NO new subsystem to build. **The wiring (small):** point `MechSubsystem::IsDamaged()` (`FUN_004ac9c8`, valve
guard) at `GetPlayerLink()->`(0x274) and `Myomers::OwnerAdvancedDamage()` (`FUN_004ad7d4`) at
`GetPlayerLink()->`(0x260) — via NAMED members (databinding trap: our BTPlayer layout ≠ binary; do NOT
raw-read `player+0x260`). ⚠ Resolve first: (a) a naming conflict — `FUN_004ad7d4` is labeled BOTH
`HeatModelActive` (heat.hpp) AND `OwnerAdvancedDamage` (myomers.cpp); re-verify which body is which; (b)
the true semantic of `player+0x260/0x274` vs the scoring-derived `showKills`/`roleClassIndex` names. NOTE:
these are MODE flags — in the basic test mission the inert Myomers / dormant valve is likely AUTHENTIC
(advanced damage off); wiring makes them RESPOND when a mission enables the mode, not necessarily change
the basic-mission behavior. The MessageBoard feed is separate (StatusMessagePool, below).
- **✅ 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).
- **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
still the `gBTWeaponTrigger` bring-up.
- **✅ 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]**: the shipped binary NEVER enables it — no caller of
SetColor @004c6ee0 exists (the only child accesses are BecameActive/SetEnable forwards),
so color stays 0 and Execute early-outs: the gauge is authentically DORMANT in BTL4OPT.
The state loop is now reconstructed anyway (@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), behind the PORT dev enable BT_CONFIGMAP=1.
* **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
-> roleClassIndex(+0x274) == 0` = the ROOKIE-role lockout for advanced cockpit
systems (port bridge BTPlayerRoleLocksAdvanced, btplayer.cpp; NULL player =
unlocked [T3]; bring-up role defaults to 2 = unlocked). AUDIT TAIL: the other
powersub.cpp sites annotated FUN_004ac9c8 (the coolant-draw gate ~:427,
ForceShortRecovery ~:444, the Generator/PowerWatcher site ~:1085) still call the
heat-family IsDamaged (simulationState != 0) stand-in -- each needs its raw fn
re-checked and swapped to the role bridge where the binary calls 4ac9c8.
- **Myomers authentic coupling** — the structural un-stub is INERT (mover feed + heat-gen no-op).
Real coupling needs the advanced-damage gate (`OwnerAdvancedDamage`/`FUN_004ad7d4` → the owning
**BTPlayer** `mech+0x190`+0x260, NOT 0xBD3 — see the mech+0x190 item above) + `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;
heatLoad -> effectiveRange 0 = the authentic overheat cutout).
- **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 — mechanism FAITHFUL + COMPLETE; a CALIBRATION question
pending QA ground-truth (2026-07-14) [T1 mechanism / T3 pacing].** An original QA dev reports:
"all AFCs jam on their 3rd shot if coolant priority isn't boosted; boosted → almost never jam."
Full decomp hunt (2 agents) confirms the whole chain is reconstructed end-to-end and the port is
FAITHFUL — **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`). Authentic mechanism: a weapon dumps `heatCostToFire` (AFC100=6.5e7 raw) into its
OWN linked condenser; 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). Boosting
ONE condenser's valve (share ~0.17→~0.9 = ~5× conductance) drops its equilibrium temp so the
weapon dumping into it stays below `degradationTemperature`(1000) → `heatAlarm`=0 → CheckForJam
early-returns → no jam. Starved → condenser saturates → weapon crosses 1000 → the STICKY
probabilistic jam (`p=clamp(0.41·T/failT,0.05,1)`, JammedState 5, clears only on ResetToInitialState)
latches. Two distinct "jam" states: **degradation 1000 → probabilistic sticky jam** (the pilot's
"jam"); **failure 2000 → deterministic self-clearing overheat lockout** (recoil-pinned + weaponAlarm 7,
re-fires each frame until it cools). ALL thermal constants load strictly from the resource (no
stand-in fallback — a missing value ERRORS). **Empirical (BT_JAM_LOG/BT_VALVE, madcat AFC100):**
uncooled the weapon crosses 1000 at ~shot 4 and PLATEAUS ~1400 (conduction balances input), giving
~25% probabilistic jam — directionally right but NOT the QA's deterministic "3rd shot". Gap is
calibration (condenser thermalMass/conductance live in LAST.EGG, unread) + scenario (fire rate;
which condenser boosted — a uniform BT_VALVE is normalized away, need a targeted per-condenser
boost). NEXT: get QA to clarify (a) deterministic lockout vs probabilistic jam, (b) per-weapon vs
global coolant boost + which control; THEN verify calibration — do NOT tune the heat economy to
force "3rd shot" without ground-truth (would be un-faithful). Diag probes committed: `BT_JAM_LOG`
(projweap.cpp), `BT_VALVE` (heatfamily_reslice.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 (multiplayer)**: `mech.cpp:1511` + `:1613` (replicant
leg-state/stability sync in ReadUpdateRecord) — always-false BTVal comparisons ([[reconstruction-gotchas]] §2);
needs slots 0xf/0x10 promoted to named members when P6 replication work resumes.
## Multiplayer (Phase 7 / P6)
- ✅ 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); remaining = the real pod-LAN config (real IPs, bare-IP
pilot entries).
- **✅ 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)
- **.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).