Files
BT411/context/open-questions.md
T
arcattackandClaude Opus 4.8 bc4d6e5597 MP: console-death freeze NOT REPRODUCIBLE + fix the latent game-listener-close bug (task #50)
Investigated the "console death froze peer replication" open question (2026-07-14)
with a direct 2-node repro (BT_REPL_LOG on a circling peer, affinity-pinned):

- Clean relay kill -> replicant kept circling smoothly through the kill, NO
  disconnect logged, node survived. Replication did NOT freeze.
- Deliberately STUCK console (scratchpad/btconsole_stuck.py: connect, start
  mission, then stop recv() while holding the socket OPEN = the exact
  "receive pad full -> close never seen" mode hypothesized) -> replicant kept
  moving the whole run. Replication did NOT freeze.

Conclusion: peer replication is INDEPENDENT of the console (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 in 49d73dc -- or was fixed by
later MP work.

FIX (the one real bug found): L4NetworkManager::HostDisconnectedMessageHandler's
ConsoleHostType branch closed gameListenerSocket (the GAME listener) on a CONSOLE
disconnect -- a naming bug (the comment + commented-out OpenConnection intended a
CONSOLE re-listen, which CreateConsoleHost already does). Removed. Harmless to
established peer sockets (why a live match survives 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.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 15:52:35 -05:00

57 KiB
Raw Blame History

id, title, status, source_sections, related_topics
id title status source_sections related_topics
open-questions Open Questions — deferred systems, unknowns, get-from-Nick living CLAUDE.md §9; docs/HARD_PROBLEMS.md; per-subsystem deferral notes
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).

  • 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]: 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; 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 1000heatAlarm≥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 ProcessCollisionMech::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)

  • 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); 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).

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): an occasional ~2.9u leg(peer)-vs-body(mirror) channel-mismatch snap; 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).