Searchlight + ThermalSight ToggleLamp WIRED (#61) -- and a swapped table attribution ROOT-CAUSED, retracting a false "1995 latent bug"

Both classes' Receiver::MessageHandlerSet were default-constructed blackholes
(input-path audit systemic cause #1): entryCount 0, no parent chain, Find()
returns NullHandler for every id, Receive drops silently.  The new unhandled-
message trace printed it on the first press:

    [btntest] PRESS 0x14 at poll 400
    [msg] UNHANDLED: Searchlight has no handler for message id 3

Each class now has a GetMessageHandlers() function-local static chained to
PowerWatcher's (which name-resolves through HeatWatcher/MechSubsystem to
Receiver's empty ROOT set, so id 3 does NOT collide with HeatSink's
ToggleCooling), plus a correctly-typed forwarder -- Receiver::Handler is
void(const Message*) while the decomp shape is Logical(Message&), so a cast
would be UB that merely happens to work on x86 __thiscall.  DefaultData
re-pointed off the dead empty sets.  MessageArg now reads the NAMED
ReceiverDataMessageOf<int>::dataContents with a static_assert locking it to the
binary's message+0xC (was a raw offset read -- databinding rule).

ROOT CAUSE of a long-standing misattribution: @004b860c is **ThermalSight's**
ToggleLamp (table @0x51120C), NOT Searchlight's.  The two TUs emit parallel
shared-data blocks with identical stride (msg entry, +0x5C attrs, +0x84
Performance triple); what pins each entry to its TU is that "ToggleLamp" is NOT
pooled across them -- two copies exist, each emitted immediately before its own
class-name string ("Searchlight"@0x51144B, "ThermalSight"@0x511475).
[T1: reference/decomp/section_dump.txt:69661-69711]

Searchlight's own handler is @004b838c, which sits in a Ghidra EXPORT GAP (#60).
RECOVERED by raw disassembly of content/BTL4OPT.EXE (scratchpad/dis838c.py, the
EjectAmmo technique) -- and it corrected two things I had inferred wrong:
  * NO ControlsAllowLights/+0x25C novice gate.  Searchlight tests the press
    alone; that lock is ThermalSight-only.  A NOVICE pilot CAN work the lamp.
  * `or word ptr [this+0x18],1` sits AT the jle target -> the graphics-dirty bit
    (updateModel == ForceUpdate(), per #59) is raised UNCONDITIONALLY.

Consequence: the "ORIGINAL 1995 LATENT BUG -- the searchlight can never light"
claim is RETRACTED.  It compared Searchlight's Performance (@004b841c, reads
requestedOn@0x1E0) against ThermalSight's toggle (0x1DC) -- two different
classes -- and so invented a missing 0x1DC->0x1E0 bridge.  Every sibling toggles
the field its own Performance reads.  The searchlight DID light in the arcade,
the searchlight->fog swap was NOT inert there, and building PullFogRenderable is
FAITHFUL rather than a designer-intent deviation (the 2026-07-13 "left as-is"
decision is void; the sim needs no repair).

Verified live, real click seam (BT_BTNTEST):
  0x14 -> [light] requested ON -> reported lightState 0 -> 1     (lamp lights)
  0x12 -> [light] thermal sight requested ON -> thermalActive 0 -> 1
UNHANDLED lines for both classes gone; 40 LNK2019 unchanged (the pre-existing
CreateStreamedSubsystem + Entity__SharedData::DefaultData families only).

Swept the misattribution out of searchlight.cpp/.hpp, thermalsight.cpp/.hpp,
hud.cpp:282 (it had claimed @00511180/@004b838c as HUD's), btplayer.cpp's +0x25C
consumer list, context/{decomp-reference,subsystems,rendering,open-questions,
pod-hardware,experience-levels}.md, docs/{GLASS_COCKPIT,INPUT_PATH_AUDIT}.md.
checkctx.py CLEAN.

Still open (documented, not fixed): ThermalSight has no visible IR effect
(ToggleGlobalThermalVision is a marked no-op, pvision unported, IsLocallyViewed
returns False); ThermalSight publishes "LightState"->0x1D8 where the binary has
"LightOn"->0x1D8 and "LightState"->0x1E0; Searchlight's commandedOn@0x1DC has no
identified role and measured 21 live, hinting our SubsystemResource +0x28 differs
from the binary's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-25 10:46:59 -05:00
co-authored by Claude Opus 5
parent 62513b2f8a
commit f3bdb3b85a
17 changed files with 667 additions and 76 deletions
+4 -3
View File
@@ -188,9 +188,10 @@ So: **0x2C = the COOLANT FLUSH button** (Reservoir InjectCoolant, WORKS), **0x2F
0x2A/0x29 = per-condenser VALVE buttons** (MoveValve, WORK), **0x1A-0x1D = Generator A-D ON/OFF**
(`ToggleGeneratorOnOff` id 4, table @0050fb90 fn @004b1ed0 — DEAD, unreconstructed). Also new:
0x13 → Mech **DuckRequest** (0x1a @0049fa00 — the manual's CROUCH button!), 0x28 → Mech
**BalanceCoolant** (0x16 @0049f728), 0x12 → ThermalSight toggle (id 3), 0x14 → Searchlight +
Searchlight2 **ToggleLamp** (id 3 @004b860c — body reconstructed but `Searchlight::
MessageHandlers` is default-constructed EMPTY, so it never fires: the quick win). Weapon
**BalanceCoolant** (0x16 @0049f728), 0x12 → ThermalSight **ToggleLamp** (id 3, table
@0x51120C fn @004b860c), 0x14 → Searchlight + Searchlight2 **ToggleLamp** (id 3, table @0x51117C
fn **@004b838c**) — ✅ both WIRED 2026-07-25 (#61); the two fn addresses were swapped here before
that (see `context/decomp-reference.md`). Weapon
Eng-page extras: msg 0x3 = **ToggleCooling** (HeatableSubsystem @0050e41c fn @004ad6f8, unwired),
msg 0xb = **ToggleSeekVoltage** (energy @004ba478) / **EjectAmmo** (ammo weapons @004bb9b8),
Myomers msg 9 = ToggleSeekVoltage (@004b8a48; body exists, unwired). Unhandled messages are
+3 -3
View File
@@ -79,8 +79,8 @@ Ranked by impact. **Reverse thrust `0x3F` (key LALT / pad B) is FIXED** and conf
| 8 | **Any clickable button latches if the mouse-up is lost** (worst on `0x3F` reverse, `0x40` trigger) | A momentary click always releases | Neither WndProc handles `WM_CAPTURECHANGED` or `WM_KILLFOCUS` — grep over `engine/` + `game/` returns **zero** hits. Alt-tab or a system dialog between press and release leaves `gCkPressed` set and `SetScreenButton(a,0)` never runs | **M** — trap: `btl4main.cpp:137` calls `ReleaseCapture()` itself, which re-enters `WM_CAPTURECHANGED`; guard on `gCkPressed != -1` (it is cleared before `ReleaseCapture`) | Click-hold `0x3F`, alt-tab, release outside the window: `BT_PAD_LOG=1` shows no release line and `rev` stays 1 |
| 9 | **Every HOTAS / flight stick / rudder** (whole `joydev/joyaxis/joybutton/joyhat` grammar) | Drive the pod channels from a DirectInput device | `content/bindings.txt` is 89 lines and has **zero** joy rows; the joy poll is gated on `joyAxisBindingCount>0 \|\| …` (`L4PADRIO.cpp:775-777`) so `BTJoyInit/BTJoyPoll` are never called. `PadBindingProfile::Load` writes the default only when the file is **absent** (`L4PADBINDINGS.cpp:708-713`), and the file is gitignored (`.gitignore:23`) — so the zip ships **this machine's stale file** | **S** now (ship without it / refresh it) · **M** later (version marker + additive merge) | `content/jstest.log`: `bindings loaded: 40 keys, 10 pad buttons, 5 pad axes, 0 joy axes…`; no `[joy]` line exists in any log in `content/` |
| 10 | **`joyconfig.bat` re-runs the wizard after it finishes** | Run once, then play | `BT_JOYCONFIG` is never cleared (`game/btl4main.cpp:287-291`), and the front end relaunches the exe inheriting the environment (`btl4console.cpp:138-174` clears only `BT_FE_EGG/PODS/SECS/LOOP`) → the mission process re-enters the wizard and blocks on `_getch()` behind the 3D window | **S** (1 line) | Run `joyconfig.bat`, finish it, watch for a second `=== BattleTech joystick setup ===` banner |
| 11 | **`0x14`** Searchlight (+Searchlight2) | Turn the light on for night maps | `Receiver::MessageHandlerSet Searchlight::MessageHandlers;` default-constructed (`searchlight.cpp:50-51`, used `:72`) — the `ToggleLamp` body at `:158` is unreachable. **And wiring it changes nothing visible**: the 1995 binary writes `commandedOn@0x1DC` while the simulation reads `requestedOn@0x1E0` (`open-questions.md:183-200`), faithfully preserved at `searchlight.cpp:189` | **S** to wire (no visible payoff) · **L** to make the light work (needs a fidelity decision) | After wiring, `ToggleLamp` logs twice per press and `lightState` still stays 0 — that is the correct, faithful outcome. **`docs/GLASS_COCKPIT.md:192-194` calling this "the quick win" is misleading** |
| 12 | **`0x12`** Thermal sight | IR view | Two layers dead: `ThermalSight::MessageHandlers` default-constructed (`thermalsight.cpp:52-53`) **and** no body — `ToggleGlobalThermalVision() {}` is an explicit no-op (`thermalsight.hpp:153`), plus the thermal render path is unported | **L** | Not testable until both land |
| 11 | **`0x14`** Searchlight (+Searchlight2) | Turn the light on for night maps | **FIXED 2026-07-25 (#61).** `Searchlight::MessageHandlers` was default-constructed, so the `ToggleLamp` body was unreachable; it now chains `PowerWatcher::GetMessageHandlers()` with id 3. **My "wiring it changes nothing visible" verdict in this row was WRONG** — it rested on the swapped attribution this same report flags in §Corrections: @004b860c (which writes `commandedOn@0x1DC`) is **ThermalSight's** handler, not Searchlight's. Searchlight's is **@004b838c** → toggles `requestedOn@0x1E0`, the field its own Performance reads. There was never a 1995 latent bug | **S — done** | **Verified live**: `BT_BTNTEST=0x14,400,900``[light] requested ON` then `[light] reported lightState 0 -> 1`. The lamp lights. (`scratchpad/lamptest.py`) |
| 12 | **`0x12`** Thermal sight | IR view | **handler + body FIXED 2026-07-25 (#61).** `ThermalSight::MessageHandlers` was default-constructed AND it had no `ToggleLamp` at all — the exported body @004b860c had been transcribed into `searchlight.cpp` by mistake. Both now correct: id 3 → `requestedOn@0x1DC`, read by `ThermalSightSimulation` @004b8648. ⚠ Still **no visible IR effect**: `ToggleGlobalThermalVision()` is a marked no-op and the thermal render path (pvision palette flip) is unported, and `IsLocallyViewed()` returns False | **S done** · **L** for the renderer | **Verified live**: `BT_BTNTEST=0x12,400,900``[light] thermal sight requested ON` then `reported thermalActive 0 -> 1`. State machine + alarm authentic; screen unchanged (`scratchpad/thermtest.py`) |
| 13 | **Numpad cluster with NumLock OFF** | Aim the torso | `L4PADRIO.cpp:583-584` reads `VK_NUMPAD8..9` verbatim; with NumLock off Windows delivers `VK_UP`/`VK_DOWN`/`VK_LEFT`/`VK_RIGHT`, which `bindings.txt:26,27,30,31` map to **throttle and pedals** — "aim up" accelerates the mech. `NUMPAD5/7/9``VK_CLEAR/HOME/PRIOR`, unbound | **S** (startup warning) · **M** (extended-key flag) | NumLock off, hold NUMPAD8 with `BT_MPPR_TRACE=1`: `thr=` rises |
| 14 | **Duplicate rows on one axis add instead of aliasing** (`W`+`UP`+`LSHIFT` = 3× lever rate; `A`+`LEFT`+`NUMPAD7` = 3× pedal) | One lever, one rate | `slewDelta[channel] += rate*dt` per **binding** with no per-channel fold (`L4PADRIO.cpp:604-620`). End state is identical (clamped `:956-961`, spring uses max `:157-168`) — only the ramp rate multiplies, so LSHIFT+W doubles throttle rate | **S** but same file as #4**bundle and defer** | `BT_GAIT_TRACE=1`, hold `W` alone vs `W`+`UP`, compare lever delta/second |
@@ -187,4 +187,4 @@ The whole class exists because **three failure modes produce zero output**. Each
5. **Promote the auditors' `BTL4.RES` decoder to `tools/`.** Records are 24 bytes (`controlsGroup, controlsElement, mappingType, subsystemID, modeMask, attributeID|messageID` per `CONTROLS.h:783-803`); type-19 `ControlMappingStream` per mech, validated against `content/ctrlmap.log` at 121/121 records with zero mismatches. It makes "is this button mapped on this mech, on this page?" a two-second static query instead of a per-mech log hunt — and it already produced the authoritative answer, identical on all 18 mechs: **MAPPED** = `0x00-0x15, 0x18, 0x1A-0x1D, 0x20-0x2F, 0x3F, 0x40-0x44, 0x47`; **NEVER MAPPED** = `0x16, 0x17, 0x19, 0x1E, 0x1F, 0x30-0x3E, 0x45, 0x46`.
### KB sweep owed in the same commits (per the CLAUDE.md correction mandate)
`context/pod-hardware.md:121-135` (false AddOrErase premise — also in `mechmppr.cpp:894-919`, `docs/GLASS_COCKPIT.md` §2a); `context/pod-hardware.md:50-61` (`0x28` BalanceCoolant, weapon `msg 0xb`, Myomers `msg 9` are all LIVE since 2026-07-21/23); the hat labels `0x41-0x44` (the shipped RES says `0x41` lookBehind, **`0x42` TORSO CENTER**, `0x43` lookRight, `0x44` lookLeft — fix `content/CONTROLS.MAP:20-22`, `L4PADBINDINGS.cpp:159-160`, `pod-hardware.md`); `context/open-questions.md:172-182` (census stale in both directions — `0x28`/ToggleCooling/ToggleSeekVoltage/EjectAmmo now live, hotbox never listed); `btl4mppr.cpp:1332` calls `0x200000` "ModePreset1" — the surviving T0 header names it **ModeEject** (`game/original/BT_L4/BTL4MODE.HPP:74`, which is authoritative and not yet cited by any context file); `searchlight.cpp:51`/`hud.cpp:282` table-address citations are +4 and mis-paired (real: Searchlight `@0x51117C → @004b838c`, ThermalSight `@0x51120C → @004b860c`); stale `//ButtonPanic=0x38` at `L4CTRL.cpp:112`.
`context/pod-hardware.md:121-135` (false AddOrErase premise — also in `mechmppr.cpp:894-919`, `docs/GLASS_COCKPIT.md` §2a); `context/pod-hardware.md:50-61` (`0x28` BalanceCoolant, weapon `msg 0xb`, Myomers `msg 9` are all LIVE since 2026-07-21/23); the hat labels `0x41-0x44` (the shipped RES says `0x41` lookBehind, **`0x42` TORSO CENTER**, `0x43` lookRight, `0x44` lookLeft — fix `content/CONTROLS.MAP:20-22`, `L4PADBINDINGS.cpp:159-160`, `pod-hardware.md`); `context/open-questions.md:172-182` (census stale in both directions — `0x28`/ToggleCooling/ToggleSeekVoltage/EjectAmmo now live, hotbox never listed); `btl4mppr.cpp:1332` calls `0x200000` "ModePreset1" — the surviving T0 header names it **ModeEject** (`game/original/BT_L4/BTL4MODE.HPP:74`, which is authoritative and not yet cited by any context file); `searchlight.cpp:51`/`hud.cpp:282` table-address citations are +4 and mis-paired (real: Searchlight `@0x51117C → @004b838c`, ThermalSight `@0x51120C → @004b860c`) — ✅ **SWEPT 2026-07-25 (#61)** in `searchlight.cpp/.hpp`, `thermalsight.cpp/.hpp`, `hud.cpp:282`, `btplayer.cpp:1957`, `context/decomp-reference.md`, `context/pod-hardware.md`, `docs/GLASS_COCKPIT.md`; this swap had also manufactured a false "1995 latent bug" now retracted in `context/subsystems.md`, `context/rendering.md`, `context/open-questions.md`; stale `//ButtonPanic=0x38` at `L4CTRL.cpp:112`.