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>
1171 lines
33 KiB
C++
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);
|
|
}
|
|
}
|