From e1e5a9a6db585a1513840e3eaf787baad21ebff2 Mon Sep 17 00:00:00 2001 From: Cyd Date: Wed, 22 Jul 2026 22:15:07 -0500 Subject: [PATCH] BT410 Phase 5.3.12: experience gates + coolant system -- novice/expert modes REAL The player-experience system now gates the entire heat/jam economy, verified in BOTH directions with a one-token egg edit (TESTNOV.EGG, experience=novice): NOVICE = 212 fire cycles all pinned at T=77, zero jams, zero shutdowns (the heat model authentically absent); EXPERT = the full economy (duty cycles, 119 shutdowns + 2 authentic jams under forced fire). Zero Fail in both. - BTPLAYER: the flag block renamed to its TRUE semantics (simLive @0x25c, heatModelOn @0x260 -- the FUN_004ad7d4 master switch, advancedDamageOn pair, levelFlag26c/270, experienceLevel); the ctor rows were already binary-accurate (nov 0000 / std 1011 / vet 1111 / exp 1101); accessors + [exp] sentinel. - HeatSink::HeatModelActive() + ProjectileWeapon::LiveFireEnabled(): owner mech -> Entity::GetPlayerLink() -> the BTPlayer flags, NULL-permissive. Gates: both HeatSinkSimulation phases, every weapon fire-heat dump, and CheckForJam is now the AUTHENTIC form (LiveFireEnabled + heatLoad<=0 early-outs + the minJamChance floor; the interim heat-degraded gate retired). - THE LOAD-BEARING FIX: Mech ctor SetValidFlag(). Every 1995 entity ctor tail marks itself valid; ours didn't -- Entity::Dispatch routes messages to an INVALID entity into the deferred event queue, so the PlayerLink bind (and every directly-dispatched mech message) silently never landed and the gates read a NULL player forever. - THE COOLANT SYSTEM (authentic bodies, byte-verified constants: HeatLoadScale 0.002 -> heatLoad now in [0,1]; equalize eps 1e-4; draw floor/ON 0.0025/0.003): UpdateCoolant (damage-scaled draw -- an undamaged mech leaks nothing), BalanceCoolant (full clamp chain from ConductHeat), DrawCoolant base=0 with the RESERVOIR override as THE SOURCE; Reservoir reconstructed (capacity overlays thermalCapacity, CoolantSimulation + the full InjectCoolant flush distribution, BT_FORCE_FLUSH dev hook); Condenser RefrigerationSimulation (massScale = (1-damage)*refrigerationFactor >= 1 -- the heat pump that chills the bank; valveState inits 1; digit-suffix number fix). Deferred: AggregateHeatSink family, cockpit-button handlers (MoveValve/ToggleCooling/InjectCoolant), TrackSeekVoltage charge model. Research driven by a 4-agent workflow dossier over the BT411 RE (verbatim bodies for every function above + the egg experience plumbing). Co-Authored-By: Claude Fable 5 --- restoration/source410/BT/BTPLAYER.CPP | 62 +++-- restoration/source410/BT/BTPLAYER.HPP | 46 +++- restoration/source410/BT/BTPLAYER.NOTES.md | 14 + restoration/source410/BT/EMITTER.CPP | 20 +- restoration/source410/BT/HEAT.CPP | 296 ++++++++++++++++++-- restoration/source410/BT/HEAT.HPP | 33 +++ restoration/source410/BT/HEAT.NOTES.md | 72 ++++- restoration/source410/BT/MECH.CPP | 10 + restoration/source410/BT/MECH.NOTES.md | 10 + restoration/source410/BT/MISLANCH.CPP | 10 +- restoration/source410/BT/PROJWEAP.CPP | 64 ++++- restoration/source410/BT/PROJWEAP.HPP | 7 + restoration/source410/BT/RESERVR.CPP | 301 ++++++++++++++++++++- restoration/source410/BT/RESERVR.HPP | 95 ++++++- 14 files changed, 934 insertions(+), 106 deletions(-) diff --git a/restoration/source410/BT/BTPLAYER.CPP b/restoration/source410/BT/BTPLAYER.CPP index 2641466d..b5e1b8b9 100644 --- a/restoration/source410/BT/BTPLAYER.CPP +++ b/restoration/source410/BT/BTPLAYER.CPP @@ -155,41 +155,51 @@ BTPlayer::BTPlayer( else { // - // Master: derive the game-mode flags from the mission experience - // level (novice / standard / veteran / expert) and advanced-damage - // technician flag. See BTPLAYER.NOTES.md for the flag mapping. + // Master: derive the game-mode flags from the mission experience level + // (the egg's per-pilot "experience" token). The binary rows for + // (simLive, heatModelOn, levelFlag26c, levelFlag270): + // nov 0,0,0,0 / std 1,0,1,1 / vet 1,1,1,1 / exp 1,1,0,1. + // Standard mode is LIVE but has no heat model -- authentic. // Check(bt_mission); - switch (bt_mission->ExperienceLevel()) + experienceLevel = bt_mission->ExperienceLevel(); + switch (experienceLevel) { case BTMission::NoviceMode: - showDamageReceived = 0; - showKills = 0; - roleReturnDelay2 = 0; - showScore = 0; + simLive = 0; + heatModelOn = 0; + levelFlag26c = 0; + levelFlag270 = 0; break; case BTMission::StandardMode: - showDamageReceived = 1; - showKills = 0; - roleReturnDelay2 = 1; - showScore = 1; + simLive = 1; + heatModelOn = 0; + levelFlag26c = 1; + levelFlag270 = 1; break; case BTMission::VeteranMode: - showDamageReceived = 1; - showKills = 1; - roleReturnDelay2 = 1; - showScore = 1; + simLive = 1; + heatModelOn = 1; + levelFlag26c = 1; + levelFlag270 = 1; break; case BTMission::ExpertMode: - showDamageReceived = 1; - showKills = 1; - roleReturnDelay2 = 0; - showScore = 1; + simLive = 1; + heatModelOn = 1; + levelFlag26c = 0; + levelFlag270 = 1; break; } - showDamageInflicted = bt_mission->AdvancedDamageOn(); - roleReturnDelay = bt_mission->AdvancedDamageOn(); - roleClassIndex = bt_mission->ExperienceLevel(); + advancedDamageOn = bt_mission->AdvancedDamageOn(); + advancedDamageOn2 = advancedDamageOn; + + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[exp] master player experience=" << experienceLevel + << " simLive=" << (int)simLive + << " heatModelOn=" << (int)heatModelOn + << " advDamage=" << (int)advancedDamageOn << endl << flush; + } } killCount = 0; @@ -403,6 +413,12 @@ void playerVehicle->Dispatch(&player_link_message); playerVehicle->DispatchToReplicants(&player_link_message); + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[link] InitializePlayerLink -> vehicle playerLink=" + << (void *)playerVehicle->GetPlayerLink() << endl << flush; + } + Check_Fpu(); } diff --git a/restoration/source410/BT/BTPLAYER.HPP b/restoration/source410/BT/BTPLAYER.HPP index df349d32..b24d6f11 100644 --- a/restoration/source410/BT/BTPLAYER.HPP +++ b/restoration/source410/BT/BTPLAYER.HPP @@ -173,24 +173,46 @@ suppressConsole; // - // Game-mode flags derived in the constructor from the mission's - // experience level (novice / standard / veteran / expert) and - // advanced-damage technician flag. See BTPLAYER.NOTES.md. + // EXPERIENCE-LEVEL game-mode flags, derived in the constructor from the + // mission's experience level (novice / standard / veteran / expert) and + // advanced-damage technician flag. TRUE SEMANTICS (the early "display + // toggles" reading of this block was wrong -- see BTPLAYER.NOTES.md): + // binary rows for (simLive, heatModelOn, levelFlag26c, levelFlag270) = + // nov 0,0,0,0 / std 1,0,1,1 / vet 1,1,1,1 / exp 1,1,0,1. + // simLive -- the novice lockout: jams, searchlight, powersub + // short events (CheckForJam's LiveFireEnabled gate); + // heatModelOn -- THE heat-model master switch (HeatModelActive): + // weapon-fire heat, the heat sim, coolant, movement + // heat. Standard mode has NO heat consequences -- + // authentic, not a gap; + // advancedDamageOn/2 -- the egg's technician flag (an int pair in the + // binary; NOT part of the level switch); + // experienceLevel -- the raw 0..3 (the ==0 novice-lockout predicate). // Logical - showDamageReceived; + simLive; Logical - showKills; + heatModelOn; Logical - showDamageInflicted; + advancedDamageOn; Logical - showScore; - Scalar - roleReturnDelay; - Scalar - roleReturnDelay2; + advancedDamageOn2; + Logical + levelFlag26c; + Logical + levelFlag270; int - roleClassIndex; + experienceLevel; + + public: + Logical + IsSimLive() const { Check(this); return simLive; } + Logical + IsHeatModelOn() const { Check(this); return heatModelOn; } + Logical + IsAdvancedDamageOn() const { Check(this); return advancedDamageOn; } + + protected: int killCount; diff --git a/restoration/source410/BT/BTPLAYER.NOTES.md b/restoration/source410/BT/BTPLAYER.NOTES.md index 3aa38199..2a89c1f4 100644 --- a/restoration/source410/BT/BTPLAYER.NOTES.md +++ b/restoration/source410/BT/BTPLAYER.NOTES.md @@ -147,3 +147,17 @@ marker), zero Fail. The mech BODY is still `DoNothingOnce` (its real 3. Score / ScoreInflicted / ScoreUpdate handlers (scoring wave); the console SCORE-delta flush inside PlayerSimulation. 4. DropZoneReply respawn tail (`Mech::Reset` in-place heal/move). + +## Experience flags renamed to TRUE semantics (2026-07-22, Phase 5.3.12) + +The `show*` / `roleReturn*` block was the old "per-role display toggles" +misread (the memory note flagged it). Renamed + retyped to the binary truth: +`simLive` @0x25c (novice lockout: jams/lights/powersub short events), +`heatModelOn` @0x260 (THE heat-model master switch, FUN_004ad7d4), +`advancedDamageOn`/`advancedDamageOn2` (the egg technician flag pair), +`levelFlag26c` (0/1/1/0, consumer unlocated), `levelFlag270` (0/1/1/1), +`experienceLevel` (raw 0..3). The ctor rows were already binary-accurate. +Public accessors IsSimLive()/IsHeatModelOn()/IsAdvancedDamageOn(); a gated +`[exp]` sentinel prints the derived flags per master player. Consumers: +HeatSink::HeatModelActive() (HEAT.CPP), ProjectileWeapon::LiveFireEnabled() +(PROJWEAP.CPP) -- both walk mech->GetPlayerLink(), NULL-permissive. diff --git a/restoration/source410/BT/EMITTER.CPP b/restoration/source410/BT/EMITTER.CPP index a22b6d73..ada57a12 100644 --- a/restoration/source410/BT/EMITTER.CPP +++ b/restoration/source410/BT/EMITTER.CPP @@ -127,15 +127,19 @@ void ComputeOutputVoltage(); // - // Dump the firing heat into our own thermal accumulator; the HeatSink step - // absorbs it next frame and conducts it toward the linked Condenser bank. - // The heat chain is 1e7-unit-native (the BT411 calibration audit): the - // authored heatCostToFire is the closed form's full-charge value / 1e7 - // (PPC: 11 -> 1.1e8 units -> +632 K on its own 174000-mass sink). - // (PARTIAL: the (1-dF)*E*chargeRatio^2 charge-scaling joins with the - // electrical-charge wave.) + // Dump the firing heat into our own thermal accumulator, under the + // heat-model experience gate (novice / standard fire generates no heat -- + // authentic); the HeatSink step absorbs it next frame and conducts it + // toward the linked Condenser bank. The heat chain is 1e7-unit-native + // (the BT411 calibration audit): the authored EMITTER heatCostToFire is + // the closed form's full-charge value / 1e7 (PPC: 11 -> 1.1e8 units -> + // +632 K on its own 174000-mass sink). (PARTIAL: the + // (1-dF)*E*chargeRatio^2 charge-scaling joins the electrical-charge wave.) // - AddPendingHeat(heatCostToFire * 10000000.0f); + if (HeatModelActive()) + { + AddPendingHeat(heatCostToFire * 10000000.0f); + } if (getenv("BT_MECH_LOG")) { diff --git a/restoration/source410/BT/HEAT.CPP b/restoration/source410/BT/HEAT.CPP index 08d6df84..2ab3d865 100644 --- a/restoration/source410/BT/HEAT.CPP +++ b/restoration/source410/BT/HEAT.CPP @@ -19,8 +19,26 @@ # 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 @@ -286,17 +304,39 @@ Logical // //############################################################################# -// HeatSinkSimulation -- the per-frame thermal step (binary @004ad924). -// Absorb the pending heat into the thermal mass, recompute the temperature and -// the smoothed heat-load reading, conduct into the linked sink, then drive the -// degradation / failure alarm from the authored thresholds. +// 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). +//############################################################################# // -// PARTIAL: the heat model runs unconditionally -- the authentic gate is the -// player experience level (HeatModelActive: novice mode disables the heat -// model / jams; joins with the player-link accessor wave). UpdateCoolant -// (coolant depletion / venting) is deferred with the coolant wave; the -// coolant level stays at capacity, which holds the conduction term at its -// full-coolant value. +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 @@ -304,11 +344,19 @@ void { Check(this); - heatEnergy += pendingHeat; - currentTemperature = heatEnergy / thermalMass; - UpdateHeatLoad(); - pendingHeat = 0.0f; - ConductHeat(time_slice); + if (HeatModelActive()) + { + heatEnergy += pendingHeat; + currentTemperature = heatEnergy / thermalMass; + UpdateHeatLoad(); + pendingHeat = 0.0f; + ConductHeat(time_slice); + } + + if (HeatModelActive()) + { + UpdateCoolant(time_slice); + } // // Drive the degradation / failure alarm. @@ -326,15 +374,32 @@ void 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 it through the -// 15-sample running-average filter to produce the smoothed heatLoad reading -// (binary @004ad7f0; the HeatLoadScale / min / max shaping constants join with -// the gauge-calibration wave). +// 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 @@ -343,14 +408,25 @@ void Check(this); radiatedHeat = currentTemperature * coolantLevel; - heatFilter.Add(radiatedHeat); + + 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 (binary @004ad8ac). The -// coolant rebalance (BalanceCoolant) is deferred with the coolant wave. +// ConductHeat -- conduct heat into the linked sink, then rebalance coolant so +// the hotter side sheds load (binary @004ad8ac). //############################################################################# // void @@ -364,6 +440,116 @@ void 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; } } @@ -403,10 +589,17 @@ Scalar 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) { - Fail("HeatSink::DrawCoolant -- heat.cpp not yet reconstructed"); + Check(this); return 0.0f; } @@ -519,14 +712,41 @@ Condenser::Condenser( Check(owner); Check_Pointer(subsystem_resource); - valveState = 0; + // + // 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 suffix ('A' -> 1 ...). + // 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] - 0x40; + 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(); } @@ -546,3 +766,27 @@ Logical { 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); +} diff --git a/restoration/source410/BT/HEAT.HPP b/restoration/source410/BT/HEAT.HPP index e4eb6703..e920da88 100644 --- a/restoration/source410/BT/HEAT.HPP +++ b/restoration/source410/BT/HEAT.HPP @@ -200,6 +200,7 @@ public HeatableSubsystem { friend class Condenser; + friend class Reservoir; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Shared Data Support // @@ -251,6 +252,14 @@ activePerformance = (Simulation::Performance)performance; } + // + // The heat-model master switch (binary FUN_004ad7d4): the owning + // BTPlayer's heatModelOn experience flag, reached through the mech's + // playerLink. ON only for veteran / expert; a NULL player reads ON. + // + Logical + HeatModelActive(); + void HeatSinkSimulation(Scalar time_slice); void @@ -259,6 +268,10 @@ ConductHeat(Scalar time_slice); Scalar ComputeHeatFlow(HeatSink *other, Scalar time_slice); + void + UpdateCoolant(Scalar time_slice); + void + BalanceCoolant(Scalar time_slice); virtual Scalar DrawCoolant(Scalar requested); @@ -340,6 +353,26 @@ ); ~Condenser(); + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Per-frame refrigeration (binary @4ae4d8): the condenser actively pumps + // heat toward the central bank -- massScale is recomputed each frame as + // (1 - own damage) * refrigerationFactor, clamped >= 1, so an undamaged + // condenser behaves refrigerationFactor-times "hotter" in the conduction + // exchange and chills itself below ambient (which is what cools the + // weapons equalizing against it). Damage degrades it toward a plain sink. + // + public: + typedef void + (Condenser::*Performance)(Scalar time_slice); + void + SetPerformance(Performance performance) + { + Check(this); + activePerformance = (Simulation::Performance)performance; + } + void + RefrigerationSimulation(Scalar time_slice); + protected: int valveState; int condenserNumber; diff --git a/restoration/source410/BT/HEAT.NOTES.md b/restoration/source410/BT/HEAT.NOTES.md index e05f93b3..72508d54 100644 --- a/restoration/source410/BT/HEAT.NOTES.md +++ b/restoration/source410/BT/HEAT.NOTES.md @@ -100,11 +100,71 @@ The weapon-side consequences are wired and VERIFIED under sustained fire: thermal duty cycle (fire at ~1930° → spike 2562° → shutdown → cool → refire); the lasers oscillate around ~2200°. +## The experience gates + the coolant wave (Phase 5.3.12, 2026-07-22) + +**The player-experience gates are LIVE** (the workflow dossier recovered every +body from the BT411 RE): + +- `HeatSink::HeatModelActive()` (binary FUN_004ad7d4): owner mech → + `Entity::GetPlayerLink()` → `BTPlayer::heatModelOn` — ON only for veteran / + expert. Gates BOTH phases of HeatSinkSimulation (absorb/conduct AND + UpdateCoolant) and every weapon fire-heat dump. Standard mode has NO heat + consequences — authentic. NULL player link reads ON (dev-permissive). +- `ProjectileWeapon::LiveFireEnabled()`: playerLink → `simLive` — 0 only for + novice, whose ballistics never jam. `CheckForJam` is now the AUTHENTIC form: + LiveFireEnabled early-out + the `heatLoad <= 0` early-out (with the model off + heatLoad stays 0, so standard mode never jams either) + the minJamChance + floor (a veteran+ launcher carries its authored ~5% cold jam chance — the + interim heat-degraded gate is retired). +- BTPlayer's flag block renamed to its TRUE semantics (simLive @0x25c, + heatModelOn @0x260, advancedDamageOn/2, levelFlag26c/270, experienceLevel) — + the ctor rows were already binary-accurate; only the names/types were the + old "display toggles" misread. + +**THE LOAD-BEARING FIX found on the way: `Mech` ctor `SetValidFlag()`.** +Every 1995 entity ctor tail marks itself valid; ours didn't — and +`Entity::Dispatch` routes messages to an INVALID entity into the deferred +event queue, so the PlayerLink bind (and every directly-dispatched mech +message) silently never landed. The experience gates read a NULL player +forever (permissive ON) until this one authentic line was restored. + +**The coolant system** (all bodies authentic from the dossier, constants +byte-verified: HeatLoadScale 0.002 → heatLoad now lives in the [0,1] band, +HeatEqualizeEpsilon 1e-4, draw floor 0.0025 / ON 0.003, draw gate 1e-4): +- `UpdateCoolant` (@004adbf8): coolantDraw = own structural damage × heatLoad + (an UNDAMAGED mech leaks nothing — the bars stay full), drain, top-up via + the virtual `DrawCoolant`, and the active-flag hysteresis. +- `BalanceCoolant` (@004ada94): full clamp chain, called from ConductHeat. +- `DrawCoolant` base = 0; **Reservoir overrides it as THE SOURCE** (@4af3b0). +- **Reservoir reconstructed** (@4af408): capacity overlays thermalCapacity + (streamed 20), squirtMass 0.5, `CoolantSimulation` + `InjectCoolant` + (@4aefa4, the full flush distribution: condensers → weapons → heatables → + master with intentional duplicate weighting, squirts only leave the tank, + the chill only cools). `BT_FORCE_FLUSH=1` dev hook (the cockpit + InjectCoolant button joins the message wave). Deferred: the + AggregateHeatSink master-bank attach + the 0.05×count capacity rescale. +- **Condenser refrigeration** (@4ae4d8): valveState inits 1, massScale = + (1 − own damage) × refrigerationFactor (streamed 3.0) clamped ≥ 1 each + frame — the condenser pumps heat as if 3× hotter, chilling itself below + ambient; that chill is what cools the weapons. INTERIM: coolantFlowScale + stays 1.0 (authentic streams 0 + the MoveValve/BTRecomputeCondenserValves + share assignment — the cockpit-button wave; zeroing it now would strand + condenser heat). + +**VERIFIED headlessly, both directions of the experience lever** (TESTNOV.EGG += TEST.EGG with `experience=novice`): +- NOVICE: 212 fire cycles ALL at T=77, zero jams, zero shutdowns, coolant + static — the heat model authentically absent. +- EXPERT: playerLink resolves, the full economy runs (PPC duty cycle now + sawing ~650-1200° — the refrigeration pump drains harder than before — 119 + shutdowns + 2 authentic jams under forced sustained fire). Zero Fail in + both. + ## Still deferred -UpdateCoolant / BalanceCoolant (coolant depletion + venting), the -HeatModelOff / LiveFireEnabled experience gates (novice mode), the electrical -charge model (TrackSeekVoltage / voltage sag / I²R), the mech-disabled halves -of the weapon gates (damage wave), the central sink's forward-linked drain, -Condenser MoveValve handling, jam recovery via ResetToInitialState (mission -reset path). +The electrical charge model (TrackSeekVoltage / voltage sag / I²R), the +mech-disabled halves of the weapon gates (damage wave), the AggregateHeatSink +family (the central bank's count/radiator loop + reservoir attach + capacity +rescale + its forward-linked drain), the MoveValve / ToggleCooling / +InjectCoolant cockpit-button message handlers + BTRecomputeCondenserValves, +Myomers movement heat, jam recovery via ResetToInitialState (mission reset). diff --git a/restoration/source410/BT/MECH.CPP b/restoration/source410/BT/MECH.CPP index 45266633..c7e3de09 100644 --- a/restoration/source410/BT/MECH.CPP +++ b/restoration/source410/BT/MECH.CPP @@ -462,6 +462,16 @@ Mech::Mech( // SetPerformance(&Mech::Simulate); + // + // The entity is complete -- mark it VALID, like every 1995 entity ctor tail + // (CamShip / DoorFrame / DropZone / ...). LOAD-BEARING: Entity::Dispatch + // routes messages to an INVALID entity into the deferred event queue, so + // without this the mech never receives a directly-dispatched message -- + // the PlayerLink bind (and with it every player-experience gate) silently + // never lands. + // + SetValidFlag(); + Check_Fpu(); } diff --git a/restoration/source410/BT/MECH.NOTES.md b/restoration/source410/BT/MECH.NOTES.md index b7d22793..51ed97f8 100644 --- a/restoration/source410/BT/MECH.NOTES.md +++ b/restoration/source410/BT/MECH.NOTES.md @@ -355,3 +355,13 @@ invalidates every bucket; newest BT/BT_L4 header mtime additionally invalidates the game buckets) so header edits force the dependent recompiles. CODE/ is immutable and needs no stamp. When in doubt: purge `build410/obj/*` for a clean baseline. + +## PHASE 5.3.12: the missing SetValidFlag (2026-07-22) + +The Mech ctor tail now calls `SetValidFlag()` -- every 1995 entity ctor does +(CamShip/DoorFrame/DropZone/...), and ours didn't. LOAD-BEARING: +`Entity::Dispatch` routes messages to an INVALID entity into the deferred +event queue, so the mech never received a directly-dispatched message -- the +PlayerLink bind silently never landed and every player-experience gate read a +NULL player (dev-permissive heat-always-on) until this line was restored. +Found while verifying the novice-mode heat lockout end-to-end. diff --git a/restoration/source410/BT/MISLANCH.CPP b/restoration/source410/BT/MISLANCH.CPP index e7f436bc..fd1ef5c0 100644 --- a/restoration/source410/BT/MISLANCH.CPP +++ b/restoration/source410/BT/MISLANCH.CPP @@ -104,9 +104,13 @@ void // // Ballistic heatCostToFire is stored 1e7-unit-NATIVE in the stream (SRM6 // reads 5.06e7 = +641K on its own sink -- the same design magnitude as the - // PPC's +632K); dump it raw. (The EMITTER's authored value is small and - // its 1e7 comes from the energy algebra -- see EMITTER.CPP.) - AddPendingHeat(heatCostToFire); + // PPC's +632K); dump it raw, under the heat-model experience gate (novice / + // standard fire generates no heat -- authentic). (The EMITTER's authored + // value is small and its 1e7 comes from the energy algebra.) + if (HeatModelActive()) + { + AddPendingHeat(heatCostToFire); + } if (getenv("BT_MECH_LOG")) { diff --git a/restoration/source410/BT/PROJWEAP.CPP b/restoration/source410/BT/PROJWEAP.CPP index 31736b21..3a939978 100644 --- a/restoration/source410/BT/PROJWEAP.CPP +++ b/restoration/source410/BT/PROJWEAP.CPP @@ -27,6 +27,10 @@ # include #endif +#if !defined(BTPLAYER_HPP) +# include +#endif + Derivation ProjectileWeapon::ClassDerivations( MechWeapon::ClassDerivations, @@ -150,9 +154,14 @@ void // Ballistic heatCostToFire is stored 1e7-unit-NATIVE in the stream (SRM6 // reads 5.06e7 = +641K on its own sink -- the same design magnitude as the - // PPC's +632K); dump it raw. (The EMITTER's authored value is small and - // its 1e7 comes from the energy algebra -- see EMITTER.CPP.) - AddPendingHeat(heatCostToFire); + // PPC's +632K); dump it raw, under the heat-model experience gate (novice / + // standard fire generates no heat -- authentic). (The EMITTER's authored + // value is small and its 1e7 comes from the energy algebra -- see + // EMITTER.CPP.) + if (HeatModelActive()) + { + AddPendingHeat(heatCostToFire); + } if (getenv("BT_MECH_LOG")) { @@ -165,18 +174,39 @@ void // //############################################################################# -// CheckForJam -- the heat-scaled jam roll (binary @4bbfcc): -// p = 0.41 * currentTemperature / failureTemperature, -// clamped to [minJamChance, 1.0], rolled against a uniform random. -// A jammed launcher clears only on ResetToInitialState (mission reset). +// LiveFireEnabled -- the "sim live" novice lockout: owner mech -> +// Entity::playerLink -> BTPlayer::simLive, 0 only for NOVICE experience. A +// NULL player link reads LIVE (the unlinked dev mech / target dummy). +//############################################################################# // -// TWO interim gates stand in for deferred systems (the BT411 port hit exactly -// this): the authentic leading gate is LiveFireEnabled() -- the player -// experience switch (novice mode = no jams), which needs the player-link -// accessor wave -- and the binary's own `heatLoad <= 0` early-out is trivially -// true once the heat model runs. Until LiveFireEnabled lands, gate the roll -// on the sink actually being heat-degraded, so cold weapons fire reliably and -// jams appear exactly when the heat economy says the weapon is cooking. +Logical + ProjectileWeapon::LiveFireEnabled() +{ + Check(this); + + if (owner == NULL) + { + return True; + } + BTPlayer *player = Cast_Object(BTPlayer *, owner->GetPlayerLink()); + if (player == NULL) + { + return True; + } + return player->IsSimLive(); +} + +// +//############################################################################# +// CheckForJam -- the heat-scaled jam roll (binary @4bbfcc), AUTHENTIC form: +// novice never jams (LiveFireEnabled); a cold heat model never jams (heatLoad +// stays 0 with the model off -- UpdateHeatLoad only runs under the experience +// gate); otherwise +// p = 0.41 * currentTemperature / failureTemperature, +// clamped to [minJamChance, 1.0], rolled against the uniform random. Note the +// floor: even a cool launcher at veteran+ carries the authored minimum jam +// chance per shot (SRM6: 5%) -- authentic pod behavior. A jammed launcher +// clears only on ResetToInitialState (mission reset). //############################################################################# // Logical @@ -184,7 +214,11 @@ Logical { Check(this); - if (GetHeatState() < DegradationHeat) // interim LiveFireEnabled stand-in + if (!LiveFireEnabled()) + { + return False; + } + if (heatLoad <= 0.0f) { return False; } diff --git a/restoration/source410/BT/PROJWEAP.HPP b/restoration/source410/BT/PROJWEAP.HPP index f4f495e0..7ea259f7 100644 --- a/restoration/source410/BT/PROJWEAP.HPP +++ b/restoration/source410/BT/PROJWEAP.HPP @@ -80,6 +80,13 @@ Logical CheckForJam(); + // + // The "sim live" novice lockout (the owning BTPlayer's simLive + // experience flag): novice ballistics never jam. + // + Logical + LiveFireEnabled(); + public: typedef ProjectileWeapon__SubsystemResource SubsystemResource; diff --git a/restoration/source410/BT/RESERVR.CPP b/restoration/source410/BT/RESERVR.CPP index 46ce00a0..c200e1bc 100644 --- a/restoration/source410/BT/RESERVR.CPP +++ b/restoration/source410/BT/RESERVR.CPP @@ -1,16 +1,301 @@ +//===========================================================================// +// File: reservr.cpp // +// Project: BattleTech Brick: Mech subsystems // +// Contents: Reservoir -- the coolant store // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// + #include #pragma hdrstop + #if !defined(RESERVR_HPP) # include #endif + #if !defined(MECH_HPP) # include #endif -Derivation Reservoir::ClassDerivations(HeatSink::ClassDerivations, "Reservoir"); -Reservoir::SharedData Reservoir::DefaultData(Reservoir::ClassDerivations, Subsystem::MessageHandlers, Subsystem::AttributeIndex, Subsystem::StateCount); -Reservoir::Reservoir(Mech *owner, int subsystem_ID, SubsystemResource *r, SharedData &shared_data) - : HeatSink(owner, subsystem_ID, r, shared_data) -{ Check(owner); Check_Pointer(r); Check_Fpu(); } -Reservoir::~Reservoir() {} -Logical Reservoir::TestClass(Mech &) { return True; } -Logical Reservoir::TestInstance() const { return IsDerivedFrom(ClassDerivations); } + +#if !defined(MECHWEAP_HPP) +# include +#endif + +#include + +Derivation + Reservoir::ClassDerivations( + HeatSink::ClassDerivations, + "Reservoir" + ); + +Reservoir::SharedData + Reservoir::DefaultData( + Reservoir::ClassDerivations, + Subsystem::MessageHandlers, + Subsystem::AttributeIndex, + Subsystem::StateCount + ); + +// +//############################################################################# +// The coolant store (binary ctor @4af408): CoolantCapacity overlays the +// inherited HeatSink thermalCapacity slot; the charge starts full; the flush +// flow scale is zero (a reservoir never conducts like an ordinary sink). A +// master reservoir registers the CoolantSimulation performance. +// +// Deferred with the AggregateHeatSink wave: the authentic master path also +// attaches the reservoir into the central bank's radiator loop and rescales +// the capacity by 0.05 x the bank's heat-sink count (the central sink here is +// still a plain HeatSink, which has no count) -- until then the streamed +// capacity is used as-is (a larger tank than authentic; noted). +//############################################################################# +// +Reservoir::Reservoir( + Mech *owner, + int subsystem_ID, + SubsystemResource *r, + SharedData &shared_data +): + HeatSink(owner, subsystem_ID, r, shared_data) +{ + Check(owner); + Check_Pointer(r); + + reservoirAlarm.Initialize(2); + + squirtEfficiency = 0.5f; + thermalCapacity = r->coolantCapacity; + coolantSquirtMass = r->coolantSquirtMass; + coolantLevel = thermalCapacity; + coolantFlowScale = 0.0f; + injectAccumulator = 0.0f; + reservoirAlarm.SetLevel(0); + + if (getenv("BT_POWER_LOG")) + { + DEBUG_STREAM << "[resv] '" << GetName() + << "' capacity=" << thermalCapacity + << " squirtMass=" << coolantSquirtMass << endl << flush; + } + + if (owner->GetInstance() != Entity::ReplicantInstance) + { + SetPerformance(&Reservoir::CoolantSimulation); + } + + Check_Fpu(); +} + +Reservoir::~Reservoir() +{ +} + +Logical + Reservoir::TestClass(Mech &) +{ + return True; +} + +Logical + Reservoir::TestInstance() const +{ + return IsDerivedFrom(ClassDerivations); +} + +// +//############################################################################# +// DrawCoolant -- the SOURCE (binary @4af3b0): hand out up to coolantLevel of +// the requested amount and deduct it from the charge. +//############################################################################# +// +Scalar + Reservoir::DrawCoolant(Scalar requested) +{ + Check(this); + + Scalar supplied = 0.0f; + if (requested >= 0.0f) + { + supplied = requested; + if (coolantLevel < requested) + { + supplied = coolantLevel; + } + } + coolantLevel -= supplied; + return supplied; +} + +// +//############################################################################# +// CoolantSimulation -- the registered Performance (binary @4aef78): while +// injection is active, accumulate elapsed time and run the coolant +// distribution. The InjectCoolant COCKPIT BUTTON (the id-4 message handler +// that raises the alarm) joins with the cockpit-button message wave; the DEV +// hook BT_FORCE_FLUSH=1 raises it here so the flush machinery is exercisable +// headlessly (one press ~4 s after the sim starts ticking). +//############################################################################# +// +void + Reservoir::CoolantSimulation(Scalar time_slice) +{ + Check(this); + + { + static int forceFlush = -1; + if (forceFlush < 0) + { + forceFlush = (getenv("BT_FORCE_FLUSH") != NULL) ? 1 : 0; + } + if (forceFlush == 1) + { + static Scalar armAccum = 0.0f; + armAccum += time_slice; + if (armAccum >= 4.0f && reservoirAlarm.GetLevel() != 1 + && coolantLevel > 0.0f) + { + forceFlush = 2; + reservoirAlarm.SetLevel(1); + injectAccumulator = 0.0f; + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[resv] FLUSH ON (charge=" + << coolantLevel << ")" << endl << flush; + } + } + } + } + + if (reservoirAlarm.GetLevel() == 1) + { + injectAccumulator += time_slice; + InjectCoolant(time_slice); + } +} + +// +//############################################################################# +// InjectCoolant -- the coolant-flush distribution (binary @4aefa4). Walk the +// roster gathering the flush targets in the authentic pass order -- the +// condensers, then the weapons, then every heat sink, then the linked master +// (the duplicate visits are intentional weighting) -- and squirt +// (coolantSquirtMass x the target's coolantFlowScale x dt) into each, +// crediting a negative pending-heat chill for the moved mass. Squirts only +// ever leave the tank; the chill only ever cools. +//############################################################################# +// +void + Reservoir::InjectCoolant(Scalar time_slice) +{ + Check(this); + + if (fabs(coolantLevel) <= 1.0e-4f) // the tank is empty + { + return; + } + + enum { kMaxWork = 96 }; + HeatSink *work[kMaxWork]; + int workCount = 0; + Entity *own = (Entity *)owner; + int count = own->GetSubsystemCount(); + int i; + + for (i = 2; i < count && workCount < kMaxWork; ++i) + { + Subsystem *s = own->GetSubsystem(i); + if (s != NULL && s->IsDerivedFrom(Condenser::ClassDerivations)) + { + work[workCount++] = (HeatSink *)s; + } + } + for (i = 2; i < count && workCount < kMaxWork; ++i) + { + Subsystem *s = own->GetSubsystem(i); + if (s != NULL && s->IsDerivedFrom(MechWeapon::ClassDerivations)) + { + work[workCount++] = (HeatSink *)s; + } + } + for (i = 2; i < count && workCount < kMaxWork; ++i) + { + Subsystem *s = own->GetSubsystem(i); + if (s != NULL && s->IsDerivedFrom(HeatSink::ClassDerivations)) + { + work[workCount++] = (HeatSink *)s; + } + } + { + HeatSink *link = (HeatSink *)linkedSinks.Resolve(); + if (link != NULL && workCount < kMaxWork) + { + work[workCount++] = link; + } + } + + for (int w = 0; w < workCount; ++w) + { + if (fabs(coolantLevel) <= 1.0e-4f) // ran dry mid-pass + { + return; + } + HeatSink *sink = work[w]; + if (sink->coolantFlowScale == 0.0f) + { + continue; + } + + // + // squirt = -(squirtMass x flowScale x dt), clamped to [-coolantLevel, 0]. + // + Scalar move = -coolantSquirtMass + * sink->coolantFlowScale + * time_slice; + Scalar lo = -coolantLevel; + if (move < lo) move = lo; + if (move > 0.0f) move = 0.0f; + + // + // The heat delta riding the moved mass (computed BEFORE the level + // updates, as in the binary). + // + Scalar den = (fabs(sink->coolantLevel) > 1.0e-4f) + ? sink->coolantLevel + : sink->thermalCapacity; + Scalar moved = (move < 0.0f) ? -move : move; + Scalar fracSink = moved / den; + Scalar fracRes = moved / coolantLevel; + Scalar heatDelta = + sink->heatEnergy * fracSink + - heatEnergy * fracRes; + + coolantLevel += move; // the reservoir drains (move <= 0) + sink->coolantLevel -= move; // the sink gains + if (sink->coolantLevel >= 0.0f) + { + if (sink->coolantLevel > sink->thermalCapacity) + { + sink->coolantLevel = sink->thermalCapacity; + } + } + else + { + sink->coolantLevel = 0.0f; + } + + Scalar cap = sink->thermalMass * startingTemperature; + if (heatDelta > cap) + { + heatDelta = cap; + } + Scalar chill = -heatDelta; + if (chill > 0.0f) + { + chill = 0.0f; // the flush only ever COOLS + } + sink->pendingHeat += chill; + } +} diff --git a/restoration/source410/BT/RESERVR.HPP b/restoration/source410/BT/RESERVR.HPP index 0d422a27..2154cca8 100644 --- a/restoration/source410/BT/RESERVR.HPP +++ b/restoration/source410/BT/RESERVR.HPP @@ -1,19 +1,104 @@ +//===========================================================================// +// File: reservr.hpp // +// Project: BattleTech Brick: Mech subsystems // +// Contents: Reservoir -- the coolant store // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// + #if !defined(RESERVR_HPP) # define RESERVR_HPP + # if !defined(HEAT_HPP) # include # endif + +//##################### Forward Class Declarations ####################### class Mech; - struct Reservoir__SubsystemResource : public HeatSink::SubsystemResource {}; - class Reservoir : public HeatSink + +//########################################################################### +//#################### Reservoir Model Resource ######################### +//########################################################################### +// +// CoolantCapacity overlays the inherited HeatSink thermalCapacity slot at +// construction; CoolantSquirtMass is the per-second flush rate base. +// + struct Reservoir__SubsystemResource: + public HeatSink::SubsystemResource + { + Scalar coolantCapacity; + Scalar coolantSquirtMass; + }; + +//########################################################################### +//############################# Reservoir ############################### +//########################################################################### +// +// The coolant store: a HeatSink that holds the mech's coolant charge and +// "squirts" it into the other sinks on demand -- it is the DrawCoolant SOURCE +// (every sink's UpdateCoolant top-up resolves here through the virtual), and +// the manual coolant FLUSH (the InjectCoolant cockpit button) distributes +// charge into the condensers / weapons / heatables, crediting each a negative +// pending-heat chill. +// + class Reservoir: + public HeatSink { public: static Derivation ClassDerivations; static SharedData DefaultData; - static Logical TestClass(Mech &); - Logical TestInstance() const; + + static Logical + TestClass(Mech &); + Logical + TestInstance() const; + + public: typedef Reservoir__SubsystemResource SubsystemResource; - Reservoir(Mech *owner, int subsystem_ID, SubsystemResource *r, SharedData &shared_data = DefaultData); + + Reservoir( + Mech *owner, + int subsystem_ID, + SubsystemResource *r, + SharedData &shared_data = DefaultData + ); ~Reservoir(); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // The coolant-store interface. + // + public: + // + // The DrawCoolant SOURCE: hand out up to coolantLevel of the requested + // amount and deduct it from the charge. + // + virtual Scalar + DrawCoolant(Scalar requested); + + typedef void + (Reservoir::*Performance)(Scalar time_slice); + void + SetPerformance(Performance performance) + { + Check(this); + activePerformance = (Simulation::Performance)performance; + } + + void + CoolantSimulation(Scalar time_slice); + void + InjectCoolant(Scalar time_slice); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Local Data. The inject flag is the reservoir alarm's level (1 = flushing). + // + protected: + AlarmIndicator reservoirAlarm; + Scalar coolantSquirtMass; + Scalar injectAccumulator; + Scalar squirtEfficiency; }; + #endif