Files
BT411/game/reconstructed/heat.cpp
T
arcattackandClaude Fable 5 6f5a264835 Gitea #47 COMPLETE: the ENG-button attention FLASH is live (jam / bay fire) -- MechTech status scan + BTL4GaugeAlarmManager + lamp chain, all from the binary
The pod behaviour Cyd described -- "on a jam or bay fire the display eng button
for the system flashes" -- is authored data + a five-stage chain, now running:

  MechTech::TechnicalAssistance (@004ad33c, per frame)
    edge-scans every monitored subsystem's GetStatusFlags() 7-bit condition
    mask (TechStatusType: Destroyed 0, Damaged 1, CoolantLeaking 2,
    Overheating 3, AmmoBurning 4, Jammed 5, BadPower 6)
  -> Start/StopEntityAlarmMessage (@00436688 id 7 size 0x20 / @004366b8 id 8
     size 0x1C; broadcast @004364e4; port shape: direct
     Start/StopEntityAlarmImplementation calls on the gauge renderer)
  -> GaugeAlarmManager::Activate(alarmModel) -- alarmModel = MechTech+0x100 =
     the 'mechalrm' ModelList (id 83) -> SearchList(83, type 31) -> the baked
     GaugeAlarmStream (id 331, 11 items {condition, lampCode}), decoded:
         Destroyed      -> gotoEngineering + engCooling + engBusMode
         CoolantLeaking -> gotoEngineering + engCooling
         AmmoBurning    -> gotoEngineering + engEject
         Jammed         -> gotoEngineering + engEject
         BadPower       -> gotoEngineering + engBusMode
  -> BTL4GaugeAlarmManager::ReadGaugeAlarmStreamItem -- THE REAL BODY, from
     @004cc2fc + helpers @004cc108/148/1a0/264/27c + tables @0051cf1c..0x51d084
     (gotoEngineering 0x80 = the subsystem's QUAD-SELECT bezel button via
     lamp[aux]/mode[aux] tables; 0x81+ descend the eng-page bank; Condenser /
     Generator specials on CoolantLeaking; <0x80 = the heat-bank fixed map).
     btl4galm.cpp's old bodies were admitted fabrications and its provenance
     note ("no override body exists in the image") was wrong -- corrected.
  -> LampManager::FindLamp (@00444c80) -> Lamp::SetAlertState (@00444e64, a
     COUNTER so stacked alarms hold the flash) -> L4Lamp::NotifyOfStateChange
     emits RIO flashFast states 0x37/0x13 (== T0 L4LAMP.cpp:234-239) -> the
     pod's physical lamps AND the glass panels (PadRIO IS rioPointer there).

FOUR load-bearing defects found and fixed en route -- each independently fatal
to the feature:

  1. HeatableSubsystem's Derivation chained Subsystem directly, SKIPPING
     MechSubsystem (written before the WAVE-1 re-basing) -- so EVERY subsystem
     on both family branches failed IsDerivedFrom(MechSubsystem), which is
     exactly MechTech's monitor filter: the status scan watched NOTHING.
  2. MechSubsystem::GetStatusFlags was non-virtual (binary: vtable slot 12) --
     the scan's MechSubsystem* call bound statically to the base tier and the
     weapon bits could never surface.
  3. ProjectileWeapon::GetStatusFlags sat in a Ghidra export gap -- raw-disasm
     @004bbf88: base | 0x20 (weaponAlarm==5 Jammed) | 0x10 (linked bin
     cookOffArmed@0x18C AmmoBurning).  The port had "defer to base".
  4. Mech::Reset's respawn sweep blanket-cast every roster entry to
     MechSubsystem and called RespawnRepair -- wrong for the Subsystem-level
     entries (MechTech 0xBDC, SubsystemMessageManager 0xBD3).  On MechTech the
     recon damageZone slot lands on its subsystemMonitors chain head: NULL
     while the scan was broken (fix #1's bug MASKED this one), but the moment
     the monitors populated, RespawnRepair virtual-called through a
     SubsystemMonitor as if it were a DamageZone -> load-time crash in
     StateIndicator::SetState (caught with cdb; call [edx+14h] on code bytes).
     The sweep now filters IsDerivedFrom(MechSubsystem) before the cast.

Also: GUID identities pinned -- 0x50f4bc = PoweredSubsystem::ClassDerivations
(via @004b1208 = its TestInstance; btl4gau2's two "Generator" comments were
wrong, swept), 0x50fb60 = Generator's.  A shadow-field instance documented for
the deferred de-shadow: MechSubsystem::damageZone re-declares the PUBLIC engine
Subsystem::damageZone (SUBSYSTM.h:159) -- mechtech's naive `sub->damageZone`
read the never-written engine member (0 monitors again); now reads the recon
member via GetDamageZoneProxy().  Logged in open-questions.

Wiring: BTL4GaugeRenderer now constructs + assigns the BTL4GaugeAlarmManager
(the base ctor NULLs it and Activate Check()s it); MechTech's Report*/
StatusMessageSink stubs are real; alarmModel confirmed baked (= 83) at runtime.

VERIFIED LIVE (scratchpad/lampflash.py, BT_LAMP_LOG chain trace):
    [techstat] MechTech id 32 monitors 29 subsystems, alarmModel 83
    [techstat] LRM15_1 condition 4 SET (alarmModel 83)      <- bay fire armed
    [galarm] condition 4 code 0x80 -> lamp 0xd mode 0x1 FLASH
    [lamp] 0xd <- 0x37  (FLASHING)                          <- the select button
    [galarm] condition 4 code 0x85 -> lamp 0xb mode 0x4 FLASH   <- engEject
    [techstat] LRM15_1 condition 4 CLEARED                  <- detonation clears
    [techstat] AmmoBinLRM15_1 condition 0 SET               <- bin Destroyed
Regressions: baytest PASS (bay fire kills), baypurge PASS (purge extinguishes),
sim3 3-pod brawl PASS with ZERO crashes (6 kills, organic heat-route bay fires,
respawns clean through the new sweep filter).

Env: BT_LAMP_LOG ([techstat]/[galarm]/[lamp]).  KB: gauges-hud (the flash
section), decomp-reference (the GaugeAlarm closure + tables + GUIDs),
open-questions (built-note + the de-shadow deferred item), btl4gau2 comment
sweep.  checkctx CLEAN.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 16:04:34 -05:00

1275 lines
44 KiB
C++

//===========================================================================//
// File: heat.cpp //
// Project: BattleTech Brick: Entity Manager //
// Contents: Heatable subsystems -- temperature model, heat sinks, condenser //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// --/--/95 ?? Initial coding. //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary. Behaviour follows the Ghidra
// pseudo-C in heat_cluster.c; method names / member names follow HEAT.TCP
// and the sibling subsystem sources (gauss.cpp, sensor.hpp, ppc.hpp).
// Each non-trivial method cites the originating @ADDR. Hex float constants
// have been converted to decimal:
// 0x3f800000 = 1.0f 0x3f000000 = 0.5f 0x3ecccccd = 0.4f
//
// Helper-function name mapping (engine internals referenced by the decomp):
// FUN_004ac644 HeatableSubsystem base constructor
// FUN_0041c648 Subsystem destructor
// FUN_004ac22c Subsystem::ResetToInitialState
// FUN_004ac144 Subsystem::GetStatusFlags
// FUN_004ac1d4 Subsystem::HandleMessage
// FUN_004ac0bc Subsystem (slot 9 base impl)
// FUN_004ac8c0 Subsystem::PrintState
// FUN_0043ad4f FilteredScalar::Initialize(count,value)
// FUN_0043ade4 FilteredScalar::AddSample(value)
// FUN_0043ae0b FilteredScalar::Average()
// FUN_00417ab4 SharedData::Resolve() -> linked HeatSink*
// FUN_0041b9ec AlarmIndicator(levels)
// FUN_0041bbd8 AlarmIndicator::SetLevel(n)
// FUN_004dca38 expf() FUN_004dcd00 fabsf()
// FUN_0040385c Verify()/assert(msg,file,line)
// FUN_0041a1a4 IsDerivedFrom(classDerivations)
//
#include <bt.hpp>
#include <map> // BT_HEAT_LOG census (diag only)
#pragma hdrstop
#if !defined(HEAT_HPP)
# include <heat.hpp>
#endif
#if !defined(APP_HPP)
# include <app.hpp>
#endif
#if !defined(TESTBT_HPP)
# include <testbt.hpp>
#endif
#define JM_CLOSE_ENOUGH 0.0005f
//
// Tuning constants RECOVERED from the shipped image (.data / inline literal
// pool addresses below; raw bytes read from decomp/recovered/section_dump.txt).
// _DAT_004ad870 : 80-bit extended `3b df 4f 8d 97 6e 12 83 f6 3f`
// = 1.023992 x 2^-9 == 0.002f (heat-load normalising scale:
// radiatedHeat ~= currentTemp*coolant ~= 300 -> band [0,1]).
// _DAT_004ad87c : 00 00 00 00 == 0.0f
// _DAT_004ad880 : 00 00 80 3f == 1.0f
// _DAT_004ada90 : 00 00 80 3f == 1.0f (the "1 - exp(...)" unit term)
// _DAT_004adbf4 : 17 b7 d1 38 == 1.0e-4f (heat-equalise threshold)
// _DAT_004adcfc : 17 b7 d1 38 == 1.0e-4f (DrawCoolant |amount| gate)
// _DAT_0050e3d8 : 0a d7 23 3b == 0.0025f (draw-zero floor + OFF hysteresis)
// _DAT_0050e3d4 : a6 9b 44 3b == 0.003f (coolantActive ON threshold)
//
static const Scalar HeatLoadScale = 0.002f; // _DAT_004ad870 (80-bit extended)
static const Scalar HeatLoadMinimum = 0.0f; // _DAT_004ad87c
static const Scalar HeatLoadMaximum = 1.0f; // _DAT_004ad880
static const Scalar HeatEqualizeEpsilon = 1.0e-4f; // _DAT_004adbf4
// task #9 correction: the old single CoolantDrawEpsilon (1e-4) served THREE
// distinct byte-verified constants -- the draw floor / OFF hysteresis are
// 0.0025 and the ON threshold 0.003 (the 1e-4 reading made the leak floor +
// status-bit hysteresis fire ~25-30x too eagerly).
static const Scalar CoolantDrawGate = 1.0e-4f; // _DAT_004adcfc (DrawCoolant |amount|)
static const Scalar CoolantDrawFloor = 0.0025f; // _DAT_0050e3d8 (zero floor + OFF)
static const Scalar CoolantActiveOn = 0.003f; // _DAT_0050e3d4 (ON threshold)
//###########################################################################
// GaugeAlarm54 watcher sockets. The binary's 0x54 alarm (FUN_0041b9ec) IS a
// StateIndicator -- its three sub-indicators @0x18/0x2c/0x40 are the audio/
// video/gauge watcher chains, level@0x14 is currentState. These out-of-line
// bodies let an AudioStateWatcher bind to any subsystem alarm BY NAME
// (GeneratorState / CondenserState / ReservoirState / ...); SetLevel fires the
// registered watchers on a level change, so the state audio plays on transition.
// Empty sockets iterate to nothing, so alarms with no audio watchers are a no-op.
//###########################################################################
void GaugeAlarm54::AddAudioWatcher(Component *watcher) { audioWatcherSocket.Add(watcher); }
void GaugeAlarm54::AddVideoWatcher(Component *watcher) { videoWatcherSocket.Add(watcher); }
void GaugeAlarm54::AddGaugeWatcher(Component *watcher) { gaugeWatcherSocket.Add(watcher); }
void GaugeAlarm54::NotifyWatchers()
{
Component *watcher;
{
SChainIteratorOf<Component*> iterator(audioWatcherSocket);
while ((watcher = iterator.ReadAndNext()) != NULL) watcher->Execute();
}
{
SChainIteratorOf<Component*> iterator(videoWatcherSocket);
while ((watcher = iterator.ReadAndNext()) != NULL) watcher->Execute();
}
{
SChainIteratorOf<Component*> iterator(gaugeWatcherSocket);
while ((watcher = iterator.ReadAndNext()) != NULL) watcher->Execute();
}
}
//###########################################################################
//###########################################################################
// HeatableSubsystem
//###########################################################################
//###########################################################################
//#############################################################################
// Shared Data Support
//
HeatableSubsystem::SharedData
HeatableSubsystem::DefaultData(
HeatableSubsystem::GetClassDerivations(),
HeatableSubsystem::GetMessageHandlers(),
HeatableSubsystem::GetAttributeIndex(),
HeatableSubsystem::StateCount
);
Derivation*
HeatableSubsystem::GetClassDerivations()
{
// #47 CORRECTION: was chained to Subsystem:: directly, SKIPPING
// MechSubsystem -- written before the WAVE-1 re-basing established
// `HeatableSubsystem : MechSubsystem`. That made EVERY subsystem on
// both family branches fail IsDerivedFrom(MechSubsystem::ClassDerivations),
// which is exactly the filter MechTech's monitor walk applies
// (FUN_0041a1a4(**sub[3], 0x50de2c)) -- so the status scan monitored
// NOTHING and no jam/bay-fire annunciation could ever fire. The
// derivation chain now mirrors the class hierarchy, as every other
// family file already does (Searchlight -> PowerWatcher -> HeatWatcher...).
static Derivation classDerivations(&MechSubsystem::ClassDerivations, "HeatableSubsystem");
return &classDerivations;
}
//#############################################################################
// Construction / Destruction
//
// @004ac644 (base ctor body lies below the captured decomp window).
//
HeatableSubsystem::HeatableSubsystem(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
MechSubsystem(owner, subsystem_ID,
(MechSubsystem::SubsystemResource *)subsystem_resource, shared_data)
{
Check(owner);
Check_Pointer(subsystem_resource);
// owner + simulationState are set by the MechSubsystem base ctor; the old
// flags/statusFlags/statusBits/destroyed were shadows of base state (removed).
ResetToInitialState(True);
Check_Fpu();
}
//
// @004ac868 -- releases the shared model object and the ref-counted resource
// then chains to ~Subsystem.
//
HeatableSubsystem::~HeatableSubsystem()
{
Check(this);
Check_Fpu();
}
//###########################################################################
// ResetToInitialState -- HeatableSubsystem
//
// HEAT.TCP ground truth.
//
void
HeatableSubsystem::ResetToInitialState(Logical /*powered*/)
{
currentTemperature = 300.0f;
heatLoad = 0.0f;
}
//
// DeathReset (Gitea #55): the respawn sweep's entry for a bare HeatableSubsystem.
// Before this, the sweep hit the empty `Subsystem::DeathReset` base and nothing in
// the heat family was reset on respawn at all.
//
void
HeatableSubsystem::DeathReset(int /*reset_command*/)
{
ResetToInitialState(True);
}
//###########################################################################
// TestClass -- HeatableSubsystem
//
// HEAT.TCP ground truth.
//
Logical
HeatableSubsystem::TestClass(Mech &)
{
return True;
}
Logical
HeatableSubsystem::TestInstance() const
{
return IsDerivedFrom(*GetClassDerivations());
}
//
// Base damageable-subsystem virtual surface. The engine `Subsystem` does not
// carry these; the heat/power families override them. Base implementations
// are the neutral defaults the recovered overrides chain into.
//
LWord
HeatableSubsystem::GetStatusFlags() { return 0; }
Logical
HeatableSubsystem::HandleMessage(int) { return False; }
void
HeatableSubsystem::PrintState() {}
void
HeatableSubsystem::Simulation(Scalar) {}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CreateStreamedSubsystem -- HeatableSubsystem
//
// @004ac9ec parses the base "damageable subsystem" record (WeaponDamagePoints,
// Collision/Ballistic/Explosive/Laser/EnergyDamagePoints, VitalSubsystem,
// SiteOffset, VideoObjectName, PrintSimulationState, CriticalHitScoreBonus).
// That logic belongs to the shared damageable base; reproduced here in
// summary form -- a human should fold it back into the proper base class.
//
int
HeatableSubsystem::CreateStreamedSubsystem(
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes
)
{
if (
!Subsystem::CreateStreamedSubsystem(
model_file, model_name, subsystem_name,
subsystem_resource, subsystem_file, directories
)
)
{
return False;
}
subsystem_resource->subsystemModelSize = sizeof(*subsystem_resource);
// TODO: verify against @004ac9ec -- parses damage-point fields, the
// "VitalSubsystem" True/False flag, "SiteOffset", "VideoObjectName",
// "PrintSimulationState" and "CriticalHitScoreBonus" and validates that
// WeaponDamagePoints / CriticalHitScoreBonus are present.
Check_Fpu();
return True;
}
//###########################################################################
//###########################################################################
// Condenser
//###########################################################################
//###########################################################################
//
// NOTE: the shipped Condenser implementation lies past the captured decomp
// window (@0x4ae1xx onward). Bodies below are reconstructed from HEAT.TCP
// plus the standard subsystem pattern and are BEST-EFFORT.
//
//#############################################################################
// Shared Data Support
//
// CondenserState -> condenserAlarm (@0x1DC). Chained to HeatSink so the inherited
// coolant/temp attrs stay reachable; dense from HeatSink::NextAttributeID.
const Condenser::IndexEntry
Condenser::AttributePointers[]=
{
ATTRIBUTE_ENTRY(Condenser, CondenserState, condenserAlarm) // @0x1DC (0x54 StateIndicator-compatible alarm)
};
Condenser::AttributeIndexSet&
Condenser::GetAttributeIndex()
{
static Condenser::AttributeIndexSet attributeIndex(
ELEMENTS(Condenser::AttributePointers),
Condenser::AttributePointers,
HeatSink::GetAttributeIndex()
);
return attributeIndex;
}
Condenser::SharedData
Condenser::DefaultData(
Condenser::GetClassDerivations(),
Condenser::GetMessageHandlers(),
Condenser::GetAttributeIndex(),
Condenser::StateCount
);
Derivation*
Condenser::GetClassDerivations()
{
static Derivation classDerivations(HeatableSubsystem::GetClassDerivations(), "Condenser");
return &classDerivations;
}
// ~~~ Condenser ctor/dtor: the REAL bodies live in heatfamily_reslice.cpp
// (@4ae568, which sets valveState=1 / refrigerationFactor / condenserNumber).
// These STUBs were ODR-duplicates that WON under /FORCE (heat.obj links before
// heatfamily_reslice.obj) -> valveState was left 0xCDCDCDCD (garbage valve gauge).
// #if 0'd so the real ctor is the sole definition. DefaultData / GetClassDerivations
// / ResetToInitialState below are NOT duplicated in the reslice TU -- kept here.
#if 0
Condenser::Condenser(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
HeatSink(owner, subsystem_ID, subsystem_resource, shared_data)
{
Check(owner);
Check_Pointer(subsystem_resource);
Check_Fpu();
}
Condenser::~Condenser()
{
Check(this);
Check_Fpu();
}
#endif
//###########################################################################
// ResetToInitialState -- Condenser (HEAT.TCP)
//
void
Condenser::ResetToInitialState(Logical /*powered*/)
{
HeatableSubsystem::ResetToInitialState(True);
}
//
// DeathReset (Gitea #55): the respawn sweep's entry for a Condenser -- a coolant
// loop's valve. NOTE the body above chains HeatableSubsystem's (temperature +
// heatLoad) and so does NOT run HeatSink's coolant refill, even though Condenser
// derives from HeatSink. That mirrors the existing reconstruction; whether the
// binary's Condenser reset also restores the VALVE SETTING is not established from
// the decomp yet, so valve detents may still persist across a respawn.
// [T3 -- do NOT claim valves are fixed; tracked on the issue.]
//
void
Condenser::DeathReset(int /*reset_command*/)
{
ResetToInitialState(True);
}
//###########################################################################
// TestClass / TestInstance / CreateStreamedSubsystem -- Condenser
//
// The REAL bodies live in heatfamily_reslice.cpp (@4ae63c / @4ae658, the latter
// parsing "RefrigerationFactor"). These STUBs were ODR-duplicates -- #if 0'd so
// the reslice definitions are the sole ones (see the ctor note above).
//
#if 0
Logical
Condenser::TestClass(Mech &)
{
return True;
}
Logical
Condenser::TestInstance() const
{
return IsDerivedFrom(*GetClassDerivations());
}
int
Condenser::CreateStreamedSubsystem(
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes
)
{
// TODO: verify -- decomp for the Condenser parser was not captured.
if (
!HeatableSubsystem::CreateStreamedSubsystem(
model_file, model_name, subsystem_name,
subsystem_resource, subsystem_file, directories, passes
)
)
{
return False;
}
subsystem_resource->subsystemModelSize = sizeof(*subsystem_resource);
Check_Fpu();
return True;
}
#endif
//###########################################################################
//###########################################################################
// HeatSink
//###########################################################################
//###########################################################################
//#############################################################################
// Attribute Support
//
// The named attributes the cockpit gauges bind to (config content/GAUGE/L4GAUGE.CFG:
// HeatSink/CoolantMass, HeatSink/CoolantCapacity, HeatSink/CurrentTemperature).
// Chained to the parent index so SimulationState (id 1) is preserved; the ids run
// contiguously from HeatableSubsystem::NextAttributeID so the built index stays dense
// (AttributeIndexSet::Find strcmps every slot -- a gap would read a garbage name).
//
const HeatSink::IndexEntry
HeatSink::AttributePointers[]=
{
ATTRIBUTE_ENTRY(HeatSink, CoolantMass, coolantLevel), // @0x12C (live: UpdateCoolant depletes it)
ATTRIBUTE_ENTRY(HeatSink, CoolantCapacity, thermalCapacity), // @0x128
ATTRIBUTE_ENTRY(HeatSink, CurrentTemperature, currentTemperature), // @0x114 (inherited from HeatableSubsystem)
// --- gauge data-binding wave: the config's GenericHeatGauges vertBar binds
// Condenser<N>/DegradationTemperature (@4=warn) + FailureTemperature (@5=max);
// without these the two-part temp bar's warn/max endpoints were NULL -> the
// bar could not scale (division by a zero max). Condenser/Reservoir inherit
// this table, so publishing here covers all 6 condensers + both coolant banks.
ATTRIBUTE_ENTRY(HeatSink, DegradationTemperature, degradationTemperature), // @0x118 (constant warn threshold)
ATTRIBUTE_ENTRY(HeatSink, FailureTemperature, failureTemperature), // @0x11C (constant max threshold)
ATTRIBUTE_ENTRY(HeatSink, NormalizedPressure, heatLoad), // @0x120 (smoothed radiated heat)
ATTRIBUTE_ENTRY(HeatSink, DegradationPressure, coolantEfficiency), // @0x124
ATTRIBUTE_ENTRY(HeatSink, CoolantMassLeakRate, coolantDraw), // @0x130 (LeakGauge; damage-driven)
ATTRIBUTE_ENTRY(HeatSink, HeatSink, linkedSinks), // @0x164 (Eng linked-sink temp readout)
ATTRIBUTE_ENTRY(HeatSink, ValveSetting, coolantFlowScale), // @0x15C (condenser valve slider @2; init 1.0f)
// (AUDIO_FIDELITY F6) the coolant-leak warning: binary id 12 @0x138 = the
// coolantActive leak flag (binary table @0x50e4c8; watchers match ==1/==0).
// PoweredSubsystem (weapons/sensor/myomers) DERIVES from HeatSink, so this
// single row serves ALL 19 authored leak watchers through the chained
// index -- exactly like the binary (one ReportLeak row in the image).
ATTRIBUTE_ENTRY(HeatSink, ReportLeak, coolantActive) // @0x138 (leak-active hysteresis flag)
};
HeatSink::AttributeIndexSet&
HeatSink::GetAttributeIndex()
{
static HeatSink::AttributeIndexSet attributeIndex(
ELEMENTS(HeatSink::AttributePointers),
HeatSink::AttributePointers,
HeatableSubsystem::GetAttributeIndex()
);
return attributeIndex;
}
//#############################################################################
// Shared Data Support
//
HeatSink::SharedData
HeatSink::DefaultData(
HeatSink::GetClassDerivations(),
HeatSink::GetMessageHandlers(),
HeatSink::GetAttributeIndex(),
HeatSink::StateCount
);
Derivation*
HeatSink::GetClassDerivations()
{
static Derivation classDerivations(HeatableSubsystem::GetClassDerivations(), "HeatSink");
return &classDerivations;
}
//#############################################################################
// Construction / Destruction
//
// @004adda0 (the one function tagged file=bt/heat.cpp in the decomp).
//
HeatSink::HeatSink(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
HeatableSubsystem(owner, subsystem_ID, subsystem_resource, shared_data),
linkedSinks(),
heatAlarm(3) // FUN_0041b9ec(...,3) -- 3 alarm levels
{
// heatState/heatModelFlag/field_1d0 deleted: the heat-state code lives INSIDE
// heatAlarm (+0x14 == subsystem+0x184, so heatAlarm.GetLevel()); heatModelFlag was
// a spurious duplicate of coolantActive@0x138; field_1d0 was a spurious tail slot.
Check(owner);
Check_Pointer(subsystem_resource);
currentTemperature = subsystem_resource->startingTemperature; // +0xE4
degradationTemperature = subsystem_resource->degradationTemperature; // +0xE8
failureTemperature = subsystem_resource->failureTemperature; // +0xEC
heatLoad = 0.0f;
coolantEfficiency = 0.5f;
thermalCapacity = 1.0f;
coolantLevel = thermalCapacity;
coolantDraw = 0.0f;
coolantAvailable = 1;
coolantActive = 0;
startingTemperature = currentTemperature;
thermalConductance = subsystem_resource->thermalConductance; // +0xF0
heatFilter.Initialize(15, 0.0f); // FUN_0043ad4f(this+0x144, 0xF, 0)
filterDecay = 0.4f;
thermalMass = subsystem_resource->thermalMass; // +0xF4
heatEnergy = thermalMass * startingTemperature;
coolantFlowScale = 1.0f;
massScale = 1.0f;
pendingHeat = 0.0f;
//
// A "master" heat sink (segment flagged 0x100, not a sub-/damaged copy)
// drives the per-frame thermal simulation.
//
// INTEGRATION (gate reconcile): the binary master-gate reads the OWNER's
// simulationFlags (param_2+0x28), NOT the per-segment resource flags. The
// recovered C across all subsystem ctors is uniformly
// (*(uint*)(param_2+0x28) & 0xc)==0 && (*(uint*)(param_2+0x28) & 0x100)!=0
// (owner+0x28), which is the authoritative path the working AmmoBin/projweap
// gate already used. Reading subsystem_resource->subsystemFlags here was a
// reconstruction mis-attribution (that field streams 0 in our data → the gate
// never armed → HeatSinkSimulation was never installed → the sink decayed to
// DoNothingOnce after frame 1). The SAME correction applies to the linked-sink
// block below: the raw decomp of @004adda0 reads the OWNER flags (param_2+0x28)
// there too (lines 47/56), NOT the per-segment resource flags. With subsystemFlags
// streaming 0 the Add-gate was dead → linkedSinks empty → weapon/subsystem heat
// never conducted to its designated sink → the mech never heated. Fixed to owner
// flags to match the binary; the per-subsystem TARGET is still heatSinkIndex.
if (
(owner->simulationFlags & SegmentCopyMask) == 0 // (owner flags & 0xC) == 0
&& (owner->simulationFlags & MasterHeatSinkFlag) != 0 // owner flags & 0x100
)
{
SetPerformance(&HeatSink::HeatSinkSimulation); // this[7..9] = &HeatSinkSimulation
}
resource = subsystem_resource;
//
// Resolve and attach the linked heat sink referenced by heatSinkIndex.
// task #9 GUARD CORRECTION: the binary's @0041a1a4 test is against GUID
// 0x50e590 = the AGGREGATE bank (0xBBE "HeatSinkBank"), NOT Condenser --
// only the BANK skips the link-attach (its authored SinkIdx=5 is dead;
// it radiates to ambient instead). The old Condenser guard BLOCKED the
// condenser->bank conduction links, which (with the ambient radiator
// deferred) closed the system: heat could never leave -- the observed
// monotonic runaway once emitters went authentic.
//
if (!IsDerivedFrom("HeatSinkBank")) // FUN_0041a1a4(*this[3], 0x50e590)
{
// ⚠ ROOT-CAUSE FIX (the BGF-load heap corruption): heatSinkIndex indexes the
// owner's SUBSYSTEM ROSTER, NOT the skeleton segment table. Raw @004adda0
// (part_012.c:16999): if (res->heatSinkIndex < owner->subsystemCount /*+0x124*/)
// linked = owner->subsystemArray[heatSinkIndex] /*+0x128*/;
// The earlier draft resolved it via owner->GetSegment(index) — an EntitySegment
// (288 bytes) cast to HeatSink* — so every per-frame ConductHeat/BalanceCoolant
// wrote pendingHeat/coolantLevel 100/20 bytes PAST that block: thousands of OOB
// heap writes during fire, detected later at an unrelated free (bld08.bgf load).
// subsystemArray is zeroed up front (mech.cpp), so a not-yet-built roster slot
// reads NULL -> the binary's "missing" warn path, exactly as the oracle.
Subsystem *linked = 0;
if (subsystem_resource->heatSinkIndex >= 0
&& subsystem_resource->heatSinkIndex < owner->GetSubsystemCount())
{
linked = owner->GetSubsystem(subsystem_resource->heatSinkIndex);
}
else
{
// @004adda0: "Bad subsystem resource ->heatSink" HEAT.CPP:0x25F
Verify(False, "Bad subsystem resource ->heatSink", __FILE__, 0x25F);
}
if (getenv("BT_HEAT_LOG"))
{
DEBUG_STREAM << "[heat-link] " << GetName()
<< " sinkIdx=" << subsystem_resource->heatSinkIndex
<< " linked=" << (linked ? linked->GetName() : "<NULL/not-built-yet>")
<< " mass=" << subsystem_resource->thermalMass
<< " k=" << subsystem_resource->thermalConductance
<< std::endl;
}
if (
(owner->simulationFlags & SegmentCopyMask) == 0 // param_2+0x28 & 0xc == 0
&& (owner->simulationFlags & MasterHeatSinkFlag) != 0 // param_2+0x28 & 0x100
)
{
if (linked == 0)
{
// HEAT.CPP:0x26B
Verify(False, "Master heatable subsystem is missing", __FILE__, 0x26B);
}
else
{
linkedSinks.Add(linked); // (**(this[0x59]+4))(this+0x59, linked)
}
}
else if (
(owner->simulationFlags & SegmentCopyMask) == 4 // param_2+0x28 & 0xc == 4
&& linked != 0
)
{
linkedSinks.Add(linked);
}
}
UpdateHeatLoad(); // FUN_004ad7f0
Check_Fpu();
}
//
// @004adfd4
//
HeatSink::~HeatSink()
{
Check(this);
// members (heatAlarm @0x5C, linkedSinks @0x59, heatFilter @0x51)
// are torn down by their own destructors; base chain handles the rest.
Check_Fpu();
}
Logical
HeatSink::TestInstance() const
{
// @004ae034 -> FUN_0041a1a4(**this[3], 0x50e3ec)
return IsDerivedFrom(*GetClassDerivations());
}
//###########################################################################
// TestClass -- HeatSink (HEAT.TCP)
//
Logical
HeatSink::TestClass(Mech &)
{
return True;
}
//###########################################################################
// ResetToInitialState -- HeatSink
//
// @004ad760
//
void
HeatSink::ResetToInitialState(Logical /*powered*/)
{
currentTemperature = startingTemperature;
heatEnergy = startingTemperature * thermalMass;
coolantLevel = thermalCapacity;
coolantDraw = 0.0f;
coolantAvailable = 1;
coolantFlowScale = 1.0f;
ClearHeatFilter(); // FUN_004ad884
UpdateHeatLoad(); // FUN_004ad7f0
coolantActive = 0;
//
// CHAIN CORRECTION (Gitea #55). This used to call
// `HeatableSubsystem::ResetToInitialState(True)` with the comment
// "FUN_004ac22c" -- but @004ac22c is NOT HeatableSubsystem's body.
// Disassembled: @004ac22c reads the subsystem's damage zone (`this+0xe0`),
// clears `zone+0x158`, then `Set_Alarm_Level(zone+0x10, 0)` and
// `Set_Alarm_Level(this+0x2c, 0)` -- it is **Subsystem::ResetToInitialState**,
// the terminus that heals the subsystem's own crit zone and status alarm.
// And the binary's own HeatSink @004ad760 calls exactly three things:
// @004ad884 (ClearHeatFilter), @004ad7f0 (UpdateHeatLoad), @004ac22c -- it
// never touches HeatableSubsystem's body.
//
// Calling HeatableSubsystem's body here was actively harmful: it sets
// `currentTemperature = 300.0f`, **clobbering the `startingTemperature` this
// function assigned four lines above**.
//
// The terminus work (zone heal + status alarm) is already done for every
// subsystem by `MechSubsystem::RespawnRepair()`, which `Mech::Reset` calls
// immediately after `DeathReset` in the same loop (mech4.cpp:1790) -- so the
// chain ends here rather than duplicating it.
//
}
//
// DeathReset -- the respawn sweep's entry point (Gitea #55). Serves HeatSink and
// every derived class that has no reset body of its own -- notably **Reservoir**
// (the coolant tank), which is what refills coolant on respawn. Before this, the
// sweep hit the empty `Subsystem::DeathReset` base and you respawned with whatever
// coolant your previous life had left (user-reported: "respawned with drained
// coolant").
//
void
HeatSink::DeathReset(int /*reset_command*/)
{
ResetToInitialState(True); // @004ad760
// Gitea #55 regression guard: the respawn refill must actually land. Verified
// live 2026-07-25 -- coolant == capacity on every heat sink across 4 respawn
// cycles, and the temperature restored to startingTemperature (77) instead of
// the 300 the old HeatableSubsystem chain clobbered it with. Silent unless it
// regresses.
if (coolantLevel < thermalCapacity)
DEBUG_STREAM << "[heat] BUG (Gitea #55): respawn left coolant "
<< coolantLevel << " / " << thermalCapacity << " -- not refilled\n"
<< std::flush;
}
//#############################################################################
// Per-frame simulation
//
//
// @004ad924 -- the registered Performance for a master heat sink.
//
void
HeatSink::HeatSinkSimulation(Scalar time_slice)
{
Check(this);
if (HeatModelActive()) // FUN_004ad7d4 (entity heat-model flag)
{
heatEnergy += pendingHeat;
currentTemperature = heatEnergy / thermalMass;
UpdateHeatLoad();
// DIAG census (BT_HEAT_LOG, viewpoint mech): PER-INSTANCE 5-s timers --
// the old shared static timer aliased to whichever instance crossed the
// tick (the diagnostic-sampler trap), which manufactured the task-#10
// "heat pools in Condenser1" misread.
if (getenv("BT_HEAT_LOG") && application != 0
&& (Entity *)owner == application->GetViewpointEntity())
{
static std::map<const void *, Scalar> s_census;
Scalar &acc = s_census[this];
acc += time_slice;
if (acc >= 5.0f)
{
acc = 0.0f;
DEBUG_STREAM << "[heat-t] " << GetName()
<< " T=" << currentTemperature
<< " absorbed=" << pendingHeat
<< " cool=" << coolantLevel << "/" << thermalCapacity
<< " load=" << heatLoad << "\n" << std::flush;
}
}
pendingHeat = 0.0f;
ConductHeat(time_slice); // FUN_004ad8ac
}
if (HeatModelActive())
{
UpdateCoolant(time_slice); // FUN_004adbf8
}
//
// Drive the degradation / failure alarm.
//
if (currentTemperature > failureTemperature)
{
heatAlarm.SetLevel(FailureHeat); // FUN_0041bbd8(this+0x5C, 2)
}
else if (currentTemperature > degradationTemperature)
{
heatAlarm.SetLevel(DegradationHeat); // 1
}
else
{
heatAlarm.SetLevel(NormalHeat); // 0
}
Check_Fpu();
}
//
// @004ad7f0 -- recompute the radiated/instantaneous heat and feed it through
// the running-average filter to produce the smoothed heatLoad reading.
//
void
HeatSink::UpdateHeatLoad()
{
radiatedHeat = currentTemperature * coolantLevel;
Scalar sample = HeatLoadScale * radiatedHeat;
if (sample < HeatLoadMinimum)
{
sample = HeatLoadMinimum;
}
else if (sample > HeatLoadMaximum)
{
sample = HeatLoadMaximum;
}
heatFilter.AddSample(sample); // FUN_0043ade4
heatLoad = heatFilter.Average(); // FUN_0043ae0b
}
//
// @004ad884 -- flush the 15-sample filter back to zero.
//
void
HeatSink::ClearHeatFilter()
{
for (int i = 0; i < 15; ++i)
{
heatFilter.AddSample(0.0f);
}
}
//
// @004ad8ac -- conduct heat into the linked sink and rebalance coolant.
//
void
HeatSink::ConductHeat(Scalar time_slice)
{
HeatSink *other = (HeatSink *)linkedSinks.Resolve(); // FUN_00417ab4(this+0x164)
if (other != 0 && coolantAvailable != 0)
{
Scalar flow = ComputeHeatFlow(other, time_slice); // FUN_004ad9ec
other->pendingHeat += flow;
pendingHeat -= flow;
BalanceCoolant(time_slice); // FUN_004ada94
}
}
//
// @004ad9ec -- conductive heat-exchange between this sink and 'other'.
//
// tau = thermalMass / massScale
// denom = tau + other->thermalMass
// q = (currentTemperature*massScale
// - (other->heatEnergy + other->pendingHeat + heatEnergy) / denom)
// * tau
// * (1 - exp( -dt * thermalConductance
// * (coolantLevel / thermalCapacity)
// * coolantFlowScale / denom ))
//
Scalar
HeatSink::ComputeHeatFlow(HeatSink *other, Scalar time_slice)
{
Scalar tau = thermalMass / massScale; // this+0x154 / this+0x160
Scalar denom = tau + other->thermalMass; // + other+0x154
Scalar exponent =
(-time_slice
* thermalConductance // this+0x140
* (coolantLevel / thermalCapacity) // this+0x12C / this+0x128
* coolantFlowScale) // this+0x15C
/ denom;
Scalar decay = expf(exponent); // FUN_004dca38
return (currentTemperature * massScale
- (other->heatEnergy + other->pendingHeat + heatEnergy) / denom)
* tau
* (1.0f - decay); // _DAT_004ada90 == 1.0f
}
//
// @004ada94 -- move coolant between this sink and its linked sink so that the
// hotter side sheds load. Clamped on both ends so neither sink goes below 0
// or above its capacity.
//
void
HeatSink::BalanceCoolant(Scalar time_slice)
{
HeatSink *other = (HeatSink *)linkedSinks.Resolve(); // FUN_00417ab4(this+0x164)
if (other == 0)
{
return;
}
if (fabsf(radiatedHeat - other->radiatedHeat) <= HeatEqualizeEpsilon) // FUN_004dcd00
{
return;
}
Scalar delta = other->radiatedHeat / currentTemperature - coolantLevel;
// clamp delta to +/- (thermalCapacity * dt)
Scalar limit = thermalCapacity * time_slice;
if (delta < -limit)
{
delta = -limit;
}
else if (delta > limit)
{
delta = limit;
}
// clamp so this sink stays within [0, thermalCapacity]
Scalar hi = thermalCapacity - coolantLevel;
Scalar lo = -coolantLevel;
if (delta < lo) delta = lo;
else if (delta > hi) delta = hi;
// clamp so the other sink stays within its own [0, thermalCapacity]
Scalar otherLo = -(other->thermalCapacity - other->coolantLevel);
if (delta < otherLo) delta = otherLo;
else if (delta > other->coolantLevel) delta = other->coolantLevel;
delta = coolantFlowScale * delta;
coolantLevel += delta;
other->coolantLevel -= delta;
}
//
// @004adbf8 -- consume coolant proportional to current load, request a
// top-up from the central cooling system, and update the draw state machine.
//
void
HeatSink::UpdateCoolant(Scalar time_slice)
{
// AUTHENTIC (heatmodel decode, FUN_004adbf8): the binary reads *(this[0x38]+0x158)
// = this subsystem's OWN DamageZone.damageLevel (the engine base zone at @0xE0,
// word 0x38), NOT linkedSinks->heatEnergy. The earlier reconstruction confused
// word 0x38/@0xE0 (the DamageZone) with linkedSinks@0x164, so when the link
// resolved (heatEnergy ~1.3e7) coolantDraw became ~2e6 and would SLAM coolantLevel
// to empty every frame -- the opposite of the authentic near-static behavior.
// An undamaged subsystem has damageLevel 0 -> coolantDraw 0 -> NO leak (the coolant
// bars stay full on a pristine mech); the draw rises only as the heat sink /
// condenser itself takes battle damage. (Read the qualified engine zone -- the
// nearer MechSubsystem::damageZone is a shim SHADOW; see mechweap.cpp:252.)
::DamageZone *ownZone = this->Subsystem::damageZone; // @0xE0 (word 0x38)
Scalar zoneDamage = (ownZone != 0) ? ownZone->damageLevel : 0.0f; // +0x158
coolantDraw = zoneDamage * heatLoad; // *(this[0x38]+0x158) * this[0x48]
if (coolantDraw < CoolantDrawFloor) // _DAT_0050e3d8 = 0.0025
{
coolantDraw = 0.0f;
}
// DIAG (BT_COOL_LOG): fire whenever THIS heat subsystem carries any damage --
// answers "does damaging a mech drain its coolant?". Shows the damageLevel the
// leak reads (@0xE0), the heat load, the resulting draw, and the live level.
if (getenv("BT_COOL_LOG") && zoneDamage > 0.001f)
DEBUG_STREAM << "[cool] " << GetName() << " dmg=" << zoneDamage
<< " heatLoad=" << heatLoad << " draw=" << coolantDraw
<< " coolantLevel=" << coolantLevel << "/" << thermalCapacity
<< "\n" << std::flush;
Scalar amount = coolantDraw * time_slice;
if (coolantLevel < amount)
{
amount = coolantLevel;
}
coolantLevel -= amount;
if (fabsf(amount) > CoolantDrawGate) // _DAT_004adcfc = 1e-4
{
coolantLevel += DrawCoolant(amount); // virtual: (**(*this+0x38))(this, amount)
}
//
// Draw state machine (this+0x138).
//
if (coolantActive == 0 && coolantDraw > CoolantActiveOn) // _DAT_0050e3d4 = 0.003
{
coolantActive = 1;
}
else if (coolantActive == 1 && coolantDraw < CoolantDrawFloor) // _DAT_0050e3d8 = 0.0025
{
coolantActive = 0;
}
}
//
// vtable slot 14 (@vtable+0x38). Base sinks supply nothing on their own;
// a central-cooling-system subclass overrides this.
// TODO: verify against the real slot-14 target (not captured in decomp).
//
Scalar
HeatSink::DrawCoolant(Scalar /*requested*/)
{
return 0.0f;
}
//#############################################################################
// Subsystem virtual overrides
//
//
// @004add30 -- base flags, plus heat-active / coolant bits.
//
LWord
HeatSink::GetStatusFlags()
{
LWord flags = HeatableSubsystem::GetStatusFlags(); // FUN_004ac144
if (heatAlarm.GetLevel() != 0) // this+0x184 (heat-state Normal/Deg/Fail)
{
flags |= 0x8;
}
if (coolantActive != 0 // this+0x138
&& HeatModelActive()) // FUN_004ad7d4
{
flags |= 0x4;
}
return flags;
}
//
// @004add6c -- message 1 forces the linked master's heatEnergy to a known
// value; everything else falls through to the base handler.
//
Logical
HeatSink::HandleMessage(int message)
{
if (message == 1)
{
// @004add6c:16948 *(this[0x38]+0x158) = 0.5 == damageZone->damageLevel = 0.5
// this[0x38] is the inherited MechSubsystem damageZone (@0xE0), NOT linkedSinks
// (@0x164): a message-driven "set this sink to half structural damage". The
// prior read targeted the linked master's heatEnergy -- wrong object + field.
SetSubsystemDamageLevel(0.5f);
return True;
}
return HeatableSubsystem::HandleMessage(message); // FUN_004ac1d4
}
//
// @004ae050 -- prints "<name> NormalHeat | DegradationHeat | FailureHeat".
//
void
HeatSink::PrintState()
{
HeatableSubsystem::PrintState(); // FUN_004ac8c0
switch (heatAlarm.GetLevel()) // this+0x184 (heat-state field, inside heatAlarm)
{
case NormalHeat:
DebugStream << GetName() << " NormalHeat" << endl;
break;
case DegradationHeat:
DebugStream << GetName() << " DegradationHeat" << endl;
break;
case FailureHeat:
DebugStream << GetName() << " FailureHeat" << endl;
break;
default:
DebugStream << GetName() << " Unknown Heat State!" << endl;
break;
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CreateStreamedSubsystem -- HeatSink
//
// @004ae150
//
int
HeatSink::CreateStreamedSubsystem(
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes
)
{
if (
!HeatableSubsystem::CreateStreamedSubsystem( // FUN_004ac9ec
model_file, model_name, subsystem_name,
subsystem_resource, subsystem_file, directories, passes
)
)
{
return False;
}
subsystem_resource->subsystemModelSize = sizeof(*subsystem_resource);
subsystem_resource->classID = RegisteredClass::HeatSinkClassID;
if (passes == 1)
{
// first pass: prime all heat fields to "unset" (-1.0f / -1)
subsystem_resource->startingTemperature = -1.0f;
subsystem_resource->degradationTemperature = -1.0f;
subsystem_resource->failureTemperature = -1.0f;
subsystem_resource->thermalConductance = -1.0f;
subsystem_resource->thermalMass = -1.0f;
subsystem_resource->heatSinkIndex = -1;
}
if (
!model_file->GetEntry(subsystem_name, "StartingTemperature",
&subsystem_resource->startingTemperature)
&& subsystem_resource->startingTemperature == -1.0f
)
{
DebugStream << subsystem_name << " missing StartingTemperature!";
return False;
}
if (
!model_file->GetEntry(subsystem_name, "DegradationTemperature",
&subsystem_resource->degradationTemperature)
&& subsystem_resource->degradationTemperature == -1.0f
)
{
DebugStream << subsystem_name << " missing DegradationTemperature!";
return False;
}
if (
!model_file->GetEntry(subsystem_name, "FailureTemperature",
&subsystem_resource->failureTemperature)
&& subsystem_resource->failureTemperature == -1.0f
)
{
DebugStream << subsystem_name << " missing FailureTemperature!";
return False;
}
if (
!model_file->GetEntry(subsystem_name, "ThermalConductance",
&subsystem_resource->thermalConductance)
&& subsystem_resource->thermalConductance == -1.0f
)
{
DebugStream << subsystem_name << " missing ThermalConductance!";
return False;
}
if (
!model_file->GetEntry(subsystem_name, "ThermalMass",
&subsystem_resource->thermalMass)
&& subsystem_resource->thermalMass == -1.0f
)
{
DebugStream << subsystem_name << " missing ThermalMass!";
return False;
}
//
// "HeatSink" names the segment that this sink links to. Resolve the
// name to a segment index (biased by +2 to leave room for sentinels).
//
const char *heatSinkName = "Unspecified";
int found = model_file->GetEntry(subsystem_name, "HeatSink", &heatSinkName);
if (!found && subsystem_resource->heatSinkIndex == -1)
{
DebugStream << subsystem_name << " missing HeatSink!";
return False;
}
if (strcmp(heatSinkName, "Unspecified") != 0)
{
subsystem_resource->heatSinkIndex =
Get_Segment_Index(model_file, model_name, directories, heatSinkName); // FUN_004215b0
}
if (subsystem_resource->heatSinkIndex < 0)
{
DebugStream << subsystem_name << " has an invalid heat sink!";
return False;
}
subsystem_resource->heatSinkIndex += 2;
Check_Fpu();
return True;
}
//===========================================================================//
// WAVE 2 factory bridges -- construct a real heat subsystem for mech.cpp's
// roster factory (which can't #include heat.hpp: its local RECON_SUBSYS stubs
// collide). Keep the binary's alloc SIZE; the Check guard turns a sizeof
// overrun (placement-new heap corruption) into an immediate assert.
//===========================================================================//
Subsystem *CreateCondenserSubsystem(Mech *owner, int id, void *seg)
{
Check(sizeof(Condenser) <= 0x230);
return (Subsystem *) new (Memory::Allocate(0x230))
Condenser(owner, id, (Condenser::SubsystemResource *)seg, Condenser::DefaultData);
}
// CreateHeatSinkBankSubsystem (0xBBE) now lives in heatfamily_reslice.cpp -- it
// builds the real AggregateHeatSink (which needs that TU's class definition).
//===========================================================================//
// Gauge cooling-loop bridge (task #7 regression fix, 2026-07-17).
//
// The dev-gauge CoolingLoopConnection (btl4gau2.cpp @004c31a0) reached the
// cooling master with a RAW attribute index (GetAttributePointer(3)) and then
// raw-read master+0x1d4. The binary heat attribute table (@0x50e438..0x50e4c8)
// shows id 3 == "HeatSink" == the linkedSinks plug, and +0x1d4 on the resolved
// master is Condenser::condenserNumber (the loop number that selects the
// image-strip frame). The numeric-id read crashed when the AUDIO_FIDELITY rows
// (ReportLeak et al., cc2b109) grew the chained tables: index 3 landed on a
// scalar, the resolve walked garbage, and the gauge background pass AV'd
// (BT_DEV_GAUGES=1, the pod launch flag). House rule (gotcha 8): named members
// via a complete-type TU -- never a raw numeric attribute index.
//===========================================================================//
int BTCoolingLoopFrame(void *subsystem_v)
{
// [loopfeed] diagnostic (env BT_LOOP_LOG): why does the per-weapon cooling-loop
// lamp read 0 (the blank frame)? Logs the outcome + reason for the first ~80
// samples so every weapon prints once.
static int s_loopLog = -1;
if (s_loopLog < 0) s_loopLog = getenv("BT_LOOP_LOG") ? 1 : 0;
static int s_loopN = 0;
int trace = s_loopLog && (s_loopN++ < 80);
Subsystem *sub = (Subsystem *)subsystem_v;
if (sub == 0 || !sub->IsDerivedFrom(*HeatSink::GetClassDerivations()))
{
if (trace) DEBUG_STREAM << "[loopfeed] " << (sub ? sub->GetName() : "(null)")
<< " -> 0 (not a HeatSink)\n" << std::flush;
return 0;
}
HeatSink *sink = (HeatSink *)sub;
if (sink->coolantAvailable != 1) // the binary's src+0x134 gate
{
if (trace) DEBUG_STREAM << "[loopfeed] " << sub->GetName()
<< " -> 0 (coolantAvailable=" << sink->coolantAvailable << ")\n" << std::flush;
return 0;
}
Subsystem *master = sink->linkedSinks.Resolve(); // attr id 3 "HeatSink" + FUN_00417ab4
if (master == 0)
{
if (trace) DEBUG_STREAM << "[loopfeed] " << sub->GetName()
<< " -> 0 (linkedSinks EMPTY -- not linked to a condenser)\n" << std::flush;
return 0;
}
if (!master->IsDerivedFrom(*Condenser::GetClassDerivations()))
{
if (trace) DEBUG_STREAM << "[loopfeed] " << sub->GetName()
<< " -> 0 (linkedSinks='" << master->GetName() << "' is NOT a Condenser)\n" << std::flush;
return 0;
}
int frame = ((Condenser *)master)->condenserNumber; // master+0x1d4 (loop/frame number)
if (trace) DEBUG_STREAM << "[loopfeed] " << sub->GetName() << " -> frame " << frame
<< " (condenser '" << master->GetName() << "')\n" << std::flush;
return frame;
}
//
// Gitea #47 bridges (BTL4GaugeAlarmManager::ReadGaugeAlarmStreamItem):
// the Condenser family tests + fields the alarm lamp mapping needs
// (@004cc2fc gates on 0x50e4fc; @004cc264 reads condenserNumber @0x1D4).
//
int BTSubsystemIsCondenser(::Subsystem *sub)
{
return (sub != 0 && sub->IsDerivedFrom(*Condenser::GetClassDerivations())) ? 1 : 0;
}
int BTCondenserNumber(::Subsystem *sub)
{
return (sub != 0) ? ((Condenser *)sub)->condenserNumber : -1; // +0x1D4
}