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:
co-authored by
Claude Opus 5
parent
62513b2f8a
commit
f3bdb3b85a
+17
-12
@@ -73,18 +73,23 @@ DECLOUDS must stay in the sky pass). Authored TEXTURE **SCROLL** (BMF TEXTURE ta
|
||||
`SetFogStyle(searchLightOn/OffFogStyle)`). TWO independent reasons it never fires:
|
||||
(1) **Port:** `PullFogRenderable` is NEVER CONSTRUCTED (grep: only its .h decl + .cpp def; no `new`);
|
||||
`searchLightOn/Off` is called from nowhere else (BT_FOG_LOG: over 22s only `SetFogStyle(0)`, zero 2/3).
|
||||
(2) **Original 1995 LATENT BUG [T1, decomp]:** even if wired, the Searchlight never lights —
|
||||
`ToggleLamp` (@004b860c) toggles `commandedOn` @0x1DC (part_013.c:6087) but `SearchlightSimulation`
|
||||
(@004b841c) reads `requestedOn` @0x1E0 (part_013.c:5978), which the ctor zeroes and NOTHING ever writes;
|
||||
no bridge 0x1DC→0x1E0 exists anywhere in the binary. So `lightState`@0x1D8 is perpetually 0. The
|
||||
self-consistent sibling **ThermalSight** reads the SAME field it toggles (0x1DC) — the tell that
|
||||
Searchlight's sim reads the wrong slot. The port reproduces this faithfully (searchlight.cpp:189 reads
|
||||
requestedOn). **DECISION (2026-07-13): left as-is + documented** (faithful to the buggy original); the
|
||||
port keeps the static lights-ON `fog=` values. To make the swap WORK is a DESIGNER-INTENT fix that
|
||||
DEVIATES from the shipped binary — repair the sim to read `commandedOn` (mirror ThermalSight) + construct
|
||||
`PullFogRenderable` at btl4vid.cpp `MakeMechRenderables` (the reticle-build/inside pass, == arcade
|
||||
part_014.c:5173, ExecutionType Dynamic, bound to each Searchlight `lightState`) + a toggle input. See
|
||||
[[open-questions]]. Core per-map/time/weather fog is unaffected.
|
||||
(2) ~~**Original 1995 LATENT BUG**~~ — ❌ **RETRACTED 2026-07-25 (#61). There is no 1995 bug.** The
|
||||
claim rested on a **swapped table attribution**: @004b860c is **ThermalSight's** `ToggleLamp`
|
||||
(table @0x51120C), not Searchlight's, so pitting its `0x1DC` write against Searchlight's
|
||||
`SearchlightSimulation` (@004b841c, reads `requestedOn`@0x1E0) compared two different classes and
|
||||
manufactured a missing "0x1DC→0x1E0 bridge". Searchlight's own handler is **@004b838c** (table
|
||||
@0x51117C) and toggles the field its own Performance reads, `0x1E0` — exactly as self-consistently as
|
||||
ThermalSight does with `0x1DC`. [T1 attribution: un-pooled `"ToggleLamp"` strings sit adjacent to
|
||||
their own class names, `section_dump.txt:69661-69711`. Body: @004b838c sits in a Ghidra export gap
|
||||
(#60) but was **recovered by raw disasm** (`scratchpad/dis838c.py`) — it toggles 0x1E0. All T1.]
|
||||
**The searchlight DID light in the arcade, and the fog swap was NOT inert there.** Searchlight's
|
||||
handler is now wired (#61) and **verified live**: pad `0x14` → `requestedOn 0→1` → `lightState 0→1`.
|
||||
So the ONLY remaining blocker is (1), the un-constructed `PullFogRenderable` — and building it is
|
||||
**FAITHFUL, not a designer-intent deviation** (the old "DECISION 2026-07-13: left as-is" is void;
|
||||
the sim needs NO repair). Remaining work: construct `PullFogRenderable` at btl4vid.cpp
|
||||
`MakeMechRenderables` (the reticle-build/inside pass, == arcade part_014.c:5173, ExecutionType
|
||||
Dynamic, bound to each Searchlight `lightState`). See [[open-questions]]. Core per-map/time/weather
|
||||
fog is unaffected.
|
||||
- **Projection / stencil (task #55):** the BTDPL.INI `viewangle=60` is the **HORIZONTAL** FOV —
|
||||
authentic 60×47 frustum at 4:3; fovY derived via `BTFovYFromHorizontal` (L4VIDEO.cpp), applied at
|
||||
all 5 projection sites (it was mis-applied as vertical). The device depth-stencil is now
|
||||
|
||||
Reference in New Issue
Block a user