Files
BT411/context/open-questions.md
arcattackandClaude Fable 5 bb795e2805 MP live-play wave: collision economy, missiles, radar transform, panel polarity, comm ticker
The interactive 2-node playtest wave -- every fix decomp-grounded and live-verified:

COLLISION ECONOMY (the ram one-shot): StaticBounce mutates worldLinearVelocity
per contact and ProcessCollisionList walks EVERY touched solid per frame; with
2007 terrain-as-solids the reflections compounded x4-x40 within one frame and a
walking bump one-shot a pristine mech for 112,375 pts (62-pt authentic economy).
Fix: frameEntryWorldVelocity restore per contact (damage always priced at the
real approach speed -- all the binary's physics ever saw); Mech::Reset zeroes
the mover motion (respawn = teleport); [collide-tx]/[mp-hdlr] telemetry.
Gotcha #16 (engine-facility drift class).

MISSILES: peer-visible salvos (the launcher record extension carries a salvo
counter + aim point; ForceUpdate actually enqueues it -- the dirty flag alone
never serialized), the authentic arc (authored MuzzleVelocity vector + the
Seeker's 200m/0.1/300 loft + gain-4 steering, decoded from @004beae4/@004bef78),
world-impact bursts (rounds detonate on cave geometry instead of phasing
through), contact-only damage (flight-cap expiry = fizzle, no more teleport
damage), live re-lead, and ballistic (unguided) shells for autocannons.
projweap's stale BTPushProjectile extern (the /FORCE signature trap, gotcha #6
corollary) crashed the Avatar's first AFC100 shot -- fixed + sweep rule recorded.

RADAR: two transcription bugs made the scope permanently empty -- FUN_0040b244
is the affine INVERSE (not a copy) and FUN_0040adec writes ONLY the 3x3 rotation
(never the translation); worldToView now Invert(view) built rotation-first.
CulturalIcons sorted out of the moving grid (the phantom red pips were map
props), visible-radius culls on all three draw passes, live pip verified at
|delta| x ppm px.  Gotcha #17 (verify the FUN_ body, not its call shape).

WEAPON PANELS (the frozen-dial hunt): the binary's *(subsystem+0x40) means
FAILED -- the recon's 'operating' name was backwards, inverting the destroyed-X
lamps, the panel look, the children enable and the ready-lamp gate (which had
NEVER executed).  Polarity chain corrected end-to-end (failedState, fed by real
damage saturation).  Root cause of the freezes: MFD page-mode gating -- the dev
composite shows ALL pages at once, so off-page dials legitimately stopped; under
BT_DEV_GAUGES the 15 page-plane bits stay active (the exclusive secondary trio
untouched).  The SEH gauge guard now names its kills; repaint-heal resets the
incremental arc after panel repaints; [panel]/[arc] probes added.

COMM/SCORE: MessageBoard LIVE (the engine already shipped the whole
Player__StatusMessage queue; wired the binary's one producer -- the kill branch,
victim's name, 6s -- plus the consumer bridge and a lazy source bind); MP DEATHS
counted via the observed-death tally (each node scores every pilot from locally
observed events, the same model as the KILLS credit) and the -2/-1 engine seed
clamped for display.

DEV UX: node-tagged window titles (-net port), gauge panel reworked (1320x480,
true 4:3 MFD cells, the portrait secondary UNROTATED upright, linear filtering,
BT_GAUGE_SCALE), fixed close spawns via BT_SPAWN_XZ, Boreas flies an Avatar
(first second-chassis live outing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 17:24:15 -05:00

423 lines
35 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.
## Deferred subsystems / feeds (authentic path scoped, marked in code)
- **✅ 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. **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).
- **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.
- **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)
- Authentic per-mech TURN-RATE constant (currently a bring-up constant rate).
- 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).
## 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).