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