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>
723 lines
31 KiB
C++
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 ¤tState@+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
|