TASK #17 -- AUTHENTIC RADAR SYMBOLOGY (the cross-blip stand-in retired). The recon proved the 'missing pip raster set' never existed as BT game code: pips are the ENGINE's L4GaugeImage vector-stroke system (T0 source in tree, L4GAUIMA.cpp == FUN_0046f0c0 line-for-line), the 'pip table' is L4Warehouse:: gaugeImageBin keyed by Entity::resourceID, and BTL4.RES ships 110 type-0x12 shapes (every mech/vehicle/building/tree). The six btl4rdr stubs are wired to the real facilities: contacts draw their authentic model silhouettes with LOD selection, the target gets the binary's 4px-inflated box highlight, and player-name labels resolve via Mission::GetSmallNameBitmap (the prebuilt 64x16 egg rasters keyed by the player's bitmapindex). DECODED: the invented 'VideoObject' was Entity::owningPlayer all along (+0x190; nameID = playerBitmapIndex@0x1E0, target = BTPlayer::objectiveMech@0x284 -- new bridge BTPlayerObjectiveMechOf); the 'LabelledEntity' class is Landmark (cultural.h; label path dormant -- no landmark content ships, no runtime landmarkID writer). The L4GREND BT_DEV_GAUGES warehouse guard is removed (its AV had a different culprit, below); resource type 18 corrected to GaugeImageStream in decomp-reference (was mislabeled 'ModelList'). TASK #14 -- the Myomers ODR duplicate ELIMINATED: the powersub.cpp/hpp 'Myomers' (classID 0xBC3 -- actually Sensor) is retired whole; it duplicated ?DefaultData@Myomers@@ against the real class (dumpbin-verified) and /FORCE picked the winner by link order. The real Myomers (0xBC6) now chains PoweredSubsystem's handler set (ids 4-8) and publishes its SEVEN binary attributes (@00511588: SpeedEffect/Current/Recommended/Min/MaxSeekVoltage- Index/SeekVoltage/OutputVoltage) -- the old empty unchained index starved the Myomer engineering panel of every resolve. TASK #16 -- attribute parity: MechWeapon publishes the FULL binary table @0x511890 (11 entries; ids renumbered to binary truth -- the port aliases had squatted the binary's DistanceToTarget/TargetWithinRange ids; the streamed TriggerState 0x13 binding unchanged). Binary names resolved two TODO members: pipState -> estimatedReadyTime (attr 0x1A), and the EXT-model flag is the binary's RearFiring (0x1B). ThermalSight LightState published. HUD (offset conflicts) + missile-side tables (id encoding suspect) documented for a re-dump instead of publishing blind. THE CRASH THIS EXPOSED [T2, cdb-verified]: gotcha #11's dense-table gap is a LATENT AV, not a guaranteed one -- the old table's 0x0D..0x12 gap (task #5) survived on heap luck; the renumber reshuffled allocations and Find() AV'd on a garbage entryName in WeaponCluster's PercentDone resolve. Fixed with five named PAD entries (the mech.cpp attrPad idiom) + a static_assert locking the pad base to PoweredSubsystem::NextAttributeID. Gotcha #11 amended with the proof. TASK #15 -- stale-ledger sweep: GAUGE_COMPOSITE ('composite not yet built', Reservoir shadow, PlayerStatus/vehicleSubSystems 'remaining', valve-dormant- until-0xBD3, sensor guard, the superseded 'Heat MFD near-static' reframe -- all banner-corrected), gauges-hud frontmatter, L4VB16 + powersub comments. Verified live: 50/50 config attribute bindings resolve, 0 NULLs, 0 parse skips, mech spawns and simulates 31/31 subsystems, no cross-blip fallbacks, the pip cache fills through entity registration without the old guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
255 lines
9.5 KiB
C++
255 lines
9.5 KiB
C++
//===========================================================================//
|
|
// File: thermalsight.cpp //
|
|
// Project: BattleTech //
|
|
// Contents: ThermalSight (thermal / infra-red vision mode) 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 @004b8718..@004b881c. 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_0045fe44 ToggleGlobalThermalVision (flips _DAT_004f1ab8 "pvision",
|
|
// prints "pvision ON/OFF", re-applies the view palette)
|
|
// FUN_004022d0 operator delete / global free
|
|
// PTR_DAT_004fd4b8 the active L4 application/viewport (its +0x48 flag is
|
|
// non-zero only when THIS mech is the locally-viewed mech)
|
|
//
|
|
#include <bt.hpp>
|
|
#pragma hdrstop
|
|
|
|
#if !defined(THERMALSIGHT_HPP)
|
|
# include <thermalsight.hpp>
|
|
#endif
|
|
#if !defined(APP_HPP)
|
|
# include <app.hpp>
|
|
#endif
|
|
#if !defined(TESTBT_HPP)
|
|
# include <testbt.hpp>
|
|
#endif
|
|
|
|
//###########################################################################
|
|
// Shared Data Support
|
|
//
|
|
Derivation
|
|
ThermalSight::ClassDerivations( // @00511238
|
|
PowerWatcher::GetClassDerivations(),
|
|
"ThermalSight"
|
|
);
|
|
|
|
Receiver::MessageHandlerSet
|
|
ThermalSight::MessageHandlers;
|
|
|
|
// task #16: publish the binary attr table @00511270 -- one entry,
|
|
// {LightState (id 4) -> thermalActive @0x1D8} (offset encoded 0x1D9 = 0x1D8|1).
|
|
// Function-local statics per the static-init-order rule.
|
|
ThermalSight::AttributeIndexSet&
|
|
ThermalSight::GetAttributeIndex()
|
|
{
|
|
static const ThermalSight::IndexEntry entries[]=
|
|
{
|
|
ATTRIBUTE_ENTRY(ThermalSight, LightState, thermalActive) // id 4 @0x1D8
|
|
};
|
|
static ThermalSight::AttributeIndexSet attributeIndex(
|
|
ELEMENTS(entries), entries,
|
|
PowerWatcher::GetAttributeIndex()
|
|
);
|
|
return attributeIndex;
|
|
}
|
|
|
|
ThermalSight::SharedData
|
|
ThermalSight::DefaultData( // @00511228
|
|
&ThermalSight::ClassDerivations,
|
|
ThermalSight::MessageHandlers,
|
|
ThermalSight::GetAttributeIndex(),
|
|
ThermalSight::StateCount
|
|
);
|
|
|
|
//###########################################################################
|
|
// Construction -- @004b8718
|
|
//
|
|
// Chains to the PowerWatcher ctor (FUN_004b18a4) with &DAT_00511228, installs
|
|
// the ThermalSight vtable (PTR_FUN_00511498), builds the 2-level state
|
|
// AlarmIndicator (@0x1E0). When this is a live (non-copy) segment with the
|
|
// has-performance flag set it registers ThermalSightSimulation
|
|
// (this[7..9] <- PTR_FUN_00511290).
|
|
//
|
|
ThermalSight::ThermalSight(
|
|
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 4 cross-family shim fields are gone; the accessors
|
|
// read the inherited base state directly. Only the own fields init here.
|
|
thermalActive = 0; // @0x1D8
|
|
requestedOn = 0; // @0x1DC
|
|
stateAlarm.Initialize(2); // FUN_0041b9ec(this+0x1E0, 2)
|
|
|
|
// GATE FIX (the functional bug the de-shim unblocks): the binary reads OWNER
|
|
// simulationFlags (param_2+0x28, raw part_013.c:6140), NOT the old shadow
|
|
// `segmentFlags` (seeded 0 -> gate always FALSE -> Performance never
|
|
// installed). No else branch (matches the binary; unlike Searchlight/Torso).
|
|
if (((owner->simulationFlags & 0xC) == 0) &&
|
|
((owner->simulationFlags & 0x100) != 0))
|
|
{
|
|
SetPerformance(&ThermalSight::ThermalSightSimulation); // this[7..9] <- PTR_FUN_00511290
|
|
}
|
|
|
|
Check_Fpu();
|
|
}
|
|
|
|
//
|
|
// Destruction -- @004b8794 (vtable slot0). Reinstalls the vtable, tears down
|
|
// the AlarmIndicator (@0x1E0), chains to ~PowerWatcher (FUN_004b1930), frees
|
|
// the block when the deleting-dtor bit is set.
|
|
//
|
|
ThermalSight::~ThermalSight()
|
|
{
|
|
Check(this);
|
|
stateAlarm.Finalize(); // FUN_0041baa4(this+0x1E0, 2)
|
|
Check_Fpu();
|
|
}
|
|
|
|
//###########################################################################
|
|
// TestInstance -- @004b881c
|
|
//
|
|
Logical
|
|
ThermalSight::TestInstance() const
|
|
{
|
|
return IsDerivedFrom(ClassDerivations); // FUN_0041a1a4(**this[3], 0x00511238)
|
|
}
|
|
|
|
Logical
|
|
ThermalSight::TestClass(Mech &)
|
|
{
|
|
return True; // BEST-EFFORT (family convention)
|
|
}
|
|
|
|
//###########################################################################
|
|
// ThermalSightSimulation -- @004b8648 (Performance)
|
|
//
|
|
// Same power/heat gating as the Searchlight, but the watched quantity is the
|
|
// thermal-vision mode, and when the active state CHANGES on the locally-viewed
|
|
// mech it toggles the global "pvision" render flag:
|
|
// * base tick (FUN_004b181c).
|
|
// * thermalActive follows requestedOn (@0x1DC) unless the host is shut down
|
|
// (this[0x40] == 1), in which case it is forced off.
|
|
// * the watched-voltage alarm level (@0x198) must read Ready(4); else blank.
|
|
// * heat-state level (@0x140): <2 keeps it, ==2 blanks it.
|
|
// * push on/off into the AlarmIndicator (@0x1E0).
|
|
// * if the state changed AND this mech is the one being viewed
|
|
// (PTR_DAT_004fd4b8 -> +0x48 != 0), call ToggleGlobalThermalVision
|
|
// (FUN_0045fe44) to flip the cockpit IR view.
|
|
//
|
|
void
|
|
ThermalSight::ThermalSightSimulation(Scalar time_slice)
|
|
{
|
|
Check(this);
|
|
|
|
PowerWatcher::Simulation(time_slice); // FUN_004b181c (base per-frame update)
|
|
int previous = thermalActive; // @0x1D8
|
|
|
|
thermalActive = (HostShutDown()) ? 0 : requestedOn; // this[0x40]==1 ? 0 : @0x1DC
|
|
|
|
if (WatchedVoltageLevel() == 4) // @0x198 == Ready
|
|
thermalActive = (thermalActive != 0);
|
|
else
|
|
thermalActive = 0;
|
|
|
|
if (HeatStateLevel() < 2) // @0x140
|
|
thermalActive = (thermalActive != 0);
|
|
else if (HeatStateLevel() == 2)
|
|
thermalActive = 0;
|
|
|
|
stateAlarm.SetLevel(thermalActive); // FUN_0041bbd8(this+0x1E0, thermalActive)
|
|
|
|
if ((previous != thermalActive) && IsLocallyViewed()) // was LocalViewport()->viewingThisMech
|
|
{
|
|
ToggleGlobalThermalVision(); // FUN_0045fe44 -- bring-up no-op (pvision unported)
|
|
}
|
|
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// CreateStreamedSubsystem -- ThermalSight @004b87d4
|
|
//
|
|
// Chains to PowerWatcher::CreateStreamedSubsystem (FUN_004b198c) then stamps
|
|
// classID 0x0BDE (+0x20) and model size 0xF4 (+0x24). No thermal-specific
|
|
// fields are read.
|
|
//
|
|
int
|
|
ThermalSight::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::ThermalSightClassID; // 0x0BDE, +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 ThermalSight at a raw offset past
|
|
// its own fields, and the shared BT_LOCAL_ALARM_SHIM stays 4 bytes because
|
|
// searchlight.hpp shares that guard.)
|
|
//===========================================================================//
|
|
struct ThermalSightLayoutCheck
|
|
{
|
|
static_assert(offsetof(ThermalSight, thermalActive) == 0x1D8, "ThermalSight thermalActive @0x1D8 (PowerWatcher ends 0x1D8)");
|
|
static_assert(offsetof(ThermalSight, requestedOn) == 0x1DC, "ThermalSight requestedOn @0x1DC");
|
|
static_assert(offsetof(ThermalSight, stateAlarm) == 0x1E0, "ThermalSight stateAlarm @0x1E0 (proves base ends 0x1D8)");
|
|
static_assert(sizeof(ThermalSight) <= 0x234, "sizeof(ThermalSight) must fit the factory Memory::Allocate(0x234)");
|
|
};
|
|
|
|
|
|
//===========================================================================//
|
|
// WAVE 4 factory bridge -- ThermalSight (factory case 0xBDE, "MechDisplay" stub).
|
|
// The real class at 0xBDE (ctor @004b8718) is ThermalSight; the factory built
|
|
// a MechDisplay RECON_SUBSYS stub. No cache write; not a weapon (raw
|
|
// part_012.c:10186-10195 stores roster-only).
|
|
//===========================================================================//
|
|
Subsystem *CreateThermalSightSubsystem(Mech *owner, int id, void *seg)
|
|
{
|
|
return (Subsystem *) new (Memory::Allocate(0x234))
|
|
ThermalSight(owner, id, (ThermalSight::SubsystemResource *)seg, ThermalSight::DefaultData);
|
|
}
|