Files
BT411/game/reconstructed/heat.hpp
T
arcattackandClaude Opus 4.8 cbc5ff9532 Wire ToggleCooling (msg 3): restore the coolant on/off button + handler chain
Coolant priority (Lynx: weapon MFD -> Display -> Coolant) was unreachable: the
ToggleCooling handler (@004ad6f8, HeatableSubsystem table @0x50E41C id 3) was
undefined AND the handler chain skipped it -- PoweredSubsystem::GetMessageHandlers
chained straight to the Receiver root, bypassing HeatSink/HeatableSubsystem, so a
weapon's dispatch never saw id 3.

Reconstructed ToggleCooling from the disassembly (tools/disas2.py 0x4ad6f8): a
per-subsystem coolant on/off TOGGLE -- novice-locked (owner->BTPlayer->
roleClassIndex+0x274==0, the same guard as MoveValve), press-only, then flip
coolantAvailable(+0x134) 0<->1 and coolantFlowScale(+0x15C) 0.0f<->1.0f.  Cutting a
system's cooling frees the shared loop for the rest -- the emergent "coolant
priority" (the mechanism is a toggle, not a multi-level cycle).

- heat.hpp / heatfamily_reslice.cpp: HeatSink::ToggleCoolingMessageHandler (id 3)
  + HeatSink::GetMessageHandlers.  Registered at HeatSink (the abstract
  HeatableSubsystem never instantiates; every concrete heatable subsystem is a
  HeatSink, where the coolant fields live) -- identical coverage to the binary's
  base-table registration, no downcast.
- powersub.cpp + Condenser/Reservoir: chain their handler sets through
  HeatSink::GetMessageHandlers so id 3 reaches every heatable subsystem.
- mech4.cpp: BT_COOLTOGGLE_TEST scripted inject (dispatch id 3 to the first weapon)
  for verification.

Verified (BT_COOLTOGGLE_TEST + BT_COOL_LOG, expert egg): "[cool] PPC_1 ToggleCooling
reached" then coolant OFF(flow 0.0) <-> ON(flow 1.0) each press -- chain routes,
handler toggles, novice guard honored.  Both build/ + build-glass/ compile clean;
existing ids 4-8 unaffected (found before id 3).  NEXT: the #2 MFD button routing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 23:04:04 -05:00

688 lines
29 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);
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
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.4f
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);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 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