Oracle: "alarm is not sounding despite an active leak" ... "was sounding,
resolved a leak, but there was another".
RETRACTION FIRST. I previously told the user the coolant alarm was "genuinely
unbuilt -- there is no alarm implementation to starve". That was wrong, and it
was asserted without checking. The alarm is fully implemented and the entire
chain works. Traced live, in order:
Condenser6.ReportLeak (= HeatSink::coolantActive) changes 0 -> 1
-> [watchpoll] CHANGE on that exact address
-> [matchfire] val=1 -> ctl 1 (StartAudioControlID)
-> [seqstart] the alarm AudioControlSequence, events=25 looped=1
-> [seqsend] ctl 8/16 (select), 6/1 (volume), 1/0 (START), 2/0 (STOP), ...
a three-part looping alarm: two chirps then an 8s sustained tone.
The authored design, read from BTL4.RES: 19 subsystems per mech each bind TWO
AudioLogicalTriggers to their ReportLeak flag -- match 1 -> Start, match 0 ->
Stop -- and ALL of them drive ONE shared alarm sequence.
SO WHY WAS IT SILENT IN .674? Almost certainly the audio source pool, fixed
after that build in ad9dfad. Every player log from .674 is saturated with
ACQUIRE FAILED (3k-6.5k lines each, starting ~10% in and never recovering); an
alarm that cannot acquire an OpenAL source is silent. The bench here shows 0
acquire failures on the current build. ⚠ NOT PROVEN: this bench has no audio
device (pool census reads live=0 pooled=0), so the control chain is verified but
final playback is not. Field confirmation needed.
THE SECOND SYMPTOM IS A REAL, REPRODUCED DEFECT AND IT IS IN THE AUTHORED DATA.
All 19 subsystems -- Condenser1-6, GeneratorA-D, Myomers, PPC_1/2, ERMLaser_1-3,
SRM6_1/2, Avionics -- share the single alarm sequence, and EACH one's leak-clear
sends an UNCONDITIONAL Stop. So the moment any one of them stops leaking the
alarm goes silent, even while others are still leaking. That is exactly what was
reported. Since both the authoring and the MUNGA watcher code are authentic,
this is 1995 behaviour, and changing it is a deliberate divergence -- flagged for
a fidelity call rather than "fixed" unilaterally.
Diagnostics added (all under the existing BT_ATTRBIND_LOG gate):
[seqcfg] extended: the sequence's authored event list
[seqstart] extended: events / looped / tempo / divisionsPerBeat
[seqrun] RunSequence entry state (isRunning / iterator / current event)
[seqwait] an event exists but is not yet ready, with the time delta
[seqsend] the sequence actually emitting a control
[idlewatch] which components receive the idle tick that advances sequences
[hsparm] extended with &ReportLeak so audio bindings can be correlated
plus scratchpad/night8/leakaudio.sh.
NOTE ON THE PROBES, because it cost real time twice today: [seqrun] first used a
single shared static counter, which the busy sequences consumed so the alarm
sequence never printed -- reading as "RunSequence is never called". It is now
throttled PER SEQUENCE (runProbeCount). A capped diagnostic that is silent is
not evidence of absence; the same trap produced a false "seqsend x0" and a false
"the trigger never fires" earlier in this investigation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1353 lines
48 KiB
C++
1353 lines
48 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(reset_command != 0); // #55: arg forwarded
|
|
}
|
|
|
|
//###########################################################################
|
|
// 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.
|
|
//
|
|
// (#47 follow-up, 2026-07-25) NOT a neutral default: the binary's
|
|
// HeatSink::GetStatusFlags @004add30 OPENS with `call 0x4ac144` --
|
|
// MechSubsystem::GetStatusFlags, the DAMAGE TIER (damageLevel >= 1.0 -> bit 0
|
|
// Destroyed; > 0 -> bit 1 Damaged). There is no zero-returning
|
|
// HeatableSubsystem override in the image; this stub silently dropped bits 0/1
|
|
// for the ENTIRE heat branch (every weapon/sensor/generator/emitter), which
|
|
// (a) made Destroyed/Damaged unable to reach MechTech's annunciator scan --
|
|
// gutting half the #47 mechalrm table -- and (b) let PoweredSubsystem's
|
|
// AutoConnect gate (`GetStatusFlags() == 0`, powersub.cpp:358) treat a DAMAGED
|
|
// subsystem as pristine. Chaining the real tier reproduces @004add30 exactly.
|
|
LWord
|
|
HeatableSubsystem::GetStatusFlags() { return MechSubsystem::GetStatusFlags(); }
|
|
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(reset_command != 0); // #55: arg forwarded
|
|
}
|
|
|
|
//###########################################################################
|
|
// 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)
|
|
// @004b8fec stores 0.15f here (param_1[0x54] = 0x3e19999a). The value is
|
|
// otherwise unread in the port, but it IS the leak gauge's full-scale
|
|
// divisor: BitMapInverseWipe divides by subsystem+0x150 (#97).
|
|
filterDecay = 0.15f; // was 0.4f -- did not match the image
|
|
thermalMass = subsystem_resource->thermalMass; // +0xF4
|
|
heatEnergy = thermalMass * startingTemperature;
|
|
coolantFlowScale = 1.0f;
|
|
massScale = 1.0f;
|
|
|
|
pendingHeat = 0.0f;
|
|
|
|
// #96 -- "each mech has a unique heating profile". The heat-family COUNT is
|
|
// identical on every chassis (6 condensers, 1 bank, 1 reservoir), so if
|
|
// per-mech cooling variation exists it must live in these authored values.
|
|
// Dump them so that is a measured fact rather than an assumption.
|
|
if (getenv("BT_MYO_LOG"))
|
|
DEBUG_STREAM << "[hsparm] " << (GetName() ? GetName() : "?")
|
|
<< " startT=" << startingTemperature
|
|
<< " degradeT=" << degradationTemperature
|
|
<< " failT=" << failureTemperature
|
|
<< " conductance=" << thermalConductance
|
|
<< " thermalMass=" << thermalMass
|
|
// #99: correlate with [attrbind] -- is the audio watcher actually
|
|
// bound to THIS object's leak flag?
|
|
<< " &ReportLeak=" << (void *)&coolantActive
|
|
<< "\n" << std::flush;
|
|
|
|
//
|
|
// 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(reset_command != 0); // @004ad760 (#55: arg forwarded)
|
|
// 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. (#88 fix 2026-07-31: this qualified
|
|
// engine-member read was NULL forever -- the MechSubsystem ctor only filled its
|
|
// re-declared SHADOW, so `zoneDamage` pinned 0 and a leak was structurally
|
|
// impossible no matter how much damage landed. The ctor now ALIASES the engine
|
|
// base member to the same zone (mechsub.cpp, the #64 half-fix), so this read
|
|
// sees the real crit/rattle damage.)
|
|
::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) -- the base @0x4add00, disassembled + decoded
|
|
// 2026-07-31 (the old `return 0` stub was why a leaking subsystem could never
|
|
// pull from the central tank -- the Reservoir bar never moved on damage):
|
|
// linked = resolve(linkedSinks@0x164) ; FUN_00417ab4
|
|
// return linked->DrawCoolant(requested * coolantFlowScale@0x15C)
|
|
// i.e. the draw RECURSES UP the sink linkage, scaling per hop, and terminates
|
|
// at the Reservoir's own override (@0x4af3b0: clamp to [0, coolantLevel],
|
|
// drain the tank, return the granted amount). In the binary the terminal hop
|
|
// is the bank's slot-14 override @0x4ae8b0 resolving its reservoir connection
|
|
// @0x1D8; in the port the reservoir sits in the bank's linkedSinks (the
|
|
// Reservoir-ctor Attach) and Reservoir::DrawCoolant IS the @0x4af3b0 body, so
|
|
// the same chain terminates identically (the extra bank hop multiplies by its
|
|
// coolantFlowScale, ctor-default 1.0 -- a no-op). PORT GUARD: the binary
|
|
// calls through the resolved link UNGUARDED (authored topology always links);
|
|
// an unlinked port sink supplies 0 instead of faulting.
|
|
//
|
|
Scalar
|
|
HeatSink::DrawCoolant(Scalar requested)
|
|
{
|
|
HeatSink *linked = (HeatSink *)linkedSinks.Resolve(); // FUN_00417ab4(this+0x59)
|
|
requested *= coolantFlowScale; // @0x15C
|
|
if (linked == 0)
|
|
return 0.0f;
|
|
return linked->DrawCoolant(requested); // vtbl+0x38 (virtual)
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// 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;
|
|
}
|
|
//
|
|
// #97 bridge: the leak gauge's FULL-SCALE divisor. BitMapInverseWipe computes
|
|
// level = round(fullWidth * leakRate / thirdParam)
|
|
// and the binary sources thirdParam from subsystem+0x150 (@004b8fec writes
|
|
// 0.15f there). The gauge TU cannot see the HeatSink layout, and raw-reading
|
|
// +0x150 from there would land on whatever OUR layout puts at that offset --
|
|
// the databinding trap. Resolve it through the named member instead.
|
|
//
|
|
Scalar BTHeatSinkLeakFullScale(::Subsystem *sub)
|
|
{
|
|
if (sub == 0 || !sub->IsDerivedFrom(*HeatSink::GetClassDerivations()))
|
|
return 0.15f; // the authored default
|
|
Scalar fs = ((HeatSink *)sub)->LeakGaugeFullScale();
|
|
return (fs > 0.0f) ? fs : 0.15f; // never divide by zero
|
|
}
|
|
|
|
|
|
int BTCondenserNumber(::Subsystem *sub)
|
|
{
|
|
return (sub != 0) ? ((Condenser *)sub)->condenserNumber : -1; // +0x1D4
|
|
}
|
|
|
|
|
|
//
|
|
// #83 bridge: the collision-damage distributor (mech.cpp, FUN_0049ffcc) tests
|
|
// roster members against this family's derivation chain (binary GUID 0x50e590).
|
|
// Lives here because HeatSink is a complete type only in this TU.
|
|
//
|
|
Derivation *
|
|
BTHeatSinkFamily()
|
|
{
|
|
return HeatSink::GetClassDerivations();
|
|
}
|