Files
BT411/context/open-questions.md
T
arcattackandClaude Fable 5 0a77d8e54b Camera-seat ranking window LIVE: the 1995 broadcast overlay reimplemented
The stubbed dpl-instance ranking display is reborn as screen-space
quads in CameraShipHUDRenderable::Render:
  - followed-player callsign banner (bottom center; the old direct
    index was correct -- Execute already converts to the 0-based
    texture slot)
  - the RANKING WINDOW: one row per scoring player, [ordinal][callsign]
    in rank order right of center; visibility = the Director's
    authentic flash logic (10s on / 15s off, solid final 30s); rows
    follow LIVE playerRank pointers so they re-sort as scores change
  - discovery: each 128x32 ordinal bitmap packs TWO ordinals side by
    side ('1st|2nd', '3rd|4th' -- why 4 bitmaps serve 8 players);
    draw = texture rank/2 with a u-half selected by rank parity
  - alpha-blended A4R4G4B4 white-on-transparent textures x green
    diffuse = the authentic green look

BT_SHOT moved AFTER the 2D pass -- it captured the backbuffer pre-HUD,
so overlays were on screen but invisible to screenshots (cost one
false debugging round).  GetOrdinalTexture accessor added.

Screenshot-verified: '1st MAVERICK' standings + MAVERICK banner over
live auto-directed coverage; 2-node mech smoke PASS (un-regressed;
the new render path only executes when a CameraDirector HUD exists).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 16:32:23 -05:00

63 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.
  • Did the pod throttle quadrant have MECHANICAL detents (how many / lever positions)? The software path is notch-free [T1, pod-hardware], but the gait SM's walk/run dead band (locomotion) only makes sense if the hardware discouraged parking the lever mid-band — "5 speeds" pod lore suggests physical notches. Status: OPEN.
  • Which mechs used rear fire ANSWERED from data (2026-07-16, task #68): the Blackhawk's ERMLaser_2/3 (back gun ports) are the only rear weapons in the roster; see combat-damage. Remaining: bind the side/down look views to keys (only 'V' look-back is bound); pod look-button mappings for Phase 8.
  • The cameras\ directory (per-map camera-network files) RESOLVED from our own data (2026-07-18, same day): the cameras\<mapname> FILE path (CAMMGR.cpp:390-415, RP-style loose .CAM notation — present for RP maps in the nick-games 410SRC archive) is only the DEV OVERRIDE; the shipped binary falls back to BTL4.RES resource TYPE 27 (0x1b) named after the map (FUN_0042be3c probe → FUN_0042c078(res, mapname, 0x1b); binary records {count; per-cam {size; type 1|2; pos+quat+...}}, parsers @0042ad54/@0042b9f8) — and our BTL4.RES has ALL 8 maps' networks (arena1 = 62 cameras; cavern/dbase/grass ~57KB each). [T1] CAMERA SEAT LIVE (2026-07-18, same day) [T2]: the type-27 loader ALREADY existed in the engine (CreateStreamedCameraInstances, CAMMGR.cpp:530 — the file probe falls back to it; 62 arena1 cameras load, [cam] diag under BT_CAM_LOG). The REAL missing piece was Mech::PlayerLinkMessageHandler (@0049f624) — the BT override the reconstruction never filled: base resolves mech→player; BT adds the REVERSE link player->playerVehicle = this on EVERY node (replicants included — how the director/scoreboard find a remote player's mech), clears NonScoringPlayerFlag (0x4000 = bit 14: a pilot with a vehicle SCORES/ranks — this also makes cross-node RANKINGS work), and on the master seeds the heat bank's ambientTemperature from the mission temperature= (mech+0x7dc bank +0x1d4 ← BTMission+0xf4 — the heat family's "frozen 300" note stands CORRECTED). Verified live: mech+camera 2-seat relay session — replicated player links (bmp=1+veh), director locks the goal (30s timer), camera ship tracks the moving mech (sees=1) and CUTS between authored cameras (screenshots). Egg: pilot page hostType=1 + vehicle=camera. RANKING WINDOW LIVE (same day): the stubbed 1995 overlay reimplemented as screen-space quads in CameraShipHUDRenderable::Render — the followed player's callsign banner (bottom center, the Execute index is ALREADY 0-based) + a rank-ordered row per scoring player ([ordinal][callsign], right of center), visibility = the Director's flash logic (10s on/15s off, solid final 30s), live playerRank pointers re-sort rows as scores change. Each 128×32 ordinal bitmap packs TWO ordinals side by side ("1st|2nd", "3rd|4th" — why 4 bitmaps serve 8 players): texture rank/2, u-half by parity. BT_SHOT now captures AFTER the 2D pass (it used to dump pre-HUD — overlays were on screen but invisible to screenshots). Screenshot-verified: "1st MAVERICK" + banner over live coverage. Remaining: operator-app camera-seat row helper (hostType not in the roster UI), shot-composition polish. Status: broadcast display COMPLETE.

Pod-hardware readiness gaps (Phase 8 assessment, 2026-07-17) pod-hardware

Everything below EXISTS in the tree but has NEVER run against real pod hardware. Mechanism status vs the pod's 7-display / RGB-splitter / dual-sound-card rig:

  • CARRIED + plausibly functional (untested on hardware):
    • RIO serial controls: real Win32 COM (L4SERIAL CreateFileA/SetCommState), the full chain RIO AnalogReply → Ranger → ControlsManager → streamed .CTL mappings → mapper traced [T1]; keyboard bridge disable = BT_KEY_BRIDGE=0. Pod look/config buttons are .CTL data-driven.
    • RGB-splitter channel packing: L4GraphicsPort::ChannelEnableID (Red/Green/Blue/All + transparent-zero variants) — each gauge draws into ONE color channel of the shared 640x480x16 surface, feeding one monochrome CRT per channel through the splitter. The gauge CONTENT is complete (same L4GaugeRenderer verified daily in the BT_DEV_GAUGES window).
    • Multi-output selection: SVGA16::BuildWindows (a fullscreen D3D9 device per gauge/MFD surface), adapter env PRIMGAUGE/SECGAUGE/MFDGAUGE/MFDGAUGE2/SPANDISABLE, MFD span = width x2 across two displays; content/SETENV.BAT = the authentic pod env preset.
    • The L4PLASMA.cpp serial plasma-display driver (L4PLASMA=com2).
  • EXPECTED TO NEED REAL WORK on modern hardware:
    1. Multi-head display management: the original used TWO video cards; simultaneous exclusive-fullscreen D3D9 devices on multiple heads of one modern GPU is fragile — likely needs a borderless-window-per-head modernization of BuildWindows.
    2. The REAR sound card is folded away: the pod ran AWE_FRONT/AWE_REAR as two physical cards for the 4-speaker front/rear split; our OpenAL opens ONE device (front/rear collapses to stereo). Feasible fix: OpenAL Soft quad output (surround config) or a second device.
    3. Serial protocol timing/handshake vs a real RIO board (and the intercom path) — unverified.
    4. -platform pod deliberately does NOT auto-enable L4GAUGE (each surface needs its own device); the pod bring-up procedure itself is undocumented → the Nick items above.

BINARY-COVERAGE AUDIT discoveries (2026-07-13, 6-agent decomp census) [T1 verified-uncited]

New unaccounted functionality no prior list knew (addresses verified absent from game/+context/+docs):

  • BTL4VideoRenderer::StartEntityEffectImplementation @004d097c — DONE (2026-07-13) [T2]. The per-zone EFFECT DISPATCHER (+helpers @4d0c14 StopAll / @4d0c5c) is reconstructed (btl4vid.cpp:889) + WIRED: MechDamageZone change → BTStartZoneEffect → RendererManager::StartEntityEffect → this virtual → the band effect ATTACHED to the struck zone's segment (smoke/fire follows the mech); the Destroyed descriptor fires on kill (mechdmg.cpp:1120). Band smoke/fire (psfx ≥1000) render through the live PFX layer (BTStartPfxAttached); the 3-15 specialfx band + scaling booms/flames render through the reconstructed L4VIDEO specialfx engine + ScalingExplosionRenderable (see next bullet). LIVENESS-VERIFIED (2026-07-14): BT_CRIT_PROBE=8 on solo DEV.EGG (hammers a local zone) drove the full chain — [zonefx] entity 1:321 seg 12 psfx 12/13 at(527,5.8,-669): TakeDamage → the mechdmg damage-effect loop → BTStartZoneEffect → StartEntityEffectImplementation resolves the struck segment's world pos → BTStartPfxAttached. (The 2-node harness scratchpad/fx_liveness.sh did NOT engage — nodes spawned but never targeted/drove into combat, mechPicks=0; an MP-harness spawn/connect issue, orthogonal to FX. BT_CRIT_PROBE is the reliable solo FX-liveness path.)
  • The impact/destruction FX visual chain — RECONSTRUCTED (Fire VISUALS wave 48c9c84 + Impact-FX FORENSICS wave 065c114, 2026-07-12/13) [T2]. The i860-firmware-decoded specialfx engine (L4VIDEO.cpp: specialfxN descriptor pages, per-round detonations, authored firesmoke sheet, vertex-alpha effect cards), the case-4 wreck dressing, and ScalingExplosionRenderable (L4VIDRND.cpp:3413 — real ctor/Execute, the scaling-boom motion) are all live. The audit's six renderable-body addresses (@4540ac/45447c/455eb8/456cf0/4589e0/458e5c) map to this reconstructed set; a byte-level coverage cross-check of each vs the raw bodies is a minor open audit, NOT a dead chain. (NOTE: the //STUBBED: DPL RB 1/14/07 markers scattered in L4VIDRND are benign 2007 empty ctor/dtor notes on UNRELATED renderables — InnerProjectile/DPLObjectWrapper/ChildLight — not the explosion FX; do not mistake them for a gap.)
  • FUN_00472480 (273 ln) — unnamed analog→discrete stepper Execute in the gauge band (hysteresis + key-repeat), no caller found; the one hole in "gauge system complete". Settle via vtable lookup (plausibly a config/roster scroller).
  • FUN_00454a70 (193 ln) — the PNAME1-8/PLACE1-8.bgf MP name-billboard loader (the known "MP target identification" gap's exact function).
  • Explosion::SplashDamage @0042fad0 — RECONSTRUCTED (task #62, 2026-07-13) [T1/T2]. ONE call site, Missile::Perform (FUN_004bef78, part_013.c:10097), in the collision branch (fires on ANY missile impact — mover OR world), gated on missile+0x360, radius = missile+0x364. Radius source corrected: it is the ROUND's OWN GameModel (type-0xf) +0x50, seeded from the launcher's linked AmmoBin ammoModelFile @0x1e8 (part_013.c:8778 → missile ctor SearchList @10184) — NOT the launcher's ExplosionModelFile. The MissileThruster parser FUN_004bf8ec writes "SplashRadius" to +0x50 (size-0x54 record). Burst falloff baseBurst / dist^exp floored at 1 (arcade 1.25 / WinTesla EXPLODE.cpp:209 1.2f). Only missiles splash (AC tracer is not a Missile). The engine EXPLODE.cpp diff is DONE — the T0 source confirmed the model; the only 1995-vs-WinTesla drift is the exponent. Port: BTResolveSplashRadius/BTApplySplashDamage (mech4.cpp), see combat-damage. Remaining T3: the per-player enable sub-gate missile+0x360 = BTPlayer+0x264 (its writers read as a per-frame toggle, not a config flag; the showDamageInflicted@0x264 label is a guess) — port treats authored SplashRadius>0 as the enable. The mech's DeathSplashDamage/DeathSplashRadius (mech+0x520/0x524) stay VESTIGIAL (written, never read; no mech-death splash mechanic — death is visual only).
  • The binary's own VelociRender BGF/BMF/BSL/VTX loader (~4,550 ln, dead code) — a free ground-truth cross-check for bgf-format/asset-formats claims. Full ranked audit: the workflow output (2026-07-13); top-10 ranking cross-checked against this register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTORY in the KB (task #8 "landed" vs the kShotDamage=12 bring-up residue) — audit before reworking.

Deferred subsystems / feeds (authentic path scoped, marked in code)

  • 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). Real-IP pod-LAN config VALIDATED 2026-07-15; **D1 relay mode
    • UDP channel + LAN discovery + relay-assigned seats + operator console shipped 2026-07-18** (see multiplayer D1). Remaining (config, not code): an actual two-physical-machine internet session through the relay (ports verified open; players/join.bat ready).
  • Subsystem-record replication IS wired — the AC just wasn't using it (task #61, 2026-07-13) [T2, CORRECTED]. (An earlier draft of this note WRONGLY claimed "no subsystem-record channel" — that was concluded from an AC-only test before the AC's ForceUpdate gap was found. The channel exists and works.) MECHANISM: the mech's per-frame tick calls subsystem->PerformAndWatch(till, update_stream) for every subsystem (mech4.cpp:3893) → base Simulation::WriteSimulationUpdate walks the subsystem's updateModel and serializes any set record; Entity::UpdateMessageHandler (ENTITY.cpp:387) routes each incoming record to GetSimulation(subsystemID-1)->ReadUpdateRecord on the peer. A subsystem replicates its fire IFF its FireWeapon calls ForceUpdate() (sets the updateModel bit). The EMITTER (beam) and MISSILELAUNCHER (salvo mirror, mislanch.cpp:326) both do → enemy lasers
    • missiles ARE visible on the peer. The AUTOCANNON set only simulationFlags |= 0x1 (the +0x28 instance flag, NOT updateModel) and had no fire record → its shot never serialized → the enemy's cannon (tracer + DAFC muzzle flash) was invisible. FIX: added ProjectileWeapon::WriteUpdateRecord/ReadUpdateRecord (fire counter + aim, the AC twin of the salvo mirror) + ForceUpdate() in FireWeapon. Verified live: watching node now logs REPLICANT AC shots + muzzle flashes. LESSON (methodology): the binary-coverage audit is a function-PRESENCE census — it can't see "reconstructed but inert" code (a function that exists but is never CALLED, like the salvo mirror was thought to be, or a missing ForceUpdate). A LIVENESS audit (does a live call path reach each reconstructed fn?) would catch this class.

Rendering follow-ups (non-blocking)

  • Per-pilot mech PAINT (color/badge/patch)DONE 2026-07-17, verified live (crimson MadCat + yellow VGL emblems + hip hazard stripes). Mechanics + the vehicletable color/badge/patch name tables in rendering §Per-pilot mech PAINT. Residual nit: eggs saying color=Red mean Crimson (table has no Red; the binary Fail()ed on it, the port warns + drops).
  • .PFX effect-layer polish (deferred 2026-07-08; the layer itself is LIVE + verified). The reconstructed BT particle layer (L4VIDEO.cpp, see its banner) renders the authentic .PFX content (fire + smoke, premultiplied blend, impact-frame oriented — weapon hits AND damage bands via lastInflictingID, now written by Mech::TakeDamageMessageHandler). Remaining nuances, all cosmetic: (a) atten/attenv distance attenuation not honoured; (b) the per-file texture name (btfx:firesmoke1_scr_tex) — all effects share the radial-masked grit sheet; decoding FIRE.BSL would give the authored sheet; (c) beams draw before particles, so a beam through thick smoke reads too bright (pass-ordering); (d) colorWarp/alphaWarp applied as t^(1/warp) [T4 convention]; (e) emission uses rate×releasePeriod batching [T4 convention vs the exact 1995 emitter].
  • Replacement-LOD selection for NON-additive multi-LOD models (needs the board's hot-spot/ reference-point semantics; leads: s_dplobject.lod_ranges[16]/lod_hot_spot, 0x2047/0x2048).
  • Day/night MATERIAL path priority (BMF/IMG) — infrastructure done, gated BT_MATPRI (over-applies to shared terrain mats); needs the terrain-material model understood.
  • RGBA4444 alpha (TREE.BSL cutout) decoded but not alpha-tested outside punch batches; VTX/TGA paths.

Content build sub-project (low priority)

  • Lab/other-build maps (des/burnt/frstrm) are source-only (.map in CONTENT/BT/MAPS/); would need compiling into a RES via the DOS btl4tool.exe. The 8 RES maps cover testing.

Key Relationships

  • Feeds from: every subsystem/render topic (their deferral notes collect here).
  • Gating master: source-completeness (the missing BT source).

Peer coupled-motion architecture — DONE (task #50, 2026-07-14/15) [T1/T2]

RESOLVED. (This block was the old COMPLETION PLAN from ea39af1; it was left stale after the work actually landed — reconciled 2026-07-15.) The coupled single-source gait pipeline is reconstructed, DEFAULT ON, and the residual was root-caused. See multiplayer two sections: "Authentic coupled peer motion — DONE" (linear position = BODY gait channel integrated into projectedOrigin@0x260; angular = replicated-velocity slerp; master SEND-mirror runs the SAME IntegrateMotion predictor fed the last-sent bodyTargetSpeed) and "Peer motion: the 'random shakiness' is single-box packet jitter, NOT the game".

  • Landed: coupled path promoted to default (c52a1ad + b013742); peer poses from the BODY channel (96a896a); turn-step on the body channel (f094d78); true-mirror cadence driving the clip from actual replicated motion (23f1532). Groundwork: exact quaternion integration (a8eb8a4), incremental peer heading (d78e77b), clock guard (93456be), commanded-speed cadence feed (6374efc), Abs-macro fix (7615ecd).
  • The old "needs the MASTER HALF" conclusion was SUPERSEDED: the leftover accel/decel shakiness was ROOT-CAUSED as single-box packet jitter (49d73dc) — two Debug nodes on one box batch TCP delivery, so records arrive bursty (BT_RXJIT burstiness 3-7x) and the peer dead-reckons across the gaps then snaps. Pinning each node to a disjoint core set (ProcessorAffinity 0x00F/0x3C0, baked into tools/mp_launch.sh) restored even ~17ms delivery and the shakiness vanished (user-confirmed). Real pods are dedicated machines and never see it. DO NOT add an interpolation/jitter buffer to mask a rig artifact.
  • Minor non-gating follow-ups (tracked in multiplayer): the peer accel/decel drift (flagged ~2.9u, but MEASURED ~0.64u max on the affinity rig with the peer on the body channel -- BT_SNAPLOG + BT_DRIVE_SWEEP0, 2026-07-15; sub-unit, effectively resolved, no fix warranted); the exact _DAT_004ab9cc offset-decay constant; whether IntegrateMotion's 2-stage angular integrate is intended.
  • Speed model decomp-settled (2c6db6a): analog throttle, '5 speeds' false, throttleState@0x4a4 is actually the fall-surface material (RENAME PENDING).