From 1356870e56273c177d307f0b2ecdb174d657d6cf Mon Sep 17 00:00:00 2001 From: arcattack Date: Tue, 7 Jul 2026 09:47:37 -0500 Subject: [PATCH] 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) --- CLAUDE.md | 26 ++++++ docs/HARD_PROBLEMS.md | 35 ++++++++ game/reconstructed/emitter.cpp | 7 +- game/reconstructed/heat.cpp | 12 +-- game/reconstructed/heat.hpp | 105 +++++++++++++++++----- game/reconstructed/heatfamily_reslice.cpp | 30 ++++--- game/reconstructed/heatfamily_reslice.hpp | 41 +++++---- game/reconstructed/myomers.cpp | 15 ++-- game/reconstructed/myomers.hpp | 8 +- game/reconstructed/powersub.cpp | 16 ++-- game/reconstructed/powersub.hpp | 34 +++++-- 11 files changed, 251 insertions(+), 78 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6b5678c..ab3311e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 + diff --git a/docs/HARD_PROBLEMS.md b/docs/HARD_PROBLEMS.md index 5cc22a4..c3a4d1e 100644 --- a/docs/HARD_PROBLEMS.md +++ b/docs/HARD_PROBLEMS.md @@ -237,3 +237,38 @@ point tagged `pre-heatleaf-rebase`. The `heat-leaf-layout-audit` workflow derive MEASUREMENT TECHNIQUE (reusable): a `template struct HSOff;` + `friend struct XProbe;` + `struct XProbe { HSOff a; ... };` -- the "uses undefined struct 'HSOff'" 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. diff --git a/game/reconstructed/emitter.cpp b/game/reconstructed/emitter.cpp index 13ec655..b3ed382 100644 --- a/game/reconstructed/emitter.cpp +++ b/game/reconstructed/emitter.cpp @@ -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; } } diff --git a/game/reconstructed/heat.cpp b/game/reconstructed/heat.cpp index 13684e0..2f67c94 100644 --- a/game/reconstructed/heat.cpp +++ b/game/reconstructed/heat.cpp @@ -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; diff --git a/game/reconstructed/heat.hpp b/game/reconstructed/heat.hpp index 3cb6c59..27fffdd 100644 --- a/game/reconstructed/heat.hpp +++ b/game/reconstructed/heat.hpp @@ -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 diff --git a/game/reconstructed/heatfamily_reslice.cpp b/game/reconstructed/heatfamily_reslice.cpp index d8629d0..1915999 100644 --- a/game/reconstructed/heatfamily_reslice.cpp +++ b/game/reconstructed/heatfamily_reslice.cpp @@ -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 diff --git a/game/reconstructed/heatfamily_reslice.hpp b/game/reconstructed/heatfamily_reslice.hpp index 5a59868..e5e24ee 100644 --- a/game/reconstructed/heatfamily_reslice.hpp +++ b/game/reconstructed/heatfamily_reslice.hpp @@ -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)"); }; //########################################################################### diff --git a/game/reconstructed/myomers.cpp b/game/reconstructed/myomers.cpp index 595fc91..2481bf0 100644 --- a/game/reconstructed/myomers.cpp +++ b/game/reconstructed/myomers.cpp @@ -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)"); }; diff --git a/game/reconstructed/myomers.hpp b/game/reconstructed/myomers.hpp index 66960fc..e797c89 100644 --- a/game/reconstructed/myomers.hpp +++ b/game/reconstructed/myomers.hpp @@ -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 diff --git a/game/reconstructed/powersub.cpp b/game/reconstructed/powersub.cpp index a6e607f..a7105bd 100644 --- a/game/reconstructed/powersub.cpp +++ b/game/reconstructed/powersub.cpp @@ -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] { diff --git a/game/reconstructed/powersub.hpp b/game/reconstructed/powersub.hpp index 52b1298..bf4c12e 100644 --- a/game/reconstructed/powersub.hpp +++ b/game/reconstructed/powersub.hpp @@ -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)"); }; //###########################################################################