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 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"))
|
||||
{
|
||||
|
||||
@@ -19,8 +19,26 @@
|
||||
# include <mech.hpp>
|
||||
#endif
|
||||
|
||||
#if !defined(BTPLAYER_HPP)
|
||||
# include <btplayer.hpp>
|
||||
#endif
|
||||
|
||||
#include <math.h>
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"))
|
||||
{
|
||||
|
||||
@@ -27,6 +27,10 @@
|
||||
# include <random.hpp>
|
||||
#endif
|
||||
|
||||
#if !defined(BTPLAYER_HPP)
|
||||
# include <btplayer.hpp>
|
||||
#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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 <bt.hpp>
|
||||
#pragma hdrstop
|
||||
|
||||
#if !defined(RESERVR_HPP)
|
||||
# include <reservr.hpp>
|
||||
#endif
|
||||
|
||||
#if !defined(MECH_HPP)
|
||||
# include <mech.hpp>
|
||||
#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 <mechweap.hpp>
|
||||
#endif
|
||||
|
||||
#include <math.h>
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <heat.hpp>
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user