--- id: open-questions title: "Open Questions — deferred systems, unknowns, get-from-Nick" status: living source_sections: "CLAUDE.md §9; docs/HARD_PROBLEMS.md; per-subsystem deferral notes" related_topics: [source-completeness, subsystems, multiplayer, combat-damage, locomotion] --- # Open Questions Known unknowns + deliberately-deferred systems. For each: what's known, what's missing, what would resolve it, status. Deferred ≠ broken — most are marked-and-guarded stand-ins with the authentic path scoped. ## From Nick (external — gating for some paths) - ~~**[GATING] The BT game SOURCE CODE**~~ **CLOSED 2026-07-19: per the user, Nick holds NO assets beyond what has already been provided.** Route A (recompile original) is permanently off; the decomp reconstruction ([[source-completeness]]) is THE route — behavioral in `game/reconstructed/`, literal in `TeslaRel410/restoration/source410/`. Asset-type asks below are likewise closed; knowledge/lore asks (throttle detents, pod procedures) remain valid. - ~~Confirmation the PodPC image is the complete content master~~ **CLOSED 2026-07-19** (no further assets exist; the runtime `BTL4.RES` with 8 maps + full anim set is the master we have). - Pod specifics for Phase 8: the 7-monitor driver setup, the RIO cockpit I/O protocol, current Win10+wrapper pod config. Status: OPEN. - **Did the pod throttle quadrant have MECHANICAL detents** (how many / lever positions)? The software path is notch-free [T1, [[pod-hardware]]], but the gait SM's walk/run dead band ([[locomotion]]) only makes sense if the hardware discouraged parking the lever mid-band — "5 speeds" pod lore suggests physical notches. Status: OPEN. - ~~Which mechs used rear fire~~ **ANSWERED from data (2026-07-16, task #68)**: the Blackhawk's ERMLaser_2/3 (back gun ports) are the only rear weapons in the roster; see [[combat-damage]]. Remaining: bind the side/down look views to keys (only 'V' look-back is bound); pod look-button mappings for Phase 8. - ~~The `cameras\` directory (per-map camera-network files)~~ **RESOLVED from our own data (2026-07-18, same day)**: the `cameras\` 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). ✅ **DECODED 2026-07-24 (not yet ported) [T1/T2]** — it is the **MP SCOREBOARD/leaderboard**, built by the L4VIDEO video-resource factory for **resource type 0x46** (part_008.c:2902-2911, gated `res+0x28 & 0xc == 0`, alloc 0xbc). All 16 plates are ONE flat textured quad each (4 verts / 1 PCONN quad / no LOD chunk / no normals): PNAME = 1.0×0.25 m, PLACE = 0.5×0.25 m, both planar Z=0, UNIT=metre, authored 30-Nov-1995. **PNAME1-8 are literally the words "PLAYER 1".."PLAYER 8"**; **PLACE1-8 are the rank ordinals "1ST".."8TH"** — a row reads `[PLACE][PNAME]` because the PLACE node is translated x=−0.75 and spans ±0.25, so it ABUTS the PNAME plate's x∈[−0.5,+0.5] exactly. Update fn **FUN_004553ac** re-parents each player's PNAME plate into row node `pnameGeo[rank]` (rank from the per-player attr at `player+0x1cc`, players indexed by `player+0x1e0`), so the rows SORT live. Row pitch = **0.24** (double @0x004551b8), rows DCS scale 0.12 @ (0.22,0.16,−0.5), root DCS scale (0.2,0.2,1.0) @ (0,−0.15,−0.5). Materials/atlas map + the runtime-composite mechanism: [[bgf-format]] §"The PNAME/PLACE name-plate atlas". The separate single-PNAME consumer is `BTReticleRenderable` @004cc40c (no PLACE) — the in-cockpit designator label, now **RECONSTRUCTED (2026-07-24, rig-verified)**: see [[gauges-hud]] §Lock ring. The port's ranking display already ships the **2D** equivalent (egg ordinal+callsign bitmaps, `CameraShipHUDRenderable::Render`), so this 3D chain is redundant for the scoreboard. NOTE on the "PLAYER n" art: those baked words are the SHIPPED FALLBACK. The pod overwrites the atlas texels at runtime with the egg's 1bpp callsign rasters — the original source survives commented out at `engine/MUNGA_L4/L4VIDEO.cpp:5682-5710` (`FlushBitSliceTexture` → `dpl_TexmapTexels2D(…,128,64,4)` + the warning "textures for player names not defined"). So an authentic plate reads the player's CALLSIGN; "PLAYER n" only appears when the egg carries no name bitmap. [T1] - ✅ **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) - **Revolving-door Phase 2 (true mid-mission drop-in) — PLANNED, gated on a spike** (`docs/REVOLVING_DOOR_PLAN.md`, 2026-07-24). Open questions before any engine work: (a) can an `Entity::MakeMessage` be REGENERATED post-birth (serializing current state), or does the make path assume creation-time state? (b) what does the replicant-creation path do with an already-known entity ID (idempotent, or double-create — needs a guard)? (c) where exactly is the all-roster-hosts-connected launch wait, and can absent seats stay `Opening` without stalling? Go/no-go = the 2-node late-attach spike in the plan doc. Phase 1 (round carousel) is console-only and has no engine unknowns. [[multiplayer]] - **Glass panel dead-button backlog (input-coverage audit 2026-07-20) [T1 census].** 8 of the 72 panel buttons dispatch a streamed message with NO reconstructed handler (silently ignored): 0x13 → Mech `DuckRequest` @0049fa00 (CROUCH — `duckState` attr + SQUAT clips exist!), 0x28 → Mech `BalanceCoolant` @0049f728, 0x1A-0x1D → Generator A-D `ToggleGeneratorOnOff` @004b1ed0, 0x12 → ThermalSight toggle (pvision unported), 0x14 → Searchlight `ToggleLamp` (body reconstructed, handler-set unwired — note the 1995 latent bug below: even wired, the light never turns on in the original). Plus page-gated Eng-page meanings: `ToggleCooling` @004ad6f8 (msg 3), `ToggleSeekVoltage` @004ba478/@004b8a48 (msg 0xb energy / 9 Myomers), `EjectAmmo` @004bb9b8 (msg 0xb ammo weapons), and MechRIOMapper's own Keypress @004d2514 (id 0x19). Filed as the Gitea "glass panel: unreconstructed button handlers backlog" issue; tables in [[decomp-reference]] §2026-07-20; census in `docs/GLASS_COCKPIT.md`. - **Searchlight-driven fog swap — STILL DEFERRED, but the "ORIGINAL 1995 LATENT BUG" premise is ❌ RETRACTED (2026-07-25, #61).** The arcade swaps fog between `fog=` (lights on) and `nosearchlightfog=` (off) via `PullFogRenderable` watching the Searchlight's `lightState`. **The old entry claimed the 1995 binary itself could never light the lamp. That was wrong** — it compared Searchlight's Performance (@004b841c, reads `requestedOn`@0x1E0) against **ThermalSight's** `ToggleLamp` (@004b860c → `0x1DC`), i.e. two different classes, and so invented a missing "0x1DC→0x1E0 bridge". Searchlight's real handler is **@004b838c** (table @0x51117C) and toggles `0x1E0`, the field its own Performance reads. [T1 attribution: `section_dump.txt:69661-69711`, un-pooled `"ToggleLamp"` strings adjacent to their own class names. Body: @004b838c sits in a Ghidra export gap (#60) but was **recovered by raw disasm** (`scratchpad/dis838c.py`) — it toggles 0x1E0 and carries no novice gate. All T1.] Searchlight's handler set is now WIRED and **verified live**: pad `0x14` → `requestedOn 0→1` → `lightState 0→1`. So the sim needs **no repair** and the previous "DECISION: faithful to the buggy original" is void — a working fog swap is now plain FAITHFUL reconstruction. Remaining work is a single item: 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). `ControlsAllowLights()` is WIRED since issue #2 to the `player+0x25c` not-novice experience flag via the BTPlayerExperienceSimLive bridge ([[experience-levels]]). See [[rendering]] fog section. (The pre-#61 "verified inert live: BT_FOG_LOG zero `SetFogStyle(2/3)`" observation still holds — reason (1), the un-constructed renderable, remains.) - **`HandleMessage` is vtable slot 8/9 in the binary but NON-virtual across the reconstruction -- FILED AS GITEA #65 (2026-07-25, found via #46).** Ten classes declare it (ammobin/heat×2/hud/mechsub/myomers/ powersub×3/projweap) as reconstructions of vtable-slot overrides, none `virtual` — a base-typed `sub->HandleMessage(n)` statically binds to the base and SKIPS the override (bit the `BT_BAYTEST` hook; fixed there with a typed AmmoBin call). No live base-typed caller exists in the port today, so nothing is currently broken — but any future reconstruction that dispatches subsystem messages through a base pointer (the binary's crit path may) must first make the family virtual in one sweep (signatures must agree; some declare `Logical`, some `void`+int variants — audit before flipping). - **✅ #47 ENG-button attention FLASH BUILT same day (2026-07-25, evening).** All three pieces landed: the REAL `BTL4GaugeAlarmManager` bodies (btl4galm.cpp, from @004cc294/@004cc2fc + the lamp tables @0051cf1c..0x51d084), the sender (= `MechTech::TechnicalAssistance` @004ad33c — the "gauge watcher" guess was wrong; MechTech's 7-condition edge-scan builds `Start/StopEntityAlarmMessage` via @00436688/@004366b8 and broadcasts @004364e4; port shape = direct `Start/StopEntityAlarmImplementation` calls on the gauge renderer), and the lamps (the L4LampManager + streamed-mapping lamps already existed; the manager is now constructed + assigned in btl4grnd). Verified live end-to-end (`scratchpad/lampflash.py`): bay fire → `[techstat] LRM15_1 condition 4 SET (alarmModel 83)` → gotoEngineering lamp 0xD flashes 0x37 + engEject 0xB → detonation clears → the bin reports Destroyed. FOUR load-bearing fixes en route: `HeatableSubsystem`'s Derivation chained Subsystem directly (skipping MechSubsystem — the whole tree failed the monitor filter); `MechSubsystem::GetStatusFlags` made virtual (binary slot 12; static binding hid the weapon bits); `ProjectileWeapon::GetStatusFlags` raw-disasm'd from the @004bbf88 export gap (jam→0x20, bin cookOffArmed→0x10); and the Mech::Reset sweep's blanket `(MechSubsystem*)` cast filtered (it RespawnRepair'd MechTech through the wrong layout — benign only while the monitor chain head was NULL; #47 populating it exposed a load crash). Remaining risks: the wooHoo latch fields are still untested; FindLamp needs exact modeMask equality so unflashed lamps on rare pages would betray a mapping-stream mask mismatch. - **DEFERRED de-shadow: `MechSubsystem::damageZone` re-declares the PUBLIC engine `Subsystem::damageZone` (SUBSYSTM.h:159) — gotcha #1, FILED AS GITEA #64 (2026-07-25, found via #47).** All recon code coherently uses the derived member; the ENGINE base member stays NULL forever, and engine base paths that use it (`Subsystem::TakeDamage` derefs it unguarded!) would AV if ever reached. mechtech's monitor filter hit this (naive `sub->damageZone` resolved to the engine one → 0 monitors). De-shadowing = make the recon write the ENGINE member and delete the re-declaration — a layout-neutral but call-site-wide sweep; do it deliberately, with the ReconDamageZone-proxy offset-0-vptr alias (#46's crash) retired in the same pass. - **(superseded) the original #47 remainder note follows for the record:** machinery fully mapped, 3 pieces unbuilt (2026-07-25).** The authored data SHIPS: `BTL4.RES` carries one `GaugeAlarmStream` (type 31). The full chain + addresses: [[decomp-reference]] §GaugeAlarm. To make it live: (1) reconstruct the REAL `BTL4GaugeAlarmManager` override bodies from @004cc148..@004cc2fc (btl4galm.cpp is best-effort scaffolding and its provenance note — "no override body exists in the image" — is WRONG; that code region IS the override, including the ≥0x80 subsystem-relative eng-button lamp mapping); (2) register the gauge-side StateIndicator watcher that sends `StartEntityAlarm` (id 7) on subsystem-alarm transitions (the AUDWTHR parallel on the gaugeWatcherSocket); (3) build the LampManager lamps for the aux/eng bezel buttons (LAMP.cpp/L4LAMP.cpp are T0-compiled; the glass panels + pod RIO both consume `RIO::SetLamp`, so the flash lands on-screen AND on the pod). Decode the type-31 stream format first — it names which conditions flash which lamps. - **PlayerStatus (the mission-review panels) reads the compiled player at RAW BINARY OFFSETS -- FILED AS GITEA #63 (2026-07-25, found via #48) [T1 code-read].** btl4gau3.cpp `PlayerStatus::Execute`: `player+0x1FC` (vehicle), `+0x1C8` (score, drawn as a numeric), `+0x1C4` (alive/dead -- a state flip draws a 160x240 filled box) -- all 1995 offsets, garbage on our compiled layout (the databinding trap). DORMANT in-game (the review context no longer survives the viewpoint swap -- the #48 teardown fix) but LIVE the moment the mission-review screen actually runs at round end. Bridge all three through btplayer.cpp accessors before enabling the review screen. - **`PoweredSubsystem` AutoConnect is DEAD, and vtable slot +0x40 is misattributed -- FILED AS GITEA #62 (root cause of the #21 field report) (2026-07-25, found while auditing #47's GetStatusFlags change) [T1 decomp].** The binary (@004b0bd0 tail) calls slot +0x40 with TWO shapes: `(this, 0)` in the outer gate (require `== 0`) and `(this, candidateGenerator)` inside the roster walk (require `!= 0`). That is a ONE-ARG voltage query -- "am I unpowered?" / "would THIS source power me?" -- i.e. the `HasVoltage(source)` family, **not** `GetStatusFlags()`. powersub.cpp:358/372 model both as the no-arg `GetStatusFlags()`, so the outer gate demands "no flags at all" while the inner demands "some flag", and nothing between them can change the value -> **the loop body can never execute and AutoConnect never attaches anything.** Pre-existing (both before and after the #47 damage-tier fix); deliberately NOT fixed on release eve because it is a live power-bus behaviour change that deserves its own playtest cycle. Fix = give slot +0x40 its real signature (`Logical HasVoltage(Subsystem *source = 0)`), restore the two call shapes, and verify a mode-Auto subsystem re-attaches after its generator drops. - **Factory capability-roster loops 2-4 are STILL DEAD (task #57 discovery).** mech.cpp's post-roster loops add to `heatableSubsystems`(0x51155c)/`weaponRoster`(0x511830)/ `damageableSubsystems`(0x50e4fc) through the local `SubProxy` stub whose `IsDerivedFrom` RETURNS 0 — so those three mech-level rosters stay EMPTY (whatever consumes them sees nothing; weapons demonstrably work through other paths, so audit consumers before assuming impact). Loop 1 (the watcher CONNECT, 0x50e604) was reconstructed for real in task #57 — use the same bridge pattern (family-side derivation test + typed body) for the other three. [T2] - **Initial-snapshot torso record carries no twist extras**: the replicant's blind +0x10 read can latch 0xCDCDCDCD ONCE at spawn until the first real twist record; clamps contain it (invisible on fixed-torso mechs). Binary-authentic blind read — fix only if a visibly wrong spawn pose ever surfaces. See [[multiplayer]]. [T2] - **✅ 0xBD3 SubsystemMessageManager — UNTANGLED + LIVE (task #7, 2026-07-11) [T1/T2].** Both halves landed: (1) the factory case builds the REAL messmgr (ctor @0049bca4, 0x130, static reconstruction messmgr.cpp) cached at `mech+0x434` — the binary-wide census found exactly ONE reader (@0x4b984b in `MechWeapon::SendDamageMessage` @004b9728, which was ALSO mislabeled, as "DrawWeaponPip"); every mapper consumer re-pointed to roster slot 0 (`MappingMapper()`; `SetMappingSubsystem` @0049fe40 touches only slot 0); non-viewpoint mechs carry a slot-0 demand LATCH ([T3] accommodation for the port's wider drive/gait reachability). (2) The authentic CONSOLIDATED damage delivery is live: the beam path submits TakeDamage into `AddDamageMessage` → per-frame `ConsolidateAndSendDamage` (@0049b784) builds ONE `Entity::TakeDamageStreamMessage` (id 0x13, wire-verified 0x34+4+N×12) dispatched at the victim, whose T0 handler (ENTITY.cpp:817) re-splits it; replicant victims reroute cross-pod. Explosion bundling (weapon+0x3E4 per record, unique-queued, Explosion::Make at the impact) implemented; TWO latent chain-purge bugs fixed (records re-applied every tick). RESIDUE ([T3], next task): the weapon-side submission with per-weapon `damageData` + `inflictingSubsystemID` awaits the DAMAGE-ECONOMY reconciliation (authored 0.25-scale amounts vs the bring-up kShotDamage=12); the 0.1s explosion stagger + the terrain-hit derivation check are noted in messmgr.cpp. ⚠ The earlier "0xBD3 gates the valve/Myomers" claim remains WRONG (those read the owning BTPlayer at mech+0x190 — see the next entry); the old wiring notes are superseded by this landing. - **✅ `mech+0x190` IDENTIFIED (2026-07): it is the owning `BTPlayer`** (`Mech::GetPlayerLink()`, `MECH_OWNING_PLAYER`; ENTITY.h:430). Set by `FUN_0049f624` (the mech↔player bind: `mech+0x190 = player` AND `player->playerVehicle(+0x1fc) = mech`), which resolves the player from the mission player registry (`app+0x2c+0x54`, `FUN_0041fd18`) by the pilot key. **✅ FLAG SEMANTICS PINNED (2026-07-18) [T1]: the `player+0x25c..0x274` block = the EXPERIENCE LEVEL (egg `experience` novice/standard/veteran/expert), written by the BTPlayer ctor @4c0bc8 from `btMission(+0x1f8)->experienceLevel(+0xe4)` — full mapping, consumers, and the 4.0-manual behavior in [[experience-levels]] (+ [[decomp-reference]] §3 flag table).** Resolved en route: the old "resolve first" items — `FUN_004ad7d4` (labeled both `HeatModelActive` and `OwnerAdvancedDamage`) is ONE body reading `player+0x260` = the heat-model master switch (veteran+expert); `FUN_004ac9c8` reads `player+0x274` = the novice lockout. **✅ THE WIRING LANDED (issue #2, 2026-07-20) [T2]:** the ctor now seeds from `BTMission::ExperienceLevel()`/`AdvancedDamageOn()` via the inherited `Player::playerMission` (the binary's +0x1f8; the old `btMission` phantom member removed), the members are renamed to the true semantics (`simLive`/`heatModelOn`/`experienceLevel`/…), and all five gate stubs + the FUN_004ac9c8 family (`MechSubsystem::IsDamaged`, `BT_IsBusLive`, powersub call sites) route through the three complete-type bridges (`BTPlayerExperienceSimLive`/`…HeatModelOn`/ `BTPlayerRoleLocksAdvanced`). Every shipped egg says `experience=expert`, so default behavior is unchanged. Detail: [[experience-levels]] §Port state. - **✅ Authentic target acquisition RECONSTRUCTED (tasks #36/#39, 2026-07-08)** — the `Reticle` pick-ray chain is LIVE (see [[combat-damage]] Targeting for the full port map): the crosshair = **torso boresight** (NO free-aim mouse — the pod stick twisted the torso; you steer to aim) → pick ray → per-frame lock (boresight ON the mech = locked; off = none) → aimed zone damage + designator hotbox; `BT_FIRE_ARC` is opt-in-only now. Firing needs a target but NOT a manual lock/pinpoint — the target is AUTO-acquired (task #40, binary-verified: FireWeapon doubly-gated on mech+0x388, but 0x388 has 11 reads / 0 direct stores across CODE → set indirectly). Residue: (a) the binary's own per-frame reticle→mech copy + the AUTO-TARGET SELECTOR are still in an un-exported gap (a message/selector; our port auto-targets the single living enemy — for MP, reconstruct the real multi-target selection: nearest/most-aligned/cycle); (b) the eyepoint now rides the parent DCS (task #55 authentic eye), so torso pose reaches the view; on a TWIST-CAPABLE mech (selectable via `BT_FORCE_MODEL`) the crosshair should deflect with the torso twist (`BTTwistToReticleX` is wired for it) AND the torso should visibly lead the legs — verify the reticle deflection on a twist mech (the BLH is fixed-torso, boresight always centred); (c) pre-burial, the pick still tests the sinking wreck's collision box at its parked position (pre-existing wreck-targeting behavior). - **✅ WORLD STRUCTURES (garages/walls) now targetable (task #50, 2026-07-15)** — the world-pick's non-mech tier previously only sampled the visual heightfield (`BTGroundRayHit`), which on arena1 is a single flat `'sky'` ground mesh → shots passed THROUGH the garages and only mechs moved the range axis (user-reported regression). Resolved by ray-testing the **zone's static collision solid tree** (the geometry that already blocks the walk) via the authentic engine query `Mover::FindBoxedSolidHitBy`'s static-world tail — factored into `Mover::FindStaticSolidHitBy` + `Mech::WorldStructurePick`. See [[combat-damage]] "WORLD STRUCTURES ARE TARGETABLE". This also closes the old "0x388 writer is un-exported / unknown world-pick mechanism" residue: the authentic non-mech world pick is the static-solid-tree ray query (`BoxedSolidTree::FindBoundingBoxHitBy`), the same one `FindBoxedSolidHitBy` uses for mover-vs-world collision. Interactive aim is user-verified (headless has no `BTGetAimRay`); the `BT_WSWEEP` ray-fan is the headless proof. - **Cockpit HUD leftovers (tasks #35-#38 residue, 2026-07-08)** — the reticle + 7 weapon pips + ALL the instrument dynamics are LIVE (the Execute @004cdcf0 gap was RECOVERED via capstone disasm, task #37; the authentic Lock producer + the simple-X mode landed in #38 — see [[gauges-hud]] for the full instrument map). Remaining: (a) the **`*_cop` canopy shell** — SOLVED for all 8 (task #55): authentic eye + the PUNCH stencil-cut kit + double-sided cop geometry + unlit constant frame colour (`BT_COP_FRAME`) — see [[cockpit-view]] Rendering/FINAL; shows by DEFAULT now (`BT_HIDE_COCKPIT=1` hides). NOTE the old claim here ("canopy windows are punch texels") was WRONG — the shell has no texture; the openings are geometry. ✅ RESOLVED (2026-07-11): PUNCH = a 3-chunk STENCIL-CUT kit (mask/hull/twin, i860-firmware-decoded) executed as a D24S8 stencil cut; visible canopy = hull-minus-apertures ([[cockpit-view]] §FINAL). All 8 mechs render connected dark frames with clear viewports (the intermediate "frameless bhk1/loki/vulture/avatar" reading was wrong); thor matches the pod footage exactly. Eye placement verified exact per-mech incl. Thor's +1.13 offset cockpit ([[cockpit-view]]). (b) ~~the **3D marker chain** ([0x31c]/[0x320] dpl objects + the PNAME1-8.bgf player-name meshes over the locked target — matters for MP)~~ **DONE 2026-07-24: the target NAME PLATE is reconstructed + rig-verified (the target's callsign under the crosshair, lock-gated) — [[gauges-hud]] §Lock ring.** (c) the compass rotation SIGN is derived-not-verified [T3] (turn left → the stem should swing right; flip the sin sign in Draw if footage disagrees). - **✅ StatusMessagePool / MessageBoard — LIVE (2026-07-12) [T1/T2].** Decoded: FUN_0042e580 = the engine `Player::AddStatusMessage` (chain add at +0x1e4); FUN_0042e5a0 = `StatusMessageUpdate` (+0x1dc = the displayed message; +0x14 displayTime countdown, expiry-deleted) — ALL already in the engine [T0 PLAYER.cpp]. The ONE binary producer = BTPlayer::ScoreMessageHandler's KILL branch (@004c02e4 type-2): pool-alloc @00512f6c + ctor(victim's player, strip 0, 6.0s) + AddStatusMessage. Port: engine `new Player__StatusMessage` + Register_Object (the engine reclaims with delete — the binary pool is an allocation detail); consumer = BTResolveMessageBoard reads the mission player's statusMessagePointer + resolves the name via Mission::GetSmallNameBitmap; the board's SetSource binder (no recovered caller) is replaced by a lazy viewpoint-entity bind in Execute. Ticker pops on BOTH nodes (each node's death transition posts its local KillScore). - **Gyroscope (0xBC4) — RE-ENABLED LIVE (task #56, 2026-07-10).** The NaN revert's root causes are fixed byte-exact (ctor field map @004b3778 — springConstant@0x1E8/dampingConstant@0x1F4 were mislabelled, the 0x254-0x2B3 block was missing, clamps/accumulators uninitialised; the integrators' state-minus-target/componentwise-damping-overwrite/X-Z-crossing semantics; the writers' one-node-two-channels form; dispatch from the MECH performance tail, not the gyro Performance). Layout locked (sizeof==0x3D0); runtime clean (joints type 5, finite state, no NaN). Empirical: spring targets are SYMMETRIC → eye equilibrium (0,0,0) → the gyro is a ±0.1-0.15u BOUNCE mechanism, NOT a steady offset (the earlier hypothesis is disproven). **FAN-OUT LANDED (2026-07-11):** FUN_004b2980 re-disassembled from raw bytes and reconstructed (`Gyroscope::ApplyDamageResponse`) + wired at the take-damage hub, the crushable crunch and the firing recoil — the hit-BOUNCE is live and verified (damped eye oscillation, no NaN). REMAINING (task #56 tail): (a) the alternate-gait engage jolt + engaged-gait rumble (@4aa158-4aa365 — [T3] gate naming, mutates the #49/#50-stabilized gait machine; byte recipe in the wf_6880e605 synthesis); (b) the mech+0x3F0 overspeed sway model (swayBias fed 0); (c) the per-frame `EyepointRotation.pitch = torso pitch` writer (FUN_004b66b4) + mapper glance-look states (FUN_004afd10 — yaw/pitch from mech+0x564..0x570); (d) senders filling `Damage::damageForce` for directional bounce (random-fallback is binary-legal meanwhile). Also: `deathAnimationLatched`/`legResetLatch` were never ctor-initialised (0xCDCDCDCD gate bug, fixed in mech.cpp ctor). See [[cockpit-view]]. - **MechControlsMapper look/eyepoint commit** — reconstructed but its offsets collide with declared members; arbitrate before enabling (no port consumer yet). (The fire-trigger-through-mapper is NO LONGER the bring-up: the `gBT*Trigger` globals are retired / never-read — the authentic LBE4 buttonGroup path replaced them, task #5 `8ed6184`.) - **✅ Weapon groups — the AUTHENTIC defaults + fire path LIVE (task #5, 2026-07-11) [T1/T2].** The system decoded end-to-end: a fire "channel" = a physical `buttonGroup` slot in the global `LBE4ControlsManager` (fire buttons 0x40 Trigger / 0x45 Pinky / 0x46 ThumbLow / 0x47 ThumbHigh); binding = a permanent mode-0x10000 DIRECT mapping button → the weapon's **TriggerState** attr (id **0x13**, = `fireImpulse@0x31C`; the binary MechWeapon table's ONLY attribute @0x511890); the weapon self-edge-detects (CheckFireEdge @4b9608). **The default grouping is NOT messages — it is the per-mech type-6 controls-map resource in BTL4.RES** ("Thrustmaster"/"L4" variants; e.g. madcat: Trigger={4 weapons}, ThumbHigh={2}), installed at `MakeViewpointEntity` by the ENGINE's `CreateStreamedMappings` (T0, L4CTRL.cpp) — which the port already called; it needed only the TriggerState attribute published (id PINNED to 0x13 — our attr-id chain is 6 short of the binary's; full binary attr-table dump in the task #5 scan) + an input feed. Keyboard now pushes press/release edges into the buttonGroups (SPACE→Trigger, '2'→ThumbLow, '3'/CTRL→ ThumbHigh; BT_AUTOFIRE pulses the Trigger); the gBT*Trigger bypasses + pulse hack are retired. VERIFIED 2-node at the BINDING level (⚠ honest correction, task #8: those kills still flowed through the retired mech4 bring-up block -- the emitter DISCHARGE chain was latched shut by the NaN edge bug below until 2026-07-11; the full weapon-fire chain is now verified solo). - **✅ THE CONFIG-MODE SESSION — RECONSTRUCTED (task #6, 2026-07-11) [T1 anchors].** The pod's in-cockpit weapon-REGROUPING UI, fully decoded from the binary + T0 engine: * **The flow**: the pilot HOLDS a weapon's configure button (a per-weapon type-6 EventMapping, id 9, on the MFD-quadrant aux panels; e.g. BTL4.RES @0xAA3DC) → MechWeapon handler id 9 "ConfigureMappables" @004b9550 (table @0x511860; the old "Myomers fns" label swept) → mapper(slot 0)->EnterConfiguration @004d1840 (vtbl+0x38): StartMappableButtonsConfigure (T0 CONTROLS.cpp:291 -- latches the active weapon for the gauge, flips the GLOBAL mode NonMapping 0x10000 → Mapping 0x8000, so every normal fire mapping goes dormant), re-arms the held button (+0x8000, id 9), temp-maps the four fire buttons 0x40/0x45/0x46/0x47 → id 10. Tapping a fire button → handler id 10 "ChooseButton" @004b95b8 → RIO AddOrErase(direct) @004d262c: buttonGroup[v-1].AddOrErase (0x10000, &fireImpulse) — a TOGGLE; the permanent 0x10000 instances ARE the commit. Release → ExitConfiguration @004d18dc: StopMappableButtonsConfigure + strip the 0x8000 layer. Only the RIO (pod) mapper implements AddOrErase; L4/Thrustmaster carry RET no-ops @004d195c/@004d1964 (a joystick cockpit cannot regroup). * **Vtable truth (swept)**: base @0050F45C +0x38 Enter(Fail @004b0280) / +0x3C Exit(Fail @004b029c) / +0x40 AddOrErase-event(Fail @004b02b8) / +0x44 AddOrErase-direct(Fail @004b02d4) / +0x48,+0x4C Notify pair. There is NO "secondary vtable @0050f498" and NO "CreateTemporaryEventMappings" virtual (RP-name drift). @004afbc4 (the mapper's shared aux/zoom handler) is a pure Fail thunk ("Unhandled button mapping!", line 0x7a), not an AddOrErase router. * **Port landed**: MechWeapon handlers 9/10 + the GetMessageHandlers() accessor chain (MechWeapon → Emitter/ProjectileWeapon → MissileLauncher, + GAUSS/PPC.CPP; dispatch resolves through the CONCRETE class's SharedData — the empty per-class sets silently swallowed the messages); the 6-arg Enter/1-arg Exit reshape; the L4 Enter/Exit full bodies; MechSubsystem +0xE8/+0xEC corrected to controlDestination/controlMessageID (the old hostEntity/subsystemId2 labels were wrong; MechWeapon ctor defaults the destination to &fireImpulse per @004b99a8); dev harness = HOLD 'G' (BT_CONFIG_SLOT selects the weapon; BT_CONFIG_TEST scripts a headless session). * **ConfigMapGauge finding [T1] — CORRECTED 2026-07-21**: the old "authentically DORMANT / no SetColor caller" claim was WRONG. @004c6ee0 is the virtual GaugeBase::LinkToEntity override (vtbl @0051a1b8 slot 9 +0x24; @0x94 = linkedEntity, the Execute gate), and the engine broadcasts LinkToEntity(viewpointEntity) at viewpoint bind (APP.cpp:1277 → GAUGREND.cpp:3011) — the gauge IS live in the shipped game. The state loop (@004c6f1c: 4 {y,button} pairs from DAT_00518eb4 = {13,Pinky}{37,ThumbLow}{61,Trigger} {85,ThumbHigh}, x=0xc; buttonGroup[btn].GetMapState(controlDestination, subsys, controlMessageID, 0x10000) → cm_off/cm_other/cm_only/cm_both) now runs on the authentic LinkToEntity path; the BT_CONFIGMAP dev enable was deleted. * **Bonus [T1]**: the binary MechWeapon ATTRIBUTE table @0x511890 has ELEVEN entries (0x12 PercentDone .. 0x1C WeaponState) — future gauge-wave feeds; the "TriggerState is the only MechWeapon attribute" claim was wrong (fixed in mechweap.hpp). * Tails: weapon+0x110 (write 0/-1 on session open/close; port name vitalSubsystemIndex) has NO confirmed reader yet; @004d1acc (+0x48 L4 Notify override) is unexported — re-export from Ghidra. * **Task #12 (2026-07-11) [T2]: the PoweredSubsystem POWER-ROUTING handlers landed** -- ids 4-8 registered (table @0x50F4EC): SelectGeneratorA-D = FindGeneratorByNumber (@004b0b18 ROSTER walk matching generatorNumber@0x1E0) -> AttachToVoltageSource -> modeAlarm=Connected; ToggleGeneratorMode (@004b0abc) = <2 -> AutoConnect, ==2 -> Detach + ManualConnect. Weapons inherit via the MechWeapon handler chain (the old qualified GetMessageHandlers() calls silently resolved to the engine Receiver ROOT set -- same trap as the task #6 weapon handlers). The AutoConnect hunt's GetSegment() scan corrected to the subsystem ROSTER (the +0x128 gotcha). Desktop: F5-F8 assign the selected weapon (BT_CONFIG_SLOT) to Generator A-D, F9 toggles Manual/Auto; BT_GENSEL_TEST scripts a headless re-tap. Note: Myomers/Sensor still chain the root set (their binary tables unverified) -- extend if aux maps prove they receive these ids. TWO MORE TRAPS found landing this: (a) MESSAGE_ENTRY tables must be function-local statics INSIDE the accessor (cross-TU static-init order emptied the table -- see [[reconstruction-gotchas]] #9 last bullet); (b) **FUN_004ac9c8 is NOT IsDamaged** -- raw body: `owner(+0xD0) -> mech+0x190 player -> +0x274 == 0` = the NOVICE-experience lockout for advanced cockpit systems (2026-07-18 correction: +0x274 = the egg experience level, NOT a role class -- see [[experience-levels]]; port bridge BTPlayerRoleLocksAdvanced, btplayer.cpp; NULL player = unlocked [T3]; seeded from the egg experience since issue #2). ✅ AUDIT TAIL CLOSED (issue #2, 2026-07-20): every powersub.cpp site annotated FUN_004ac9c8 (PoweredSubsystem::HandleMessage @4b0efc, ForceShortRecovery @4b11bc, Generator::HandleMessage @4b21d0, PowerWatcher::HandleMessage @4b179c) plus MechSubsystem::IsDamaged and BT_IsBusLive now call the bridge; the state-based HeatSink::IsDamaged shadow remains only for the gauge tint. - **Myomers authentic coupling** — the structural un-stub is INERT (mover feed + heat-gen no-op). The heat-model gate (`OwnerAdvancedDamage`/`FUN_004ad7d4` → the owning **BTPlayer** `mech+0x190`+0x260 = veteran/expert experience — see [[experience-levels]]) is WIRED since issue #2, but the sim stays arithmetically inert (the Owner* motion accessors return neutral 0s). Real coupling still needs those accessors + `MoverAttach` routing into the LIVE JointedMover (must be reconciled with the gait cutover first). - **SeekVoltageGraph** — 4 Seek* attrs unpublished (a cluster-child, not config-called; non-blocking). - **✅ DAMAGE ECONOMY — AUTHENTIC (task #8, 2026-07-11) [T1/T2].** The whole economy closes: `_DAT_004bafbc` is an x87 float80 = **1e-7** (the port's kDamageScale=1.0 was the mystery) -- it cancels the ctor's ×1e7 energy bookkeeping, so **damagePortion = authored DamageAmount × (charge/seekV[rec])²** (port: the closed form at fire time; the bring-up charge cycle keeps the degenerate EC=1 electrical model). Authored values (BTL4.RES type-0x11, +0x19C; scanner scratchpad/scan_weapsub.py): madcat AC=25/LRM15 salvo=50/ERLL=6/ERSL=2; bhk1 PPC=12/SRM6=35/ ERML=3.5. Zone model already byte-exact (Δlevel = amount/armorPoints, legs ×0.5; armor dump in the task #8 synthesis) → the pod's 4-8-hit heavy-weapon pacing. The weapon-side SendDamageMessage (@004b9728) is LIVE from Emitter::FireWeapon (damageData filled incl. **damageForce = target−muzzle → the gyro's directional hit-bounce feed**); the mech4 bring-up damage block + flat kShotDamage are retired to diag hooks **(CORRECTED, task #60: `kShotDamage=12` was only HALF-retired — still LIVE at the kill-score `mech4.cpp:1551` until task #60 replaced it with `lastInflictingDamage`, the real killing-blow magnitude; only the cross-pod force hook `mech4.cpp:3255` was env-gated. See the task #60 reconciliation below).** **ROOT-CAUSE find: the CheckFireEdge NaN latch** -- TriggerState carries ControlsButton INTS (release −65 = a negative NaN); the binary's x87 unordered-compare treated it as "released" but an IEEE-correct float compare latched the detector shut after the first release (why the emitters never discharged in-game until now); fixed with bit-pattern sign compares (@004b9608). Residues [T3]: heat stays on the bring-up scale (authentic = heatCost×1e7 energy units, the missile path already feeds it -- the HEAT-CALIBRATION audit); LODReuseHysteresis corrected 0.82→0.33 (a double); the MP beam-kill live-verify awaits a clear-sightline spawn (mechanism identical to solo; force-dmg cross-pod cycles verified). - **✅ DAMAGE-ECONOMY RECONCILIATION (task #60, 2026-07-13) [T1/T2].** Full 5-path audit vs the decomp (5-finder + adversarial-verify workflow). **Clean as-is:** energy beam (`emitter.cpp`, closed form byte-exact; `_DAT_004bafbc` dumped = x87 1e-7), autocannon (`projweap.cpp`, full authored DamageAmount from resource +0x19C; the 0.0625 at :667 is the shooter's own gyro recoil, not the round), zone-armor BASE model (`damageLevel += amount·damageScale[type]`, engine DAMAGE.cpp:379, legs ×0.5, 1.0=destroyed). **3 stand-ins FIXED:** (A) `mechdmg.cpp:451` `mech->stance` (phantom, perma-0 → the leg-shot-out fall/death branch was DEAD) → `MovementMode()` (mech+0x40, @part_012.c:6910); (B) `mechdmg.cpp:458` `IsAirborne()` (always-0 stub) → `IsDisabled()` (@0049fb54) — the task-#52 wreck-graphic corruption fixed AT SOURCE (was only masked by the IsMechDestroyed latch); (C) `mech4.cpp:1551` flat `kShotDamage=12` kill-score → `lastInflictingDamage` (real killing-blow, latched in TakeDamageMessageHandler; the score handler @0x4c02e4 derives the whole kill award from it, so a flat 12 scored every kill identically). Phantom `int stance` slot reused for `lastInflictingDamage`. **KB corrected:** the "damageScale[5] EVEN=1/armorPoints" prose is a mild inaccuracy — the array is 5 cells indexed by damageType directly (Collision/Ballistic/Explosive/Laser/Energy); the per-type value IS ≈1/armorPoints for the destructive types (net model unchanged). See [[combat-damage]]. - **DEFERRED (task #60-D): the missile CLUSTER model.** The port fires N flying rounds each carrying `authored/N` damage (net salvo total = authored, so per-armor damage is correct), but the binary (@004bcc60) fires ONE missile whose `burstCount` is a RANDOM cluster-hit roll (`min(rand(0..N-1)+N/4, N)`, FUN_004bef78) applied to ONE cylinder-resolved zone. The port thus loses (i) cluster-hit VARIANCE (always full N) and (ii) single-zone CONCENTRATION (N rounds scatter across zones). Net-neutral on total armor, so LOW priority. **OPEN semantic gating the severity:** does `burstCount` multiply ARMOR damage or only the gyro kick? DAMAGE.h:49-54 [T0] says "times to apply the damage" (→ apply-count → port total authentic); settle by reading the consumer chain FUN_004bef78 → FUN_004be078 → EXPLODE.cpp:209-210 (does it loop burstCount over DamageZone::TakeDamage or apply once?). Fix only if made byte-faithful (roll clusterHits, thread `burst` through BTPushProjectile, one damage-bearing round + rest visual-only). - Also open (bring-up, not stand-ins): `scoreAward=0` (btplayer.cpp:1526 — the authentic kill bonus lives in the un-exported master-perf writer 0x4a9770-0x4ab188, T4); the exact authentic kill `damageAmount` from that same writer (msg+0x24) is unrecovered — the killing-blow magnitude is the faithful stand-in until it is. - **HEAT (task #9, 2026-07-11): authentic 1e7-unit heat LIVE; conduction ROUTING defect remains [T2/T3].** Landed + byte-anchored: emitters emit `heatCostToFire x 1e7 x (charge/seekV)^2` (closed form; PPC = 1.1e8 -> +632 K on its 174000-mass sink); the missing projectile/missile heat adds (raw pre-scaled resources); the bank's AMBIENT RADIATOR @4ae73c (the system's only heat exit: relax toward 300 K, conductance x 0.1 x HeatSinkCount [_DAT_004ae974 float80 = 0.1 -- the audit's "1.7375" was a misread]); the link-attach guard corrected (skip = the 0xBBE bank, NOT Condenser -- the inversion had closed the system); three coolant epsilon corrections (0.0025/0.003/1e-4 for the old single 1e-4) + CoolantCapacityScale = 0.05 float80 (both prior readings wrong). Verified: heat flows, exits (~2/3 of a 1e10 spam run drained), weapons overheat under max-rate autofire exactly as the authored-constant simulation predicts (thermal spam is unsustainable BY DESIGN; the authentic overheat cutout is the FailureHeat CHARGE-HOLD -- `ResetFiringState` + currentLevel=0 until it cools, @004baa88 -- NOT a range collapse. ⚠ CORRECTION (`aab7a8a`, task #50): effectiveRange = (1 - HOST-ZONE damage) x weaponRange reads `Subsystem::damageZone->damageLevel` (weapon+0xE0), NOT heatLoad; the old "heatLoad -> effectiveRange 0" mechanism here was the same @0xE0-DamageZone-vs-heat misattribution fixed in heat.cpp:803). - **Task #10 -- the "scrambled routing" RESOLVED (2026-07-11) [T2].** The routing was NEVER scrambled: a `[heat-link]` attach log proved every subsystem links its authored sink exactly (bhk1: PPC_1->C4, PPC_2->C6, ERM_1/2/3->C1/C6/C4, SRM6s->C2, generators->C1/C2/C3/C5, condensers+reservoir->bank@slot2). The "pools in Condenser1" observation was the DIAGNOSTIC-SAMPLER ALIASING trap (one shared static 1-Hz timer, pendingHeat-gated); the census log is now per-instance. The REAL defect was the fire RATE: emitter.cpp's local `FUN_00417ab4` stub returned NULL, so although the PoweredSubsystem ctor attaches the authored generator to `voltageSource@0x1D0`, the Emitter TU could never see it -- the whole electrical model was inert and the E7 force-charge recharged every emitter in ONE frame (~0.3 s cycle, 1501 fires/90 s, ~1.7e9 heat/s input). Fixed authentically [T1, disasm + byte-verified constants]: * Emitter ctor @004bb120: seekVoltage[i] = authored fraction x generator ratedVoltage (authored 10000); EC = energyTotal/(seekV[rec]^2 x 0.5); **voltageScale@0x310 = (RechargeRate / -ln(1 - 0.0001 x seekV)) / EC** -- the exponential charge reaches seekV[rec] in EXACTLY the authored RechargeRate on a cold generator (PPC 5 s, ERLLaser 4 s, SRM6 3 s, ERMLaser 2 s, LRM15 6 s). The gate reads the OWNER's simulationFlags (the usual ctor-gate gotcha; the old this-flags read never armed). * `PoweredSubsystem::ChargeTimeScale()` (@004b0d50, was a =1.0 stub): voltageScale x (1 + thermalResistivityCoefficient x max(0, srcT - srcT0)) -- a HOT generator charges slower ("voltageScale is never read back" was wrong -- this reads it every tick). * TrackSeekVoltage @004ba838: charging I^2R (`seekRate^2 x dtScale x dt`) lands in the GENERATOR's pendingHeat@0x1c8 (~3.5e8 per full PPC charge -> generators self-heat -> conduct to their condensers -> slow further charging). This closes the heat/firepower feedback economy. * **FailureHeat consumers found [T1, disasm]:** `this+0x184` (the weapon's own heatAlarm status level) == 2 gates BOTH families -- @004baa88 (emitter: ResetFiringState + currentLevel=0 until it cools) and @004bbd36 (ballistic: recoil pinned to rechargeRate + alarm state 7). Emitter::GetFaultState() now returns heatAlarm.GetLevel() (was a 0 stub). * ProjectileWeaponSimulation @004bbd04 opens with `call 0x4b0bd0` (disasm-verified) -- the launchers now run the powered/heat step (their task-#9 firing heat previously accumulated in pendingHeat forever; SRM6s now census at ~950 under sustained fire). Verified live (120 s max-rate autofire): PPC ~470-500 (was 55,000), bank plateaus ~600 and sheds, generators 1100-1400, ERMLaser self-regulates at the 2000 failure threshold (shutdown -> cool -> resume). Remaining tails: the HeatWatcher/AmmoBin cook-off watchedLink; the mech-level heatAlarm@0x450 producer; the cockpit power-routing message handlers (@004b099c..@004b0abc: assign weapon to Generator 1-4 + auto/manual toggle via modeAlarm@0x2CC -- wired to buttons, not yet reachable). - **Task #11 -- the live-play regressions (2026-07-11) [T2].** User-reported after the electrical model landed. THREE distinct causes, all resolved: (1) *Phantom fire on the enemy*: the mech4 bring-up shot block painted an explosion at the victim on its OWN 0.3s cadence whenever fire was held -- desynced once real recharges landed. RETIRED (the authentic impact visual flows from each real discharge via the messmgr's SubmitExplosion). (2) *Weapons bricking one by one* ("fire ~1s then cuts out"): the FRAME-PACING TRAP -- see [[reconstruction-gotchas]] §12: the Loading->Loaded snap window (+-0.01 around seekV) assumes the pod's locked 60 fps; a dt spike jumps it, the overshoot clamp zeroes rechargeLevel, and the weapon sticks in Loading at level ~10000 forever. Fixed with pod-frame (1/60s) sub-stepping of the binary's own Loading tick (also fixes the I^2R integral overfeeding generator heat on big steps). (3) *Slow kills / short fire windows*: AUTHENTIC -- beams last the authored DischargeTime, recharges take the authored 2-5s stretched by hot generators (ChargeTimeScale), and max-rate autofire equilibrates generators in the degradation band (1100-1500 K; the FailureHeat breaker never trips in solo max-abuse -- duty-cycle measured). The pilot's authentic counters are the systems under reconstruction: config-mode regrouping (task #6), generator reassignment (ids 4-8), coolant valves. - **✅ AUTOCANNON JAM / coolant-priority — FAITHFUL + CONFIRMED against QA ground-truth, SOLVED purely from the decomp (2026-07-14) [T1/T2].** An original QA dev reported: "all AFCs jam on their 3rd shot if coolant priority isn't boosted; boosted → almost never jam." VERDICT: the whole chain is reconstructed end-to-end, FAITHFUL, and reproduces the reported behavior — no bug, no stand-in. **DO NOT "fix" it by driving a weapon's `coolantFlowScale` from the valve; that field being a constant 1.0 for weapons is AUTHENTIC** (the ONLY writer of any HeatSink `+0x15C` from a valve value is `RecomputeCondenserValves`/FUN_0049f788, which iterates ONLY the condenser chain `mech+0x7cc`). Mechanism: a weapon dumps `heatCostToFire` (AFC100=6.5e7 raw) into its OWN linked condenser (AFC100→ Condenser4); the condenser sheds to the shared bank at a rate scaled by its valve SHARE (`coolantFlowScale = valveState_i / Σ valveState`, MoveValve msg id 4, detents 1/5/50/0). The weapon's OWN thermal conductance (35500) caps how fast it sheds into its condenser, so coolant priority shifts the weapon's equilibrium only ~100 K — but that is EXACTLY enough to straddle the `degradationTemperature`(1000) jam line at combat fire rates. Cross `1000` → `heatAlarm`≥1 → the jam roll arms (`p=clamp(0.41·T/failT, 0.05, 1)`); on a hit → sticky `JammedState`(5), clears only on ResetToInitialState. (Distinct: `failureTemperature`(2000) → self-clearing overheat lockout, recoil-pinned + weaponAlarm 7.) **ALL constants VERIFIED AUTHENTIC vs the raw BTL4.RES bytes** (`scratchpad/afc_dump.py`): AFC100 startT=77, degradT=1000, failT=2000, thermalConductance=35500, thermalMass=94700, heatCostToFire(+0x1A4)=6.5e7, recharge(+0x190)=8, damage=25, minJamChance=0.05 — no mis-scaling. **THE ANSWER IS FIRE-RATE-DEPENDENT** (measured, BT_JAM_LOG/BT_VALVE_BOOST/BT_AF_PERIOD, madcat AFC100 → Condenser4): at MAX spam (8s recharge) the AFC overheats regardless (~1460 boosted ~1360 — both jam); at a SLOW cadence (16s) it plateaus ~770 and never jams; at the ~11s COMBAT cadence it is DECISIVE — **not boosted → 1058 K (jams), boosted (Condenser4 valve→50, others→1) → 953 K (never jams)**. So the QA is right and the reconstruction is faithful; the earlier "boost is weak" read was an artifact of testing at max spam (above the window). Diag probes committed: `BT_JAM_LOG` (projweap.cpp), `BT_VALVE`/`BT_VALVE_BOOST=` (heatfamily_reslice.cpp), `BT_AF_PERIOD=` (mech4.cpp). See [[combat-damage]], [[subsystems]] heat. - **Subsystem-panel online/offline gate — fields unidentified [T4] (2026-07-12).** The binary's SubsystemCluster draw-state reads `*(subsystem+0x40)==1` and `*(subsystem+0x278)!=4`; neither 1995 field is identified (weapon simulationState is 0/2/3/4 -- +0x40 isn't it). The port pins the panel ONLINE (GetDrawState()==1) because the old raw reads were layout NOISE that flapped the state and black-filled the panel over the recharge arc (the frozen-PPC-dial bug; the WeaponCluster repaint-heal also landed). Identify both fields to restore the authentic destroyed-subsystem dark-panel look. ## Locomotion / combat polish (non-gating) - **✅ Interior vs exterior gait CLIP SET — DONE 2026-07-13 (task #59) [T2].** The local cockpit mech was leaning −8° into every walk because the authentic ctor clip-set gate (@part_012.c:10308-10320) was a no-op (`LoadLowDetailBody`/`LoadHighDetailBody` mislabeled the FUN_004a80d4/86c8 clip-loader addresses) and `LoadLocomotionClipsExt` was a stub aliased to the exterior loader. Reconstructed the real 4-char INTERIOR (`'i'`-suffix) loader (omits `jointhip`, shakes `jointshakey`). Because the port never sets the replicant copy bit the ctor gate lands everyone on interior, so `Mech::MaintainViewClipSet()` picks the set by VIEWPOINT each frame (your mech → interior/level; every other mech → exterior/lean). Both verified live: cockpit level, peer replica leans −8/−11°. See [[locomotion]] "The walk lean IS authored". - FOLLOW-UP (non-blocking): if the copy bit ever gets set at construction for other reasons, the ctor gate already handles it and MaintainViewClipSet becomes a no-op agreement — no conflict, but revisit whether the per-frame maintenance is still needed. - ✅ **Authentic per-mech TURN-RATE — DONE (task #64b, 2026-07-14) [T1/T2].** The master-perf disasm (0x4aa3d3-0x4aa4ff) turn-rate lerp (walkingTurnRate@0x574 → runningTurnRate@0x578 by ground speed, run-speed refinement) is wired into the drive, replacing the `kDriveTurnRate` bring-up constant; the rates (which the `Wword(0x15d/0x15e)` no-op was discarding) are real members now. Also the trn-in-place DISPATCHER (entry/exit) reconstructed from the same disasm, replacing the task-#64 `0.25×`/fast-forward stand-ins. See [[locomotion]]. - Body-callback gimp handlers (states 16-19, targets 0x70b2/0x7161 undecoded → fall back to stand); airborne callbacks (`FUN_004a6344`/`FUN_004a7970`). - Wall-block-vs-climb tuning. **✅ Collision DAMAGE application — DONE 2026-07-08 [T2].** The two deferred dispatches in `Mech::ProcessCollision` (mech4.cpp: mech-vs-mech `:15324-15358` + icon-crunch `:15369-15401`) now fire: on a collision with another Mech or a CulturalIcon, an `Entity::TakeDamageMessage{zone==−1}` is dispatched to the victim (via `BTDispatchCollisionDamage`, the engine ctor — same as the weapon path), and STEP 6's cylinder table resolves the impact point → a zone on the receiver. Terrain (walls/hills) matches neither branch → blocks without damage (faithful). Reachability is guaranteed (`Mover::ProcessCollisionList` calls the **virtual** `ProcessCollision` → `Mech::ProcessCollision`); built + stable. The live dispatch wasn't captured headlessly (the solo auto-walker never rammed a tree/mech), but the path is proven reachable and composed of runtime-verified pieces. **Note:** validated STEP 6's height ref along the way — `collisionTemplate->maxY ≈ 7.1` (a real mech height), confirming `CylinderReferenceHeight` reads a height, not the heat value the `mech+0x2ec` dual-labeling (heat-gauge sink vs groundRef, mech4.cpp:2697) hinted at. - **✅ Cylinder hit-location (STEP 6) — DONE + LIVE 2026-07-08 [T2].** Built + runtime-verified: the `dmgtable.cpp` classes now have real storage + a working `ResolveHit→zone`, the mech ctor loads the type-0x1d table by the DamageZoneStream name (`[cyl] table 'bhk1' layers=7`), and `Mech::TakeDamageMessageHandler` resolves unaimed (zone==−1) hits before base-routing. ⚠ The FIRST "verified" claim was PARTIAL: the table was cached via `Wword(0x111)` (the ABSORBER — stores nothing) so every unaimed hit silently no-op'd; fixed 2026-07-08 by promoting the cache to the named member `Mech::damageLookupTable` ([[reconstruction-gotchas]] §2). Now verified end-to-end (19 zones resolving, `BT_AUTOFIRE` harness). Full change list in [[combat-damage]] "STEP 6 COMPLETE". Historical investigation notes retained below for provenance. **Re-investigated 2026-07 (see [[combat-damage]] for the full algorithm):** the table (height×angle grid, resource type 0x1d) IS built (recon `StandingAnimation` @Mech[0x111], but with an EMPTY name → 0 rows, because `ResourceFindByName` is a **no-op template stub**). **CORRECTION to the earlier note:** the LOOKUP is **exported** [T1] — `FUN_0049eb54` (table height→row) + `FUN_0049e678` (row angle→cell), NOT unexported. Only the thin `Mech::TakeDamageMessageHandler` glue (zone==-1 → call the lookup → `damageZones[zone]->TakeDamage`) is unexported, and it's fully implied by pieces already reconstructed → no disassembly needed. Remaining: 6b real `FindByName` (name = the mech's type-0x14 DamageZoneStream name @ResourceDescription+0xc); 6c the 3 container ctors + the lookup as a `BTCylinderResolveZone` bridge (ground the cell zone-dict stream format vs the real .RES bytes); 6d register the handler + verify. Feasible; the only open risk is the cell stream format. The earlier-cited `FUN_004a0230`/`FUN_0049ed0c` don't exist. - **Death sequence — CORE DONE 2026-07-08 [T2]; effects+anim deferred.** `Mech::UpdateDeathState()` (mech4.cpp) reconstructs the death STATE machine (collapse `movementMode` → RP `VTV::DeathShutdown` subsystem loop → settle to disabled/frozen; wreck stays, no crash) — runtime-verified (`BT_DEATH_LOG`). Full map + citations in [[combat-damage]] "Death SEQUENCE". ✅ (b) `MechDeathHandler` DONE (mechdmg.cpp) + ✅ the render-side **RemakeEntity** destroyed-mesh swap DONE 2026-07-08 (btl4vid.cpp `RemakeEntityRenderables` + `BTRemakeMechModel` bridge; in-place `SetDrawObj` swap — the tree dtor does NOT cascade so never rebuild). ✅ (a) **RESOLVED — no collapse animation exists in BT 4.11** [T1]: the fall latch (modes 5-8 → table slots 0x1c-0x1f) is a vestige — no loader fills the slots, no fall clip ships, and firing it would bind resource 0 (an AUDIO stream) as keyframes. ✅ The authentic death VISUAL is the **WRECK-HULK SWAP** (death ModelList `blhdead` → effect 104 → the victim becomes its `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` (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=` diag): 66 plugs bound at ctor per mech, zone destroyed → crits damaged/DESTROYED with authentic accumulation. Residue: roster idx 0/1 (mapper installed post-ctor / voltage-bus stub) stay unbound with a logged skip — none of the 8 mechs' streams crit-list them in practice. - **✅ Two dead Wword comparison branches — DONE (task #1, `c9f0c2a`).** `Mech::ReadUpdateRecord` case 3 now reads real named fields off `Mech__StateUpdateRecord` (legState / stability / legResetLatch / speedDemand); the always-false Wword/BTVal branches are gone (the old `mech.cpp:1511`/`:1613` line numbers no longer point at that code). ## Multiplayer (Phase 7 / P6) - **Gitea #12 MP incident (2026-07-19) — root causes found; BOTH FIXES LANDED 2026-07-19 (awaiting the human MP death-and-survive verification).** Findings [T1/T2 — see the #12 comments + `scratchpad/incident_2157/`]: (1) *Gauge-window freeze* = lobby-built gauge tree dangling over a mission-launch mech RE-STREAM after a pre-launch host drop/rejoin; SEH guard disables each faulted gauge permanently — see [[gauges-hud]] §MP gauge-window FREEZE. ✅ FIX LANDED: `BTL4GaugeRenderer::TearDownForViewpointRestream()` (alarms + Remove(0) + entity grids — the entity-bound half of the engine `ShutdownImplementation`), called from `MakeViewpointEntity` before `ConfigureForModel` on a viewpoint RE-make; the tree rebuilds bound to the new mech. Sentinel: `[gauge] viewpoint re-stream: tearing the gauge tree down`. (2) *Missiles cycle-but-never-launch*: ✅ FIX LANDED — the port's RivetGun-approximation ProjectileWeaponSimulation replaced with the disasm-exact @004bbd04 machine ([[decomp-reference]] §5; projweap.cpp): fault gate reads `simulationState@0x40==1`; the `simulationFlags|=0x1` DelayWatchersFlag poisoning removed everywhere ([[reconstruction-gotchas]] §20 — emitter SetDirty split into ForceUpdate/ExecuteOnUpdate per site); view/target gating + the `bin->FeedAmmo` pull + recoil set moved into the caller's Loaded case with the authentic 4-blip stay-Loaded denial; recoil bleeds ONLY in Loading at electrical-Ready and clamps at 0; slot-17 @004b9c9c called from Loading/7 (the launcher recharge dial now ANIMATES). State 7 kept binary-faithful (re-asserted unconditionally — no exit INSIDE the Execute machine; recovery via ResetToInitialState/mission reset, OR — per the 2026-07-23 EjectAmmo @004bb9b8 disasm, [[decomp-reference]] §message-tables — an EJECT TAP, which overwrites the alarm from outside the machine: bin still holding rounds → weaponAlarm 3 + reload restart, so even a FailureHeat 7 recovers while ammo remains; the empty-bin gate 2 re-pins 7 only when the bay is actually dry. Same handler: state-5 jams clear the same way — the earlier "no in-mission unjam" claim was wrong). Solo-verified on 4.11.388+: 54-60 `[projectile] PUSH` + impact smoke; denial window (BT_LOOK_TEST look-back) = trig pulses, state STAYS 2, zero launches, resumes on release; disabled-mech gate pins enemy launchers at 7+full recoil; STREAK-6 dial sweeps across screenshots; lasers fire throughout. Open fidelity question REMAINS: is launcher FailureHeat/jam-rate at sustained autofire authentic-intensity? (both SRM6s jam within ~2 min of held autofire via the bring-up CheckForJam heat gate; emitters never trip it — task #11 duty-cycle measurement.) (3) *J/K/L preset hypothesis REFUTED for solo* [T2]: BT_PRESET_TEST cycling on 387 — missiles kept launching after presets (36 PUSH before / 24 after), pip count unchanged, gauges live, [mode] masks touch ONLY ModeMFD bits 0-14 (`reticleElementMask` is a different object). Corroborating: a 2-player LAN session on the same build (no J/K/L) had working missiles + normal pips. UNTESTED: J/K/L during a live RELAY MP session. (4) *"WAY MORE pips" (BLH, symptom 4)* — UNRESOLVED: the incident registered 10 pips (8×3016 @750/500×5/225×2 + 2×3024 @800) which matches NO stock arsenal (solo dumps: bhk1=7, madcat=6, thor=7); the reticle built identically TWICE (lobby + launch re-stream). Needs the incident egg / a live 3-seat repro to identify the mech that owned those 10 weapons (custom operator-egg loadout? viewpoint mis-assignment at build time?). - **✅ Console-death peer-replication freeze — NOT REPRODUCIBLE + the one real bug FIXED (2026-07-15) [T2].** The 2026-07-14 report (btconsole.py died mid-match → peer↔peer replication froze both ways) did NOT reproduce in the current build under EITHER failure mode, tested directly (BT_REPL_LOG on a circling peer, affinity-pinned): (1) a clean relay kill — the replicant kept circling smoothly through the kill, NO disconnect logged, node survived; (2) a deliberately STUCK console (`scratchpad/btconsole_stuck.py` — connects, starts the mission, then stops recv()'ing while holding the socket OPEN = the exact "receive pad full → close never seen" mode hypothesized) — the replicant kept moving the entire run. So **peer replication is INDEPENDENT of the console**: the pods replicate peer-to-peer over the GAME socket; the console is a separate egg/mission/status channel. The original freeze was a transient (plausibly the same single-box packet-jitter / CPU-contention artifact root-caused 2026-07-15, `49d73dc`) or was fixed by later MP work. **The flagged latent bug IS real and is now FIXED:** `HostDisconnectedMessageHandler`'s ConsoleHostType branch closed **`gameListenerSocket`** (the GAME listener) on a CONSOLE disconnect — a naming bug; the comment + the commented-out `OpenConnection` intended a CONSOLE re-listen, which `CreateConsoleHost()` already does. Removed (L4NET.CPP:969). Harmless to established peer sockets (why a live match keeps replicating through console loss), but it would have blocked a NEW peer from joining after a console cycle. Verified: 2-node still connects (`All connections completed!`) + the peer circles. Diag harness retained: `scratchpad/btconsole_stuck.py`. - ✅ Cross-pod COMBAT — DONE (tasks #46/#47: replicant targeting + damage rerouted to the owning master, full cross-pod kill verified). ✅ Replicant GAIT animation — DONE (task #50: replicant derives speedDemand from the replicated velocity, full stand→walk→run lifecycle verified). See [[multiplayer]] — the entries here were stale (swept 2026-07-11). - ✅ **Mech-level update records (@0x4a0c2c) — DONE (task #1, 2026-07-11):** all 9 types transcribed + 2-node verified; the peer's death SINK/burial, knockdown, heat and state now replicate (the observer's replicant runs its own wreck loop off the replicated simulationState). Full mechanics in [[multiplayer]] "Mech-level update records". Residue [T3]: stream the model's UpdatePositionDiffrence/TurnVelocity/TurnDegree deadband constants to the senders (stand-in thresholds meanwhile). - The WinSock2 stack (L4NET.CPP) is 2-node verified end-to-end (cross-pod kills, beams, replicant gait — tasks #46-#51). Real-IP pod-LAN config VALIDATED 2026-07-15; **D1 relay mode + UDP channel + LAN discovery + relay-assigned seats + operator console shipped 2026-07-18** (see [[multiplayer]] D1). Remaining (config, not code): an actual two-physical-machine internet session through the relay (ports verified open; `players/join.bat` ready). - **✅ Subsystem-record replication IS wired — the AC just wasn't using it (task #61, 2026-07-13) [T2, CORRECTED].** (An earlier draft of this note WRONGLY claimed "no subsystem-record channel" — that was concluded from an AC-only test before the AC's ForceUpdate gap was found. The channel exists and works.) MECHANISM: the mech's per-frame tick calls `subsystem->PerformAndWatch(till, update_stream)` for every subsystem (mech4.cpp:3893) → base `Simulation::WriteSimulationUpdate` walks the subsystem's `updateModel` and serializes any set record; `Entity::UpdateMessageHandler` (ENTITY.cpp:387) routes each incoming record to `GetSimulation(subsystemID-1)->ReadUpdateRecord` on the peer. A subsystem replicates its fire IFF its `FireWeapon` calls **`ForceUpdate()`** (sets the updateModel bit). The EMITTER (beam) and MISSILELAUNCHER (salvo mirror, mislanch.cpp:326) both do → enemy lasers + missiles ARE visible on the peer. The AUTOCANNON set only `simulationFlags |= 0x1` (the +0x28 instance flag, NOT updateModel) and had no fire record → its shot never serialized → the enemy's cannon (tracer + DAFC muzzle flash) was invisible. FIX: added `ProjectileWeapon::WriteUpdateRecord/ReadUpdateRecord` (fire counter + aim, the AC twin of the salvo mirror) + `ForceUpdate()` in FireWeapon. Verified live: watching node now logs REPLICANT AC shots + muzzle flashes. **LESSON (methodology):** the binary-coverage audit is a function-PRESENCE census — it can't see "reconstructed but inert" code (a function that exists but is never CALLED, like the salvo mirror was thought to be, or a missing ForceUpdate). A LIVENESS audit (does a live call path reach each reconstructed fn?) would catch this class. ## Rendering follow-ups (non-blocking) - ~~Per-pilot mech PAINT (color/badge/patch)~~ — **DONE 2026-07-17, verified live** (crimson MadCat + yellow VGL emblems + hip hazard stripes). Mechanics + the vehicletable color/badge/patch name tables in [[rendering]] §Per-pilot mech PAINT. Residual nit: eggs saying `color=Red` mean **Crimson** (table has no Red; the binary Fail()ed on it, the port warns + drops). - **.PFX effect-layer polish (deferred 2026-07-08; the layer itself is LIVE + verified).** The reconstructed BT particle layer (L4VIDEO.cpp, see its banner) renders the authentic .PFX content (fire + smoke, premultiplied blend, impact-frame oriented — weapon hits AND damage bands via `lastInflictingID`, now written by `Mech::TakeDamageMessageHandler`). Remaining nuances, all cosmetic: (a) `atten/attenv` distance attenuation not honoured; (b) the per-file texture name (`btfx:firesmoke1_scr_tex`) — all effects share the radial-masked grit sheet; decoding FIRE.BSL would give the authored sheet; (c) beams draw before particles, so a beam through thick smoke reads too bright (pass-ordering); (d) `colorWarp/alphaWarp` applied as `t^(1/warp)` [T4 convention]; (e) emission uses rate×releasePeriod batching [T4 convention vs the exact 1995 emitter]. - Replacement-LOD selection for NON-additive multi-LOD models (needs the board's hot-spot/ reference-point semantics; leads: `s_dplobject.lod_ranges[16]/lod_hot_spot`, 0x2047/0x2048). - Day/night MATERIAL path priority (BMF/IMG) — infrastructure done, gated `BT_MATPRI` (over-applies to shared terrain mats); needs the terrain-material model understood. - RGBA4444 alpha (TREE.BSL cutout) decoded but not alpha-tested outside punch batches; VTX/TGA paths. ## Content build sub-project (low priority) - Lab/other-build maps (`des`/`burnt`/`frstrm`) are source-only (`.map` in CONTENT/BT/MAPS/); would need compiling into a RES via the DOS `btl4tool.exe`. The 8 RES maps cover testing. ## Key Relationships - Feeds from: every subsystem/render topic (their deferral notes collect here). - Gating master: [[source-completeness]] (the missing BT source). ## Peer coupled-motion architecture — ✅ DONE (task #50, 2026-07-14/15) [T1/T2] RESOLVED. (This block was the old COMPLETION PLAN from `ea39af1`; it was left stale after the work actually landed — reconciled 2026-07-15.) The coupled single-source gait pipeline is reconstructed, DEFAULT ON, and the residual was root-caused. See [[multiplayer]] two sections: **"Authentic coupled peer motion — DONE"** (linear position = BODY gait channel integrated into `projectedOrigin@0x260`; angular = replicated-velocity slerp; master SEND-mirror runs the SAME `IntegrateMotion` predictor fed the last-sent `bodyTargetSpeed`) and **"Peer motion: the 'random shakiness' is single-box packet jitter, NOT the game"**. - Landed: coupled path promoted to default (`c52a1ad` + `b013742`); peer poses from the BODY channel (`96a896a`); turn-step on the body channel (`f094d78`); true-mirror cadence driving the clip from actual replicated motion (`23f1532`). Groundwork: exact quaternion integration (`a8eb8a4`), incremental peer heading (`d78e77b`), clock guard (`93456be`), commanded-speed cadence feed (`6374efc`), Abs-macro fix (`7615ecd`). - **The old "needs the MASTER HALF" conclusion was SUPERSEDED:** the leftover accel/decel shakiness was ROOT-CAUSED as single-box **packet jitter** (`49d73dc`) — two Debug nodes on one box batch TCP delivery, so records arrive bursty (BT_RXJIT burstiness 3-7x) and the peer dead-reckons across the gaps then snaps. Pinning each node to a disjoint core set (`ProcessorAffinity 0x00F/0x3C0`, baked into `tools/mp_launch.sh`) restored even ~17ms delivery and the shakiness vanished (user-confirmed). Real pods are dedicated machines and never see it. **DO NOT add an interpolation/jitter buffer** to mask a rig artifact. - Minor non-gating follow-ups (tracked in [[multiplayer]]): the peer accel/decel drift (flagged ~2.9u, but **MEASURED ~0.64u max** on the affinity rig with the peer on the body channel -- `BT_SNAPLOG` + `BT_DRIVE_SWEEP0`, 2026-07-15; sub-unit, effectively resolved, no fix warranted); the exact `_DAT_004ab9cc` offset-decay constant; whether `IntegrateMotion`'s 2-stage angular integrate is intended. - Speed model decomp-settled (`2c6db6a`): analog throttle, '5 speeds' false, `throttleState@0x4a4` is actually the fall-surface material (RENAME PENDING). ## Missile launcher trips FailureHeat -> permanent NoAmmo brick (REPRODUCED 2026-07-20) Playtest: "lost the ability to fire missiles mid-fight, no pips, sad noise -- did I run out?" Added the `[ammo]` fire trace (env `BT_AMMO_LOG`, projweap.cpp) and reproduced it: the SRM6 is NOT out of ammo -- after ~5 volleys it trips **gate 1 FailureHeat** (`heatAlarm==2`) and latches into the NoAmmo roach-motel (weaponAlarm 7, never released) with **19 rounds still in the bin**: [ammo] SRM6_2 FIRED, rounds left=19 [ammo] SRM6_2 -> NoAmmo (gate1): destroyed=0 failHeat=1 heatLvl=2 mechDisabled=0 This is the heat-economy open question made concrete (gauges-hud.md "degenerate bring-up heat scale"; projweap.cpp:730 "whether FailureHeat is reachable at authentic play intensity"): our autofire test fires 60/s (extreme), but the user hit it in MANUAL play too -> either the per-shot heat / FailureTemperature scaling is too aggressive, or the roach-motel (no recovery when heat falls) is the wrong reading. NEXT: audit the SRM6 heatPerShot vs FailureTemperature vs the mech's dissipation, and whether the binary's gate-1 FailureHeat is a LATCH or clears when heat drops. Trace it live with `BT_AMMO_LOG=1` (+ `BT_PROJ_LOG=1` for per-tick heat/recoil).