Files
BT411/game/reconstructed/heatfamily_reslice.hpp
T
arcattackandClaude Fable 5 4ed2bbc293 Heat: authentic 1e7-unit heat + the ambient radiator land (task #9)
- Emitters: heatPortion closed form = heatCostToFire x 1e7 x (charge/seekV)^2
  (PPC 1.1e8 -> +632K on its own sink); the missing projectile/missile heat
  adds (raw -- resources author pre-scaled 1e7 units).
- The bank's AMBIENT RADIATOR @4ae73c reconstructed (the system's ONLY heat
  exit; conductance x 0.1 x HeatSinkCount -- _DAT_004ae974 float80 = 0.1) +
  the link-attach guard corrected (skip = the 0xBBE bank, not Condenser; the
  inversion blocked condenser->bank links and closed the system).
- Constant corrections (all byte-verified float80s): coolant epsilons
  0.0025/0.003/1e-4 (was a single 1e-4 serving three sites);
  CoolantCapacityScale 0.05 (was 1.738).
- Verified: heat flows + exits; max-rate autofire overheats weapons into the
  authentic heatLoad range-cutout (thermal spam unsustainable by design).
- KNOWN REMAINING: linked-sink routing scrambled (heat pools in Condenser1;
  authored map says PPC->C4/C6) -- the heat-stream offset audit, filed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 16:21:37 -05:00

393 lines
19 KiB
C++

//===========================================================================//
// File: heatfamily_reslice.hpp //
// Project: BattleTech Brick: Entity Manager //
// Contents: Re-slice of the heat/power family class bodies that fell OUTSIDE //
// the first heat decomp window (gap 0x4ae4d8 - 0x4afbe0). //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary (heatfamily_gap.c). This header is a
// companion to heat.hpp: it COMPLETES the Condenser class that heat.hpp/.cpp
// left best-effort, and adds the three other heat/power-family classes whose
// bodies live in the same gap:
//
// class primary vtable ctor classID base
// --------------- -------------- -------- ------- -----------------
// Condenser 0050ed88 @4ae568 0x0BBD HeatSink
// <HeatSink agg> 0050ed4c/ed50 @4ae8d0 0x0BBE HeatSink (name unconfirmed)
// HeatWatcher 0050ed10 @4aeb40 0x0BBF HeatableSubsystem
// Reservoir 0050ecd4/ecd8 @4af408 0x0BC0 HeatSink
//
// classID order (resource record +0x20) dovetails with the powersub family:
// ...0x0BBD Condenser, 0x0BBE <agg>, 0x0BBF HeatWatcher, 0x0BC0 Reservoir,
// 0x0BC1 Generator, 0x0BC2 PoweredSubsystem, ... 0x0BD5 PowerWatcher.
//
// HeatWatcher is the missing intermediate base of powersub's PowerWatcher
// (ctor @4b18a4 chains to the HeatWatcher base ctor @4aeb40). CLASSMAP.md
// previously listed PowerWatcher directly under Subsystem; the real chain is
// PowerWatcher : HeatWatcher : HeatableSubsystem : Subsystem.
//
// Field offsets are byte offsets into the shipped object (word index * 4).
// Hex float constants resolved from the CODE-section literal pool:
// _DAT_004ae530 = 1.0f _DAT_004ae708 = -1.0f _DAT_004ae89c = 300.0f
// _DAT_004ae8a0 = 3.0f _DAT_004ae8a4 = 1.0f _DAT_004ae8a8 = 0.0f
// _DAT_004ae8ac = 1.0e-4f _DAT_004aee28 = -1.0f _DAT_004aeedc = 0.0f
// _DAT_004af3a0 = 1.0e-4f _DAT_004af3a4 = 0.0f _DAT_004af3a8 = 0.0f
// _DAT_004af404 = 0.0f _DAT_004af694 = -1.0f
// _DAT_004ae974 ~= 1.738 (double 0x3FFBCC.., thermalMass-per-heat-sink scale)
// _DAT_004af518 ~= 0.x (double, coolant-capacity-per-master scale; low word)
//
#if !defined(HEATFAMILY_RESLICE_HPP)
# define HEATFAMILY_RESLICE_HPP
#if !defined(HEAT_HPP)
# include <heat.hpp> // HeatableSubsystem, HeatSink, Condenser base decls
#endif
//###########################################################################
//############### Watcher-branch-LOCAL layout types #####################
//###########################################################################
//
// BASE-CHAIN RE-BASE: the binary's HeatWatcher/PowerWatcher use a 0xC-byte
// connection (watchedLink) and a 0x54-byte gauge alarm (heatAlarm/watchdogAlarm
// -- a full GaugeAlarm), NOT the 4-byte SubsystemConnection / 8-byte HeatAlarm
// used on the HeatSink/PoweredSubsystem heat-leaf branch. These Watcher-LOCAL
// types close the 156-byte base deficit so a Watcher-branch leaf's own fields
// begin at 0x1D8 (== Torso::currentTwist, the gyro cross-link target), WITHOUT
// resizing the shared heat.hpp types the working heat leaves depend on. All
// access is through the API (Resolve/GetLevel), never a raw internal offset, so
// the interior padding layout is immaterial -- only the total sizeof must match.
//
struct WatchedConnection // binary watchedLink: 0xC bytes
{
Subsystem *linked;
char _pad[0x0C - sizeof(Subsystem*)];
WatchedConnection(int = 0) { linked = 0; }
void Add(Subsystem *s) { linked = s; }
Subsystem* Resolve() const { return linked; }
void Clear() { linked = 0; }
};
static_assert(sizeof(WatchedConnection) == 0x0C, "Watcher connection must be 0xC to close the deficit");
// The Watcher-branch 0x54 alarm is now the SAME type the heat-leaf branch uses
// (heat.hpp GaugeAlarm54): identical layout (levelCount@0, level@4, pad to 0x54),
// so the already-locked Watcher LayoutChecks stay valid, and there is one shared
// 0x54 alarm across the whole subsystem hierarchy.
typedef GaugeAlarm54 WatcherGaugeAlarm; // binary watcher alarm: 0x54 bytes
static_assert(sizeof(WatcherGaugeAlarm) == 0x54, "Watcher alarm must be 0x54 to close the deficit");
//###########################################################################
//############################# Condenser ###############################
//###########################################################################
//
// COMPLETION of heat.hpp's Condenser. It is NOT a passive subsystem (as
// heat.hpp guessed) -- it derives from HeatSink, models a refrigeration
// output that fights the master heat-sink's stored heat, carries a 3-way
// valve, and reports a condenser number parsed from its instance name.
//
// vtable @0050ed88 ctor @4ae568 dtor @4ae5fc classID CondenserClassID
//
// NOTE: the Condenser class + its resource record (Condenser__SubsystemResource)
// are now declared in heat.hpp (the completed HeatSink-derived form); this file
// only supplies the recovered method bodies for it.
// --- Condenser instance layout (HeatSink + the following) ---
// Scalar refrigerationOutput; // @0x160 (word 0x58) recomputed each frame
// 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
// Scalar (unused/flow @0x15C set 0)// @0x15C (word 0x57)
// AlarmIndicator condenserAlarm; // @0x1DC (word 0x77) 3 levels
//
// Methods recovered:
// @4ae4d8 RefrigerationSimulation(Scalar) -- vtable slot 9
// @4ae568 Condenser(owner,id,res,shared) -- ctor
// @4ae5fc ~Condenser(byte flags) -- vtable slot 0
// @4ae63c TestInstance() -- IsDerivedFrom 0x50e4fc
// @4ae658 CreateStreamedSubsystem(...) -- parses RefrigerationFactor
// @4afbe0 MoveValve(message) [best-effort] -- cycles valveState 0->1->2->0,
// drives owner HUD gauges
//###########################################################################
//########################### HeatWatcher ###############################
//###########################################################################
//
// Abstract monitor that watches ANOTHER subsystem's temperature and raises
// a 3-level degradation/failure alarm. Base class of powersub's
// PowerWatcher (the "PowerWatcher base ctor @4aeb40" called out in the task).
//
// vtable @0050ed10 ctor @4aeb40 dtor @4aebe8 classID 0x0BBF
//
// RESOURCE-LAYOUT FIX (RESOURCE_AUDIT.md root cause A): HeatWatcher is a light
// MechSubsystem, NOT a full HeatableSubsystem -- verified from the binary: ctor
// @4aeb40 -> FUN_004ac644 (MechSubsystem base ctor), parser @4aec54 -> FUN_004ac9ec
// (MechSubsystem parse) + only Degradation/Failure/Watched, record ENDS 0xF0. The
// resource MUST inherit MechSubsystem__SubsystemResource (ends 0xE4); inheriting
// HeatableSubsystem::SubsystemResource (0xFC) slid these 3 fields to 0xFC/0x100/0x104.
struct HeatWatcher__SubsystemResource:
public MechSubsystem__SubsystemResource
{
int watchedSubsystem; // +0xE4 "WatchedSubsystem" (index, +2 bias)
Scalar degradationTemperature; // +0xE8 "DegradationTemperature" (sentinel -1.0f)
Scalar failureTemperature; // +0xEC "FailureTemperature" (sentinel -1.0f)
}; // ends 0xF0
// Compile-time guard: the record is loaded verbatim at binary offsets, so these MUST hold.
static_assert(offsetof(HeatWatcher__SubsystemResource, watchedSubsystem) == 0xE4, "HeatWatcher watchedSubsystem must be at 0xE4");
static_assert(offsetof(HeatWatcher__SubsystemResource, degradationTemperature) == 0xE8, "HeatWatcher degradationTemperature must be at 0xE8");
static_assert(offsetof(HeatWatcher__SubsystemResource, failureTemperature) == 0xEC, "HeatWatcher failureTemperature must be at 0xEC");
static_assert(sizeof(HeatWatcher__SubsystemResource) == 0xF0, "HeatWatcher record must end at 0xF0");
class HeatWatcher:
public MechSubsystem // was HeatableSubsystem (approximation); real base is
// MechSubsystem (ctor @4aeb40 -> FUN_004ac644). Re-basing
// also un-shadows degradation/failureTemperature.
{
public:
static Derivation *GetClassDerivations(); // name "HeatWatcher"
static SharedData DefaultData;
typedef HeatWatcher__SubsystemResource SubsystemResource;
typedef void
(HeatWatcher::*Performance)(Scalar time_slice);
void
SetPerformance(Performance performance)
{
Check(this);
activePerformance = (Simulation::Performance)performance;
}
static Logical TestClass(Mech&);
Logical TestInstance() const; // @4aec38
void ResetToInitialState(Logical powered); // @4aea9c (slot 10)
HeatWatcher(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data = DefaultData
); // @4aeb40
~HeatWatcher(); // @4aebe8
// @4aeac4 -- the registered Performance: resolve the watched subsystem,
// read its currentTemperature (+0x114) and drive the alarm.
void WatchSimulation(Scalar time_slice);
static int CreateStreamedSubsystem( // @4aec54 (468 bytes)
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes = 1
);
public:
WatchedConnection watchedLink; // @0x114 (0xC) resolved subsystem -> 0x120
Scalar degradationTemperature; // @0x120 (word 0x48) resource +0xE8
Scalar failureTemperature; // @0x124 (word 0x49) resource +0xEC
int watchedSubsystem; // @0x128 (word 0x4a) resource +0xE4
WatcherGaugeAlarm heatAlarm; // @0x12C (0x54) 3 levels -> ends 0x180
}; // HeatWatcher own region 0x114..0x180 (0x6C); PowerWatcher own follows at 0x180.
// BASE-CHAIN RE-BASE locks: prove the Watcher base ends where the binary needs.
// (engine Subsystem 0xE0 + MechSubsystem 0x34 = 0x114 == HeatWatcher start.)
static_assert(offsetof(HeatWatcher, watchedLink) == 0x114, "HeatWatcher watchedLink must be at 0x114");
static_assert(offsetof(HeatWatcher, heatAlarm) == 0x12C, "HeatWatcher heatAlarm must be at 0x12C (0x54-byte alarm)");
static_assert(sizeof(HeatWatcher) == 0x180, "HeatWatcher must end at 0x180 (PowerWatcher own follows)");
//###########################################################################
//############################ Reservoir ################################
//###########################################################################
//
// Coolant reservoir: a HeatSink that stores a coolant charge and "squirts"
// it into the linked sinks on demand (the slot-14 DrawCoolant SOURCE).
//
// vtable @0050ecd4/ecd8 ctor @4af408 classID 0x0BC0
//
struct Reservoir__SubsystemResource:
public HeatableSubsystem::SubsystemResource
{
Scalar coolantCapacity; // +0xFC "CoolantCapacity" (sentinel -1.0f)
Scalar coolantSquirtMass; // +0x100 "CoolantSquirtMass" (sentinel -1.0f)
};
class Reservoir:
public HeatSink
{
public:
static Derivation *GetClassDerivations(); // name "Reservoir"
static SharedData DefaultData;
typedef Reservoir__SubsystemResource SubsystemResource;
typedef void
(Reservoir::*Performance)(Scalar time_slice);
void
SetPerformance(Performance performance)
{
Check(this);
activePerformance = (Simulation::Performance)performance;
}
static Logical TestClass(Mech&);
Logical TestInstance() const; // @4af564 -> IsDerivedFrom 0x50e650
Reservoir(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data = DefaultData
); // @4af408
// @4aef78 -- registered Performance (this[7..9] = &CoolantInjection via 0050e6b4)
void CoolantSimulation(Scalar time_slice);
// @4aefa4 (1019 bytes) -- distribute coolant ("squirt") across the
// linked heat-sink network, clamped per-sink and per-frame.
void InjectCoolant(Scalar time_slice);
// @4af3b0 -- slot-14 DrawCoolant override: hand out up to coolantLevel.
Scalar DrawCoolant(Scalar requested);
Logical HandleMessage(int message); // @4aee70 ("InjectCoolant" msg)
void PrintState(); // @4aeef8 (ReservoirState)
void GetState(/*...*/); // @4aef40 (slot 7 state query)
static int CreateStreamedSubsystem( // @4af580
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes = 1
);
public:
// HeatSink base (ends 0x1D0) ... then Reservoir adds (ctor @4aef.., dtor param_1[0x74..0x8b]):
// coolantCapacity is the inherited HeatSink thermalCapacity@0x128 (word 0x4a);
// "injectActive" is reservoirAlarm's level (@0x1e4 == alarm+0x14, raw @4aef78);
// both were redeclared members that over-sized the object past the 0x230 alloc.
GaugeAlarm54 reservoirAlarm; // @0x1D0 (word 0x74) 0x54 alarm -> ends 0x224; level@0x1e4 = inject flag
Scalar coolantSquirtMass; // @0x224 (word 0x89) resource +0x100
Scalar injectAccumulator; // @0x228 (word 0x8a) inject elapsed-time accumulator
Scalar squirtEfficiency; // @0x22C (word 0x8b) init 0.5f -> ends 0x230
// coolantLevel @0x12C (word 0x4b) init = capacity (inherited).
friend struct ReservoirLayoutCheck;
};
struct ReservoirLayoutCheck {
static_assert(offsetof(Reservoir, reservoirAlarm) == 0x1D0, "Reservoir::reservoirAlarm @0x1D0 (word 0x74)");
static_assert(offsetof(Reservoir, coolantSquirtMass) == 0x224, "Reservoir::coolantSquirtMass @0x224 (word 0x89)");
static_assert(offsetof(Reservoir, injectAccumulator) == 0x228, "Reservoir::injectAccumulator @0x228 (word 0x8a)");
static_assert(offsetof(Reservoir, squirtEfficiency) == 0x22C, "Reservoir::squirtEfficiency @0x22C (word 0x8b)");
static_assert(sizeof(Reservoir) == 0x230, "sizeof(Reservoir) 0x230 (factory alloc)");
};
//###########################################################################
//######################## AggregateHeatSink ############################
//###########################################################################
//
// The mech's aggregate / master heat-sink BANK (classID 0x0BBE). A HeatSink
// subclass that parses "HeatSinkCount" and holds a frozen "AmbientTemperature"
// setpoint (300 K); the numeric-R cockpit gauge binds HeatSink/AmbientTemperature
// to this class (L4GAUGE.CFG:4552, the last config-binding NULL).
//
// vtable @0050ed4c/ed50 ctor @4ae8d0 classID 0x0BBE own GUID 0x50e590
//
// DELIBERATE DEVIATION (databinding-safe, zero-regression): the shipped ctor
// installs Performance @4ae73c (relax toward the ambient setpoint), which reads
// a RAW inherited-base pointer self+0xE0 -> [+0x158] (the engine subsystem's
// status/damage model, FUN_0041de1c). That offset does NOT map in our compiled
// layout (garbage ptr -> AV/NaN, and this ctor runs for EVERY mech). Since
// ambientTemperature is a FROZEN constant (@4ae73c never writes it, so the gauge
// reads 300 either way), we keep the VERIFIED base HeatSinkSimulation the HeatSink
// base ctor already installs and do NOT reimplement @4ae73c / deref self+0xE0.
// The authentic relaxation model is DEFERRED (see the deferred item in §10 / P4).
//
struct AggregateHeatSink__SubsystemResource:
public HeatableSubsystem::SubsystemResource // ends 0xFC
{
int heatSinkCount; // +0xFC "HeatSinkCount" (parser @4ae9dc; ctor reads res+0xFC)
};
class AggregateHeatSink:
public HeatSink
{
public:
static Derivation *GetClassDerivations(); // own GUID 0x50e590, name "HeatSinkBank"
static SharedData DefaultData;
enum {
HeatSinkCountAttributeID = HeatSink::NextAttributeID, // @0x1D0
AmbientTemperatureAttributeID, // @0x1D4 FROZEN 300
NextAttributeID
};
private:
static const IndexEntry AttributePointers[];
public:
static AttributeIndexSet& GetAttributeIndex();
static Logical TestClass(Mech&);
Logical TestInstance() const; // @4ae9c0 -> IsDerivedFrom 0x50e590
typedef AggregateHeatSink__SubsystemResource SubsystemResource;
AggregateHeatSink(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data = DefaultData
); // @4ae8d0
~AggregateHeatSink();
// @4ae73c -- the authentic bank Performance (task #9): absorb + the
// AMBIENT RADIATOR (the system's only heat exit) + coolant top-up.
void RadiatorSimulation(Scalar time_slice);
static int CreateStreamedSubsystem( // @4ae9dc (offline content-build only)
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes = 1
);
public:
// HeatSink base ends 0x1D0; the aggregate adds:
int heatSinkCount; // @0x1D0 (word 0x74) res +0xFC
Scalar ambientTemperature; // @0x1D4 (word 0x75) = 300.0f, FROZEN
SubsystemConnection helper; // @0x1D8 (word 0x76) 0xC link node (vestigial/inert)
// object ends @0x1E4
friend struct AggregateHeatSinkLayoutCheck;
};
struct AggregateHeatSinkLayoutCheck {
static_assert(offsetof(AggregateHeatSink__SubsystemResource, heatSinkCount) == 0xFC, "res heatSinkCount @0xFC");
static_assert(offsetof(AggregateHeatSink, heatSinkCount) == 0x1D0, "heatSinkCount @0x1D0 (word 0x74)");
static_assert(offsetof(AggregateHeatSink, ambientTemperature) == 0x1D4, "ambientTemperature @0x1D4 (word 0x75)");
static_assert(offsetof(AggregateHeatSink, helper) == 0x1D8, "helper @0x1D8 (word 0x76)");
static_assert(sizeof(AggregateHeatSink) == 0x1E4, "sizeof 0x1E4 == factory alloc @9993");
};
//###########################################################################
//############# Engine collection-iterator helpers (NOT heat) ###########
//###########################################################################
//
// The thunks @4af9cf..@4afb96 are tiny ctor/dtor wrappers for the engine's
// generic collection/iterator library (vtables 0050eccc, 0050ecc4, 0050ecbc,
// 0050ec6c, 0050ec1c), used as locals inside Reservoir::InjectCoolant
// (@4aefa4). They are out of scope for the heat family and are documented
// in heatfamily_reslice.cpp only for completeness.
#endif