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>
426 lines
17 KiB
C++
426 lines
17 KiB
C++
//===========================================================================//
|
|
// File: searchlight.cpp //
|
|
// Project: BattleTech //
|
|
// Contents: Searchlight (illumination) subsystem //
|
|
//---------------------------------------------------------------------------//
|
|
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
|
|
// PROPRIETARY AND CONFIDENTIAL //
|
|
//===========================================================================//
|
|
//
|
|
// RECONSTRUCTED from the shipped binary (Ghidra pseudo-C, recovered shard
|
|
// part_013.c). Cluster @004b84dc..@004b8648. Each method cites its @ADDR.
|
|
//
|
|
// Helper-function name mapping:
|
|
// FUN_004b18a4 PowerWatcher base constructor (powersub.cpp)
|
|
// FUN_004b198c PowerWatcher::CreateStreamedSubsystem (powersub.cpp)
|
|
// FUN_004b181c PowerWatcher base per-frame update (powersub.cpp)
|
|
// FUN_004b179c PowerWatcher::HandleMessage (slot 9) (powersub.cpp)
|
|
// FUN_004b1804 PowerWatcher::ResetToInitialState (slot10) (powersub.cpp)
|
|
// FUN_004b1930 ~PowerWatcher (powersub.cpp)
|
|
// FUN_0041b9ec AlarmIndicator(levels)
|
|
// FUN_0041baa4 ~AlarmIndicator
|
|
// FUN_0041bbd8 AlarmIndicator::SetLevel(n)
|
|
// FUN_0041a1a4 IsDerivedFrom(classDerivations)
|
|
// FUN_0041bd34 Subsystem::ReadUpdate (base, slot 6)
|
|
// FUN_0041c500 Subsystem::WriteUpdate (base, slot 7)
|
|
// FUN_004022d0 operator delete / global free
|
|
//
|
|
#include <bt.hpp>
|
|
#pragma hdrstop
|
|
|
|
#if !defined(SEARCHLIGHT_HPP)
|
|
# include <searchlight.hpp>
|
|
#endif
|
|
#if !defined(APP_HPP)
|
|
# include <app.hpp>
|
|
#endif
|
|
#if !defined(TESTBT_HPP)
|
|
# include <testbt.hpp>
|
|
#endif
|
|
|
|
//###########################################################################
|
|
// Shared Data Support
|
|
//
|
|
Derivation
|
|
Searchlight::ClassDerivations( // @005111a8
|
|
PowerWatcher::GetClassDerivations(),
|
|
"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;
|
|
|
|
// gauge data-binding wave: publish "LightOn" -> lightState@0x1D8 (the button-5
|
|
// searchlight lamp; the empty default-constructed index resolved NOTHING).
|
|
// Chained to PowerWatcher's dense index.
|
|
const Searchlight::IndexEntry
|
|
Searchlight::AttributePointers[]= // @005111e0
|
|
{
|
|
ATTRIBUTE_ENTRY(Searchlight, LightOn, lightState) // @0x1D8 (reported on/off)
|
|
};
|
|
|
|
Searchlight::AttributeIndexSet
|
|
Searchlight::AttributeIndex(
|
|
ELEMENTS(Searchlight::AttributePointers),
|
|
Searchlight::AttributePointers,
|
|
PowerWatcher::GetAttributeIndex()
|
|
);
|
|
|
|
Searchlight::SharedData
|
|
Searchlight::DefaultData( // @00511198
|
|
&Searchlight::ClassDerivations,
|
|
Searchlight::GetMessageHandlers(), // was the EMPTY MessageHandlers (blackhole) -- #61
|
|
Searchlight::AttributeIndex,
|
|
Searchlight::StateCount
|
|
);
|
|
|
|
//###########################################################################
|
|
// Construction -- @004b84dc
|
|
//
|
|
// Chains to the PowerWatcher ctor (FUN_004b18a4) with &DAT_00511198, installs
|
|
// the Searchlight vtable (PTR_FUN_005114d4), builds the 2-level state
|
|
// AlarmIndicator (@0x1E4) and seeds commandedOn from the segment field
|
|
// (+0x28). When this is a live (non-copy) segment with the has-performance
|
|
// flag set it registers SearchlightSimulation (this[7..9] <- PTR_FUN_00511200);
|
|
// otherwise it marks the instance "no per-frame performance" (flag bit 2).
|
|
//
|
|
Searchlight::Searchlight(
|
|
Mech *owner,
|
|
int subsystem_ID,
|
|
SubsystemResource *subsystem_resource,
|
|
SharedData &shared_data
|
|
):
|
|
PowerWatcher(owner, subsystem_ID, subsystem_resource, shared_data)
|
|
{
|
|
Check(owner);
|
|
Check_Pointer(subsystem_resource);
|
|
|
|
// (WAVE 4 de-shim) the 7 cross-family shim fields are gone; the accessors
|
|
// read the inherited base state directly. Only the own fields init here.
|
|
lightState = 0; // @0x1D8
|
|
requestedOn = 0; // @0x1E0
|
|
stateAlarm.Initialize(2); // FUN_0041b9ec(this+0x1E4, 2)
|
|
commandedOn = subsystem_resource->segmentIndex; // @0x1DC <- inherited resource +0x28
|
|
|
|
// GATE FIX (the functional bug the de-shim unblocks): the binary reads OWNER
|
|
// simulationFlags (param_2+0x28, raw part_013.c:6012), NOT the old shadow
|
|
// `segmentFlags` -- which was seeded 0, so the gate was ALWAYS FALSE and
|
|
// SearchlightSimulation NEVER installed. A live master segment arms it; a
|
|
// copy/replicant gets DontExecute (flag 2). Mirrors torso.cpp:181-182.
|
|
if (((owner->simulationFlags & 0xC) == 0) &&
|
|
((owner->simulationFlags & 0x100) != 0))
|
|
{
|
|
SetPerformance(&Searchlight::SearchlightSimulation); // this[7..9] <- PTR_FUN_00511200
|
|
}
|
|
else
|
|
{
|
|
SetInstanceFlag(2); // this[10] |= 2 (no active performance)
|
|
}
|
|
|
|
Check_Fpu();
|
|
}
|
|
|
|
//
|
|
// Destruction -- @004b8568 (vtable slot0). Reinstalls the vtable, tears down
|
|
// the AlarmIndicator (@0x1E4), chains to ~PowerWatcher (FUN_004b1930), frees
|
|
// the block when the deleting-dtor bit is set.
|
|
//
|
|
Searchlight::~Searchlight()
|
|
{
|
|
Check(this);
|
|
stateAlarm.Finalize(); // FUN_0041baa4(this+0x1E4, 2)
|
|
Check_Fpu();
|
|
}
|
|
|
|
//###########################################################################
|
|
// TestInstance -- @004b85f0
|
|
//
|
|
Logical
|
|
Searchlight::TestInstance() const
|
|
{
|
|
return IsDerivedFrom(ClassDerivations); // FUN_0041a1a4(**this[3], 0x005111a8)
|
|
}
|
|
|
|
Logical
|
|
Searchlight::TestClass(Mech &)
|
|
{
|
|
return True; // BEST-EFFORT (family convention)
|
|
}
|
|
|
|
//###########################################################################
|
|
// Message handling -- table @0051117c
|
|
//
|
|
// 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) -- press only
|
|
{
|
|
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;
|
|
}
|
|
|
|
//###########################################################################
|
|
// SearchlightSimulation -- @004b841c (Performance)
|
|
//
|
|
// Gates the reported light state by power and heat:
|
|
// * base tick (FUN_004b181c).
|
|
// * if the host is shut down (this[0x40] == 1) the light is forced off,
|
|
// else it follows requestedOn (@0x1E0).
|
|
// * the watched-voltage alarm level (@0x198) must read Ready(4) for the
|
|
// light to stay lit; otherwise it blanks.
|
|
// * the heat-state level (@0x140): <2 keeps the light, ==2 blanks it.
|
|
// * pushes the resulting on/off into the state AlarmIndicator (@0x1E4) and,
|
|
// if it changed, raises the subsystem "graphics dirty" bit (this[0x18] |= 1).
|
|
//
|
|
void
|
|
Searchlight::SearchlightSimulation(Scalar time_slice)
|
|
{
|
|
Check(this);
|
|
|
|
PowerWatcher::Simulation(time_slice); // FUN_004b181c (base per-frame update)
|
|
int previous = lightState; // @0x1D8
|
|
|
|
lightState = (HostShutDown()) ? 0 : requestedOn; // this[0x40]==1 ? 0 : @0x1E0
|
|
|
|
if (WatchedVoltageLevel() == 4) // @0x198 == Ready
|
|
lightState = (lightState != 0);
|
|
else
|
|
lightState = 0;
|
|
|
|
if (HeatStateLevel() < 2) // @0x140
|
|
lightState = (lightState != 0);
|
|
else if (HeatStateLevel() == 2)
|
|
lightState = 0;
|
|
|
|
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();
|
|
}
|
|
|
|
//###########################################################################
|
|
// ReadUpdate / WriteUpdate -- network state replication
|
|
//
|
|
// slot 6 @004b83b8: chain base ReadUpdate (FUN_0041bd34), then take the
|
|
// replicated on/off (packet +0x10) as lightState (@0x1D8) and drive the
|
|
// alarm (@0x1E4).
|
|
// slot 7 @004b83f0: chain base WriteUpdate (FUN_0041c500), then stamp the
|
|
// record kind 0x14 and write lightState into record field [4].
|
|
//
|
|
// The decomp's NetworkPacket.value / NetworkRecord{kind,value} do not exist on
|
|
// the engine UpdateRecord (SIMULATE.h). The replicated on/off maps to
|
|
// UpdateRecord::simulationState and the 0x14 record tag to ::recordID.
|
|
//
|
|
void
|
|
Searchlight::ReadUpdateRecord(UpdateRecord *message)
|
|
{
|
|
PowerWatcher::ReadUpdateRecord(message); // inherited Simulation::ReadUpdateRecord
|
|
lightState = (int)message->simulationState; // @0x1D8 <- replicated on/off
|
|
stateAlarm.SetLevel(lightState); // FUN_0041bbd8(this+0x1E4, lightState)
|
|
}
|
|
|
|
void
|
|
Searchlight::WriteUpdateRecord(UpdateRecord *message, int update_model)
|
|
{
|
|
PowerWatcher::WriteUpdateRecord(message, update_model); // Subsystem::WriteUpdateRecord
|
|
message->recordID = 0x14; // record kind tag
|
|
message->simulationState = lightState; // replicated on/off <- @0x1D8
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// CreateStreamedSubsystem -- Searchlight @004b85a8
|
|
//
|
|
// Chains to PowerWatcher::CreateStreamedSubsystem (FUN_004b198c) then stamps
|
|
// classID 0x0BD8 (+0x20) and model size 0xF4 (+0x24). No searchlight-specific
|
|
// fields are read (empty resource extension).
|
|
//
|
|
int
|
|
Searchlight::CreateStreamedSubsystem(
|
|
NotationFile *model_file,
|
|
const char *model_name,
|
|
const char *subsystem_name,
|
|
SubsystemResource *subsystem_resource,
|
|
NotationFile *subsystem_file,
|
|
const ResourceDirectories *directories,
|
|
int passes
|
|
)
|
|
{
|
|
if (
|
|
!PowerWatcher::CreateStreamedSubsystem( // FUN_004b198c
|
|
model_file, model_name, subsystem_name,
|
|
subsystem_resource, subsystem_file, directories, passes
|
|
)
|
|
)
|
|
{
|
|
return False;
|
|
}
|
|
|
|
subsystem_resource->subsystemModelSize = 0xF4; // +0x24
|
|
subsystem_resource->classID = RegisteredClass::SearchlightClassID; // 0x0BD8, +0x20
|
|
|
|
return True;
|
|
}
|
|
|
|
|
|
//===========================================================================//
|
|
// WAVE 4 -- compile-time layout locks. PowerWatcher is Torso-proven at 0x1D8,
|
|
// so the own fields land at their EXACT binary offsets; sizeof is bounded to
|
|
// the factory alloc. (BtAlarm is a 4-byte stand-in for the binary's 0x54
|
|
// GaugeAlarm -- immaterial: nothing reads Searchlight at a raw offset past its
|
|
// own fields, and the shared BT_LOCAL_ALARM_SHIM stays 4 bytes because
|
|
// thermalsight.hpp shares that guard.)
|
|
//===========================================================================//
|
|
struct SearchlightLayoutCheck
|
|
{
|
|
static_assert(offsetof(Searchlight, lightState) == 0x1D8, "Searchlight lightState @0x1D8 (PowerWatcher ends 0x1D8)");
|
|
static_assert(offsetof(Searchlight, commandedOn) == 0x1DC, "Searchlight commandedOn @0x1DC (res+0x28)");
|
|
static_assert(offsetof(Searchlight, requestedOn) == 0x1E0, "Searchlight requestedOn @0x1E0");
|
|
static_assert(offsetof(Searchlight, stateAlarm) == 0x1E4, "Searchlight stateAlarm @0x1E4 (proves base ends 0x1D8)");
|
|
static_assert(sizeof(Searchlight) <= 0x238, "sizeof(Searchlight) must fit the factory Memory::Allocate(0x238)");
|
|
};
|
|
|
|
|
|
//===========================================================================//
|
|
// WAVE 4 factory bridge -- Searchlight (factory case 0xBD8, "LegSubsystem" label).
|
|
// The real class at 0xBD8 (ctor @004b84dc) is Searchlight; the factory built a
|
|
// LegSubsystem RECON_SUBSYS stub. No cache write; not a weapon (raw
|
|
// part_012.c:10166-10175 stores roster-only).
|
|
//===========================================================================//
|
|
Subsystem *CreateSearchlightSubsystem(Mech *owner, int id, void *seg)
|
|
{
|
|
return (Subsystem *) new (Memory::Allocate(0x238))
|
|
Searchlight(owner, id, (Searchlight::SubsystemResource *)seg, Searchlight::DefaultData);
|
|
}
|