P7: byte-exact re-base of the CORE heat leaf (HeatSink/Condenser/Reservoir/Generator/Myomers)

The reconstruction modeled the binary's shared alarm/connection types with undersized
stand-ins, sliding every field above them low (the 72-byte auxScreenNumber gap). Fix the
foundational heat-leaf classes byte-exact + static_assert-lock them, from the ctor decomp:

Shared types corrected:
  * SubsystemConnection 4 -> 0xC  (binary link node FUN_004af9cf; FUN_00417ab4 derefs +8)
  * GaugeAlarm54 = 0x54           (real AlarmIndicator FUN_0041b9ec; STATUS level at +0x14,
                                   so subsystem+0x184 == heatAlarm+0x14 == GetLevel())
    WatcherGaugeAlarm now typedefs GaugeAlarm54 (Watcher branch locks stay valid).

Byte-exact + locked (ctor-verified):
  * HeatSink   heatEnergy@0x158 linkedSinks@0x164 heatAlarm@0x170 resource@0x1C4
               pendingHeat@0x1C8, sizeof 0x1D0  (@004adda0)
  * Condenser  valveState@0x1D0 condenserAlarm@0x1DC  (@004ae568)
  * Reservoir  reservoirAlarm@0x1D0 ... squirtEfficiency@0x22C, sizeof 0x230  (@4aef78)
  * Generator  stateAlarm@0x1FC, sizeof 0x250  (@004b225c)
  * Myomers    phantom moverConnection tail removed (fits 0x358)

Three systemic bug classes fixed (added to the checklist in CLAUDE.md / HARD_PROBLEMS.md):
  * alias field    - a subclass member re-declaring an inherited slot the ctor reuses
                     (Condenser refrigerationOutput==massScale@0x160; Reservoir
                     coolantCapacity==thermalCapacity@0x128) -> use the inherited name
  * alarm-interior - a value read at alarm+0x14 modeled as a separate member
                     (HeatSink heatState@0x184, Reservoir injectActive@0x1e4)
                     -> route to alarm.GetLevel()
  * phantom field  - a member past the object end (Generator shortFlag@0x25C is really
                     *(owner+0x190)+0x25c the msg-manager, @004b0efc; Myomers
                     moverConnection@0x110 a write-only base slot) -> remove it

Heat conduction now reads the REAL heatEnergy=1.34e7 (not garbage); combat DESTROYED-in-8,
0 crashes, heapcheck-clean through construction.

REMAINING (measured; a distinct larger task): making PoweredSubsystem byte-exact grows it
+0x98 and cascades into MechWeapon/Emitter/PPC/Sensor/Myomers -- all model the 0x54
AlarmIndicator with 4-byte ReconAlarm / 8-byte HeatAlarm stand-ins and are short +
phantom-tailed; retyping without byte-exacting them overflows the Emitter alloc (heap
corruption). PoweredSubsystem kept on HeatAlarm(8) stand-ins (marked) pending a
subsystem-tree ALARM UNIFICATION. See docs/HARD_PROBLEMS.md P7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-07 09:47:37 -05:00
co-authored by Claude Opus 4.8
parent 16ad741282
commit 1356870e56
11 changed files with 251 additions and 78 deletions
+26
View File
@@ -1283,6 +1283,32 @@ instantiate the subsystem). Cascade notes: re-basing a Watcher class off Heatabl
must redirect to the real base (`MechSubsystem::HandleMessage`, `HeatWatcher::ResetToInitialState`); a re-based
class's header needs a guarded `#include` of its new base's header (powersub.hpp now includes heatfamily_reslice.hpp).
**✅ HEAT-LEAF OBJECT-LAYOUT RE-BASE — the CORE heat leaf is now byte-exact (P7 core DONE; full status
`docs/HARD_PROBLEMS.md` §P7).** The OBJECT-layout analog of the resource-layout audit: the reconstruction modeled
the binary's shared alarm/connection types with UNDERSIZED stand-ins, sliding every field above them low (the
72-byte `PoweredSubsystem::auxScreenNumber` gap). FIXED byte-exact + `static_assert`-LOCKED: **`HeatSink`**
(heatEnergy@0x158/linkedSinks@0x164/heatAlarm@0x170/pendingHeat@0x1C8, sizeof 0x1D0), **`Condenser`** (0x230
region), **`Reservoir`** (sizeof 0x230), **`Generator`** (sizeof 0x250), **`Myomers`** (phantom tail removed).
The shared types are now correct: **`SubsystemConnection` = 0xC** (was 4; binary link node, `FUN_00417ab4`
two-level-derefs +8) and **`GaugeAlarm54` = 0x54** (the real `AlarmIndicator` `FUN_0041b9ec`; STATUS level at
**+0x14**, so `subsystem+0x184 == heatAlarm+0x14 == GetLevel()`; `WatcherGaugeAlarm` typedefs to it). Heat
conduction reads the REAL `heatEnergy=1.34e7`; combat DESTROYED-in-8, 0 crashes, heapcheck-clean through
construction. **THREE new systemic bug classes fixed (add to the checklist):** (1) an **alias field** = a subclass
member re-declaring an inherited slot the ctor reuses under a new name (Condenser `refrigerationOutput`==inherited
`massScale@0x160`; Reservoir `coolantCapacity`==`thermalCapacity@0x128`) → delete the member, use the inherited
name; (2) an **alarm-interior field** = a value the binary reads at `alarm+0x14` modeled as a separate member
(HeatSink `heatState@0x184`, Reservoir `injectActive@0x1e4`) → route to `alarm.GetLevel()`; (3) a **phantom field**
= a member at an offset PAST the object (Generator `shortFlag@0x25C` is really `*(owner+0x190)+0x25c` the msg-manager;
Myomers `moverConnection@0x110` a write-only base slot) → remove it, read the real source. **⚠ THE REMAINING SCOPE
(measured, larger than the heat leaf):** making `PoweredSubsystem` byte-exact grows it +0x98 (two 0x54 alarms) and
cascades into EVERY subclass — `MechWeapon`/`Emitter`/`PPC`/`Sensor`/`Myomers` ALL model the 0x54 `AlarmIndicator`
with 4-byte `ReconAlarm` (`AlarmIndicator`==`ReconAlarm`==4B!) or 8-byte `HeatAlarm` stand-ins and are short +
phantom-tailed; retyping PoweredSubsystem's alarms without also byte-exacting them overflows the Emitter alloc
(0x478) → heap corruption. So PoweredSubsystem stays on `HeatAlarm(8)` stand-ins (marked, powersub.hpp) pending a
**subsystem-tree ALARM UNIFICATION** (retype every stand-in to `GaugeAlarm54` + de-phantom each weapon/power class
vs its ctor + lock) — a distinct multi-class task. `runtime Check(sizeof<=alloc)` in a factory bridge does NOT fail
the build (it's a runtime assert); use a `static_assert` sizeof lock to catch alloc overflow at COMPILE time.
**✅ MechControlsMapper REVIVED — the real input-interpretation tick runs LIVE (gated `BT_REAL_CONTROLS=1`).**
The fully-reconstructed mapper family (mechmppr.cpp `MechControlsMapper` @004afbe0-@004b08c0; btl4mppr.cpp
`L4MechControlsMapper`/`MechThrustmasterMapper`/`MechRIOMapper` @004d11e8-@004d27f8) was already built +
+35
View File
@@ -237,3 +237,38 @@ point tagged `pre-heatleaf-rebase`. The `heat-leaf-layout-audit` workflow derive
MEASUREMENT TECHNIQUE (reusable): a `template<int N> struct HSOff;` + `friend struct XProbe;` +
`struct XProbe { HSOff<offsetof(Class, field)> a; ... };` -- the "uses undefined struct 'HSOff<NNN>'" build
error reports each field's COMPILED offset in decimal. Compare to the binary offset from the ctor decomp.
### P7 — STATUS: the CORE heat leaf is DONE + byte-exact; PoweredSubsystem + the weapon subtree are a larger follow-up
**✅ DONE this session (byte-exact, static_assert-LOCKED, combat+heat verified):** `HeatSink`,
`HeatableSubsystem`, `Condenser`, `Reservoir`, `Generator` (a HeatSink subclass, alloc 0x250) + `Myomers`
(phantom tail removed). The shared types are now correct: **`SubsystemConnection` = 0xC** (was 4; the binary
`FUN_004af9cf` link node, `FUN_00417ab4` two-level derefs +8) and **`GaugeAlarm54` = 0x54** (the real
`AlarmIndicator` from `FUN_0041b9ec`; its STATUS level is at **+0x14**, so `subsystem+0x184 == heatAlarm+0x14
== GetLevel()`; `WatcherGaugeAlarm` is now a typedef of it). Confirmed from the ctors: `HeatSink` heatEnergy@0x158,
linkedSinks@0x164, heatAlarm@0x170, resource@0x1C4, pendingHeat@0x1C8, sizeof **0x1D0** (ctor @004adda0);
`Condenser` valveState@0x1D0, condenserAlarm@0x1DC (ctor @004ae568); `Reservoir` reservoirAlarm@0x1D0,
sizeof **0x230** (raw @4aef78); `Generator` stateAlarm@0x1FC, sizeof **0x250** (ctor @004b225c). Heat conduction
now reads the REAL `heatEnergy=1.34e7` (not garbage); combat DESTROYED-in-8, 0 crashes, heapcheck-clean through
construction. `heatState`/`heatModelFlag`/`field_1d0` deleted (reads -> `heatAlarm.GetLevel()` / `coolantActive`);
several **alias-field bugs** fixed the same way (Condenser `refrigerationOutput`==inherited `massScale@0x160`;
Reservoir `coolantCapacity`==inherited `thermalCapacity@0x128`, `injectActive`==`reservoirAlarm.GetLevel()`) and
**phantom fields** removed (Generator `shortFlag@0x25C` -- really `*(owner+0x190)+0x25c`, the msg-manager, raw
@004b0efc; Myomers `moverConnection@0x110` -- a write-only base slot).
**◐ REMAINING (the newly-measured true scope -- a SYSTEMIC alarm-stand-in unification, NOT a heat-only fix):**
making **`PoweredSubsystem`** byte-exact (its two 0x54 alarms `electricalStateAlarm@0x264`/`modeAlarm@0x2B8`)
grows it **+0x98**, which cascades into EVERY subclass -- `MechWeapon`, `Emitter`, `PPC`, `Myomers`, `Sensor` --
and they ALL model the binary's 0x54 `AlarmIndicator` with 4-byte `ReconAlarm` (`weaponAlarm`) or 8-byte
`HeatAlarm` stand-ins, so they are themselves short AND carry phantom/duplicate/misplaced tail fields
(measured: `MechWeapon` 0x3C4 vs binary 0x3F0 with ~0x24 of phantom tail; `Emitter` 0x4B0 vs binary 0x478 with
a duplicate `outputVoltage`==`rechargeLevel@0x320` + ~0x64 of oversized own fields). Retyping PoweredSubsystem's
alarms WITHOUT also byte-exacting MechWeapon/Emitter/PPC overflowed the Emitter factory alloc (0x478) -> a heap
corruption during Emitter construction. So PoweredSubsystem was kept on `HeatAlarm(8)` stand-ins (marked in
powersub.hpp) and the whole tree still fits its allocs. **The real fix is a subsystem-tree ALARM UNIFICATION:
retype every binary-`AlarmIndicator` stand-in (ReconAlarm/HeatAlarm) to `GaugeAlarm54(0x54)` and de-phantom
each weapon/power class against its ctor** (MechWeapon @004b99a8, Emitter @004ba478, PPC, Sensor). Each class
needs: alarm retype + phantom/alias-field removal + a `static_assert` sizeof/offset lock vs its factory alloc.
This is a multi-class effort (a separate task, bigger than the heat leaf) -- the core heat leaf above is the
foundational prerequisite and is complete. Same techniques apply (ctor decomp -> offsets, alias/phantom checks,
`friend struct XLayoutCheck` locks). The aux-screen gauge reads + Sensor RadarPercent stay approximate until then.
+6 -1
View File
@@ -355,7 +355,12 @@ void
if (FUN_004ad7d4(this)) // heatable/online
{
src->heatLoad += seekRate * seekRate * dtScale * time_slice; // src+0x1c8
// Raw @004ba838:7544 feeds rate^2*dt into the RESOLVED SOURCE's pendingHeat
// (src+0x1c8). The VoltageSource overlay here is a compact shortcut reading
// offsets 0/4/8/0xc, NOT the real 0x1dc/0x1c8 -- inert in bring-up (src never
// resolves, guarded above). FOLLOW-UP (Emitter re-base tail): type src as the
// real HeatSink-derived source and write src->pendingHeat@0x1c8.
src->heatLoad += seekRate * seekRate * dtScale * time_slice;
}
}
+6 -6
View File
@@ -380,9 +380,9 @@ HeatSink::HeatSink(
linkedSinks(),
heatAlarm(3) // FUN_0041b9ec(...,3) -- 3 alarm levels
{
heatState = NormalHeat;
heatModelFlag = 1;
field_1d0 = 0.0f;
// heatState/heatModelFlag/field_1d0 deleted: the heat-state code lives INSIDE
// heatAlarm (+0x14 == subsystem+0x184, so heatAlarm.GetLevel()); heatModelFlag was
// a spurious duplicate of coolantActive@0x138; field_1d0 was a spurious tail slot.
Check(owner);
Check_Pointer(subsystem_resource);
@@ -801,11 +801,11 @@ LWord
{
LWord flags = HeatableSubsystem::GetStatusFlags(); // FUN_004ac144
if (coolantActive != 0) // this+0x184 (TODO: confirm offset)
if (heatAlarm.GetLevel() != 0) // this+0x184 (heat-state Normal/Deg/Fail)
{
flags |= 0x8;
}
if (/* heat model running */ this->heatModelFlag != 0 // this+0x138
if (coolantActive != 0 // this+0x138
&& HeatModelActive()) // FUN_004ad7d4
{
flags |= 0x4;
@@ -837,7 +837,7 @@ void
{
HeatableSubsystem::PrintState(); // FUN_004ac8c0
switch (coolantActive) // this+0x184 (heat-state field)
switch (heatAlarm.GetLevel()) // this+0x184 (heat-state field, inside heatAlarm)
{
case NormalHeat:
DebugStream << GetName() << " NormalHeat" << endl;
+84 -21
View File
@@ -79,8 +79,39 @@ class Mech;
// bodies call.
//
// A 3-/5-level degradation indicator (the original AlarmIndicator surface:
// SetLevel/GetLevel/Initialize). Backed by a GaugeAlarm in the shipped build.
// 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) { levelA = levelB = levels; level = 0; }
void Initialize(int levels) { levelA = levelB = levels; level = 0; }
void SetLevel(int n) { level = n; }
int GetLevel() const { return level; }
int Level() const { return level; }
protected:
// Interior mirrors FUN_0041b9ec: base GaugeAlarm header (+0x00), three count
// words at +0x0c/+0x10/+0x14, three sub-indicators at +0x18/+0x2c/+0x40. The
// STATUS level the binary reads (HeatSink+0x184 == alarm+0x14, GetStatusFlags
// @004add30 / PrintState @004ae050) is param_1[5] at +0x14, so `level` lands
// there -- a raw `subsystem+0x184` read now returns GetLevel().
char _hdr[0x0c]; // +0x00 base GaugeAlarm (vtable + FUN_004178cc header)
int levelA; // +0x0c param_1[3] (level count)
int levelB; // +0x10 param_1[4]
int level; // +0x14 param_1[5] -- the status level (Normal/Deg/Fail)
char _tail[0x54 - 0x18]; // +0x18 three sub-indicators to 0x54
};
// 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:
@@ -108,16 +139,26 @@ private:
// 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; }
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;
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
@@ -416,38 +457,51 @@ inline int
// Offsets are byte offsets into the shipped object.
//
public:
// --- temperature thresholds (read from resource at +0xE8/+0xEC) ---
Scalar degradationTemperature; // @0x118 resource +0xE8
Scalar failureTemperature; // @0x11C resource +0xEC
// 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 ---
// --- 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 (draw state machine)
int heatState; // @0x184 heat-state code (Normal/Degradation/Failure)
LWord heatModelFlag; // @0x138 heat-model running flag
int coolantActive; // @0x138 init 0 (heat-model running flag; GetStatusFlags bit 0x4)
// --- thermal parameters ---
// --- 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
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 ---
SubsystemConnection linkedSinks; // @0x164 connection to master/linked heat sink
HeatAlarm heatAlarm; // @0x170 3-level alarm (Normal/Degradation/Failure)
// --- 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
Scalar pendingHeat; // @0x1C8 init 0 (heat delta queued for next frame)
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
Scalar field_1d0; // @0x1D0 (master-sink scaling field; best-effort)
// 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");
};
//###########################################################################
@@ -520,11 +574,20 @@ inline int
// Local data.
//
public:
Scalar refrigerationOutput; // @0x160 (word 0x58) recomputed each frame
// 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
HeatAlarm condenserAlarm; // @0x1DC (word 0x77) 3 levels
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
+17 -13
View File
@@ -73,8 +73,8 @@ Condenser::Condenser(
// *this = &PTR_FUN_0050ed88;
valveState = 1; // this[0x74] @0x1D0
coolantFlowScale = 0.0f; // this[0x57] @0x15C (== "word57")
refrigerationOutput = subsystem_resource->refrigerationFactor; // this[0x58] = res +0xFC
refrigerationFactor = refrigerationOutput; // this[0x76] @0x1D8
massScale = subsystem_resource->refrigerationFactor; // this[0x58] @0x160 (refrigeration output reuses massScale)
refrigerationFactor = massScale; // this[0x76] @0x1D8
simulationFlags |= 0x8; // this[10] |= 8 (participates in heat model)
// condenserNumber = atoi(lastChar(name)):
@@ -123,12 +123,12 @@ void
Condenser::RefrigerationSimulation(Scalar time_slice) // FUN_004ae4d8
{
HeatSink *master = (HeatSink *)linkedSinks.Resolve(); // this[0x38]
refrigerationOutput =
massScale = // refrigeration output reuses massScale @0x160 (word 0x58)
(1.0f - master->heatEnergy) // _DAT_004ae530 = 1.0f ; master +0x158
* refrigerationFactor; // this[0x76]
if (refrigerationOutput < 1.0f) // _DAT_004ae530
if (massScale < 1.0f) // _DAT_004ae530
{
refrigerationOutput = 1.0f;
massScale = 1.0f;
}
HeatSink_Step(time_slice); // FUN_004ad748(this, time_slice)
}
@@ -449,12 +449,11 @@ Reservoir::Reservoir(
{
// *this = &PTR_LAB_0050ecd4;
squirtEfficiency = 0.5f; // this[0x8b] @0x22C
coolantCapacity = subsystem_resource->coolantCapacity; // this[0x4a] @0x128 = res +0xFC
thermalCapacity = subsystem_resource->coolantCapacity; // this[0x4a] @0x128 (coolantCapacity reuses thermalCapacity)
coolantSquirtMass = subsystem_resource->coolantSquirtMass; // this[0x89] @0x224 = res +0x100
coolantLevel = coolantCapacity; // this[0x4b] @0x12C
coolantLevel = thermalCapacity; // this[0x4b] @0x12C
coolantFlowScale = 0.0f; // this[0x57] @0x15C (== "word57")
reservoirAlarm.SetLevel(0); // FUN_0041bbd8(this+0x74, 0)
injectActive = 0; // this[0x8a] @0x228
reservoirAlarm.SetLevel(0); // FUN_0041bbd8(this+0x74, 0) -- inject flag = 0 (was redundant injectActive=0)
if ((subsystem_resource->subsystemFlags & SegmentCopyMask) == 0
&& (subsystem_resource->subsystemFlags & MasterHeatSinkFlag) != 0)
@@ -462,8 +461,13 @@ Reservoir::Reservoir(
SetPerformance(&Reservoir::CoolantSimulation); // this[7..9] = PTR 0050e6b4 -> @4aef78
HeatSink *link = (HeatSink *)linkedSinks.Resolve(); // FUN_00417ab4(this+0x59)
link->Attach(this); // (**(link+0x1d8+4))(link+0x1d8, this)
coolantCapacity = CoolantCapacityScale * link->field_1d0 * coolantCapacity; // scale by master
coolantLevel = coolantCapacity;
// The binary reads *(float*)(link+0x1d0) -- the master heat sink's first OWN
// field (HeatSink now ends exactly at 0x1d0, so this is the master subclass's
// own scale word). field_1d0 was deleted from HeatSink; mirror the raw byte
// read (the master's dynamic subclass isn't known statically here).
Scalar masterScale = *(Scalar *)((char *)link + 0x1d0);
thermalCapacity = CoolantCapacityScale * masterScale * thermalCapacity; // scale by master (coolantCapacity reuses thermalCapacity)
coolantLevel = thermalCapacity;
}
else
{
@@ -494,7 +498,7 @@ Logical
}
else
{
if (injectActive != 1 && currentTemperature > 0.0f) // _DAT_004aeedc, this+0x12c
if (reservoirAlarm.GetLevel() != 1 && currentTemperature > 0.0f) // injectActive == alarm level @0x1e4
{
reservoirAlarm.SetLevel(1);
}
@@ -512,7 +516,7 @@ Logical
void
Reservoir::CoolantSimulation(Scalar time_slice) // FUN_004aef78
{
if (injectActive == 1) // this+0x1e4
if (reservoirAlarm.GetLevel() == 1) // injectActive == alarm level @0x1e4
{
injectAccumulator += time_slice; // this+0x228
InjectCoolant(time_slice); // FUN_004aefa4
+23 -18
View File
@@ -72,17 +72,11 @@
};
static_assert(sizeof(WatchedConnection) == 0x0C, "Watcher connection must be 0xC to close the deficit");
struct WatcherGaugeAlarm // binary watcher alarm: 0x54 bytes (full GaugeAlarm)
{
int levelCount;
int level;
char _pad[0x54 - 2 * sizeof(int)];
WatcherGaugeAlarm(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; }
};
// 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");
//###########################################################################
@@ -275,14 +269,25 @@
);
public:
// HeatSink base ... then Reservoir adds:
HeatAlarm reservoirAlarm; // @0x1D0 (word 0x74) 2 levels
Scalar coolantCapacity; // @0x128 overlays HeatSink thermalCapacity
// 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
int injectActive; // @0x228 (word 0x8a) init 0
Scalar squirtEfficiency; // @0x22C (word 0x8b) init 0.5f
Scalar injectAccumulator; // @0x228 inject elapsed-time accumulator
// coolantLevel @0x12C (word 0x4b) init = capacity.
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)");
};
//###########################################################################
+10 -5
View File
@@ -143,9 +143,8 @@ Myomers::Myomers(
// (Subsystem flags @0x28 |= 8; optional graphic-state hook -- see header note)
// (WAVE 6 de-shim) the owner*/damageStructure shim backing fields are gone --
// their accessors return neutral defaults directly. Only moverConnection
// (used by ConnectToMover) remains a member.
moverConnection = -1; // @0x110 (detached)
// their accessors return neutral defaults directly. The mover handle lives at
// base+0x110 (not a Myomers own field); the coupling is inert, so no init here.
// Register the per-tick Performance. The binary ctor @004b8fec stores the
// MyomersSimulation pointer-to-member into activePerformance (this[7..9] =
@@ -481,9 +480,9 @@ void Myomers::ConnectToMover(SubsystemMessage &message)
int index = abs(message.value) - 1; // message+0xC
if (message.value < 1) {
MoverDetach(index); // owner mover vtable +0x3C
this->moverConnection /*@0x110*/ = -1;
/* base+0x110 = -1 (detached) -- inert, no base accessor yet */
} else {
this->moverConnection /*@0x110*/ = 0;
/* base+0x110 = 0 (attached) -- inert, no base accessor yet */
MoverAttach(index, &speedEffect); // owner mover vtable +0x38, feed @0x31C
// (original args: index,&speedEffect,this,10,9,0)
}
@@ -528,6 +527,12 @@ void Myomers::ToggleSeekVoltage()
//===========================================================================//
struct MyomersLayoutCheck
{
// OVERFLOW lock only: the PoweredSubsystem base is NOT byte-exact (its 0x54 alarms
// are modeled as HeatAlarm(8) pending the subsystem-tree alarm unification -- see
// powersub.hpp), so Myomers own fields don't land at their binary offsets yet. The
// phantom moverConnection tail was removed (it was a write-only base+0x110 shim), so
// the object fits the 0x358 factory alloc. Byte-exact offsets return with the P7
// weapon/power-subtree re-base.
static_assert(sizeof(Myomers) <= 0x358, "sizeof(Myomers) must fit the factory Memory::Allocate(0x358)");
};
+7 -1
View File
@@ -316,7 +316,11 @@ class Mech;
SegmentCopyMask = 0x0C, // (flags & 0xC): 0 == master, 4 == damaged copy
MasterHeatSinkFlag = 0x0100 // live-master segment bit
};
int moverConnection; // @0x110 mover attachment handle (-1 == detached; set by ConnectToMover)
// The mover attachment handle lives at BASE+0x110 (a MechSubsystem slot), NOT a
// Myomers own field -- declaring it here appended a 4-byte tail that overflowed the
// 0x358 alloc once the PoweredSubsystem base became byte-exact. The coupling is
// inert (ConnectToMover -> no-op MoverAttach), so the handle is write-only today;
// when the real mover feed is revived, write base+0x110 via a base accessor.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Local data. Offsets are byte offsets into the shipped object.
@@ -333,6 +337,8 @@ class Mech;
Scalar heatRangeSquared; // @0x34C ctor = heatRange*heatRange (degradation-curve denominator)
Scalar velocityEfficiency; // @0x350 resource +0x190
Scalar accelerationEfficiency; // @0x354 resource +0x194
friend struct MyomersLayoutCheck;
};
#endif
+10 -6
View File
@@ -358,11 +358,15 @@ Logical
{
HeatSink::HandleMessage(message); // FUN_004ad748
Generator *source = (Generator *)(/* owner subsystem-message manager */
BT_GetMessageManager((Mech *)owner)); // *(this[0x34]+0x190)
// Raw @004b0efc reads *(int*)(*(owner+0x190) + 0x25c) -- the message-manager's
// short-event flag (NOT a Generator field). The manager is null in bring-up
// (BT_GetMessageManager stub), so guard the deref; the real read resumes once
// the SubsystemMessageManager (0xBD3) roster lookup is wired.
SubsystemMessageManager *msgMgr = BT_GetMessageManager((Mech *)owner); // *(this[0x34]+0x190)
if (!IsDamaged() // FUN_004ac9c8 == 0
&& source->ShortFlag() != 0 // source+0x25c != 0
&& msgMgr != 0
&& *(LWord *)((char *)msgMgr + 0x25c) != 0 // message-manager short-event flag
&& message == 4)
{
ForceShortRecovery(); // FUN_004b11bc(this)
@@ -733,7 +737,7 @@ void
//
// Heat damage (HeatSink heat-state @0x184) throttles the drive.
//
switch (heatState) // this[0x61] @0x184
switch (heatAlarm.GetLevel()) // this+0x184 (heat-state inside heatAlarm@0x170+0x14)
{
case HeatSink::NormalHeat: // 0
powered = 1;
@@ -908,7 +912,7 @@ void
{
stateAlarm.SetLevel(GeneratorIdle); // SetLevel 1
}
if (heatState == HeatSink::FailureHeat) // this[0x61] @0x184 == 2
if (heatAlarm.GetLevel() == HeatSink::FailureHeat) // this+0x184 == 2
{
stateAlarm.SetLevel(GeneratorRecovered); // SetLevel 4
}
@@ -941,7 +945,7 @@ void
case GeneratorRecovered: // 4
outputVoltage = 0.0f;
if (heatState == HeatSink::NormalHeat) // this[0x61] == 0
if (heatAlarm.GetLevel() == HeatSink::NormalHeat) // this+0x184 == 0
{
if (generatorOn == 0) // this[0x75]
{
+27 -7
View File
@@ -200,13 +200,21 @@ class Generator;
// Offsets are byte offsets into the shipped object.
//
public:
SubsystemConnection voltageSource; // @0x1D0 reference to the powering Generator segment
SubsystemConnection voltageSource; // @0x1D0 0xC reference to the powering Generator segment
int auxScreenNumber; // @0x1DC resource +0x104
int auxScreenPlacement; // @0x1E0 resource +0x108
char auxScreenLabel[64]; // @0x1E4 resource +0x10C
char engScreenLabel[64]; // @0x224 resource +0x14C
HeatAlarm electricalStateAlarm; // @0x264 5-level (see ElectricalState); level @0x278
HeatAlarm modeAlarm; // @0x2B8 3-level (see ConnectMode); level @0x2CC
// ⚠ NOT YET BYTE-EXACT: the binary electricalStateAlarm/modeAlarm are 0x54-byte
// AlarmIndicators (level @+0x14). Retyping them to GaugeAlarm54 grows PoweredSubsystem
// +0x98, which cascades into EVERY subclass (MechWeapon/Emitter/PPC/Myomers/Sensor) --
// all of which ALSO model the binary alarm with 4-byte ReconAlarm / 8-byte HeatAlarm
// stand-ins and are themselves short + phantom-tailed. Making PoweredSubsystem
// byte-exact therefore requires the SYSTEMIC subsystem-tree alarm unification (P7
// follow-up, docs/HARD_PROBLEMS.md). Kept as HeatAlarm(8) here so the whole tree
// still fits its factory allocs; the aux-screen offsets are approximate until then.
HeatAlarm electricalStateAlarm; // @0x264 (binary; approx here) 5-level; level @0x278
HeatAlarm modeAlarm; // @0x2B8 (binary; approx here) 3-level; level @0x2CC
Scalar thermalResistivityCoefficient; // @0x30C resource +0x100
Scalar voltageScale; // @0x310 init 1.0f
Scalar startTime; // @0x314 resource +0x18C
@@ -398,8 +406,9 @@ class Generator;
GeneratorStateOf() { return stateAlarm.GetLevel(); } // source+0x210
Scalar
MeasuredVoltage() { return outputVoltage; } // source+0x1dc
LWord
ShortFlag() { return shortFlag; } // source+0x25c
// The generator's "short" state is its stateAlarm level (GeneratorShorted==3) +
// outputVoltage (IsSourceShorted @004b0b5c reads 0x210/0x1dc). There is NO
// separate shortFlag field on the Generator (it ends at 0x250 == stateAlarm end).
// SourceLevel == linked master sink's stored heat energy (this[0x38]+0x158).
Scalar
SourceLevel()
@@ -423,8 +432,19 @@ class Generator;
Scalar startTimer; // @0x1F0 init = startTime
Scalar shortRecoveryTime; // @0x1F4 resource +0x108
Scalar shortTimer; // @0x1F8 init = shortRecoveryTime
HeatAlarm stateAlarm; // @0x1FC 5-level (see GeneratorState); level @0x210
LWord shortFlag; // @0x25C short-detected flag
GaugeAlarm54 stateAlarm; // @0x1FC 0x54 alarm, 5-level (GeneratorState); level @0x210 -> ends 0x250
// stateAlarm is the LAST field (ctor @004b225c: FUN_0041b9ec(param_1+0x7f,5) at 0x1FC).
// The old shortFlag@0x25C was a phantom: HandleMessage @004b0efc reads *(owner+0x190)+0x25c
// (the message-manager's short-event flag), NOT a Generator field -- Generator ends at 0x250.
friend struct GeneratorLayoutCheck;
};
struct GeneratorLayoutCheck {
static_assert(offsetof(Generator, percentVoltageAvailable) == 0x1D0, "Generator::percentVoltageAvailable @0x1D0 (word 0x74)");
static_assert(offsetof(Generator, outputVoltage) == 0x1DC, "Generator::outputVoltage @0x1DC (word 0x77)");
static_assert(offsetof(Generator, stateAlarm) == 0x1FC, "Generator::stateAlarm @0x1FC (word 0x7f)");
static_assert(sizeof(Generator) == 0x250, "sizeof(Generator) 0x250 (factory alloc, stateAlarm is last)");
};
//###########################################################################