Files
BT411/game/reconstructed/heat.hpp
T
Joe DiPrimaandClaude Opus 5 e6103d02b2 #97: leak gauge reaches all THREE levels -- the normalisation was dropped with an x87 expression
Players: "you can get up to three traingles which drains really quickly" ...
"only seeing one level (lowest) right now" (Oracle), corroborated by Draco
("3 triple delta being the top level").

The display was never the problem: BitMapInverseWipe is already built with
frames=3 and stacks three segments at two levels each (0..6).  The LEVEL
computation was wrong.

Our Execute rounded the raw leak rate:
    level = round(value)
The binary's @004c5d08 does NOT.  Ghidra renders the round as a bare
`FUN_004dcd94()` because it drops the x87 expression feeding __ftol -- exactly
KB gotcha 19, which we already had written down.  The real prologue, disassembled:

    fild  dword ptr [ebp-0x14]    ; ST0 = (float)fullWidth   (frames*2 = 6)
    fmul  dword ptr [ebx+0xb4]    ; ST0 *= value             (CoolantMassLeakRate)
    fdiv  dword ptr [ebx+0xb0]    ; ST0 /= third             (full-scale divisor)
    call  0x4dcd94                ; level = round(ST0)

i.e. level = round(fullWidth * leakRate / fullScale).

Without the normalisation the gauge rounded a value that never exceeds ~1.0
(coolantDraw = zoneDamage * heatLoad, and heatLoad is clamp(0.002*T, 0, 1) --
constants re-verified from the image, including the 80-bit extended 0.002).  So
level could only ever be 0, or 1 via the >0.0025 floor: ONE triangle, always, no
matter how bad the leak.  Three of the six condensers being mis-lamped (#98) hid
how systematic this was.

Also corrected on the way:
  * `third` was declared int but the binary FDIVs it -- it is a float.  The two
    call sites passed *(int *)(subsystem+0x150), i.e. the BIT PATTERN of a float
    read from a RAW OFFSET into our own layout (the databinding trap): at +0x150
    our layout has filterDecay, not the binary's field.  Now resolved through a
    complete-type bridge, BTHeatSinkLeakFullScale.
  * filterDecay was initialised 0.4f; @004b8fec writes 0.15f (param_1[0x54] =
    0x3e19999a).  It is written-once-never-read elsewhere in the port, so the
    correction is behaviourally safe and it IS the gauge's full scale.

Verified live: Condenser6 at 10% zone damage -> draw 0.0358 -> level 1 (one
triangle); the same formula reaches all three at heavier damage, and the mapping
spans draw 0.0026 (half of one) to 0.15 (three full).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 13:54:13 -05:00

723 lines
31 KiB
C++

//===========================================================================//
// File: heat.hpp //
// Project: BattleTech Brick: Entity Manager //
// Contents: Heatable subsystems -- temperature model, heat sinks, condenser //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// --/--/95 ?? Initial coding. //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary (Ghidra pseudo-C in heat_cluster.c)
// cross-referenced with the surviving HEAT.TCP fragment and the MUNGA
// SUBSYSTM.HPP base interface. See heat.cpp for per-method @ADDR evidence.
//
// NOTE on the engine base: the WinTesla/MUNGA `Subsystem` (subsystm.h) is a
// thin damageable-simulation base -- it does NOT carry the heat/power virtual
// surface (ResetToInitialState / GetStatusFlags / HandleMessage / PrintState /
// Simulation) that the BT game layer assumed lived on "Subsystem". Because
// this family OWNS the subsystem base classes that the other BT families
// derive from, that virtual surface is (re)introduced on HeatableSubsystem
// here, and the per-class shared-data boilerplate follows the real engine
// GetClassDerivations()/GetMessageHandlers()/GetAttributeIndex()/StateCount
// idiom (cf. RP/VTVSUB.cpp), not the 3-arg form the raw decomp guessed.
//
#if !defined(HEAT_HPP)
# define HEAT_HPP
#if !defined(SUBSYSTM_HPP)
# include <subsystm.hpp>
#endif
#if !defined(MECHSUB_HPP)
# include <mechsub.hpp> // MechSubsystem -- HeatableSubsystem's base (re-base)
#endif
#include <affnmtrx.hpp> // AffineMatrix
#include <average.hpp> // AverageOf<T>
#include <scalar.hpp> // Scalar
#include <alarm.hpp> // GaugeAlarm
#include <SCHAIN.hpp> // SChainOf<Component*> (GaugeAlarm54 watcher sockets)
class Component; // fwd -- AddAudioWatcher(Component*) defined in heat.cpp
#include <string.h>
#include <stdlib.h>
//##################### Reconstruction type aliases #######################
#if !defined(BT_RECON_TYPE_ALIASES)
# define BT_RECON_TYPE_ALIASES
typedef AffineMatrix Matrix34; // 3x4 affine (AFFNMTRX.h)
typedef GaugeAlarm AlarmIndicator; // GAUGALRM.h
typedef AverageOf<Scalar> FilteredScalar; // 15-sample running average (AVERAGE.h)
#endif
// NOTE: `DebugStream` and `endl` (the ReconStream trace artifact) come from the
// shared foundation header (mechrecon.hpp, pulled in via bt.hpp/mech.hpp).
//##################### Segment flag bits (model resource) ###############
// The "is this a master / damaged-copy segment" test. The decomp rendered
// these as owner->GetSegmentFlags() reads; the flags actually live in the
// streamed subsystem resource (subsystemFlags).
#if !defined(BT_SEGMENT_FLAG_BITS)
# define BT_SEGMENT_FLAG_BITS
enum {
SegmentCopyMask = 0x0C, // (flags & 0xC): 0 == master, 4 == copy
MasterHeatSinkFlag = 0x100 // flags & 0x100 == participates in sim
};
#endif
//##################### Forward Class Declarations #######################
class Mech;
//###########################################################################
//################# Reconstruction helper value types ###################
//###########################################################################
//
// The raw decomp leaned on three engine helpers whose real APIs differ from
// what the pseudo-C assumed. Rather than bend the engine types, the family
// owns small faithful wrappers that expose exactly the surface the recovered
// bodies call.
//
// The binary AlarmIndicator (ctor FUN_0041b9ec) is a 0x54-byte object -- base
// GaugeAlarm + three sub-indicators, level at +0x04. Every subsystem alarm the
// binary builds via FUN_0041b9ec (heatAlarm, electricalState/mode/state/weapon/
// condenser/reservoir alarms) is this size; modeling it as 8 bytes is what slid the
// whole heat-leaf branch 0x4C short. Access is ALWAYS through the named API
// (SetLevel/GetLevel), never a raw internal offset, so the interior padding is
// immaterial -- only sizeof must be 0x54. This is the SAME layout as the Watcher
// branch's WatcherGaugeAlarm (heatfamily_reslice.hpp now typedefs to it), so the
// already-locked Watcher LayoutChecks stay valid.
class GaugeAlarm54
{
public:
GaugeAlarm54(int levels = 0)
: audioWatcherSocket(0), videoWatcherSocket(0), gaugeWatcherSocket(0)
{ levelA = levelB = levels; level = 0; }
void Initialize(int levels) { levelA = levelB = levels; level = 0; }
// StateIndicator::SetState semantics: oldState (levelB@0x10) := currentState
// BEFORE the change, so the audio watcher's StateChanged(oldState,newState) is
// correct -- this is what the inverse/STOP AudioStateTriggers key on (old_state ==
// firing/charging state) to StopNote a looping charge/sustain/loading sample when
// the weapon leaves that state. (In the binary levelB@0x10 IS oldState; the
// weapon's LevelCountB() reading @0x10 is that same authentic oldState.)
void SetLevel(int n) { levelB = level; if (n != level) { level = n; NotifyWatchers(); } }
int GetLevel() const { return level; }
int Level() const { return level; }
const int *LevelPtr() const { return &level; } // stable &currentState@+0x14 (the weapon pip's attr-0x1c source)
int LevelCountB() const { return levelB; } // +0x10 (the weapon update-record "reset" source)
// ReconAlarm/AlarmIndicator API aliases (callers that predate the retype use these):
void SetState(unsigned n){ SetLevel((int)n); }
unsigned GetState() const { return (unsigned)level; }
// The binary's 0x54 alarm (FUN_0041b9ec) IS a StateIndicator: its "three
// sub-indicators" @0x18/0x2c/0x40 are the audio/video/gauge watcher sockets,
// and level@0x14 is currentState. So an AudioStateWatcher can bind to any
// subsystem alarm BY NAME (GeneratorState/CondenserState/ReservoirState/...) and
// AddAudioWatcher registers on the +0x18 socket; SetLevel fires it on change.
// (Defined out-of-line in heat.cpp so this header stays free of Component/iterator.)
void AddAudioWatcher(Component *watcher);
void AddVideoWatcher(Component *watcher);
void AddGaugeWatcher(Component *watcher);
private:
void NotifyWatchers(); // fire audio/video/gauge watchers (empty sockets == no-op)
protected:
// Interior mirrors FUN_0041b9ec / StateIndicator: base header (+0x00, a Node in
// the binary), three state words @+0x0c/+0x10/+0x14 (== stateCount/oldState/
// currentState), three watcher sockets @+0x18/+0x2c/+0x40. level@+0x14 is the
// STATUS level the binary reads (HeatSink+0x184 == alarm+0x14). levelB@+0x10 is
// left as the weapon "reset source" (LevelCountB) -- SetLevel does NOT touch it,
// so weapon alarms are unchanged; only the watcher sockets are now real.
char _hdr[0x0c]; // +0x00 base header (vtable + FUN_004178cc header)
int levelA; // +0x0c stateCount
int levelB; // +0x10 oldState slot / weapon LevelCountB
int level; // +0x14 currentState (status level)
SChainOf<Component*> audioWatcherSocket; // +0x18
SChainOf<Component*> videoWatcherSocket; // +0x2c
SChainOf<Component*> gaugeWatcherSocket; // +0x40
};
// The 8-byte modeling type kept ONLY for HUD::statusAlarm (still on the old
// HUDLayoutCheck-locked layout; retyping HUD to 0x54 is a separate follow-up F1).
// All heat/power/weapon alarms now use GaugeAlarm54 above.
class HeatAlarm
{
public:
HeatAlarm(int levels = 0) { levelCount = levels; level = 0; }
void Initialize(int levels) { levelCount = levels; level = 0; }
void SetLevel(int n) { level = n; }
int GetLevel() const { return level; }
int Level() const { return level; }
protected:
int levelCount;
int level;
};
// The 15-sample running average behind heatLoad (Initialize/AddSample/Average).
class HeatFilter
{
public:
void Initialize(int count, Scalar value) { average.SetSize((size_t)count, value); }
void AddSample(Scalar v) { average.Add(v); }
Scalar Average() { return average.CalculateAverage(); }
private:
AverageOf<Scalar> average;
};
// A ref to a linked subsystem (heat-sink linkage / voltage source / watched
// subsystem). The original modelled this with a small SharedData-derived
// connection object exposing Add/Resolve/Clear.
// The binary connection is a 0xC-byte SharedData-derived link node (the raw
// resolver FUN_00417ab4 two-level-derefs it: `if(*(p+8)) return *(*(p+8)+8)`),
// NOT a bare 4-byte pointer. Modeling it as 4 bytes slid every field after an
// embedded connection low -- the direct cause of the heat-leaf 0x4C deficit
// (HeatSink::linkedSinks, PoweredSubsystem::voltageSource). Named access
// (Add/Resolve/Clear on `linked`) is unchanged and inert in bring-up (no plug
// resolves); the interior _reserved slots reconcile with FUN_00417ab4 when link
// resolution is wired. Same 0xC layout as heatfamily_reslice.hpp WatchedConnection.
class SubsystemConnection
{
public:
SubsystemConnection(int = 0) { linked = 0; _reserved[0] = _reserved[1] = 0; }
void Add(Subsystem *s) { linked = s; }
Subsystem* Resolve() const { return linked; }
void Clear() { linked = 0; }
protected:
Subsystem *linked; // +0x00
int _reserved[2]; // pad to the binary 0xC connection size
};
static_assert(sizeof(SubsystemConnection) == 0x0C, "subsystem connection must be 0xC (SharedData link node)");
// condenserNumber = atoi(lastChar(name)).
inline int
NameTrailingNumber(const char *name)
{
if (name == 0 || *name == '\0')
return 0;
return atoi(name + (strlen(name) - 1));
}
//###########################################################################
//################# HeatableSubsystem Model Resource ####################
//###########################################################################
//
// Extends the base damageable Subsystem resource. The heat-specific fields
// begin at +0xE4 -- i.e. immediately after MechSubsystem__SubsystemResource
// (model size 0xE4), NOT after Subsystem::SubsystemResource (0x30). The class
// hierarchy is HeatableSubsystem : MechSubsystem : Subsystem, so the resource
// MUST inherit MechSubsystem__SubsystemResource; inheriting Subsystem::
// SubsystemResource directly dropped the 0x30..0xE4 chunk and slid every heat
// field 0xB4 bytes low -- so thermalMass/heatSinkIndex read neighbouring floats
// (heatSinkIndex came back as 10.0f = 1/thermalMass, GetSegment -> OOB -> no link).
//
struct HeatableSubsystem__SubsystemResource:
public MechSubsystem__SubsystemResource
{
Scalar startingTemperature; // +0xE4 "StartingTemperature"
Scalar degradationTemperature; // +0xE8 "DegradationTemperature"
Scalar failureTemperature; // +0xEC "FailureTemperature"
Scalar thermalConductance; // +0xF0 "ThermalConductance"
Scalar thermalMass; // +0xF4 "ThermalMass"
int heatSinkIndex; // +0xF8 "HeatSink" (segment index, +2 bias)
};
//###########################################################################
//################# Condenser Model Resource ############################
//###########################################################################
//
// Condenser adds the refrigeration factor (+0xFC). Declared here because the
// Condenser class itself is declared in this header (its bodies are split
// between heat.cpp [best-effort] and heatfamily_reslice.cpp [completion]).
//
struct Condenser__SubsystemResource:
public HeatableSubsystem__SubsystemResource
{
Scalar refrigerationFactor; // +0xFC "RefrigerationFactor" (sentinel -1.0f)
};
//###########################################################################
//######################### HeatableSubsystem ###########################
//###########################################################################
//
// Abstract base for any subsystem that participates in the thermal model and
// the shared "damageable subsystem" virtual surface the BT families expect.
// (vtable @0050e210, destructor @004ac868.)
//
class HeatableSubsystem:
public MechSubsystem // was: public Subsystem. mechsub.hpp's MechSubsystem
// and this are overlapping reconstructions of the SAME
// binary cluster (vtable 0050e210); re-base + de-shadow so
// owner/simulationState/damageZone/the virtual surface come
// from the one real base, not uninitialised duplicates.
{
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Shared Data Support
//
public:
static Derivation *GetClassDerivations();
static SharedData DefaultData;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Test Class Support
//
public:
static Logical
TestClass(Mech&);
Logical
TestInstance() const;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Damageable-subsystem virtual surface (engine Subsystem lacks these; the
// heat/power families override them).
//
public:
// These OVERRIDE the MechSubsystem base slots (same vtable, matching sigs) --
// not new/parallel slots. ResetToInitialState takes (Logical powered) to match.
virtual void
ResetToInitialState(Logical powered);
// RESPAWN RE-ARM (Gitea #55). `Mech::Reset` sweeps every subsystem with
// the virtual `DeathReset` (mech4.cpp:1789), but only 7 weapon/ammo
// classes overrode it -- the whole heat/coolant/power family fell through
// to the empty `Subsystem::DeathReset` base (SUBSYSTM.h:161), so the
// respawn reset was a silent no-op for them and coolant/heat/generator
// state persisted from the previous life. `ResetToInitialState` is NOT
// virtual here, so each class that has its own body needs its own
// forwarder (a derived class with no body inherits the nearest one --
// e.g. Reservoir, the coolant tank, correctly uses HeatSink's).
void
DeathReset(int reset_command);
virtual LWord
GetStatusFlags();
virtual Logical
HandleMessage(int message);
virtual void
PrintState();
virtual void
Simulation(Scalar time_slice); // heat per-frame (not a MechSubsystem slot)
Logical
IsDamaged() { return (simulationState != 0) ? True : False; }
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Construction and Destruction
//
public:
typedef HeatableSubsystem__SubsystemResource SubsystemResource;
HeatableSubsystem(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data = DefaultData
);
~HeatableSubsystem();
static int
CreateStreamedSubsystem(
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes = 1
);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Common subsystem state
//
public:
// owner / simulationState(was "destroyed") / damageZone / hostEntity etc. are
// INHERITED from MechSubsystem -- re-declaring them shadowed the real base
// (engine ctor writes the base member; the duplicate is uninitialised + at a
// wrong compiled offset). flags/statusFlags -> engine Simulation::simulationFlags;
// statusBits -> ForceUpdate()/dirty word. Only the thermal fields are own.
Scalar currentTemperature; // @0x114 HEAT.TCP: init 300.0f
Scalar degradationTemperature; // @0x118
Scalar failureTemperature; // @0x11C
Scalar heatLoad; // @0x120 HEAT.TCP: init 0.0f (filtered load)
};
//###########################################################################
//############################# HeatSink ################################
//###########################################################################
//
// Active heat sink. Accumulates heat energy, conducts heat to a linked
// sink, draws coolant, and raises a degradation/failure alarm.
// (vtable @0050edc4, ctor @004adda0, dtor @004adfd4.)
//
class HeatSink:
public HeatableSubsystem
{
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Shared Data Support
//
public:
static Derivation *GetClassDerivations();
static SharedData DefaultData;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Attribute Support -- the cockpit gauge bindings resolved by name through
// Simulation::GetAttributePointer (see engine GAUGREND.cpp ParseAttribute):
// HeatSink/CoolantMass -> coolantLevel (@0x12C, live)
// HeatSink/CoolantCapacity -> thermalCapacity (@0x128)
// HeatSink/CurrentTemperature -> currentTemperature (@0x114, inherited)
// Condenser and Reservoir derive from HeatSink and define no override, so
// their DefaultData resolves Condenser6/CoolantMass/... to this same table.
// (SimulationState id 1 is preserved by chaining to the parent index.)
//
public:
enum {
CoolantMassAttributeID = HeatableSubsystem::NextAttributeID,
CoolantCapacityAttributeID,
CurrentTemperatureAttributeID,
// --- dense-append (gauge data-binding wave): the config binds these
// per condenser/heat-sink (L4GAUGE.CFG GenericHeatGauges1/2 + the Eng
// clusters). Ids stay contiguous so AttributeIndexSet::Build has no gap.
DegradationTemperatureAttributeID, // @0x118 constant amber warn line
FailureTemperatureAttributeID, // @0x11C constant max reference
NormalizedPressureAttributeID, // @0x120 heatLoad (smoothed radiated heat)
DegradationPressureAttributeID, // @0x124 coolantEfficiency
CoolantMassLeakRateAttributeID, // @0x130 coolantDraw (damage-driven leak)
HeatSinkAttributeID, // @0x164 linkedSinks (link to master sink)
ValveSettingAttributeID, // @0x15C coolantFlowScale (condenser valve slider @2)
// (AUDIO_FIDELITY F6) the binary heat attribute table (16-byte rows
// {id, name, off+1, pad} @0x50e438..0x50e4c8, ids 3..12) confirms
// EVERY existing binding above [T1] and adds ReportLeak (id 12) ->
// +0x138 = coolantActive, the INT leak hysteresis flag UpdateCoolant
// (@004adbf8 [0x4e]) drives 1 above draw 0.003 / 0 below 0.0025.
// The authored Logical match watchers (19 subsystems, ==1 Start /
// ==0 Stop of the looped 3-note leak sequence) fire on it directly.
ReportLeakAttributeID,
NextAttributeID
};
private:
static const IndexEntry AttributePointers[];
public:
static AttributeIndexSet& GetAttributeIndex();
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Heat alarm state (this+0x138, reported by PrintState @004ae050)
//
public:
enum HeatState {
NormalHeat = 0,
DegradationHeat = 1,
FailureHeat = 2
};
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Simulation Support
//
public:
typedef void
(HeatSink::*Performance)(Scalar time_slice);
void
SetPerformance(Performance performance)
{
Check(this);
activePerformance = (Simulation::Performance)performance;
}
void
HeatSinkSimulation(Scalar time_slice); // @004ad924 (Performance)
// FUN_004ad748 -- the base per-frame step (vtable slot 9), reused by the
// Condenser / aggregate sink in heatfamily_reslice.cpp.
void
HeatSink_Step(Scalar time_slice) { HeatSinkSimulation(time_slice); }
// FUN_004ad7d4 -- the heat-model master switch [T1]: the owning
// BTPlayer's +0x260 experience flag (mech+0x190 playerLink), ON only
// for veteran/expert experience (issue #2 wiring; the old permissive
// `return True` stub ran the heat model for every tier). Routed
// through the complete-type bridge in btplayer.cpp (databinding rule:
// never raw-read player offsets). NULL player reads ON [T3].
Logical
HeatModelActive()
{
extern int BTPlayerExperienceHeatModelOn(void *owner_mech); // btplayer.cpp (FUN_004ad7d4)
return BTPlayerExperienceHeatModelOn(owner) ? True : False;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Subsystem virtual overrides (slots on vtable @0050edc4)
//
public:
LWord
GetStatusFlags(); // slot 12, @004add30
Logical
HandleMessage(int message); // slot 8, @004add6c
void
ResetToInitialState(Logical powered); // slot 10, @004ad760
// RESPAWN RE-ARM (Gitea #55). `Mech::Reset` sweeps every subsystem with
// the virtual `DeathReset` (mech4.cpp:1789), but only 7 weapon/ammo
// classes overrode it -- the whole heat/coolant/power family fell through
// to the empty `Subsystem::DeathReset` base (SUBSYSTM.h:161), so the
// respawn reset was a silent no-op for them and coolant/heat/generator
// state persisted from the previous life. `ResetToInitialState` is NOT
// virtual here, so each class that has its own body needs its own
// forwarder (a derived class with no body inherits the nearest one --
// e.g. Reservoir, the coolant tank, correctly uses HeatSink's).
void
DeathReset(int reset_command);
void
PrintState(); // slot 13, @004ae050
// slot 14 (vtable+0x38): asks the central cooling system for coolant
// and returns how much was actually supplied (overridden by Reservoir).
virtual Scalar
DrawCoolant(Scalar requested);
// FUN: link another sink/reservoir into this one (Reservoir ctor).
void
Attach(HeatSink *other) { linkedSinks.Add(other); }
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Test Class Support
//
public:
static Logical
TestClass(Mech&);
Logical
TestInstance() const;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Construction and Destruction
//
public:
typedef HeatableSubsystem__SubsystemResource SubsystemResource;
HeatSink(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data = DefaultData
);
~HeatSink();
static int
CreateStreamedSubsystem(
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes = 1
);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Internal model helpers
//
public:
void
UpdateHeatLoad(); // @004ad7f0
void
ClearHeatFilter(); // @004ad884
void
ConductHeat(Scalar time_slice); // @004ad8ac
Scalar
ComputeHeatFlow( // @004ad9ec
HeatSink *other,
Scalar time_slice
);
void
BalanceCoolant( // @004ada94
Scalar time_slice
);
void
UpdateCoolant(Scalar time_slice); // @004adbf8
// ToggleCooling (msg 3, table @0x50E41C, @004ad6f8) -- the Eng-page
// "Coolant" button on every weapon/avionics. Per-subsystem coolant
// on/off TOGGLE (coolantAvailable + coolantFlowScale); cutting one
// system's cooling frees the shared loop for the rest (Lynx's "coolant
// priority"). Novice-locked (same guard as MoveValve). The binary
// registers it on the abstract HeatableSubsystem base; we register it on
// HeatSink -- identical coverage (every concrete heatable subsystem is a
// HeatSink) + the fields live here, so no downcast.
enum { ToggleCoolingMessageID = 3 };
void
ToggleCoolingMessageHandler(ReceiverDataMessageOf<int> *message); // @004ad6f8
static Receiver::MessageHandlerSet& GetMessageHandlers();
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Local data for the heat sink class.
// Offsets are byte offsets into the shipped object.
//
public:
// degradationTemperature/failureTemperature are BASE fields (HeatableSubsystem
// @0x118/0x11C, written here by the HeatSink ctor from resource +0xE8/+0xEC) --
// re-declaring them shadowed the base + slid every own field 8 bytes low.
// --- coolant model --- (ctor @004adda0 param_1[0x49..0x4e])
Scalar coolantEfficiency; // @0x124 init 0.5f
Scalar thermalCapacity; // @0x128 init 1.0f (coolant capacity / divisor)
Scalar coolantLevel; // @0x12C init = thermalCapacity (1.0f)
Scalar coolantDraw; // @0x130 init 0 (per-frame coolant demand)
int coolantAvailable; // @0x134 init 1 (coolant supply present)
int coolantActive; // @0x138 init 0 (heat-model running flag; GetStatusFlags bit 0x4)
// --- thermal parameters --- (ctor param_1[0x4f..0x58])
Scalar startingTemperature; // @0x13C resource +0xE4 (saved initial temp)
Scalar thermalConductance; // @0x140 resource +0xF0
HeatFilter heatFilter; // @0x144 15-sample running average (12 bytes -> 0x150)
Scalar filterDecay; // @0x150 init 0.15f (@004b8fec) -- ALSO the
// leak gauge's full-scale divisor (#97)
Scalar LeakGaugeFullScale() const { return filterDecay; }
Scalar thermalMass; // @0x154 resource +0xF4
Scalar heatEnergy; // @0x158 init = thermalMass * startingTemperature
Scalar coolantFlowScale; // @0x15C init 1.0f (== "word57")
Scalar massScale; // @0x160 init 1.0f
// --- linkage / display --- (ctor param_1[0x59]=linkedSinks, [0x5c]=heatAlarm)
SubsystemConnection linkedSinks; // @0x164 0xC connection to master/linked heat sink
GaugeAlarm54 heatAlarm; // @0x170 0x54 alarm; status level (Normal/Deg/Fail) at +0x14 == subsystem+0x184
SubsystemResource *resource; // @0x1C4 saved resource pointer (ctor param_1[0x71])
Scalar pendingHeat; // @0x1C8 init 0 (heat delta queued for next frame; ctor param_1[0x72])
Scalar radiatedHeat; // @0x1CC currentTemperature * coolantLevel
// object ends @0x1D0 (Reservoir/Condenser own fields begin here)
friend struct HeatSinkLayoutCheck;
};
// Byte-exact layout locks against the ctor @004adda0 (int* param_1[N] == byte N*4).
// Compile-time proof the heat leaf now matches the shipped object; never silently regresses.
struct HeatSinkLayoutCheck {
static_assert(offsetof(HeatSink, coolantActive) == 0x138, "HeatSink::coolantActive @0x138 (param_1[0x4e])");
static_assert(offsetof(HeatSink, heatEnergy) == 0x158, "HeatSink::heatEnergy @0x158 (param_1[0x56])");
static_assert(offsetof(HeatSink, linkedSinks) == 0x164, "HeatSink::linkedSinks @0x164 (param_1[0x59])");
static_assert(offsetof(HeatSink, heatAlarm) == 0x170, "HeatSink::heatAlarm @0x170 (param_1[0x5c])");
static_assert(offsetof(HeatSink, resource) == 0x1C4, "HeatSink::resource @0x1C4 (param_1[0x71])");
static_assert(offsetof(HeatSink, pendingHeat) == 0x1C8, "HeatSink::pendingHeat @0x1C8 (param_1[0x72])");
static_assert(offsetof(HeatSink, radiatedHeat) == 0x1CC, "HeatSink::radiatedHeat @0x1CC");
static_assert(sizeof(HeatSink) == 0x1D0, "sizeof(HeatSink) 0x1D0");
};
//###########################################################################
//############################# Condenser ###############################
//###########################################################################
//
// A HeatSink subclass that models a refrigeration output fighting the master
// heat sink's stored heat, carries a 3-position valve and a condenser number.
// (vtable @0050ed88, ctor @4ae568, classID CondenserClassID.) The full body
// set lives in heatfamily_reslice.cpp; heat.cpp carries a best-effort stub.
//
class Condenser:
public HeatSink
{
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Shared Data Support
//
public:
static Derivation *GetClassDerivations();
// task #13: the Condenser handler table @0x50E52C -- exactly ONE entry,
// {4, "MoveValve", @4ae464} (PE-verified). Per-receiver-class id space:
// id 4 to a condenser = MoveValve; to a weapon = SelectGeneratorA.
// Table + set are function-local statics in the accessor (the task #12
// static-init-order trap).
static Receiver::MessageHandlerSet& GetMessageHandlers();
static SharedData DefaultData;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Attribute Support -- audio binds an AudioStateWatcher to CondenserState;
// it resolves to condenserAlarm (@0x1DC, a 0x54 StateIndicator-compatible
// GaugeAlarm54). RefrigerationSimulation SetLevel's it (2/1/0), so the
// condenser cycling audio fires on the flow-change pulse. Chained to HeatSink.
//
public:
enum {
CondenserStateAttributeID = HeatSink::NextAttributeID,
NextAttributeID
};
private:
static const IndexEntry AttributePointers[];
public:
static AttributeIndexSet& GetAttributeIndex();
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Test Class Support
//
public:
static Logical
TestClass(Mech&);
Logical
TestInstance() const;
void
ResetToInitialState(Logical powered);
// RESPAWN RE-ARM (Gitea #55). `Mech::Reset` sweeps every subsystem with
// the virtual `DeathReset` (mech4.cpp:1789), but only 7 weapon/ammo
// classes overrode it -- the whole heat/coolant/power family fell through
// to the empty `Subsystem::DeathReset` base (SUBSYSTM.h:161), so the
// respawn reset was a silent no-op for them and coolant/heat/generator
// state persisted from the previous life. `ResetToInitialState` is NOT
// virtual here, so each class that has its own body needs its own
// forwarder (a derived class with no body inherits the nearest one --
// e.g. Reservoir, the coolant tank, correctly uses HeatSink's).
void
DeathReset(int reset_command);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Simulation / overrides
//
public:
void
RefrigerationSimulation(Scalar time_slice); // @4ae4d8 (vtable slot 9)
enum { MoveValveMessageID = 4 }; // table @0x50E52C [T1]
void
MoveValveMessageHandler( // @4ae464 (valve cycle 1->5->50->0)
ReceiverDataMessageOf<int> *message
);
virtual void
SetValveSetting(int setting) { valveState = setting; }
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Construction and Destruction
//
public:
typedef Condenser__SubsystemResource SubsystemResource;
Condenser(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data = DefaultData
);
~Condenser();
static int
CreateStreamedSubsystem(
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes = 1
);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Local data.
//
public:
// The per-frame refrigeration output reuses the inherited HeatSink massScale
// slot (word 0x58 @0x160) -- ctor @004ae568 writes param_1[0x58]=res+0xFC. It
// is NOT an own Condenser field (declaring it as one slid valveState off 0x1D0).
int valveState; // @0x1D0 (word 0x74) init 1 (MoveValve 0..2)
int condenserNumber; // @0x1D4 (word 0x75) last digit of name
Scalar refrigerationFactor; // @0x1D8 (word 0x76) from resource +0xFC
GaugeAlarm54 condenserAlarm; // @0x1DC (word 0x77) 0x54 alarm, 3 levels -> ends 0x230
friend struct CondenserLayoutCheck;
};
struct CondenserLayoutCheck {
static_assert(offsetof(Condenser, valveState) == 0x1D0, "Condenser::valveState @0x1D0 (word 0x74)");
static_assert(offsetof(Condenser, condenserAlarm)== 0x1DC, "Condenser::condenserAlarm @0x1DC (word 0x77)");
};
#endif