Two coupled fixes so the condenser valve gauge (ValveSetting -> coolantFlowScale @0x15C) reads its authentic value instead of garbage/zero. STEP 7 (gating dedup): heat.cpp carried STUB Condenser ctor/dtor/TestClass/ TestInstance/CreateStreamedSubsystem that ODR-duplicated the REAL bodies in heatfamily_reslice.cpp and WON under /FORCE (heat.obj links first) -> the real ctor (which sets valveState=1) was shadowed, leaving valveState=0xCDCDCDCD. #if 0'd the heat.cpp stubs so the reslice ctor (@4ae568, byte-verified: valveState=1, coolantFlowScale=0, massScale=refrigerationFactor, condenserNumber from name) is the sole definition. DefaultData/GetClassDerivations/ResetToInitialState (not duplicated) stay in heat.cpp. STEP 9 (the real writer): FinishConstruction() at the Mech ctor tail was a no-op template stub in place of FUN_0049f788 = RecomputeCondenserValves -> coolantFlowScale was never written (stayed 0). Reconstructed it (byte-verified vs part_012.c:9264): distribute coolant flow across the mech's condensers, coolantFlowScale_i = valveState_i / sum(valveState), with the condenserAlarm@0x1DC change pulse (2-if-flow<=old-else-1, then 0). Walks the populated subsystem roster filtering Condensers via IsDerivedFrom (behaviorally identical to the binary's @mech+0x7cc condenser chain, GUID 0x50e4fc). Wired as BTRecomputeCondenserValves(this) at the Mech ctor post-init pass (binary @9457). _DAT_0049f850 fallback confirmed 0.0f (PE read). Verified: [valve] the Blackhawk's 6 condensers each read flow=0.166667 (=1/6, total=6) for both player + spawned enemy, no every-mech crash; combat TARGET DESTROYED, un-regressed. Diagnostic BT_VALVE_LOG added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1029 lines
32 KiB
C++
1029 lines
32 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>
|
|
#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 (coolant-draw 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
|
|
static const Scalar CoolantDrawEpsilon = 1.0e-4f; // _DAT_004adcfc / 0050e3d8 / 0050e3d4
|
|
|
|
|
|
//###########################################################################
|
|
//###########################################################################
|
|
// HeatableSubsystem
|
|
//###########################################################################
|
|
//###########################################################################
|
|
|
|
//#############################################################################
|
|
// Shared Data Support
|
|
//
|
|
HeatableSubsystem::SharedData
|
|
HeatableSubsystem::DefaultData(
|
|
HeatableSubsystem::GetClassDerivations(),
|
|
HeatableSubsystem::GetMessageHandlers(),
|
|
HeatableSubsystem::GetAttributeIndex(),
|
|
HeatableSubsystem::StateCount
|
|
);
|
|
|
|
Derivation*
|
|
HeatableSubsystem::GetClassDerivations()
|
|
{
|
|
static Derivation classDerivations(Subsystem::GetClassDerivations(), "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;
|
|
}
|
|
|
|
//###########################################################################
|
|
// 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
|
|
//
|
|
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);
|
|
}
|
|
|
|
//###########################################################################
|
|
// 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)
|
|
};
|
|
|
|
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.
|
|
// (Skipped for "Condenser"-class segments -- see @0041a1a4 guard.)
|
|
//
|
|
if (!IsDerivedFrom(*Condenser::GetClassDerivations())) // 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 (
|
|
(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;
|
|
|
|
HeatableSubsystem::ResetToInitialState(True); // FUN_004ac22c
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// 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();
|
|
|
|
// BRING-UP verify (1 Hz, viewpoint mech only): show the mech heat climbing as
|
|
// weapons fire and relaxing when they stop. pendingHeat just absorbed above.
|
|
static Scalar s_heatLog = 0.0f;
|
|
s_heatLog += time_slice;
|
|
if (s_heatLog >= 1.0f && application != 0
|
|
&& (Entity *)owner == application->GetViewpointEntity() && pendingHeat > 0.0f)
|
|
{
|
|
s_heatLog = 0.0f;
|
|
DEBUG_STREAM << "[heat] sink temp=" << currentTemperature
|
|
<< " heatEnergy=" << heatEnergy << " absorbed=" << pendingHeat
|
|
<< " heatLoad=" << 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 < CoolantDrawEpsilon)
|
|
{
|
|
coolantDraw = 0.0f;
|
|
}
|
|
|
|
Scalar amount = coolantDraw * time_slice;
|
|
if (coolantLevel < amount)
|
|
{
|
|
amount = coolantLevel;
|
|
}
|
|
coolantLevel -= amount;
|
|
|
|
if (fabsf(amount) > CoolantDrawEpsilon)
|
|
{
|
|
coolantLevel += DrawCoolant(amount); // virtual: (**(*this+0x38))(this, amount)
|
|
}
|
|
|
|
//
|
|
// Draw state machine (this+0x138).
|
|
//
|
|
if (coolantActive == 0 && coolantDraw > CoolantDrawEpsilon)
|
|
{
|
|
coolantActive = 1;
|
|
}
|
|
else if (coolantActive == 1 && coolantDraw < CoolantDrawEpsilon)
|
|
{
|
|
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).
|