Files
TeslaRel410/restoration/source410/BT/HEAT.CPP
T
CydandClaude Fable 5 4168f1ad4d BT410 5.3.47: the whole authored watcher set is published -- cockpit renders in the pod
Ladder rungs, each run-verified on the pod rig: ReportLeak (HeatSink),
GeneratorOn (Generator -- member existed, publication missing),
ConfigureActivePress (MechSubsystem -- likewise), AmmoState +
FireCountdownStarted + the rest of the AmmoBin table.

THE PINNED RANGE IS NOW THE AUTHENTIC SHAPE.  Publishing ReportLeak on
HeatSink and ConfigureActivePress on MechSubsystem shifts the chain down two,
and MechWeapon's bridge to its binary-pinned PercentDone (0x12) collapses
from THREE guessed pads to ONE -- which is exactly the arithmetic the shipped
string pool predicts (MechSubsystem 1, HeatableSubsystem 3, HeatSink 6,
PoweredSubsystem 5).  Three pads of guesswork replaced by a real attribute
and a real base-class row.  HeatableSubsystem and Torso rebase onto
MechSubsystem::AttributeIndex; MechControlsMapper does not (it derives from
Subsystem directly).

Types were chosen deliberately this time, per the AudioWatcher families the
engine instantiates (Motion / Hinge / Scalar / StateIndicator): every *State
name resolves to a StateIndicator, ReportLeak is a Scalar.

RESULT: the pod run now gets past every authored watcher and DRAWS THE FULL
COCKPIT -- sensor cluster, myomers, cooling, the weapon panels, kills/deaths
-- with the board booted and audio running.  It then exits without flushing
its redirected stdout, so the exit reason is not yet known; next step is a
run with the redirect removed so the console is readable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 08:09:56 -05:00

1171 lines
33 KiB
C++

//===========================================================================//
// File: heat.cpp //
// Project: BattleTech Brick: Mech subsystems //
// Contents: HeatableSubsystem -- a MechSubsystem with a thermal state //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. //
// All Rights reserved worldwide //
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
//===========================================================================//
#include <bt.hpp>
#pragma hdrstop
#if !defined(HEAT_HPP)
# include <heat.hpp>
#endif
#if !defined(MECH_HPP)
# 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
// handlers / attributes of its own).
//#############################################################################
//
Derivation
HeatableSubsystem::ClassDerivations(
MechSubsystem::ClassDerivations,
"HeatableSubsystem"
);
//
//#############################################################################
// Attribute tables -- the cockpit binds these by NAME (1995 L4GAUGE.CFG
// spellings). Contiguous IDs; each set chains its parent so a weapon or
// sensor sees the whole heat/power family.
//#############################################################################
//
const HeatableSubsystem::IndexEntry
HeatableSubsystem::AttributePointers[]=
{
ATTRIBUTE_ENTRY(HeatableSubsystem, CurrentTemperature, currentTemperature),
ATTRIBUTE_ENTRY(HeatableSubsystem, HeatLoad, heatLoad),
ATTRIBUTE_ENTRY(HeatableSubsystem, DegradationTemperature, degradationTemperature),
ATTRIBUTE_ENTRY(HeatableSubsystem, FailureTemperature, failureTemperature)
};
HeatableSubsystem::AttributeIndexSet
HeatableSubsystem::AttributeIndex(
ELEMENTS(HeatableSubsystem::AttributePointers),
HeatableSubsystem::AttributePointers,
MechSubsystem::AttributeIndex
);
HeatableSubsystem::SharedData
HeatableSubsystem::DefaultData(
HeatableSubsystem::ClassDerivations,
Subsystem::MessageHandlers,
HeatableSubsystem::AttributeIndex,
Subsystem::StateCount
);
//
//#############################################################################
//#############################################################################
//
HeatableSubsystem::HeatableSubsystem(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
MechSubsystem(
owner,
subsystem_ID,
(MechSubsystem::SubsystemResource *)subsystem_resource,
shared_data
)
{
Check(owner);
Check_Pointer(subsystem_resource);
ResetToInitialState();
Check_Fpu();
}
//
//#############################################################################
//#############################################################################
//
HeatableSubsystem::~HeatableSubsystem()
{
}
//
//#############################################################################
//#############################################################################
//
void
HeatableSubsystem::ResetToInitialState()
{
Check(this);
currentTemperature = 300.0f;
heatLoad = 0.0f;
}
//
//#############################################################################
//#############################################################################
//
Logical
HeatableSubsystem::TestClass(Mech &)
{
return True;
}
Logical
HeatableSubsystem::TestInstance() const
{
return IsDerivedFrom(ClassDerivations);
}
//
//#############################################################################
// CreateStreamedSubsystem Model-load-time construction (thermal resource +
// damage-zone stream). Not yet reconstructed (see MECHSUB.NOTES.md).
//#############################################################################
//
int
HeatableSubsystem::CreateStreamedSubsystem(
NotationFile *,
const char *,
const char *,
SubsystemResource *,
NotationFile *,
const ResourceDirectories *,
int
)
{
Fail("HeatableSubsystem::CreateStreamedSubsystem -- heat.cpp not yet reconstructed");
return 0;
}
//###########################################################################
//############################## HeatSink ###############################
//###########################################################################
//
//#############################################################################
// Shared data support
//#############################################################################
//
Derivation
HeatSink::ClassDerivations(
HeatableSubsystem::ClassDerivations,
"HeatSink"
);
//
// The cockpit cooling toggle (id 3 -- the first subsystem-family message).
//
const HeatSink::HandlerEntry
HeatSink::MessageHandlerEntries[]=
{
MESSAGE_ENTRY(HeatSink, ToggleCooling)
};
HeatSink::MessageHandlerSet
HeatSink::MessageHandlers(
ELEMENTS(HeatSink::MessageHandlerEntries),
HeatSink::MessageHandlerEntries,
Subsystem::MessageHandlers
);
const HeatSink::IndexEntry
HeatSink::AttributePointers[]=
{
ATTRIBUTE_ENTRY(HeatSink, CoolantMass, coolantLevel),
ATTRIBUTE_ENTRY(HeatSink, CoolantCapacity, thermalCapacity),
ATTRIBUTE_ENTRY(HeatSink, CoolantMassLeakRate, coolantDraw),
ATTRIBUTE_ENTRY(HeatSink, CoolantAvailable, coolantAvailable),
ATTRIBUTE_ENTRY(HeatSink, ReportLeak, reportLeak)
};
HeatSink::AttributeIndexSet
HeatSink::AttributeIndex(
ELEMENTS(HeatSink::AttributePointers),
HeatSink::AttributePointers,
HeatableSubsystem::AttributeIndex
);
HeatSink::SharedData
HeatSink::DefaultData(
HeatSink::ClassDerivations,
HeatSink::MessageHandlers,
HeatSink::AttributeIndex,
Subsystem::StateCount
);
//
//#############################################################################
// The heat sink -- a thermal mass with a coolant loop. A master sink drives
// the per-frame thermal simulation.
//#############################################################################
//
HeatSink::HeatSink(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
HeatableSubsystem(owner, subsystem_ID, subsystem_resource, shared_data),
linkedSinks(),
heatAlarm(3)
{
Check(owner);
Check_Pointer(subsystem_resource);
currentTemperature = subsystem_resource->startingTemperature;
degradationTemperature = subsystem_resource->degradationTemperature;
failureTemperature = subsystem_resource->failureTemperature;
heatLoad = 0.0f;
coolantEfficiency = 0.5f;
thermalCapacity = 1.0f;
coolantLevel = thermalCapacity;
coolantDraw = 0.0f;
coolantAvailable = 1;
reportLeak = 0.0f; // staged: no leak-report model yet
coolantActive = 0;
startingTemperature = currentTemperature;
thermalConductance = subsystem_resource->thermalConductance;
heatFilter.SetSize(15, 0.0f);
filterDecay = 0.4f;
thermalMass = subsystem_resource->thermalMass;
heatEnergy = thermalMass * startingTemperature;
coolantFlowScale = 1.0f;
massScale = 1.0f;
pendingHeat = 0.0f;
radiatedHeat = 0.0f;
//
// Wire the heat-conduction link: the resource names the roster slot of the
// sink this one drains into (weapons/equipment -> the Condenser bank, the
// Condensers -> the central HeatSink). The shipped stream orders sinks so
// the target is already constructed; an unresolvable index leaves the sink
// standalone (its own thermal mass only).
//
{
Subsystem *linked = NULL;
if (subsystem_resource->linkedSinkIndex >= 0
&& subsystem_resource->linkedSinkIndex < owner->GetSubsystemCount())
{
linked = owner->GetSubsystem(subsystem_resource->linkedSinkIndex);
}
if (linked != NULL)
{
linkedSinks.Add(linked);
}
if (getenv("BT_POWER_LOG"))
{
DEBUG_STREAM << "[heat] '" << GetName()
<< "' linkedSinkIdx=" << subsystem_resource->linkedSinkIndex
<< " -> ";
if (linked != NULL)
{
DEBUG_STREAM << linked->GetName();
}
else
{
DEBUG_STREAM << "<none>";
}
DEBUG_STREAM << " thermalMass=" << thermalMass
<< " T0=" << currentTemperature
<< " degrade=" << degradationTemperature
<< " fail=" << failureTemperature
<< " conduct=" << thermalConductance << endl << flush;
}
}
//
// Install the per-frame thermal Performance (replicant copies are driven by
// console updates instead). Derived classes (PoweredSubsystem, Generator,
// the weapons) override with their own Performance in their ctors, each of
// which chains this step.
//
if (owner->GetInstance() != Entity::ReplicantInstance)
{
SetPerformance(&HeatSink::HeatSinkSimulation);
}
Check_Fpu();
}
//
//#############################################################################
//#############################################################################
//
HeatSink::~HeatSink()
{
}
//
//#############################################################################
//#############################################################################
//
void
HeatSink::ResetToInitialState(Logical /*powered*/)
{
Check(this);
currentTemperature = startingTemperature;
heatLoad = 0.0f;
coolantLevel = thermalCapacity;
coolantDraw = 0.0f;
coolantActive = 0;
heatEnergy = thermalMass * startingTemperature;
pendingHeat = 0.0f;
radiatedHeat = 0.0f;
heatAlarm.SetLevel(0);
}
//
//#############################################################################
// DeathReset -- the respawn restore: base heal + the full thermal re-init.
//#############################################################################
//
void
HeatSink::DeathReset(Logical full_reset)
{
Check(this);
MechSubsystem::DeathReset(full_reset);
ResetToInitialState(True);
//
// The cockpit cooling toggle is not part of ResetToInitialState; a sink
// switched OFF before death must respawn conducting again.
//
coolantAvailable = 1;
coolantFlowScale = 1.0f;
}
//
//#############################################################################
//#############################################################################
//
Logical
HeatSink::TestClass(Mech &)
{
return True;
}
Logical
HeatSink::TestInstance() const
{
return IsDerivedFrom(ClassDerivations);
}
//
//#############################################################################
// HeatModelActive -- the heat-model master switch (binary FUN_004ad7d4):
// owner mech -> Entity::playerLink -> BTPlayer::heatModelOn, ON only for
// veteran / expert experience. Standard mode has NO heat consequences --
// authentic, not a gap. A NULL player link (unlinked dev mech / target dummy)
// reads ON, matching the permissive dev-rig behavior; the binary derefs
// unguarded (a linked player always exists in pod missions).
//#############################################################################
//
Logical
HeatSink::HeatModelActive()
{
Check(this);
if (owner == NULL)
{
return True;
}
BTPlayer *player = Cast_Object(BTPlayer *, owner->GetPlayerLink());
if (player == NULL)
{
return True;
}
return player->IsHeatModelOn();
}
//
//#############################################################################
// HeatSinkSimulation -- the per-frame thermal step (binary @004ad924).
// Under the heat-model experience gate: absorb the pending heat into the
// thermal mass, recompute the temperature and the smoothed heat-load reading,
// conduct into the linked sink, and run the coolant draw. The degradation /
// failure alarm drive is OUTSIDE the gate (authentic -- with the model off the
// temperature never moves, so the alarm just holds Normal).
//#############################################################################
//
void
HeatSink::HeatSinkSimulation(Scalar time_slice)
{
Check(this);
if (HeatModelActive())
{
heatEnergy += pendingHeat;
currentTemperature = heatEnergy / thermalMass;
UpdateHeatLoad();
pendingHeat = 0.0f;
ConductHeat(time_slice);
}
if (HeatModelActive())
{
UpdateCoolant(time_slice);
}
//
// Drive the degradation / failure alarm.
//
if (currentTemperature > failureTemperature)
{
heatAlarm.SetLevel(FailureHeat);
}
else if (currentTemperature > degradationTemperature)
{
heatAlarm.SetLevel(DegradationHeat);
}
else
{
heatAlarm.SetLevel(NormalHeat);
}
//
// BT_HEAT_LOG: a 5-second per-sink census (temperature / coolant / load).
//
if (getenv("BT_HEAT_LOG"))
{
static Scalar censusAccum = 0.0f;
censusAccum += time_slice;
if (censusAccum >= 5.0f)
{
censusAccum = 0.0f;
DEBUG_STREAM << "[heat-t] " << GetName()
<< " T=" << currentTemperature
<< " cool=" << coolantLevel << "/" << thermalCapacity
<< " load=" << heatLoad << endl << flush;
}
}
Check_Fpu();
}
//
//#############################################################################
// UpdateHeatLoad -- recompute the radiated heat and feed the normalised sample
// through the 15-sample running-average filter to produce the smoothed
// heatLoad reading in the [0,1] band (binary @004ad7f0; the shaping constants
// are byte-verified from the image).
//#############################################################################
//
void
HeatSink::UpdateHeatLoad()
{
Check(this);
radiatedHeat = currentTemperature * coolantLevel;
Scalar sample = HeatLoadScale * radiatedHeat;
if (sample < HeatLoadMinimum)
{
sample = HeatLoadMinimum;
}
else if (sample > HeatLoadMaximum)
{
sample = HeatLoadMaximum;
}
heatFilter.Add(sample);
heatLoad = heatFilter.CalculateAverage();
}
//
//#############################################################################
// ConductHeat -- conduct heat into the linked sink, then rebalance coolant so
// the hotter side sheds load (binary @004ad8ac).
//#############################################################################
//
void
HeatSink::ConductHeat(Scalar time_slice)
{
Check(this);
HeatSink *other = (HeatSink *)linkedSinks.Resolve();
if (other != NULL && coolantAvailable != 0)
{
Scalar flow = ComputeHeatFlow(other, time_slice);
other->pendingHeat += flow;
pendingHeat -= flow;
BalanceCoolant(time_slice);
}
}
//
//#############################################################################
// BalanceCoolant -- move coolant between this sink and its linked sink so that
// the hotter side sheds load (binary @004ada94). Clamped on both ends so
// neither sink goes below empty or above its capacity.
//#############################################################################
//
void
HeatSink::BalanceCoolant(Scalar time_slice)
{
Check(this);
HeatSink *other = (HeatSink *)linkedSinks.Resolve();
if (other == NULL)
{
return;
}
Scalar spread = radiatedHeat - other->radiatedHeat;
if (spread < 0.0f)
{
spread = -spread;
}
if (spread <= HeatEqualizeEpsilon)
{
return;
}
Scalar delta = other->radiatedHeat / currentTemperature - coolantLevel;
//
// Clamp delta to +/- (thermalCapacity * dt).
//
Scalar limit = thermalCapacity * time_slice;
if (delta < -limit)
{
delta = -limit;
}
else if (delta > limit)
{
delta = limit;
}
//
// Clamp so this sink stays within [0, thermalCapacity].
//
Scalar hi = thermalCapacity - coolantLevel;
Scalar lo = -coolantLevel;
if (delta < lo) delta = lo;
else if (delta > hi) delta = hi;
//
// Clamp so the other sink stays within its own [0, thermalCapacity].
//
Scalar otherLo = -(other->thermalCapacity - other->coolantLevel);
if (delta < otherLo) delta = otherLo;
else if (delta > other->coolantLevel) delta = other->coolantLevel;
delta = coolantFlowScale * delta;
coolantLevel += delta;
other->coolantLevel -= delta;
}
//
//#############################################################################
// UpdateCoolant -- consume coolant proportional to the current load, request a
// top-up from the central cooling system, and update the draw state machine
// (binary @004adbf8). The draw scales with THIS subsystem's own structural
// damage: an undamaged subsystem leaks nothing (the coolant bars stay full on
// a pristine mech); the draw rises only as the sink itself takes battle
// damage.
//#############################################################################
//
void
HeatSink::UpdateCoolant(Scalar time_slice)
{
Check(this);
coolantDraw = GetSubsystemDamageLevel() * heatLoad;
if (coolantDraw < CoolantDrawFloor)
{
coolantDraw = 0.0f;
}
Scalar amount = coolantDraw * time_slice;
if (coolantLevel < amount)
{
amount = coolantLevel;
}
coolantLevel -= amount;
if (amount > CoolantDrawGate || amount < -CoolantDrawGate)
{
coolantLevel += DrawCoolant(amount);
}
//
// The draw state machine (hysteresis: ON above 0.003, OFF below 0.0025).
//
if (coolantActive == 0 && coolantDraw > CoolantActiveOn)
{
coolantActive = 1;
}
else if (coolantActive == 1 && coolantDraw < CoolantDrawFloor)
{
coolantActive = 0;
}
}
//
//#############################################################################
// ComputeHeatFlow -- conductive heat exchange between this sink and 'other'
// (binary @004ad9ec):
// tau = thermalMass / massScale
// denom = tau + other->thermalMass
// q = (currentTemperature*massScale
// - (other->heatEnergy + other->pendingHeat + heatEnergy) / denom)
// * tau
// * (1 - exp( -dt * thermalConductance
// * (coolantLevel / thermalCapacity)
// * coolantFlowScale / denom ))
//#############################################################################
//
Scalar
HeatSink::ComputeHeatFlow(HeatSink *other, Scalar time_slice)
{
Check(this);
Check(other);
Scalar tau = thermalMass / massScale;
Scalar denom = tau + other->thermalMass;
Scalar equilibrium =
currentTemperature * massScale
- (other->heatEnergy + other->pendingHeat + heatEnergy) / denom;
Scalar response = 1.0f - (Scalar)exp(
-time_slice * thermalConductance
* (coolantLevel / thermalCapacity)
* coolantFlowScale / denom
);
return equilibrium * tau * response;
}
//
//#############################################################################
// DrawCoolant -- ask the central cooling system for coolant and return how
// much was actually supplied. Base sinks supply nothing on their own; the
// Reservoir (the coolant store) overrides this as the source.
//#############################################################################
//
Scalar
HeatSink::DrawCoolant(Scalar)
{
Check(this);
return 0.0f;
}
//
//#############################################################################
// ToggleCoolingMessageHandler -- the cockpit cooling on/off switch (binary
// @004ad6f8, id 3): novice-locked, press-only; toggles the coolant supply and
// the conduction flow together (OFF = this sink stops conducting into the
// bank -- its heat strands until cooling is switched back on).
//#############################################################################
//
void
HeatSink::ToggleCoolingMessageHandler(ReceiverDataMessageOf<int> *message)
{
Check(this);
Check(message);
if (NoviceLockout())
{
return;
}
if (message->dataContents <= 0) // press only
{
return;
}
if (coolantAvailable != 0)
{
coolantAvailable = 0;
coolantFlowScale = 0.0f;
}
else
{
coolantAvailable = 1;
coolantFlowScale = 1.0f;
}
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[cool] '" << GetName()
<< "' cooling toggled -> " << coolantAvailable << endl << flush;
}
}
//###########################################################################
//############################# HeatWatcher #############################
//###########################################################################
Derivation
HeatWatcher::ClassDerivations(
MechSubsystem::ClassDerivations,
"HeatWatcher"
);
HeatWatcher::SharedData
HeatWatcher::DefaultData(
HeatWatcher::ClassDerivations,
Subsystem::MessageHandlers,
Subsystem::AttributeIndex,
Subsystem::StateCount
);
HeatWatcher::HeatWatcher(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
MechSubsystem(
owner,
subsystem_ID,
(MechSubsystem::SubsystemResource *)subsystem_resource,
shared_data
),
watchedLink(),
heatAlarm(3)
{
Check(owner);
Check_Pointer(subsystem_resource);
degradationTemperature = subsystem_resource->degradationTemperature;
failureTemperature = subsystem_resource->failureTemperature;
watchedSubsystem = subsystem_resource->watchedSubsystem;
//
// The master instance runs WatchSimulation per-frame; the install is
// deferred with that (staged) method.
//
Check_Fpu();
}
HeatWatcher::~HeatWatcher()
{
}
Logical
HeatWatcher::TestClass(Mech &)
{
return True;
}
Logical
HeatWatcher::TestInstance() const
{
return IsDerivedFrom(ClassDerivations);
}
void
HeatWatcher::ResetToInitialState(Logical /*powered*/)
{
Check(this);
heatAlarm.SetLevel(0);
}
//
//#############################################################################
// DeathReset -- respawn restore for the watcher family (AmmoBin chains it).
//#############################################################################
//
void
HeatWatcher::DeathReset(Logical full_reset)
{
Check(this);
MechSubsystem::DeathReset(full_reset);
ResetToInitialState(True);
}
//
// Per-frame: resolve the watched subsystem, read its temperature, drive the
// 3-level alarm. Not yet reconstructed.
//
void
HeatWatcher::WatchSimulation(Scalar)
{
Fail("HeatWatcher::WatchSimulation -- heat.cpp not yet reconstructed");
}
//###########################################################################
//############################## Condenser #############################
//###########################################################################
Derivation
Condenser::ClassDerivations(
HeatSink::ClassDerivations,
"Condenser"
);
//
// The cockpit condenser-valve button (the binary's Condenser handler table
// has exactly ONE entry: id 4, "MoveValve"). ToggleCooling (id 3) is
// inherited from the HeatSink chain.
//
const Condenser::HandlerEntry
Condenser::MessageHandlerEntries[]=
{
MESSAGE_ENTRY(Condenser, MoveValve)
};
Condenser::MessageHandlerSet
Condenser::MessageHandlers(
ELEMENTS(Condenser::MessageHandlerEntries),
Condenser::MessageHandlerEntries,
HeatSink::MessageHandlers
);
const Condenser::IndexEntry
Condenser::AttributePointers[]=
{
ATTRIBUTE_ENTRY(Condenser, ValveSetting, coolantFlowScale),
ATTRIBUTE_ENTRY(Condenser, CondenserState, condenserState)
};
Condenser::AttributeIndexSet
Condenser::AttributeIndex(
ELEMENTS(Condenser::AttributePointers),
Condenser::AttributePointers,
HeatSink::AttributeIndex
);
Condenser::SharedData
Condenser::DefaultData(
Condenser::ClassDerivations,
Condenser::MessageHandlers,
Condenser::AttributeIndex,
Subsystem::StateCount
);
Condenser::Condenser(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
HeatSink(owner, subsystem_ID, subsystem_resource, shared_data)
{
Check(owner);
Check_Pointer(subsystem_resource);
//
// The authentic ctor (binary @4ae568): the valve opens at 1, the
// refrigeration output rides the inherited massScale slot, and the
// condenser's own coolantFlowScale starts 0 -- the valve recompute
// (RecomputeValves: MoveValve presses, and once at mech spawn) assigns
// each condenser its share of the total valve opening.
//
valveState = 1;
coolantFlowScale = 0.0f;
refrigerationFactor = subsystem_resource->refrigerationFactor;
massScale = refrigerationFactor;
//
// Condenser number from the segment-name DIGIT suffix ("Condenser1" -> 1;
// the letter form -0x40 is the GENERATOR convention).
//
const char *name = GetName();
condenserNumber = name[strlen(name) - 1] - '0';
if (getenv("BT_POWER_LOG"))
{
DEBUG_STREAM << "[cond] '" << GetName()
<< "' refrigFactor=" << refrigerationFactor
<< " #" << condenserNumber << endl << flush;
}
//
// Install the per-frame refrigeration Performance.
//
if (owner->GetInstance() != Entity::ReplicantInstance)
{
SetPerformance(&Condenser::RefrigerationSimulation);
}
Check_Fpu();
}
Condenser::~Condenser()
{
}
Logical
Condenser::TestClass(Mech &)
{
return True;
}
Logical
Condenser::TestInstance() const
{
return IsDerivedFrom(ClassDerivations);
}
//
//#############################################################################
// RefrigerationSimulation -- the condenser's per-frame step (binary @4ae4d8):
// recompute the refrigeration output as (1 - own structural damage) *
// refrigerationFactor, clamped >= 1, into the inherited massScale slot -- an
// undamaged condenser behaves refrigerationFactor-times "hotter" in the
// conduction exchange, actively pumping heat toward the central bank and
// chilling itself below ambient -- then run the base HeatSink step.
//#############################################################################
//
void
Condenser::RefrigerationSimulation(Scalar time_slice)
{
Check(this);
massScale = (1.0f - GetSubsystemDamageLevel()) * refrigerationFactor;
if (massScale < 1.0f)
{
massScale = 1.0f;
}
HeatSink::HeatSinkSimulation(time_slice);
}
//
//#############################################################################
// MoveValveMessageHandler -- the cockpit condenser-valve button (binary
// @4ae464, id 4): novice-locked, press-only; cycles THIS condenser's valve
// 1 -> 5 -> 50 -> 0 -> 1, then recomputes every condenser's flow share.
//#############################################################################
//
void
Condenser::MoveValveMessageHandler(ReceiverDataMessageOf<int> *message)
{
Check(this);
Check(message);
if (NoviceLockout())
{
return;
}
if (message->dataContents <= 0) // press only
{
return;
}
switch (valveState)
{
case 0: valveState = 1; break;
case 1: valveState = 5; break;
case 5: valveState = 50; break;
case 50: valveState = 0; break;
default: return; // unknown -> no change, no recompute
}
RecomputeValves((Entity *)owner);
}
//
//#############################################################################
// RecomputeValves -- assign every condenser its coolant flow share
// (valve / sum-of-valves) after a valve move (binary @0049f788). All valves
// at the spawn default (1) yield equal shares. (The condenser-alarm change
// pulse joins with the gauge wave.)
//#############################################################################
//
void
Condenser::RecomputeValves(Entity *owner_mech)
{
Check(owner_mech);
int count = owner_mech->GetSubsystemCount();
int total = 0;
int i;
for (i = 2; i < count; ++i)
{
Subsystem *s = owner_mech->GetSubsystem(i);
if (s != NULL && s->IsDerivedFrom(Condenser::ClassDerivations))
{
total += ((Condenser *)s)->valveState;
}
}
for (i = 2; i < count; ++i)
{
Subsystem *s = owner_mech->GetSubsystem(i);
if (s != NULL && s->IsDerivedFrom(Condenser::ClassDerivations))
{
Condenser *condenser = (Condenser *)s;
condenser->coolantFlowScale = (total > 0)
? ((Scalar)condenser->valveState / (Scalar)total)
: 0.0f;
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[valve] '" << condenser->GetName()
<< "' valve=" << condenser->valveState
<< " flow=" << condenser->coolantFlowScale << endl;
}
}
}
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << flush;
}
}
//###########################################################################
//######################### AggregateHeatSink ###########################
//###########################################################################
Derivation
AggregateHeatSink::ClassDerivations(
HeatSink::ClassDerivations,
"HeatSinkBank"
);
AggregateHeatSink::SharedData
AggregateHeatSink::DefaultData(
AggregateHeatSink::ClassDerivations,
HeatSink::MessageHandlers,
HeatSink::AttributeIndex,
Subsystem::StateCount
);
//
//#############################################################################
// The heat-sink bank (binary ctor @4ae8d0): the aggregate count scales the
// conductance (0.1 x count, byte-verified), the ambient setpoint defaults
// 300 K (the mission's [mission] temperature overwrites it in the PlayerLink
// pass -- that override joins the Mech-PlayerLink wave), and a master runs
// the RADIATOR Performance instead of the base heat step.
//#############################################################################
//
AggregateHeatSink::AggregateHeatSink(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
HeatSink(owner, subsystem_ID, subsystem_resource, shared_data)
{
Check(owner);
Check_Pointer(subsystem_resource);
heatSinkCount = subsystem_resource->heatSinkCount;
ambientTemperature = 300.0f;
thermalConductance = 0.1f * (Scalar)heatSinkCount * thermalConductance;
if (getenv("BT_POWER_LOG"))
{
DEBUG_STREAM << "[bank] '" << GetName()
<< "' heatSinkCount=" << heatSinkCount
<< " conductance=" << thermalConductance << endl << flush;
}
if (owner->GetInstance() != Entity::ReplicantInstance)
{
SetPerformance(&AggregateHeatSink::RadiatorSimulation);
}
Check_Fpu();
}
AggregateHeatSink::~AggregateHeatSink()
{
}
Logical
AggregateHeatSink::TestClass(Mech &)
{
return True;
}
Logical
AggregateHeatSink::TestInstance() const
{
return IsDerivedFrom(ClassDerivations);
}
//
//#############################################################################
// RadiatorSimulation -- the bank's per-frame step (binary @4ae73c), replacing
// the base HeatSinkSimulation: under the heat-model gate, absorb + recompute
// the load, then the AMBIENT RADIATOR -- relax the bank toward the setpoint
// target (300 - (300 - ambient) x 3) with rate k = conductance x (1 - own
// damage) x (coolant/capacity) x flowScale / mass. This is the system's ONLY
// heat exit. Tail: top the bank's coolant up from the attached store via the
// DrawCoolant virtual (the reservoir-attach routing joins that wave; the base
// supplies 0 until then, a harmless no-op).
//#############################################################################
//
void
AggregateHeatSink::RadiatorSimulation(Scalar time_slice)
{
Check(this);
if (HeatModelActive())
{
heatEnergy += pendingHeat;
currentTemperature = heatEnergy / thermalMass;
UpdateHeatLoad();
pendingHeat = 0.0f;
Scalar target = 300.0f - (300.0f - ambientTemperature) * 3.0f;
Scalar decay = 1.0f - (Scalar)exp(
(double)(-(time_slice * thermalConductance
* (1.0f - GetSubsystemDamageLevel())
* (coolantLevel / thermalCapacity) * coolantFlowScale)
/ thermalMass)
);
Scalar shed = -((currentTemperature * massScale - target)
* thermalMass * decay);
pendingHeat += shed;
if (getenv("BT_HEAT_LOG"))
{
static Scalar bankAccum = 0.0f;
bankAccum += time_slice;
if (bankAccum >= 5.0f)
{
bankAccum = 0.0f;
DEBUG_STREAM << "[heat-t] " << GetName() << " (bank)"
<< " T=" << currentTemperature
<< " shed=" << shed
<< " cool=" << coolantLevel << "/" << thermalCapacity
<< " load=" << heatLoad << endl << flush;
}
}
}
//
// Coolant top-up from the attached store.
//
Scalar deficit = thermalCapacity - coolantLevel;
if (deficit > 1.0e-4f)
{
coolantLevel += DrawCoolant(deficit);
}
}