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
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user