Files
arcattackandClaude Opus 4.8 64975ec22f gauge wave P1b: Searchlight/LightOn + fix coolant/refrigeration source misread
- Searchlight publishes "LightOn" -> lightState@0x1D8 (the button-5 searchlight
  lamp; the empty default-constructed index resolved NOTHING).  The config binds
  the name "LightOn", so the enum id is renamed LightOnAttributeID and the table
  is chained to PowerWatcher's dense index.  Verified: LightOn resolves OK.

- FIX a genuine reconstruction bug the heatmodel decode caught (heat.cpp
  UpdateCoolant + heatfamily_reslice RefrigerationSimulation): both read
  linkedSinks.Resolve()->heatEnergy where the binary reads *(this[0x38]+0x158) =
  the subsystem's OWN DamageZone.damageLevel (the engine base zone @0xE0).
  Consequences of the misread: coolantDraw = master.heatEnergy(~1.3e7) * heatLoad
  would SLAM the live CoolantMass bars to empty whenever the link resolved; and
  RefrigerationSimulation clamped massScale permanently to 1.0 (minimum) instead
  of the authentic 3.0, plus null-deref'd a Condenser whose linkedSinks is skipped.
  Now both read this->Subsystem::damageZone->damageLevel (qualified past the
  MechSubsystem shadow, per mechweap.cpp:252): undamaged 0 -> coolantDraw 0 (no
  leak, coolant bars stay full = authentic pristine) / massScale = 3.0, rising
  only as the sink/condenser itself takes battle damage.

After this only HeatSink/AmbientTemperature remains NULL (the #if 0'd aggregate
HeatSink bank -> P3).  Verified DBASE+dev gauges: no crash, combat un-regressed
(TARGET DESTROYED).  (Also noted: the Condenser dup-ctor links to the REAL
heatfamily_reslice definition -- LNK4006 keeps the first/real one; the heat.cpp
stub is ignored -- so no bug today; #if 0 cleanup deferred to the Condenser table.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 16:27:57 -05:00

302 lines
11 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"
);
Receiver::MessageHandlerSet
Searchlight::MessageHandlers; // includes { "ToggleLamp", &Searchlight::ToggleLamp } @00511210
// 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::MessageHandlers,
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)
}
//###########################################################################
// ToggleLamp -- @004b860c ("ToggleLamp" message handler)
//
// 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).
//
Logical
Searchlight::ToggleLamp(Message &message)
{
if ((0 < MessageArg(message)) && // *(message + 0xC)
ControlsAllowLights()) // *(*(owner +0xD0)+0x190)+0x25C
{
commandedOn = (commandedOn == 0); // toggle @0x1DC
}
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 (previous != lightState)
SetGraphicsDirty(); // this[0x18] |= 1
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);
}