//===========================================================================// // 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 #pragma hdrstop #if !defined(SEARCHLIGHT_HPP) # include #endif #if !defined(APP_HPP) # include #endif #if !defined(TESTBT_HPP) # include #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, dataContents) == 0xC, "ReceiverDataMessageOf::dataContents must sit at 0xC -- @004b860c reads message+0xC" ); void Searchlight::ToggleLampMessageHandler(ReceiverDataMessageOf *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); }