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:
co-authored by
Claude Opus 4.8
parent
16ad741282
commit
1356870e56
@@ -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 +
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)");
|
||||
};
|
||||
|
||||
//###########################################################################
|
||||
|
||||
@@ -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)");
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
{
|
||||
|
||||
@@ -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)");
|
||||
};
|
||||
|
||||
//###########################################################################
|
||||
|
||||
Reference in New Issue
Block a user