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
@@ -263,9 +263,24 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a
|
||||
template: `Condenser::MoveValveMessageHandler` (id 4, same novice guard). See [[experience-levels]].
|
||||
- **Generator** @0x50FB90: {4, "ToggleGeneratorOnOff"→@004b1ed0} (streamed buttons 0x1A-0x1D →
|
||||
GeneratorA-D — unreconstructed).
|
||||
- **Searchlight** @0x51120C: {3, "ToggleLamp"→@004b860c}; PowerWatcher/ThermalSight-side variant
|
||||
@0x51117C: {3, "ToggleLamp"→@004b838c}. (Reconstruction has Searchlight::ToggleLamp but the
|
||||
MessageHandlers set is default-constructed EMPTY — unwired.)
|
||||
- **Searchlight** @0x51117C: {3, "ToggleLamp"→**@004b838c**}; **ThermalSight** @0x51120C: {3,
|
||||
"ToggleLamp"→**@004b860c**}. ⚠ **CORRECTED 2026-07-25 (#61)** — these were swapped here (and in
|
||||
`searchlight.cpp:51`, `hud.cpp:282`), which propagated a false "1995 latent bug" across four
|
||||
context files; see the WAVE-4 note in [[subsystems]]. The two TUs emit PARALLEL shared-data blocks
|
||||
with identical stride — msg entry, +0x5C attribute entries, +0x84 Performance triple:
|
||||
| TU | msg entry | handler | attributes | Performance |
|
||||
|---|---|---|---|---|
|
||||
| Searchlight | @0x51117C {3,"ToggleLamp"@0x511440} | @004b838c | LightOn→0x1D8, LightState→0x1E4 | @004b841c (reads **0x1E0**) |
|
||||
| ThermalSight | @0x51120C {3,"ToggleLamp"@0x51146A} | @004b860c | LightOn→0x1D8, LightState→0x1E0 | @004b8648 (reads **0x1DC**) |
|
||||
What pins each entry to its TU: **"ToggleLamp" is NOT pooled across the two** (two copies exist)
|
||||
and each sits immediately before its own class-name string — `"Searchlight"@0x51144B` and
|
||||
`"ThermalSight"@0x511475`. [T1: `reference/decomp/section_dump.txt:69661-69711`]
|
||||
**@004b838c is a Ghidra EXPORT GAP (#60 list)** — `functions_index.tsv` jumps straight to 4b83b8.
|
||||
**RECOVERED 2026-07-25 by raw disasm** of `content/BTL4OPT.EXE` (`scratchpad/dis838c.py`, the
|
||||
EjectAmmo technique) [T1]: `if (msg+0xC > 0) [this+0x1E0] = ([this+0x1E0]==0);` then
|
||||
`or word ptr [this+0x18],1` **unconditionally** (== `ForceUpdate()`, per #59). Note it has **NO**
|
||||
novice/+0x25C gate — that is ThermalSight-only; see [[experience-levels]].
|
||||
**Both sets WIRED 2026-07-25 (#61)**; both were default-constructed blackholes before.
|
||||
- **Myomers-class** @0x51158C: {9, "ToggleSeekVoltage"→@004b8a48}; **energy-weapon** variant
|
||||
@0x511DB8: {0xb, "ToggleSeekVoltage"→@004ba478}; **ammo-weapon** @0x512210: {0xb,
|
||||
"EjectAmmo"→@004bb9b8}. **Both ToggleSeekVoltage handlers WIRED 2026-07-21 (issue #19)**:
|
||||
|
||||
@@ -43,8 +43,12 @@ then unconditionally `+0x264 = +0x268 = mission->advancedDamageOn(+0xf0)` (both
|
||||
## What each flag gates (binary consumers, all reached via `mech+0x190` → BTPlayer) [T1]
|
||||
|
||||
- **+0x25c — "sim live", off only for novice.** Consumers: ballistic jam roll `CheckForJam`
|
||||
@4bbfcc early-returns NO-JAM when 0 (projweap.cpp calls this `LiveFireEnabled`); Searchlight
|
||||
`ToggleLamp` @4b860c (searchlight.cpp `ControlsAllowLights`); PoweredSubsystem message handler
|
||||
@4bbfcc early-returns NO-JAM when 0 (projweap.cpp calls this `LiveFireEnabled`); **ThermalSight**
|
||||
`ToggleLamp` @4b860c (thermalsight.hpp `ControlsAllowLights`) — ⚠ **CORRECTED 2026-07-25 (#61):
|
||||
this was listed as *Searchlight's* gate. It is not. Raw disassembly of Searchlight's real handler
|
||||
@004b838c (a Ghidra export gap) shows it gates on the PRESS ALONE and never reads +0x25C, so a
|
||||
NOVICE pilot CAN work the searchlight; only the thermal sight is locked** [T1, `scratchpad/dis838c.py`];
|
||||
PoweredSubsystem message handler
|
||||
@4b0efc (powersub.cpp's "message-manager short-event flag" — same flag, misattributed); the mech
|
||||
combat-ineffective evaluation (part_012.c:9364: mech state ∈ {3,4} && novice is an extra OR-term
|
||||
setting `mech+0x414`). The `#if 0` block in surviving PPC.CPP:63-95 (novice PPCs deal no damage)
|
||||
@@ -105,8 +109,9 @@ The wiring landed (uncommitted during the coordination pause; issue #2):
|
||||
consumers route mech→`GetPlayerLink()`→named member; NULL player reads permissive [T3].
|
||||
- **Gates unified**: `HeatSink::HeatModelActive()` (heat.hpp; also serves mislanch/projweap/
|
||||
heatfamily call sites) + `Myomers::OwnerAdvancedDamage()` (myomers.hpp) → the +0x260 bridge;
|
||||
`ProjectileWeapon::LiveFireEnabled()` (projweap.cpp) + `Searchlight::ControlsAllowLights()`
|
||||
(searchlight.hpp) → the +0x25c bridge; `PoweredSubsystem::HandleMessage` @4b0efc (powersub.cpp)
|
||||
`ProjectileWeapon::LiveFireEnabled()` (projweap.cpp) + `ThermalSight::ControlsAllowLights()`
|
||||
(thermalsight.hpp) → the +0x25c bridge (**not** `Searchlight::ControlsAllowLights()`, which #61
|
||||
showed is unused — its handler has no novice gate; the accessor is retained but dead); `PoweredSubsystem::HandleMessage` @4b0efc (powersub.cpp)
|
||||
→ not-novice + the +0x25c bridge (the old "message-manager short-event flag" misattribution and
|
||||
its dead `BT_GetMessageManager` guard are gone).
|
||||
- **FUN_004ac9c8 family corrected**: `MechSubsystem::IsDamaged` (mechsub.cpp — was a live
|
||||
|
||||
+20
-18
@@ -180,24 +180,26 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
|
||||
@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 — DEFERRED, and it is an ORIGINAL 1995 LATENT BUG (task #63,
|
||||
2026-07-13) [T1 decomp].** The arcade swaps fog between `fog=` (lights on) and `nosearchlightfog=`
|
||||
(off) via `PullFogRenderable` watching the Searchlight's `lightState`. It never fires — in the PORT
|
||||
because `PullFogRenderable` is never constructed, AND in the BINARY ITSELF because the Searchlight
|
||||
never lights: `ToggleLamp` (@004b860c) toggles `commandedOn` @0x1DC but `SearchlightSimulation`
|
||||
(@004b841c) reads `requestedOn` @0x1E0, which is zeroed by the ctor and never written — no
|
||||
0x1DC→0x1E0 bridge exists (self-consistent sibling ThermalSight reads the field it toggles, 0x1DC).
|
||||
So `lightState` is perpetually 0. The port reproduces this faithfully (searchlight.cpp:189).
|
||||
**DECISION: left as-is + documented (faithful to the buggy original); port keeps the static
|
||||
lights-ON `fog=`.** A working swap is a DESIGNER-INTENT deviation, fully scoped if ever wanted:
|
||||
(1) sim reads `commandedOn` (mirror ThermalSight); (2) construct `PullFogRenderable` at
|
||||
btl4vid.cpp `MakeMechRenderables` reticle-build/inside pass (== arcade part_014.c:5173, Dynamic,
|
||||
bound per Searchlight `lightState` via a new `LightStatePtr()` accessor); (3) a toggle input (the
|
||||
authentic cockpit button-5→`ToggleLamp` dispatch is a controls-family reconstruction; a dev key
|
||||
stands in). `ControlsAllowLights()` (searchlight.hpp) is WIRED since issue #2 to the
|
||||
`player+0x25c` not-novice experience flag via the BTPlayerExperienceSimLive bridge
|
||||
([[experience-levels]]); the consumer (ToggleLamp) still needs the cockpit button dispatch.
|
||||
See [[rendering]] fog section. Verified inert live: BT_FOG_LOG shows zero `SetFogStyle(2/3)`.
|
||||
- **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.)
|
||||
- **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`
|
||||
|
||||
@@ -55,9 +55,13 @@ the per-condenser VALVE buttons** (MoveValve, Cond1-6 — work), **0x1A-0x1D = G
|
||||
ON/OFF** (`ToggleGeneratorOnOff` id 4, binary table @0050fb90 fn @004b1ed0 — DEAD until
|
||||
reconstructed). Newly decoded from the binary message tables: **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 the handler-set is default-constructed
|
||||
EMPTY — unwired)** — all currently dead; an unhandled message is SILENTLY ignored
|
||||
@0049f728)**, **0x12 → ThermalSight `ToggleLamp` (id 3, table @0x51120C fn @004b860c)** and **0x14 →
|
||||
Searchlight + Searchlight2 `ToggleLamp` (id 3, table @0x51117C fn @004b838c)** — ✅ **BOTH WIRED
|
||||
2026-07-25 (#61)**, previously default-constructed blackholes; verified live (0x14 →
|
||||
`lightState 0→1`, 0x12 → `thermalActive 0→1`). ⚠ The fn addresses were **swapped** in this file
|
||||
before #61 (@004b860c is ThermalSight's) — see [[decomp-reference]] for the table evidence — and
|
||||
that swap had propagated a false "1995 latent bug" (retracted, [[subsystems]] WAVE 4).
|
||||
An unhandled message is SILENTLY ignored
|
||||
(Receiver::Receive finds no handler and does nothing — dead buttons produce NO log).
|
||||
0x10/0x11 = map ZoomIn/Out, 0x15 = CycleDisplayMode, 0x18 = CycleControlMode (#6). Buttons
|
||||
with NO streamed mapping authored (authentically inert): 0x16/0x17/0x19/0x1E/0x1F/0x38-0x3E.
|
||||
|
||||
+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
|
||||
|
||||
+23
-5
@@ -41,11 +41,29 @@ Making a base byte-exact GROWS every subclass — they must be re-based TOGETHER
|
||||
- **WAVE 3** — power bus (Generator/PoweredSubsystem) + Emitter/PPC fire-path (end-to-end fire; heat
|
||||
conducts to the central sink via the linked-sink roster).
|
||||
- **WAVE 4** — standalone readouts: Sensor/Searchlight/ThermalSight/AmmoBin (de-shim, gate fixes).
|
||||
- ⚠ **Searchlight has an ORIGINAL latent bug [T1, task #63]:** `SearchlightSimulation` (@004b841c)
|
||||
reads `requestedOn`@0x1E0 (never written) while `ToggleLamp` (@004b860c) toggles `commandedOn`@0x1DC
|
||||
— no bridge, so `lightState`@0x1D8 is perpetually 0 (lamp never lights). Present in the 1995 binary
|
||||
(sibling ThermalSight reads the field it toggles, 0x1DC); the port reproduces it faithfully. Left
|
||||
as-is by decision; it makes the searchlight→fog swap inert. See [[open-questions]] + [[rendering]] fog.
|
||||
- ✅ **Searchlight + ThermalSight buttons WIRED 2026-07-25 (#61)** — both classes' handler sets were
|
||||
default-constructed blackholes (systemic cause #1, `docs/INPUT_PATH_AUDIT.md`). Each now chains
|
||||
`PowerWatcher::GetMessageHandlers()` with its own id-3 `ToggleLamp`. **Verified live**: pad `0x14`
|
||||
→ `requestedOn 0→1` → `lightState 0→1`; pad `0x12` → `requestedOn 0→1` → `thermalActive 0→1`.
|
||||
- ❌ **RETRACTED: the "ORIGINAL 1995 latent bug" (old task #63) NEVER EXISTED.** It was an artifact of
|
||||
a **swapped table attribution**: the claim compared Searchlight's Performance (@004b841c, reads
|
||||
`requestedOn`@0x1E0) against **ThermalSight's** `ToggleLamp` (@004b860c, toggles `0x1DC`) and
|
||||
concluded "no 0x1DC→0x1E0 bridge, so the lamp can never light". @004b860c is not Searchlight's.
|
||||
Searchlight's own handler is **@004b838c** (table @0x51117C) and — like every sibling — toggles the
|
||||
field its OWN Performance reads, `requestedOn`@0x1E0. Each class is internally self-consistent.
|
||||
[T1 throughout. Attribution: un-pooled `"ToggleLamp"` strings adjacent to their own class names,
|
||||
`section_dump.txt:69661-69711`. Body: @004b838c is a Ghidra EXPORT GAP (#60) but was **recovered by
|
||||
raw disasm** of `content/BTL4OPT.EXE` (`scratchpad/dis838c.py`) — it toggles 0x1E0, raises
|
||||
`updateModel` (+0x18) unconditionally, and has **NO novice gate** (ThermalSight-only).] Consequence: the searchlight→fog swap was **NOT** inert in the arcade, and
|
||||
making it work in the port is FAITHFUL, not a designer-intent deviation. See [[open-questions]] +
|
||||
[[rendering]] fog, both corrected.
|
||||
- ⚠ Still open on Searchlight: `commandedOn`@0x1DC (ctor-seeded from subsystem resource +0x28,
|
||||
@004b84dc) has **no identified role** — it is neither the toggle target nor read by @004b841c, and
|
||||
it measured **21** live (not a boolean), which hints our SubsystemResource +0x28 ≠ the binary's.
|
||||
- ⚠ ThermalSight's published attributes are mis-named: we publish `"LightState"→thermalActive@0x1D8`,
|
||||
but the binary's table @0x511268 has **`"LightOn"`(id 3)→0x1D8** and `"LightState"`(id 4)→**0x1E0**
|
||||
(the alarm). A gauge binding `ThermalSight/LightOn` currently misses. Not yet changed (gauge-binding
|
||||
change needs its own verification pass).
|
||||
- **WAVE 5** — Torso (aim twist/elevation; joint-I/O reconstructed; the BLH record disables torso →
|
||||
faithfully no visible twist).
|
||||
- **WAVE 6** — Myomers (mover-coupled; structural un-stub is INERT — no live mover/heat coupling).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
@@ -1954,7 +1954,9 @@ int BTPlayerRoleLocksAdvanced(void *owner_mech)
|
||||
// The player+0x25c "sim live" flag [T1]: 0 only for NOVICE experience. Binary
|
||||
// consumers reached through the owner mech's playerLink (mech+0x190):
|
||||
// * CheckForJam @004bbfcc (projweap.cpp LiveFireEnabled) -- no jam rolls;
|
||||
// * Searchlight::ToggleLamp @004b860c (searchlight.hpp ControlsAllowLights);
|
||||
// * ThermalSight::ToggleLamp @004b860c (thermalsight.hpp ControlsAllowLights)
|
||||
// -- and NOT Searchlight, whose own handler @004b838c has no such gate
|
||||
// (raw disasm, #61: it tests the press only), so novices keep the lamp;
|
||||
// * PoweredSubsystem::HandleMessage @004b0efc short-event path (powersub.cpp).
|
||||
// A NULL player (target dummy / unbound dev mech) reads as LIVE so dev rigs
|
||||
// keep today's behavior [T3]; the binary derefs unguarded.
|
||||
|
||||
@@ -278,9 +278,16 @@ void
|
||||
// HandleMessage -- @004b7780 (slot 9)
|
||||
//
|
||||
// BEST-EFFORT: the vtable installs a HUD-specific HandleMessage at @004b7780,
|
||||
// but no body was captured in the recovered shards. A handler table
|
||||
// @00511180 binds the message "ToggleLamp" to FUN_004b838c. Chains to the
|
||||
// PowerWatcher handler otherwise.
|
||||
// but no body was captured in the recovered shards. Chains to the PowerWatcher
|
||||
// handler otherwise.
|
||||
//
|
||||
// ATTRIBUTION CORRECTION (#61, 2026-07-25): this comment used to claim "a handler
|
||||
// table @00511180 binds the message ToggleLamp to FUN_004b838c" as if it were
|
||||
// HUD's. It is NOT -- @0051117C is **Searchlight's** message entry
|
||||
// {3, "ToggleLamp" @00511440, @004b838c}; @00511180 is merely its name+handler
|
||||
// pair (the entry base is 4 bytes lower). HUD's own data ends with its
|
||||
// Performance triple at @00511170 (@004b7830, HudSimulation). See
|
||||
// searchlight.cpp and context/decomp-reference.md for the table evidence.
|
||||
//
|
||||
Logical
|
||||
HUD::HandleMessage(int message)
|
||||
|
||||
@@ -47,8 +47,11 @@ Derivation
|
||||
"Searchlight"
|
||||
);
|
||||
|
||||
// DEAD (Gitea #61): default-constructed -- entryCount 0, no parent -- so it was
|
||||
// a total blackhole. Kept only because the binary symbol exists; DefaultData now
|
||||
// points at GetMessageHandlers() below. Do NOT bind anything to this.
|
||||
Receiver::MessageHandlerSet
|
||||
Searchlight::MessageHandlers; // includes { "ToggleLamp", &Searchlight::ToggleLamp } @00511210
|
||||
Searchlight::MessageHandlers;
|
||||
|
||||
// gauge data-binding wave: publish "LightOn" -> lightState@0x1D8 (the button-5
|
||||
// searchlight lamp; the empty default-constructed index resolved NOTHING).
|
||||
@@ -69,7 +72,7 @@ Searchlight::AttributeIndexSet
|
||||
Searchlight::SharedData
|
||||
Searchlight::DefaultData( // @00511198
|
||||
&Searchlight::ClassDerivations,
|
||||
Searchlight::MessageHandlers,
|
||||
Searchlight::GetMessageHandlers(), // was the EMPTY MessageHandlers (blackhole) -- #61
|
||||
Searchlight::AttributeIndex,
|
||||
Searchlight::StateCount
|
||||
);
|
||||
@@ -148,20 +151,122 @@ Logical
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// ToggleLamp -- @004b860c ("ToggleLamp" message handler)
|
||||
// Message handling -- table @0051117c
|
||||
//
|
||||
// Bound by the shared-data message table @00511210. On a positive command
|
||||
// (message arg @0xC > 0) and when the host mech's controls allow it
|
||||
// (mech +0xD0 -> +0x190 -> +0x25C != 0), flips commandedOn (@0x1DC).
|
||||
// Gitea #61. Searchlight's handler set was default-constructed, so its
|
||||
// ToggleLamp was reachable code bound to nothing: every id-3 press was dropped
|
||||
// in silence. Proved live by the unhandled-message trace:
|
||||
// [btntest] PRESS 0x14 at poll 400
|
||||
// [msg] UNHANDLED: Searchlight has no handler for message id 3
|
||||
//
|
||||
// ATTRIBUTION CORRECTION (this file previously cited @004b860c for Searchlight's
|
||||
// ToggleLamp -- that is ThermalSight's; see thermalsight.cpp). The two TUs emit
|
||||
// PARALLEL shared-data blocks with identical stride (msg entry, +0x5C attribute
|
||||
// entries, +0x84 performance triple):
|
||||
//
|
||||
// Searchlight msg @0051117c {3,"ToggleLamp"@00511440} -> @004b838c
|
||||
// attrs LightOn->0x1D8, LightState->0x1E4 ; perf @004b841c
|
||||
// ThermalSight msg @0051120c {3,"ToggleLamp"@0051146a} -> @004b860c
|
||||
// attrs LightOn->0x1D8, LightState->0x1E0 ; perf @004b8648
|
||||
//
|
||||
// The name pointers disambiguate them: "ToggleLamp" is NOT pooled across the two
|
||||
// TUs (two copies exist, @00511440 and @0051146a), and each is emitted
|
||||
// immediately before its own class-name string -- "Searchlight" @0051144b and
|
||||
// "ThermalSight" @00511475. Our own thermalsight.cpp independently cites
|
||||
// @004b8648 / PTR @00511290, which corroborates the split.
|
||||
// [T1: reference/decomp/section_dump.txt lines 69661-69711]
|
||||
//
|
||||
// The parent link is PowerWatcher's, which -- PowerWatcher, HeatWatcher and
|
||||
// MechSubsystem all declaring no set of their own -- name-resolves to Receiver's
|
||||
// empty root set. That keeps id 3 unambiguous here (HeatSink's ToggleCooling is
|
||||
// on a DIFFERENT chain), which the live trace confirmed before this fix landed.
|
||||
//
|
||||
Receiver::MessageHandlerSet&
|
||||
Searchlight::GetMessageHandlers()
|
||||
{
|
||||
static const Receiver::HandlerEntry entries[]=
|
||||
{
|
||||
MESSAGE_ENTRY(Searchlight, ToggleLamp) // id 3 @004b838c
|
||||
};
|
||||
static Receiver::MessageHandlerSet messageHandlers(
|
||||
ELEMENTS(entries), entries,
|
||||
PowerWatcher::GetMessageHandlers()
|
||||
);
|
||||
return messageHandlers;
|
||||
}
|
||||
|
||||
//
|
||||
// The typed forwarder the table actually binds. Receiver::Handler is
|
||||
// void(const Message*); ToggleLamp is the decomp's Logical(Message&) shape, so
|
||||
// this adapts rather than casting. The static_assert proves our named
|
||||
// dataContents member sits where the binary read the arg (message+0xC).
|
||||
//
|
||||
static_assert(
|
||||
offsetof(ReceiverDataMessageOf<int>, dataContents) == 0xC,
|
||||
"ReceiverDataMessageOf<int>::dataContents must sit at 0xC -- @004b860c reads message+0xC"
|
||||
);
|
||||
|
||||
void
|
||||
Searchlight::ToggleLampMessageHandler(ReceiverDataMessageOf<int> *message)
|
||||
{
|
||||
Check(this);
|
||||
Check_Pointer(message);
|
||||
ToggleLamp(*message);
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// ToggleLamp -- @004b838c ("ToggleLamp" message handler)
|
||||
//
|
||||
// Bound by the shared-data message table @0051117c via the forwarder above.
|
||||
//
|
||||
// @004b838c is a Ghidra EXPORT GAP (#60) -- functions_index.tsv jumps straight to
|
||||
// 4b83b8 -- so this was RAW-DISASSEMBLED from content/BTL4OPT.EXE, the same
|
||||
// technique that recovered EjectAmmo @004bb9b8. Rig: scratchpad/dis838c.py. [T1]
|
||||
//
|
||||
// 004b838c push ebp / mov ebp,esp
|
||||
// 004b838f mov eax,[ebp+8] ; this
|
||||
// 004b8392 mov edx,[ebp+0xc] ; message
|
||||
// 004b8395 mov ecx,[edx+0xc] ; message arg
|
||||
// 004b8398 test ecx,ecx
|
||||
// 004b839a jle 004b83b0 ; release / non-positive -> skip the toggle
|
||||
// 004b839c mov edx,[eax+0x1e0] ; requestedOn
|
||||
// 004b83a2 test edx,edx
|
||||
// 004b83a4 sete cl
|
||||
// 004b83a7 and ecx,1
|
||||
// 004b83aa mov [eax+0x1e0],ecx ; requestedOn = (requestedOn == 0)
|
||||
// 004b83b0 or word ptr [eax+0x18],1 ; <-- UNCONDITIONAL (the jle lands HERE)
|
||||
// 004b83b5 pop ebp / ret
|
||||
//
|
||||
// TWO corrections this disassembly forced on the first reconstruction:
|
||||
//
|
||||
// 1. **There is NO ControlsAllowLights gate.** Searchlight checks only the press
|
||||
// (`msg+0xC > 0`); it never touches mech+0xD0 -> +0x190 -> player+0x25C. That
|
||||
// novice lock is THERMALSIGHT-ONLY (@004b860c has it, this does not) -- so a
|
||||
// NOVICE pilot CAN work the searchlight but not the thermal sight.
|
||||
// 2. The `or word ptr [eax+0x18],1` sits AT the jle target, so the graphics-dirty
|
||||
// bit is raised on EVERY dispatch including the release. Per #59 that 16-bit
|
||||
// write to +0x18 is `updateModel` == ForceUpdate(), NOT `simulationFlags`.
|
||||
//
|
||||
// It toggles +0x1E0 (requestedOn) -- the field Searchlight's own Performance
|
||||
// @004b841c reads -- NOT +0x1DC. (+0x1DC is ctor-seeded from the subsystem
|
||||
// resource +0x28 and measured 21 live, i.e. not a boolean; nothing reads it.)
|
||||
//
|
||||
Logical
|
||||
Searchlight::ToggleLamp(Message &message)
|
||||
{
|
||||
if ((0 < MessageArg(message)) && // *(message + 0xC)
|
||||
ControlsAllowLights()) // *(*(owner +0xD0)+0x190)+0x25C
|
||||
if (0 < MessageArg(message)) // *(message + 0xC) -- press only
|
||||
{
|
||||
commandedOn = (commandedOn == 0); // toggle @0x1DC
|
||||
requestedOn = (requestedOn == 0); // toggle @0x1E0 (read by @004b841c)
|
||||
|
||||
if (getenv("BT_FIRE_LOG") || getenv("BT_HEAT_LOG"))
|
||||
DEBUG_STREAM << "[light] " << GetName() << " searchlight requested "
|
||||
<< (requestedOn ? "ON" : "OFF")
|
||||
<< " (reported lightState " << lightState
|
||||
<< ", voltage level " << WatchedVoltageLevel()
|
||||
<< ", heat level " << HeatStateLevel() << ")" << std::endl;
|
||||
}
|
||||
|
||||
SetGraphicsDirty(); // or word ptr [this+0x18],1 -- UNCONDITIONAL @004b83b0
|
||||
return True;
|
||||
}
|
||||
|
||||
@@ -200,9 +305,28 @@ void
|
||||
|
||||
stateAlarm.SetLevel(lightState); // FUN_0041bbd8(this+0x1E4, lightState)
|
||||
|
||||
if (getenv("BT_FIRE_LOG") || getenv("BT_HEAT_LOG"))
|
||||
{
|
||||
static int announced = 0; // one-shot: proves the Performance is INSTALLED
|
||||
if (!announced)
|
||||
{
|
||||
announced = 1;
|
||||
DEBUG_STREAM << "[light] SearchlightSimulation RUNNING (commandedOn "
|
||||
<< commandedOn << ", requestedOn " << requestedOn
|
||||
<< ", lightState " << lightState << ")" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
if (previous != lightState)
|
||||
{
|
||||
SetGraphicsDirty(); // this[0x18] |= 1
|
||||
|
||||
if (getenv("BT_FIRE_LOG") || getenv("BT_HEAT_LOG"))
|
||||
DEBUG_STREAM << "[light] reported lightState " << previous << " -> " << lightState
|
||||
<< " (commandedOn " << commandedOn << ", requestedOn " << requestedOn << ")"
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
|
||||
@@ -121,8 +121,11 @@
|
||||
//
|
||||
protected:
|
||||
int lightState; // @0x1D8 reported on/off (gated by power + heat each frame); "LightState"
|
||||
int commandedOn; // @0x1DC resource +0x28; toggled by the "ToggleLamp" message (@004b860c)
|
||||
int requestedOn; // @0x1E0 init 0; on/off input consumed by SearchlightSimulation
|
||||
int commandedOn; // @0x1DC ctor-seeded from the subsystem resource +0x28 (@004b84dc).
|
||||
// NOT the toggle target and NOT read by @004b841c; it measured
|
||||
// 21 live, so it is not a boolean -- role still unidentified.
|
||||
int requestedOn; // @0x1E0 init 0; THE on/off input consumed by SearchlightSimulation
|
||||
// (@004b841c reads +0x1E0) and toggled by ToggleLamp @004b838c
|
||||
BtAlarm stateAlarm; // @0x1E4 2-level light indicator (size 0x54)
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -148,12 +151,17 @@
|
||||
// SetGraphicsDirty -> engine Simulation::ForceUpdate() (this+0x18)
|
||||
// SetInstanceFlag -> engine Simulation::simulationFlags |= bits (this+0x28)
|
||||
// GetSegmentFlags() removed (the ctor gate now reads owner->simulationFlags).
|
||||
// ControlsAllowLights() [T1]: the ToggleLamp @004b860c gate is the
|
||||
// owning BTPlayer's +0x25c "sim live" EXPERIENCE flag (mech+0x190
|
||||
// playerLink -> player+0x25c; the old "controls-mapper lightsEnabled"
|
||||
// reading was wrong) -- 0 only for NOVICE experience. Issue #2:
|
||||
// routed through the complete-type bridge in btplayer.cpp
|
||||
// (databinding rule: never raw-read player offsets).
|
||||
// ControlsAllowLights(): ⚠ NOT USED BY Searchlight::ToggleLamp. Raw
|
||||
// disassembly of @004b838c (#61 -- it is a Ghidra export gap, see
|
||||
// searchlight.cpp) shows Searchlight gates on the PRESS ALONE and never
|
||||
// reads mech+0xD0 -> +0x190 -> player+0x25C. The +0x25C novice lock
|
||||
// belongs to the SIBLING ThermalSight (@004b860c), whose handler had been
|
||||
// mis-transcribed into this file -- which is where this accessor came
|
||||
// from. Kept because the bridge itself is correct and callers elsewhere
|
||||
// document the flag; see thermalsight.hpp for the real consumer.
|
||||
// [T1: player+0x25c is the "sim live" experience flag, 0 only for NOVICE;
|
||||
// routed through the complete-type bridge in btplayer.cpp per the
|
||||
// databinding rule.]
|
||||
Logical HostShutDown() const { return simulationState == 1; /* Destroyed */ }
|
||||
int WatchedVoltageLevel() const { return watchdogAlarm.GetLevel(); } // @0x198
|
||||
int HeatStateLevel() const { return heatAlarm.GetLevel(); } // @0x140
|
||||
@@ -164,8 +172,15 @@
|
||||
}
|
||||
void SetGraphicsDirty() { ForceUpdate(); } // this[0x18] |= 1
|
||||
void SetInstanceFlag(int bits) { simulationFlags |= bits; } // this[10] |= bits
|
||||
// The "ToggleLamp" command arg is carried at message+0xC (decomp).
|
||||
static int MessageArg(Message &m) { return *(const int *)((const char *)&m + 0xC); }
|
||||
// The "ToggleLamp" command arg is carried at message+0xC (decomp). That
|
||||
// is exactly ReceiverDataMessageOf<int>::dataContents -- the first member
|
||||
// added to Receiver::Message -- so read the NAMED member instead of the
|
||||
// raw offset (databinding rule). searchlight.cpp static_asserts the
|
||||
// 0xC equivalence so the binary citation above stays honest.
|
||||
static int MessageArg(Message &m)
|
||||
{
|
||||
return Cast_Object(const ReceiverDataMessageOf<int>*, &m)->dataContents;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Simulation Support
|
||||
@@ -189,7 +204,45 @@
|
||||
//
|
||||
public:
|
||||
Logical
|
||||
ToggleLamp(Message &message); // @004b860c (bound to "ToggleLamp" @00511210)
|
||||
ToggleLamp(Message &message); // @004b838c (bound to "ToggleLamp" @0051117c)
|
||||
|
||||
//
|
||||
// Gitea #61 (input-path audit systemic cause #1). The bare
|
||||
// `MessageHandlers` above is DEFAULT-CONSTRUCTED -- entryCount 0, no
|
||||
// parent chain -- so Receiver::MessageHandlerSet::Find() returned
|
||||
// NullHandler for EVERY id and Receiver::Receive dropped every message
|
||||
// the searchlight was ever sent, in silence. Proved live by the new
|
||||
// unhandled-message trace:
|
||||
// [btntest] PRESS 0x14 at poll 400
|
||||
// [msg] UNHANDLED: Searchlight has no handler for message id 3
|
||||
// The binary's table @0051117c holds exactly ONE entry --
|
||||
// { 3, "ToggleLamp" @00511440, @004b838c } -- with this class's attribute
|
||||
// index ("LightOn" -> 0x1D9, "LightState" -> 0x1E5) at @005111d8 and its
|
||||
// Performance triple (@004b841c) at @00511200. [T1: section_dump.txt]
|
||||
//
|
||||
// NOT @004b860c / table @0051120c -- that pair is ThermalSight's (this
|
||||
// file mis-cited it for a long time). The two TUs emit parallel blocks
|
||||
// and each keeps its OWN un-pooled "ToggleLamp" string next to its own
|
||||
// class name: @00511440+"Searchlight"@0051144b here,
|
||||
// @0051146a+"ThermalSight"@00511475 there.
|
||||
//
|
||||
// ToggleLamp's own signature is Logical(Message&) while
|
||||
// Receiver::Handler is void(const Message*), so the table binds a
|
||||
// correctly-typed forwarder rather than a cast (a cast would be UB that
|
||||
// merely happens to work on x86 __thiscall).
|
||||
//
|
||||
// No id collision with HeatSink's id 3 (ToggleCooling): PowerWatcher
|
||||
// declares no set of its own, so `PowerWatcher::GetMessageHandlers()`
|
||||
// name-resolves up through HeatWatcher and MechSubsystem to Receiver's
|
||||
// empty ROOT set -- HeatSink is not on this chain. Confirmed live: the
|
||||
// id-3 press produced the UNHANDLED line and NO coolant toggle.
|
||||
//
|
||||
enum { ToggleLampMessageID = 3 }; // table @0051120c, fn @004b860c
|
||||
|
||||
static Receiver::MessageHandlerSet& GetMessageHandlers();
|
||||
|
||||
void
|
||||
ToggleLampMessageHandler(ReceiverDataMessageOf<int> *message);
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Subsystem virtual overrides (vtable @005114d4)
|
||||
|
||||
@@ -49,6 +49,9 @@ Derivation
|
||||
"ThermalSight"
|
||||
);
|
||||
|
||||
// DEAD (Gitea #61): default-constructed -- entryCount 0, no parent -- so it was a
|
||||
// total blackhole. Kept only because the binary symbol exists; DefaultData now
|
||||
// points at GetMessageHandlers() below. Do NOT bind anything to this.
|
||||
Receiver::MessageHandlerSet
|
||||
ThermalSight::MessageHandlers;
|
||||
|
||||
@@ -72,7 +75,7 @@ ThermalSight::AttributeIndexSet&
|
||||
ThermalSight::SharedData
|
||||
ThermalSight::DefaultData( // @00511228
|
||||
&ThermalSight::ClassDerivations,
|
||||
ThermalSight::MessageHandlers,
|
||||
ThermalSight::GetMessageHandlers(), // was the EMPTY MessageHandlers (blackhole) -- #61
|
||||
ThermalSight::GetAttributeIndex(),
|
||||
ThermalSight::StateCount
|
||||
);
|
||||
@@ -143,6 +146,78 @@ Logical
|
||||
return True; // BEST-EFFORT (family convention)
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// Message handling -- table @0051120c
|
||||
//
|
||||
// See thermalsight.hpp for why @004b860c is THIS class's handler (it had been
|
||||
// transcribed into searchlight.cpp). The parent link resolves to Receiver's
|
||||
// empty root set, so id 3 is unambiguous here.
|
||||
//
|
||||
Receiver::MessageHandlerSet&
|
||||
ThermalSight::GetMessageHandlers()
|
||||
{
|
||||
static const Receiver::HandlerEntry entries[]=
|
||||
{
|
||||
MESSAGE_ENTRY(ThermalSight, ToggleLamp) // id 3 @004b860c
|
||||
};
|
||||
static Receiver::MessageHandlerSet messageHandlers(
|
||||
ELEMENTS(entries), entries,
|
||||
PowerWatcher::GetMessageHandlers()
|
||||
);
|
||||
return messageHandlers;
|
||||
}
|
||||
|
||||
//
|
||||
// The typed forwarder the table binds. Receiver::Handler is
|
||||
// void(const Message*); the decomp's shape is Logical(Message&). The
|
||||
// static_assert proves our named dataContents member sits where the binary read
|
||||
// the arg (message+0xC).
|
||||
//
|
||||
static_assert(
|
||||
offsetof(ReceiverDataMessageOf<int>, dataContents) == 0xC,
|
||||
"ReceiverDataMessageOf<int>::dataContents must sit at 0xC -- @004b860c reads message+0xC"
|
||||
);
|
||||
|
||||
void
|
||||
ThermalSight::ToggleLampMessageHandler(ReceiverDataMessageOf<int> *message)
|
||||
{
|
||||
Check(this);
|
||||
Check_Pointer(message);
|
||||
ToggleLamp(*message);
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// ToggleLamp -- @004b860c ("ToggleLamp" message handler)
|
||||
//
|
||||
// Transcribed instruction-for-instruction [T1]:
|
||||
//
|
||||
// if ((0 < *(int *)(param_2 + 0xc)) &&
|
||||
// (*(int *)(*(int *)(*(int *)(param_1 + 0xd0) + 400) + 0x25c) != 0)) {
|
||||
// *(uint *)(param_1 + 0x1dc) = (uint)(*(int *)(param_1 + 0x1dc) == 0);
|
||||
// }
|
||||
//
|
||||
// 400 == 0x190, the owning-BTPlayer link on the Mech; +0x25C is the player's
|
||||
// "sim live" experience flag (0 only for NOVICE). +0x1DC is requestedOn -- the
|
||||
// exact field ThermalSightSimulation (@004b8648) reads each frame.
|
||||
//
|
||||
Logical
|
||||
ThermalSight::ToggleLamp(Message &message)
|
||||
{
|
||||
if ((0 < MessageArg(message)) && // *(message + 0xC)
|
||||
ControlsAllowLights()) // *(*(owner +0xD0)+0x190)+0x25C
|
||||
{
|
||||
requestedOn = (requestedOn == 0); // toggle @0x1DC (read by @004b8648)
|
||||
|
||||
if (getenv("BT_FIRE_LOG") || getenv("BT_HEAT_LOG"))
|
||||
DEBUG_STREAM << "[light] " << GetName() << " thermal sight requested "
|
||||
<< (requestedOn ? "ON" : "OFF")
|
||||
<< " (reported thermalActive " << thermalActive
|
||||
<< ", voltage level " << WatchedVoltageLevel()
|
||||
<< ", heat level " << HeatStateLevel() << ")" << std::endl;
|
||||
}
|
||||
return True;
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// ThermalSightSimulation -- @004b8648 (Performance)
|
||||
//
|
||||
@@ -181,6 +256,10 @@ void
|
||||
|
||||
stateAlarm.SetLevel(thermalActive); // FUN_0041bbd8(this+0x1E0, thermalActive)
|
||||
|
||||
if ((previous != thermalActive) && (getenv("BT_FIRE_LOG") || getenv("BT_HEAT_LOG")))
|
||||
DEBUG_STREAM << "[light] reported thermalActive " << previous << " -> " << thermalActive
|
||||
<< " (requestedOn " << requestedOn << ")" << std::endl;
|
||||
|
||||
if ((previous != thermalActive) && IsLocallyViewed()) // was LocalViewport()->viewingThisMech
|
||||
{
|
||||
ToggleGlobalThermalVision(); // FUN_0045fe44 -- bring-up no-op (pvision unported)
|
||||
|
||||
@@ -152,6 +152,60 @@
|
||||
Logical IsLocallyViewed() const { return False; } // bring-up no-op (see above)
|
||||
void ToggleGlobalThermalVision() {} // bring-up no-op (pvision unported)
|
||||
|
||||
// ControlsAllowLights() [T1]: the ToggleLamp @004b860c gate is the owning
|
||||
// BTPlayer's +0x25c "sim live" EXPERIENCE flag (mech+0xD0 -> +0x190
|
||||
// playerLink -> player+0x25C), 0 only for NOVICE. Routed through the
|
||||
// complete-type bridge in btplayer.cpp (databinding rule: never raw-read
|
||||
// player offsets). Mirrors searchlight.hpp.
|
||||
Logical ControlsAllowLights() const
|
||||
{
|
||||
extern int BTPlayerExperienceSimLive(void *owner_mech); // btplayer.cpp (player+0x25c)
|
||||
return BTPlayerExperienceSimLive(owner) ? True : False;
|
||||
}
|
||||
// The "ToggleLamp" command arg is carried at message+0xC (decomp) -- that
|
||||
// is ReceiverDataMessageOf<int>::dataContents, the first member added to
|
||||
// Receiver::Message. Read the NAMED member (databinding rule);
|
||||
// thermalsight.cpp static_asserts the 0xC equivalence.
|
||||
static int MessageArg(Message &m)
|
||||
{
|
||||
return Cast_Object(const ReceiverDataMessageOf<int>*, &m)->dataContents;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Message handling -- table @0051120c
|
||||
//
|
||||
// Gitea #61 (input-path audit systemic cause #1). `MessageHandlers` above is
|
||||
// DEFAULT-CONSTRUCTED -- entryCount 0, no parent chain -- so Find() returned
|
||||
// NullHandler for every id and Receiver::Receive dropped every message the
|
||||
// thermal sight was sent, in silence.
|
||||
//
|
||||
// The binary's table @0051120c holds exactly ONE entry --
|
||||
// { 3, "ToggleLamp" @0051146a, @004b860c }. That handler was previously
|
||||
// transcribed into searchlight.cpp by mistake: @004b860c toggles +0x1DC, which
|
||||
// is THIS class's requestedOn (the field its own Performance @004b8648 reads)
|
||||
// and is a resource-seeded non-boolean in Searchlight. The un-pooled
|
||||
// "ToggleLamp" string @0051146a sits immediately before "ThermalSight"
|
||||
// @00511475, which is what pins the entry to this TU.
|
||||
// [T1: reference/decomp/section_dump.txt lines 69670-69711]
|
||||
//
|
||||
// Signature note: Receiver::Handler is void(const Message*) while the decomp's
|
||||
// shape is Logical(Message&), so the table binds a typed forwarder, not a cast.
|
||||
// Parent link: PowerWatcher declares no set of its own, so
|
||||
// PowerWatcher::GetMessageHandlers() name-resolves through HeatWatcher and
|
||||
// MechSubsystem to Receiver's empty ROOT set -- HeatSink's id 3 (ToggleCooling)
|
||||
// is NOT on this chain, so id 3 is unambiguous here.
|
||||
//
|
||||
public:
|
||||
enum { ToggleLampMessageID = 3 }; // table @0051120c, fn @004b860c
|
||||
|
||||
static Receiver::MessageHandlerSet& GetMessageHandlers();
|
||||
|
||||
Logical
|
||||
ToggleLamp(Message &message); // @004b860c
|
||||
|
||||
void
|
||||
ToggleLampMessageHandler(ReceiverDataMessageOf<int> *message);
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Simulation Support
|
||||
//
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Raw-disassemble Searchlight::ToggleLamp @004b838c -- a Ghidra export gap (#60).
|
||||
|
||||
The exported functions_index.tsv jumps straight to 4b83b8, so this body was never
|
||||
in the pseudocode. It is the handler bound by Searchlight's message table
|
||||
@0051117c {3, "ToggleLamp"@00511440, @004b838c}. Same technique that recovered
|
||||
EjectAmmo @004bb9b8 (see context/decomp-reference.md).
|
||||
|
||||
Also dumps ThermalSight's @004b860c for a side-by-side, since that one IS exported
|
||||
and is the structural sibling the reconstruction was inferred from.
|
||||
"""
|
||||
import struct
|
||||
from capstone import *
|
||||
|
||||
PATH = r"C:\git\bt411\content\BTL4OPT.EXE"
|
||||
data = open(PATH, "rb").read()
|
||||
e_lfanew = struct.unpack_from("<I", data, 0x3C)[0]
|
||||
coff = e_lfanew + 4
|
||||
num_sec = struct.unpack_from("<H", data, coff + 2)[0]
|
||||
opt_size = struct.unpack_from("<H", data, coff + 16)[0]
|
||||
opt = coff + 20
|
||||
image_base = struct.unpack_from("<I", data, opt + 28)[0]
|
||||
sec_tbl = opt + opt_size
|
||||
secs = []
|
||||
for i in range(num_sec):
|
||||
off = sec_tbl + i * 40
|
||||
vsize, va, rawsize, rawptr = struct.unpack_from("<IIII", data, off + 8)
|
||||
secs.append((va, vsize, rawptr, rawsize))
|
||||
|
||||
|
||||
def va_to_off(va):
|
||||
rva = va - image_base
|
||||
for sva, vsize, rawptr, rawsize in secs:
|
||||
if sva <= rva < sva + max(vsize, rawsize):
|
||||
return rawptr + (rva - sva)
|
||||
return None
|
||||
|
||||
|
||||
md = Cs(CS_ARCH_X86, CS_MODE_32)
|
||||
|
||||
for name, va, length in (
|
||||
("Searchlight::ToggleLamp @004b838c (EXPORT GAP -- ground truth)", 0x004b838c, 0x2c),
|
||||
("ThermalSight::ToggleLamp @004b860c (exported sibling)", 0x004b860c, 0x3c),
|
||||
):
|
||||
off = va_to_off(va)
|
||||
print("=" * 78)
|
||||
print(name)
|
||||
print("=" * 78)
|
||||
for ins in md.disasm(data[off:off + length], va):
|
||||
print(" %08x %-22s %s %s" % (ins.address, ins.bytes.hex(), ins.mnemonic, ins.op_str))
|
||||
print()
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Verify the Searchlight handler-set fix (Gitea #61, input-path audit cause #1).
|
||||
|
||||
Searchlight's MessageHandlerSet was DEFAULT-CONSTRUCTED (entryCount 0, no parent
|
||||
chain), so Receiver::Receive dropped its "ToggleLamp" message (id 3) in silence.
|
||||
Before the fix the identical rig produced:
|
||||
|
||||
[btntest] PRESS 0x14 at poll 400
|
||||
[msg] UNHANDLED: Searchlight has no handler for message id 3 -- silently dropped
|
||||
|
||||
The binary's table @0051120c holds exactly one entry { 3, "ToggleLamp", @004b860c }.
|
||||
|
||||
PASS = the UNHANDLED line is GONE **and** a [light] line shows commandedOn flipping.
|
||||
Uses the real click seam (BT_BTNTEST, L4PADRIO.cpp:393), not a synthetic call.
|
||||
|
||||
Kills only the PID it spawns.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
REPO = r"C:\git\bt411"
|
||||
LOG = os.path.join(REPO, "scratchpad_lamp.log")
|
||||
if os.path.exists(LOG):
|
||||
os.remove(LOG)
|
||||
|
||||
env = dict(os.environ)
|
||||
env.update({
|
||||
"BT_START_INSIDE": "1",
|
||||
"BT_DEV_GAUGES": "1",
|
||||
"BT_FIRE_LOG": "1", # enables the [light] proof line
|
||||
"BT_HEAT_LOG": "1",
|
||||
# press 0x14 (the searchlight button) at poll 400, release at 900, twice over
|
||||
# the run so we see the toggle go ON then back OFF.
|
||||
"BT_BTNTEST": "0x14,400,900",
|
||||
"BT_LOG": LOG,
|
||||
})
|
||||
proc = subprocess.Popen(
|
||||
[os.path.join(REPO, "build", "Release", "btl4.exe"), "-egg", "LAST.EGG"],
|
||||
cwd=os.path.join(REPO, "content"), env=env,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
print("pid", proc.pid)
|
||||
|
||||
try:
|
||||
deadline = time.time() + 240
|
||||
seen_release = False
|
||||
while time.time() < deadline:
|
||||
if os.path.exists(LOG):
|
||||
t = open(LOG, errors="replace").read()
|
||||
if "RELEASE 0x14" in t:
|
||||
seen_release = True
|
||||
break
|
||||
time.sleep(2)
|
||||
time.sleep(8)
|
||||
finally:
|
||||
proc.terminate()
|
||||
time.sleep(1)
|
||||
|
||||
t = open(LOG, errors="replace").read() if os.path.exists(LOG) else ""
|
||||
btn = re.findall(r"^\[btntest\].*$", t, re.M)
|
||||
light = re.findall(r"^\[light\].*$", t, re.M)
|
||||
unh_sl = re.findall(r"^\[msg\] UNHANDLED.*Searchlight.*$", t, re.M)
|
||||
unh_any = re.findall(r"^\[msg\] UNHANDLED.*$", t, re.M)
|
||||
|
||||
print("\n=============== RESULT ===============")
|
||||
print("reached the button press:", seen_release)
|
||||
for l in btn[:6]:
|
||||
print(" ", l[:130])
|
||||
print("\n[light] toggle lines:", len(light))
|
||||
for l in light[:6]:
|
||||
print(" ", l[:150])
|
||||
print("\n[msg] UNHANDLED mentioning Searchlight:", len(unh_sl))
|
||||
for l in unh_sl[:4]:
|
||||
print(" ", l[:140])
|
||||
print("\nall other UNHANDLED pairs still open:", len(unh_any) - len(unh_sl))
|
||||
for l in [x for x in unh_any if "Searchlight" not in x][:10]:
|
||||
print(" ", l[:140])
|
||||
|
||||
lit = [l for l in light if "reported lightState 0 -> 1" in l]
|
||||
ok = light and not unh_sl and lit
|
||||
print("\nVERDICT:", "PASS -- ToggleLamp handled AND the lamp actually lit (lightState 0 -> 1)" if ok
|
||||
else ("still UNHANDLED -- the chain did not take" if unh_sl
|
||||
else ("handler ran but lightState never reached 1 -- the toggle does not reach the lamp"
|
||||
if light else "no [light] line -- handler bound but the gate refused")))
|
||||
sys.exit(0 if ok else 2)
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Verify the ThermalSight handler-set fix (Gitea #61, input-path audit cause #1).
|
||||
|
||||
ThermalSight's MessageHandlerSet was DEFAULT-CONSTRUCTED (entryCount 0, no parent
|
||||
chain), so Receiver::Receive dropped its "ToggleLamp" message (id 3) in silence.
|
||||
Before the fix the identical rig produced:
|
||||
|
||||
[btntest] PRESS 0x12 at poll 400
|
||||
[msg] UNHANDLED: ThermalSight has no handler for message id 3 -- silently dropped
|
||||
|
||||
The binary's table @0051120c holds exactly one entry { 3, "ToggleLamp", @004b860c }.
|
||||
|
||||
PASS = the UNHANDLED line is GONE **and** a [light] line shows commandedOn flipping.
|
||||
Uses the real click seam (BT_BTNTEST, L4PADRIO.cpp:393), not a synthetic call.
|
||||
|
||||
Kills only the PID it spawns.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
REPO = r"C:\git\bt411"
|
||||
LOG = os.path.join(REPO, "scratchpad_therm.log")
|
||||
if os.path.exists(LOG):
|
||||
os.remove(LOG)
|
||||
|
||||
env = dict(os.environ)
|
||||
env.update({
|
||||
"BT_START_INSIDE": "1",
|
||||
"BT_DEV_GAUGES": "1",
|
||||
"BT_FIRE_LOG": "1", # enables the [light] proof line
|
||||
"BT_HEAT_LOG": "1",
|
||||
# press 0x12 (the thermal sight button) at poll 400, release at 900, twice over
|
||||
# the run so we see the toggle go ON then back OFF.
|
||||
"BT_BTNTEST": "0x12,400,900",
|
||||
"BT_LOG": LOG,
|
||||
})
|
||||
proc = subprocess.Popen(
|
||||
[os.path.join(REPO, "build", "Release", "btl4.exe"), "-egg", "LAST.EGG"],
|
||||
cwd=os.path.join(REPO, "content"), env=env,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
print("pid", proc.pid)
|
||||
|
||||
try:
|
||||
deadline = time.time() + 240
|
||||
seen_release = False
|
||||
while time.time() < deadline:
|
||||
if os.path.exists(LOG):
|
||||
t = open(LOG, errors="replace").read()
|
||||
if "RELEASE 0x12" in t:
|
||||
seen_release = True
|
||||
break
|
||||
time.sleep(2)
|
||||
time.sleep(8)
|
||||
finally:
|
||||
proc.terminate()
|
||||
time.sleep(1)
|
||||
|
||||
t = open(LOG, errors="replace").read() if os.path.exists(LOG) else ""
|
||||
btn = re.findall(r"^\[btntest\].*$", t, re.M)
|
||||
light = re.findall(r"^\[light\].*$", t, re.M)
|
||||
unh_sl = re.findall(r"^\[msg\] UNHANDLED.*ThermalSight.*$", t, re.M)
|
||||
unh_any = re.findall(r"^\[msg\] UNHANDLED.*$", t, re.M)
|
||||
|
||||
print("\n=============== RESULT ===============")
|
||||
print("reached the button press:", seen_release)
|
||||
for l in btn[:6]:
|
||||
print(" ", l[:130])
|
||||
print("\n[light] toggle lines:", len(light))
|
||||
for l in light[:6]:
|
||||
print(" ", l[:150])
|
||||
print("\n[msg] UNHANDLED mentioning ThermalSight:", len(unh_sl))
|
||||
for l in unh_sl[:4]:
|
||||
print(" ", l[:140])
|
||||
print("\nall other UNHANDLED pairs still open:", len(unh_any) - len(unh_sl))
|
||||
for l in [x for x in unh_any if "ThermalSight" not in x][:10]:
|
||||
print(" ", l[:140])
|
||||
|
||||
lit = [l for l in light if "reported thermalActive 0 -> 1" in l]
|
||||
ok = light and not unh_sl and lit
|
||||
print("\nVERDICT:", "PASS -- ToggleLamp handled AND the lamp actually lit (lightState 0 -> 1)" if ok
|
||||
else ("still UNHANDLED -- the chain did not take" if unh_sl
|
||||
else ("handler ran but lightState never reached 1 -- the toggle does not reach the lamp"
|
||||
if light else "no [light] line -- handler bound but the gate refused")))
|
||||
sys.exit(0 if ok else 2)
|
||||
Reference in New Issue
Block a user