//===========================================================================// // File: heat.cpp // // Project: BattleTech Brick: Mech subsystems // // Contents: HeatableSubsystem -- a MechSubsystem with a thermal state // //---------------------------------------------------------------------------// // Copyright (C) 1995, Virtual World Entertainment, Inc. // // All Rights reserved worldwide // // This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // //===========================================================================// #include #pragma hdrstop #if !defined(HEAT_HPP) # include #endif #if !defined(MECH_HPP) # include #endif #if !defined(BTPLAYER_HPP) # include #endif #include // //############################################################################# // Tuning constants (byte-verified against the shipped image in the BT411 RE: // .data / literal-pool reads). //############################################################################# // static const Scalar HeatLoadScale = 0.002f; // heat-load normalising scale -> band [0,1] static const Scalar HeatLoadMinimum = 0.0f; static const Scalar HeatLoadMaximum = 1.0f; static const Scalar HeatEqualizeEpsilon = 1.0e-4f; // BalanceCoolant no-op band static const Scalar CoolantDrawGate = 1.0e-4f; // DrawCoolant |amount| gate static const Scalar CoolantDrawFloor = 0.0025f; // draw zero-floor + ACTIVE-off hysteresis static const Scalar CoolantActiveOn = 0.003f; // coolantActive ON threshold // //############################################################################# // Shared data support -- reuses the base Subsystem sets (no boot-critical // handlers / attributes of its own). //############################################################################# // Derivation HeatableSubsystem::ClassDerivations( MechSubsystem::ClassDerivations, "HeatableSubsystem" ); HeatableSubsystem::SharedData HeatableSubsystem::DefaultData( HeatableSubsystem::ClassDerivations, Subsystem::MessageHandlers, Subsystem::AttributeIndex, Subsystem::StateCount ); // //############################################################################# //############################################################################# // 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); ResetToInitialState(); Check_Fpu(); } // //############################################################################# //############################################################################# // HeatableSubsystem::~HeatableSubsystem() { } // //############################################################################# //############################################################################# // void HeatableSubsystem::ResetToInitialState() { Check(this); currentTemperature = 300.0f; heatLoad = 0.0f; } // //############################################################################# //############################################################################# // Logical HeatableSubsystem::TestClass(Mech &) { return True; } Logical HeatableSubsystem::TestInstance() const { return IsDerivedFrom(ClassDerivations); } // //############################################################################# // CreateStreamedSubsystem Model-load-time construction (thermal resource + // damage-zone stream). Not yet reconstructed (see MECHSUB.NOTES.md). //############################################################################# // int HeatableSubsystem::CreateStreamedSubsystem( NotationFile *, const char *, const char *, SubsystemResource *, NotationFile *, const ResourceDirectories *, int ) { Fail("HeatableSubsystem::CreateStreamedSubsystem -- heat.cpp not yet reconstructed"); return 0; } //########################################################################### //############################## HeatSink ############################### //########################################################################### // //############################################################################# // Shared data support //############################################################################# // Derivation HeatSink::ClassDerivations( HeatableSubsystem::ClassDerivations, "HeatSink" ); HeatSink::SharedData HeatSink::DefaultData( HeatSink::ClassDerivations, Subsystem::MessageHandlers, Subsystem::AttributeIndex, Subsystem::StateCount ); // //############################################################################# // The heat sink -- a thermal mass with a coolant loop. A master sink drives // the per-frame thermal simulation. //############################################################################# // HeatSink::HeatSink( Mech *owner, int subsystem_ID, SubsystemResource *subsystem_resource, SharedData &shared_data ): HeatableSubsystem(owner, subsystem_ID, subsystem_resource, shared_data), linkedSinks(), heatAlarm(3) { Check(owner); Check_Pointer(subsystem_resource); currentTemperature = subsystem_resource->startingTemperature; degradationTemperature = subsystem_resource->degradationTemperature; failureTemperature = subsystem_resource->failureTemperature; heatLoad = 0.0f; coolantEfficiency = 0.5f; thermalCapacity = 1.0f; coolantLevel = thermalCapacity; coolantDraw = 0.0f; coolantAvailable = 1; coolantActive = 0; startingTemperature = currentTemperature; thermalConductance = subsystem_resource->thermalConductance; heatFilter.SetSize(15, 0.0f); filterDecay = 0.4f; thermalMass = subsystem_resource->thermalMass; heatEnergy = thermalMass * startingTemperature; coolantFlowScale = 1.0f; massScale = 1.0f; pendingHeat = 0.0f; radiatedHeat = 0.0f; // // Wire the heat-conduction link: the resource names the roster slot of the // sink this one drains into (weapons/equipment -> the Condenser bank, the // Condensers -> the central HeatSink). The shipped stream orders sinks so // the target is already constructed; an unresolvable index leaves the sink // standalone (its own thermal mass only). // { Subsystem *linked = NULL; if (subsystem_resource->linkedSinkIndex >= 0 && subsystem_resource->linkedSinkIndex < owner->GetSubsystemCount()) { linked = owner->GetSubsystem(subsystem_resource->linkedSinkIndex); } if (linked != NULL) { linkedSinks.Add(linked); } if (getenv("BT_POWER_LOG")) { DEBUG_STREAM << "[heat] '" << GetName() << "' linkedSinkIdx=" << subsystem_resource->linkedSinkIndex << " -> "; if (linked != NULL) { DEBUG_STREAM << linked->GetName(); } else { DEBUG_STREAM << ""; } DEBUG_STREAM << " thermalMass=" << thermalMass << " T0=" << currentTemperature << " degrade=" << degradationTemperature << " fail=" << failureTemperature << " conduct=" << thermalConductance << endl << flush; } } // // Install the per-frame thermal Performance (replicant copies are driven by // console updates instead). Derived classes (PoweredSubsystem, Generator, // the weapons) override with their own Performance in their ctors, each of // which chains this step. // if (owner->GetInstance() != Entity::ReplicantInstance) { SetPerformance(&HeatSink::HeatSinkSimulation); } Check_Fpu(); } // //############################################################################# //############################################################################# // HeatSink::~HeatSink() { } // //############################################################################# //############################################################################# // void HeatSink::ResetToInitialState(Logical /*powered*/) { Check(this); currentTemperature = startingTemperature; heatLoad = 0.0f; coolantLevel = thermalCapacity; coolantDraw = 0.0f; coolantActive = 0; heatEnergy = thermalMass * startingTemperature; pendingHeat = 0.0f; radiatedHeat = 0.0f; heatAlarm.SetLevel(0); } // //############################################################################# //############################################################################# // Logical HeatSink::TestClass(Mech &) { return True; } Logical HeatSink::TestInstance() const { return IsDerivedFrom(ClassDerivations); } // //############################################################################# // HeatModelActive -- the heat-model master switch (binary FUN_004ad7d4): // owner mech -> Entity::playerLink -> BTPlayer::heatModelOn, ON only for // veteran / expert experience. Standard mode has NO heat consequences -- // authentic, not a gap. A NULL player link (unlinked dev mech / target dummy) // reads ON, matching the permissive dev-rig behavior; the binary derefs // unguarded (a linked player always exists in pod missions). //############################################################################# // Logical HeatSink::HeatModelActive() { Check(this); if (owner == NULL) { return True; } BTPlayer *player = Cast_Object(BTPlayer *, owner->GetPlayerLink()); if (player == NULL) { return True; } return player->IsHeatModelOn(); } // //############################################################################# // HeatSinkSimulation -- the per-frame thermal step (binary @004ad924). // Under the heat-model experience gate: absorb the pending heat into the // thermal mass, recompute the temperature and the smoothed heat-load reading, // conduct into the linked sink, and run the coolant draw. The degradation / // failure alarm drive is OUTSIDE the gate (authentic -- with the model off the // temperature never moves, so the alarm just holds Normal). //############################################################################# // void HeatSink::HeatSinkSimulation(Scalar time_slice) { Check(this); if (HeatModelActive()) { heatEnergy += pendingHeat; currentTemperature = heatEnergy / thermalMass; UpdateHeatLoad(); pendingHeat = 0.0f; ConductHeat(time_slice); } if (HeatModelActive()) { UpdateCoolant(time_slice); } // // Drive the degradation / failure alarm. // if (currentTemperature > failureTemperature) { heatAlarm.SetLevel(FailureHeat); } else if (currentTemperature > degradationTemperature) { heatAlarm.SetLevel(DegradationHeat); } else { heatAlarm.SetLevel(NormalHeat); } // // BT_HEAT_LOG: a 5-second per-sink census (temperature / coolant / load). // if (getenv("BT_HEAT_LOG")) { static Scalar censusAccum = 0.0f; censusAccum += time_slice; if (censusAccum >= 5.0f) { censusAccum = 0.0f; DEBUG_STREAM << "[heat-t] " << GetName() << " T=" << currentTemperature << " cool=" << coolantLevel << "/" << thermalCapacity << " load=" << heatLoad << endl << flush; } } Check_Fpu(); } // //############################################################################# // UpdateHeatLoad -- recompute the radiated heat and feed the normalised sample // through the 15-sample running-average filter to produce the smoothed // heatLoad reading in the [0,1] band (binary @004ad7f0; the shaping constants // are byte-verified from the image). //############################################################################# // void HeatSink::UpdateHeatLoad() { Check(this); radiatedHeat = currentTemperature * coolantLevel; Scalar sample = HeatLoadScale * radiatedHeat; if (sample < HeatLoadMinimum) { sample = HeatLoadMinimum; } else if (sample > HeatLoadMaximum) { sample = HeatLoadMaximum; } heatFilter.Add(sample); heatLoad = heatFilter.CalculateAverage(); } // //############################################################################# // ConductHeat -- conduct heat into the linked sink, then rebalance coolant so // the hotter side sheds load (binary @004ad8ac). //############################################################################# // void HeatSink::ConductHeat(Scalar time_slice) { Check(this); HeatSink *other = (HeatSink *)linkedSinks.Resolve(); if (other != NULL && coolantAvailable != 0) { Scalar flow = ComputeHeatFlow(other, time_slice); other->pendingHeat += flow; pendingHeat -= flow; BalanceCoolant(time_slice); } } // //############################################################################# // BalanceCoolant -- move coolant between this sink and its linked sink so that // the hotter side sheds load (binary @004ada94). Clamped on both ends so // neither sink goes below empty or above its capacity. //############################################################################# // void HeatSink::BalanceCoolant(Scalar time_slice) { Check(this); HeatSink *other = (HeatSink *)linkedSinks.Resolve(); if (other == NULL) { return; } Scalar spread = radiatedHeat - other->radiatedHeat; if (spread < 0.0f) { spread = -spread; } if (spread <= HeatEqualizeEpsilon) { 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; } // //############################################################################# // UpdateCoolant -- consume coolant proportional to the current load, request a // top-up from the central cooling system, and update the draw state machine // (binary @004adbf8). The draw scales with THIS subsystem's own structural // damage: an undamaged subsystem leaks nothing (the coolant bars stay full on // a pristine mech); the draw rises only as the sink itself takes battle // damage. //############################################################################# // void HeatSink::UpdateCoolant(Scalar time_slice) { Check(this); coolantDraw = GetSubsystemDamageLevel() * heatLoad; if (coolantDraw < CoolantDrawFloor) { coolantDraw = 0.0f; } Scalar amount = coolantDraw * time_slice; if (coolantLevel < amount) { amount = coolantLevel; } coolantLevel -= amount; if (amount > CoolantDrawGate || amount < -CoolantDrawGate) { coolantLevel += DrawCoolant(amount); } // // The draw state machine (hysteresis: ON above 0.003, OFF below 0.0025). // if (coolantActive == 0 && coolantDraw > CoolantActiveOn) { coolantActive = 1; } else if (coolantActive == 1 && coolantDraw < CoolantDrawFloor) { coolantActive = 0; } } // //############################################################################# // ComputeHeatFlow -- conductive heat exchange between this sink and 'other' // (binary @004ad9ec): // 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) { Check(this); Check(other); Scalar tau = thermalMass / massScale; Scalar denom = tau + other->thermalMass; Scalar equilibrium = currentTemperature * massScale - (other->heatEnergy + other->pendingHeat + heatEnergy) / denom; Scalar response = 1.0f - (Scalar)exp( -time_slice * thermalConductance * (coolantLevel / thermalCapacity) * coolantFlowScale / denom ); return equilibrium * tau * response; } // //############################################################################# // DrawCoolant -- ask the central cooling system for coolant and return how // much was actually supplied. Base sinks supply nothing on their own; the // Reservoir (the coolant store) overrides this as the source. //############################################################################# // Scalar HeatSink::DrawCoolant(Scalar) { Check(this); return 0.0f; } //########################################################################### //############################# HeatWatcher ############################# //########################################################################### Derivation HeatWatcher::ClassDerivations( MechSubsystem::ClassDerivations, "HeatWatcher" ); HeatWatcher::SharedData HeatWatcher::DefaultData( HeatWatcher::ClassDerivations, Subsystem::MessageHandlers, Subsystem::AttributeIndex, Subsystem::StateCount ); HeatWatcher::HeatWatcher( Mech *owner, int subsystem_ID, SubsystemResource *subsystem_resource, SharedData &shared_data ): MechSubsystem( owner, subsystem_ID, (MechSubsystem::SubsystemResource *)subsystem_resource, shared_data ), watchedLink(), heatAlarm(3) { Check(owner); Check_Pointer(subsystem_resource); degradationTemperature = subsystem_resource->degradationTemperature; failureTemperature = subsystem_resource->failureTemperature; watchedSubsystem = subsystem_resource->watchedSubsystem; // // The master instance runs WatchSimulation per-frame; the install is // deferred with that (staged) method. // Check_Fpu(); } HeatWatcher::~HeatWatcher() { } Logical HeatWatcher::TestClass(Mech &) { return True; } Logical HeatWatcher::TestInstance() const { return IsDerivedFrom(ClassDerivations); } void HeatWatcher::ResetToInitialState(Logical /*powered*/) { Check(this); heatAlarm.SetLevel(0); } // // Per-frame: resolve the watched subsystem, read its temperature, drive the // 3-level alarm. Not yet reconstructed. // void HeatWatcher::WatchSimulation(Scalar) { Fail("HeatWatcher::WatchSimulation -- heat.cpp not yet reconstructed"); } //########################################################################### //############################## Condenser ############################# //########################################################################### Derivation Condenser::ClassDerivations( HeatSink::ClassDerivations, "Condenser" ); Condenser::SharedData Condenser::DefaultData( Condenser::ClassDerivations, Subsystem::MessageHandlers, Subsystem::AttributeIndex, Subsystem::StateCount ); 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); // // The authentic ctor (binary @4ae568): the valve opens at 1, the // refrigeration output rides the inherited massScale slot, and the // condenser's own coolantFlowScale streams 0 (the valve recompute -- // BTRecomputeCondenserValves, the cockpit MoveValve wave -- assigns each // condenser its share of the total valve opening). INTERIM: flow scale is // left at the inherited 1.0 until the valve-recompute wave lands -- a zero // flow scale would zero the condenser->bank conduction exponent and strand // the heat in the condensers with no way to reach the central sink. // valveState = 1; refrigerationFactor = subsystem_resource->refrigerationFactor; massScale = refrigerationFactor; // // Condenser number from the segment-name DIGIT suffix ("Condenser1" -> 1; // the letter form -0x40 is the GENERATOR convention). // const char *name = GetName(); condenserNumber = name[strlen(name) - 1] - '0'; if (getenv("BT_POWER_LOG")) { DEBUG_STREAM << "[cond] '" << GetName() << "' refrigFactor=" << refrigerationFactor << " #" << condenserNumber << endl << flush; } // // Install the per-frame refrigeration Performance. // if (owner->GetInstance() != Entity::ReplicantInstance) { SetPerformance(&Condenser::RefrigerationSimulation); } Check_Fpu(); } Condenser::~Condenser() { } Logical Condenser::TestClass(Mech &) { return True; } Logical Condenser::TestInstance() const { return IsDerivedFrom(ClassDerivations); } // //############################################################################# // RefrigerationSimulation -- the condenser's per-frame step (binary @4ae4d8): // recompute the refrigeration output as (1 - own structural damage) * // refrigerationFactor, clamped >= 1, into the inherited massScale slot -- an // undamaged condenser behaves refrigerationFactor-times "hotter" in the // conduction exchange, actively pumping heat toward the central bank and // chilling itself below ambient -- then run the base HeatSink step. //############################################################################# // void Condenser::RefrigerationSimulation(Scalar time_slice) { Check(this); massScale = (1.0f - GetSubsystemDamageLevel()) * refrigerationFactor; if (massScale < 1.0f) { massScale = 1.0f; } HeatSink::HeatSinkSimulation(time_slice); }