//===========================================================================// // 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 #include // BT_HEAT_LOG census (diag only) #pragma hdrstop #if !defined(HEAT_HPP) # include #endif #if !defined(APP_HPP) # include #endif #if !defined(TESTBT_HPP) # include #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 iterator(audioWatcherSocket); while ((watcher = iterator.ReadAndNext()) != NULL) watcher->Execute(); } { SChainIteratorOf iterator(videoWatcherSocket); while ((watcher = iterator.ReadAndNext()) != NULL) watcher->Execute(); } { SChainIteratorOf 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() { 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 // // 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); } //########################################################################### // 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/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. // 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() : "") << " 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; 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(); // 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 s_census; Scalar &acc = s_census[this]; acc += time_slice; if (acc >= 5.0f) { acc = 0.0f; DEBUG_STREAM << "[heat-t] " << GetName() << " T=" << currentTemperature << " absorbed=" << pendingHeat << " cool=" << coolantLevel << "/" << thermalCapacity << " load=" << heatLoad << "\n" << std::flush; } } pendingHeat = 0.0f; ConductHeat(time_slice); // FUN_004ad8ac } if (HeatModelActive()) { UpdateCoolant(time_slice); // FUN_004adbf8 } // // Drive the degradation / failure alarm. // if (currentTemperature > failureTemperature) { heatAlarm.SetLevel(FailureHeat); // FUN_0041bbd8(this+0x5C, 2) } else if (currentTemperature > degradationTemperature) { heatAlarm.SetLevel(DegradationHeat); // 1 } else { heatAlarm.SetLevel(NormalHeat); // 0 } Check_Fpu(); } // // @004ad7f0 -- recompute the radiated/instantaneous heat and feed it through // the running-average filter to produce the smoothed heatLoad reading. // void HeatSink::UpdateHeatLoad() { radiatedHeat = currentTemperature * coolantLevel; Scalar sample = HeatLoadScale * radiatedHeat; if (sample < HeatLoadMinimum) { sample = HeatLoadMinimum; } else if (sample > HeatLoadMaximum) { sample = HeatLoadMaximum; } heatFilter.AddSample(sample); // FUN_0043ade4 heatLoad = heatFilter.Average(); // FUN_0043ae0b } // // @004ad884 -- flush the 15-sample filter back to zero. // void HeatSink::ClearHeatFilter() { for (int i = 0; i < 15; ++i) { heatFilter.AddSample(0.0f); } } // // @004ad8ac -- conduct heat into the linked sink and rebalance coolant. // void HeatSink::ConductHeat(Scalar time_slice) { HeatSink *other = (HeatSink *)linkedSinks.Resolve(); // FUN_00417ab4(this+0x164) if (other != 0 && coolantAvailable != 0) { Scalar flow = ComputeHeatFlow(other, time_slice); // FUN_004ad9ec other->pendingHeat += flow; pendingHeat -= flow; BalanceCoolant(time_slice); // FUN_004ada94 } } // // @004ad9ec -- conductive heat-exchange between this sink and 'other'. // // tau = thermalMass / massScale // denom = tau + other->thermalMass // q = (currentTemperature*massScale // - (other->heatEnergy + other->pendingHeat + heatEnergy) / denom) // * tau // * (1 - exp( -dt * thermalConductance // * (coolantLevel / thermalCapacity) // * coolantFlowScale / denom )) // Scalar HeatSink::ComputeHeatFlow(HeatSink *other, Scalar time_slice) { Scalar tau = thermalMass / massScale; // this+0x154 / this+0x160 Scalar denom = tau + other->thermalMass; // + other+0x154 Scalar exponent = (-time_slice * thermalConductance // this+0x140 * (coolantLevel / thermalCapacity) // this+0x12C / this+0x128 * coolantFlowScale) // this+0x15C / denom; Scalar decay = expf(exponent); // FUN_004dca38 return (currentTemperature * massScale - (other->heatEnergy + other->pendingHeat + heatEnergy) / denom) * tau * (1.0f - decay); // _DAT_004ada90 == 1.0f } // // @004ada94 -- move coolant between this sink and its linked sink so that the // hotter side sheds load. Clamped on both ends so neither sink goes below 0 // or above its capacity. // void HeatSink::BalanceCoolant(Scalar time_slice) { HeatSink *other = (HeatSink *)linkedSinks.Resolve(); // FUN_00417ab4(this+0x164) if (other == 0) { return; } if (fabsf(radiatedHeat - other->radiatedHeat) <= HeatEqualizeEpsilon) // FUN_004dcd00 { return; } Scalar delta = other->radiatedHeat / currentTemperature - coolantLevel; // clamp delta to +/- (thermalCapacity * dt) Scalar limit = thermalCapacity * time_slice; if (delta < -limit) { delta = -limit; } else if (delta > limit) { delta = limit; } // clamp so this sink stays within [0, thermalCapacity] Scalar hi = thermalCapacity - coolantLevel; Scalar lo = -coolantLevel; if (delta < lo) delta = lo; else if (delta > hi) delta = hi; // clamp so the other sink stays within its own [0, thermalCapacity] Scalar otherLo = -(other->thermalCapacity - other->coolantLevel); if (delta < otherLo) delta = otherLo; else if (delta > other->coolantLevel) delta = other->coolantLevel; delta = coolantFlowScale * delta; coolantLevel += delta; other->coolantLevel -= delta; } // // @004adbf8 -- consume coolant proportional to current load, request a // top-up from the central cooling system, and update the draw state machine. // void HeatSink::UpdateCoolant(Scalar time_slice) { // AUTHENTIC (heatmodel decode, FUN_004adbf8): the binary reads *(this[0x38]+0x158) // = this subsystem's OWN DamageZone.damageLevel (the engine base zone at @0xE0, // word 0x38), NOT linkedSinks->heatEnergy. The earlier reconstruction confused // word 0x38/@0xE0 (the DamageZone) with linkedSinks@0x164, so when the link // resolved (heatEnergy ~1.3e7) coolantDraw became ~2e6 and would SLAM coolantLevel // to empty every frame -- the opposite of the authentic near-static behavior. // An undamaged subsystem has damageLevel 0 -> coolantDraw 0 -> NO leak (the coolant // bars stay full on a pristine mech); the draw rises only as the heat sink / // condenser itself takes battle damage. (Read the qualified engine zone -- the // nearer MechSubsystem::damageZone is a shim SHADOW; see mechweap.cpp:252.) ::DamageZone *ownZone = this->Subsystem::damageZone; // @0xE0 (word 0x38) Scalar zoneDamage = (ownZone != 0) ? ownZone->damageLevel : 0.0f; // +0x158 coolantDraw = zoneDamage * heatLoad; // *(this[0x38]+0x158) * this[0x48] if (coolantDraw < CoolantDrawFloor) // _DAT_0050e3d8 = 0.0025 { coolantDraw = 0.0f; } // DIAG (BT_COOL_LOG): fire whenever THIS heat subsystem carries any damage -- // answers "does damaging a mech drain its coolant?". Shows the damageLevel the // leak reads (@0xE0), the heat load, the resulting draw, and the live level. if (getenv("BT_COOL_LOG") && zoneDamage > 0.001f) DEBUG_STREAM << "[cool] " << GetName() << " dmg=" << zoneDamage << " heatLoad=" << heatLoad << " draw=" << coolantDraw << " coolantLevel=" << coolantLevel << "/" << thermalCapacity << "\n" << std::flush; Scalar amount = coolantDraw * time_slice; if (coolantLevel < amount) { amount = coolantLevel; } coolantLevel -= amount; if (fabsf(amount) > CoolantDrawGate) // _DAT_004adcfc = 1e-4 { coolantLevel += DrawCoolant(amount); // virtual: (**(*this+0x38))(this, amount) } // // Draw state machine (this+0x138). // if (coolantActive == 0 && coolantDraw > CoolantActiveOn) // _DAT_0050e3d4 = 0.003 { coolantActive = 1; } else if (coolantActive == 1 && coolantDraw < CoolantDrawFloor) // _DAT_0050e3d8 = 0.0025 { coolantActive = 0; } } // // vtable slot 14 (@vtable+0x38). Base sinks supply nothing on their own; // a central-cooling-system subclass overrides this. // TODO: verify against the real slot-14 target (not captured in decomp). // Scalar HeatSink::DrawCoolant(Scalar /*requested*/) { return 0.0f; } //############################################################################# // Subsystem virtual overrides // // // @004add30 -- base flags, plus heat-active / coolant bits. // LWord HeatSink::GetStatusFlags() { LWord flags = HeatableSubsystem::GetStatusFlags(); // FUN_004ac144 if (heatAlarm.GetLevel() != 0) // this+0x184 (heat-state Normal/Deg/Fail) { flags |= 0x8; } if (coolantActive != 0 // this+0x138 && HeatModelActive()) // FUN_004ad7d4 { flags |= 0x4; } return flags; } // // @004add6c -- message 1 forces the linked master's heatEnergy to a known // value; everything else falls through to the base handler. // Logical HeatSink::HandleMessage(int message) { if (message == 1) { // @004add6c:16948 *(this[0x38]+0x158) = 0.5 == damageZone->damageLevel = 0.5 // this[0x38] is the inherited MechSubsystem damageZone (@0xE0), NOT linkedSinks // (@0x164): a message-driven "set this sink to half structural damage". The // prior read targeted the linked master's heatEnergy -- wrong object + field. SetSubsystemDamageLevel(0.5f); return True; } return HeatableSubsystem::HandleMessage(message); // FUN_004ac1d4 } // // @004ae050 -- prints " 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).