P7: subsystem-tree alarm unification -- the whole PoweredSubsystem weapon/power subtree is byte-exact

The reconstruction modeled the binary's 0x54 AlarmIndicator (FUN_0041b9ec) with undersized
stand-ins (AlarmIndicator==ReconAlarm==4B; HeatAlarm==8B) across the entire subsystem tree, so
every field above an alarm sat at the wrong compiled offset.  Retype every such stand-in to
GaugeAlarm54(0x54) and de-phantom each class against its ctor, so the whole PoweredSubsystem
subtree becomes byte-exact.  An 8-agent read-only decomp-mapping workflow decoded every ctor
first; then hands-on implementation.  static_assert-locked chain (verified vs the raw ctors):

  HeatSink 0x1D0
   -> PoweredSubsystem 0x31C   retype electricalStateAlarm@0x264 + modeAlarm@0x2B8   @004b0f74
   -> MechWeapon      0x3F0   retype weaponAlarm@0x350; delete 5 phantom tail fields  @004b99a8
   -> Emitter         0x478   delete outputVoltage/beamLengthRatio/firingArmed aliases
                              + beamHit*/beamColor/beamHitData/energyRampTime phantoms;
                              retype beamOrientation EulerAngles->Quaternion(16B)       @004bb120
   -> PPC             0x478   (no own fields)                                           @004bb888
  PoweredSubsystem -> Sensor  0x328   (no alarm; 3 own fields)                          @004b1d18
  PoweredSubsystem -> Myomers 0x358   (no alarm)                                        @004b8fec

New systemic bug-class instances fixed (added to the CLAUDE.md checklist):
  * alias field    - Emitter outputVoltage==inherited rechargeLevel@0x320; beamLengthRatio==
                     beamScale.z@0x434; firingArmed==inherited useConfiguredPip@0x3E0
  * phantom field  - MechWeapon segmentReference/pipSegment/hasTarget/targetPoint/muzzlePoint
                     (past 0x3F0); Emitter beamHitPoint/beamImpact/beamImpactScalar/beamColor/
                     beamHitData/energyRampTime (binary writes inherited damageData/voltageScale
                     or the value is a method local)

Non-layout fixes required in the same wave:
  * outputVoltage->rechargeLevel also in the compiled GAUSS.CPP:74/93 (external readers of the
    removed Emitter field -- must grep EVERY TU, not just the class's own .cpp)
  * MechWeapon::GetMuzzlePoint reimplemented faithfully (removed muzzlePoint collided with
    Emitter's own fields at 0x3F0) via a BTResolveWeaponMuzzle void* bridge in mech4.cpp,
    resolving the weapon's mount segment (inherited this+0xdc) through the owner segment table
  * DetachFromVoltageSource fixed to set electricalStateAlarm not modeAlarm (raw @004b0e30
    writes the 0x264 alarm)

The vehicleSubSystems aux-screen gauge raw reads (btl4gau2.cpp:868/952 at subsystem+0x2b8/+0x278)
and the Sensor RadarPercent path now read the CORRECT byte offsets (garbage under the short layout).

Verified: combat DESTROYED-in-8, 28 shots, 0 crashes, heat heatEnergy=1.34e7, every static_assert
lock passes, heapcheck-clean through construction (the phase the isolated PoweredSubsystem retype
had overflowed).  LESSON: a factory-bridge runtime Check(sizeof<=alloc) does NOT fail the build --
only a static_assert sizeof lock catches alloc overflow at compile time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-07 11:49:58 -05:00
co-authored by Claude Opus 4.8
parent 1356870e56
commit 9d82be46a1
13 changed files with 235 additions and 140 deletions
+34 -13
View File
@@ -1295,19 +1295,40 @@ two-level-derefs +8) and **`GaugeAlarm54` = 0x54** (the real `AlarmIndicator` `F
conduction reads the REAL `heatEnergy=1.34e7`; combat DESTROYED-in-8, 0 crashes, heapcheck-clean through 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 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 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 `massScale@0x160`; Reservoir `coolantCapacity`==`thermalCapacity@0x128`; Emitter `outputVoltage`==inherited
name; (2) an **alarm-interior field** = a value the binary reads at `alarm+0x14` modeled as a separate member `rechargeLevel@0x320`; Emitter `beamLengthRatio`==`beamScale.z@0x434`; Emitter `firingArmed`==inherited
(HeatSink `heatState@0x184`, Reservoir `injectActive@0x1e4`) → route to `alarm.GetLevel()`; (3) a **phantom field** `useConfiguredPip@0x3E0`) → delete the member, use the inherited/existing name; (2) an **alarm-interior field** = a
= a member at an offset PAST the object (Generator `shortFlag@0x25C` is really `*(owner+0x190)+0x25c` the msg-manager; value the binary reads at `alarm+0x14` modeled as a separate member (HeatSink `heatState@0x184`, Reservoir
Myomers `moverConnection@0x110` a write-only base slot) → remove it, read the real source. **⚠ THE REMAINING SCOPE `injectActive@0x1e4`) → route to `alarm.GetLevel()`; (3) a **phantom field** = a member at an offset PAST the object
(measured, larger than the heat leaf):** making `PoweredSubsystem` byte-exact grows it +0x98 (two 0x54 alarms) and (Generator `shortFlag@0x25C` is really `*(owner+0x190)+0x25c` the msg-manager; Myomers `moverConnection@0x110` a
cascades into EVERY subclass — `MechWeapon`/`Emitter`/`PPC`/`Sensor`/`Myomers` ALL model the 0x54 `AlarmIndicator` write-only base slot; MechWeapon `segmentReference/pipSegment/hasTarget/targetPoint/muzzlePoint` past 0x3F0; Emitter
with 4-byte `ReconAlarm` (`AlarmIndicator`==`ReconAlarm`==4B!) or 8-byte `HeatAlarm` stand-ins and are short + `beamHitPoint/beamImpact/beamImpactScalar/beamColor/beamHitData/energyRampTime` — binary writes into inherited
phantom-tailed; retyping PoweredSubsystem's alarms without also byte-exacting them overflows the Emitter alloc `damageData`/`voltageScale` or the value is a method local) → remove it, read the real source or use a local.
(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 **✅✅ SUBSYSTEM-TREE ALARM UNIFICATION — DONE: the WHOLE PoweredSubsystem weapon/power subtree is now byte-exact
vs its ctor + lock) — a distinct multi-class task. `runtime Check(sizeof<=alloc)` in a factory bridge does NOT fail (P7 CLOSED; full per-class map from the 8-agent `alarm-unification-map` workflow, saved scratchpad/alarm_unify_maps.txt).**
the build (it's a runtime assert); use a `static_assert` sizeof lock to catch alloc overflow at COMPILE time. Making `PoweredSubsystem` byte-exact (its two 0x54 alarms `electricalStateAlarm@0x264`/`modeAlarm@0x2B8`) grows it
+0x98, cascading into every subclass — so all were byte-exacted TOGETHER in one build (each `static_assert`-locked
against its ctor + factory alloc; the chain HeatSink 0x1D0 → **PoweredSubsystem 0x31C** → **MechWeapon 0x3F0** →
**Emitter 0x478** → PPC 0x478; PoweredSubsystem → **Sensor 0x328**, → **Myomers 0x358**, all verified against the raw
ctors @004b0f74/@004b99a8/@004bb120/@004b1d18/@004b8fec). Each class: retype its `AlarmIndicator`(==`ReconAlarm`==4B)
/`HeatAlarm`(8B) stand-in for the binary 0x54 alarm → `GaugeAlarm54`; delete phantom/alias/duplicate fields;
`friend struct XLayoutCheck` with `offsetof`/`sizeof` locks (protected members need the friend; public fields work
at namespace scope). **KEY non-layout fixes required in the same wave:** (a) `outputVoltage`→`rechargeLevel` also in
the compiled **`GAUSS.CPP:74/93`** (external readers of the removed Emitter field — grep EVERY TU, not just the
class's own .cpp); (b) `MechWeapon::GetMuzzlePoint` reimplemented faithfully (the removed `muzzlePoint` member
collided with Emitter's own fields at 0x3F0) via a **`BTResolveWeaponMuzzle` void\* bridge in mech4.cpp** (a
complete-Mech TU with the segment API; mechweap.cpp reaches `owner` only as a raw pointer) resolving the weapon's
mount segment (inherited `this+0xdc`) through `GetSegment(int)`+segment→world; (c) `DetachFromVoltageSource` was
setting the WRONG alarm (`modeAlarm`→`electricalStateAlarm`, raw @004b0e30 writes 0x264). Verified: combat
DESTROYED-in-8, 28 shots, 0 crashes, heat `heatEnergy=1.34e7`, all locks pass. **The vehicleSubSystems aux-screen
gauge raw reads (`btl4gau2.cpp:868/952` at `subsystem+0x2b8`/`+0x278`) + the Sensor RadarPercent path now read the
CORRECT byte offsets** (they were garbage under the short layout). ⚠ `runtime Check(sizeof<=alloc)` in a factory
bridge does NOT fail the build (it's a runtime assert → heap overflow at construction); use a `static_assert`
sizeof lock to catch alloc overflow at COMPILE time (this is why the earlier isolated PoweredSubsystem retype
silently overflowed the Emitter alloc). **TECHNIQUE:** a read-only decomp-mapping Workflow (one agent per class
decodes its ctor → offset map + phantom/alias findings, + adversarial verify of the composed chain) then hands-on
implementation — the right shape for a systemic multi-class byte-exact re-base.
**✅ MechControlsMapper REVIVED — the real input-interpretation tick runs LIVE (gated `BT_REAL_CONTROLS=1`).** **✅ 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 The fully-reconstructed mapper family (mechmppr.cpp `MechControlsMapper` @004afbe0-@004b08c0; btl4mppr.cpp
+24 -16
View File
@@ -256,19 +256,27 @@ Reservoir `coolantCapacity`==inherited `thermalCapacity@0x128`, `injectActive`==
**phantom fields** removed (Generator `shortFlag@0x25C` -- really `*(owner+0x190)+0x25c`, the msg-manager, raw **phantom fields** removed (Generator `shortFlag@0x25C` -- really `*(owner+0x190)+0x25c`, the msg-manager, raw
@004b0efc; Myomers `moverConnection@0x110` -- a write-only base slot). @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):** **✅✅ SUBSYSTEM-TREE ALARM UNIFICATION -- DONE (P7 CLOSED; the whole PoweredSubsystem weapon/power subtree is
making **`PoweredSubsystem`** byte-exact (its two 0x54 alarms `electricalStateAlarm@0x264`/`modeAlarm@0x2B8`) byte-exact).** Making **`PoweredSubsystem`** byte-exact (its two 0x54 alarms `electricalStateAlarm@0x264`/
grows it **+0x98**, which cascades into EVERY subclass -- `MechWeapon`, `Emitter`, `PPC`, `Myomers`, `Sensor` -- `modeAlarm@0x2B8`) grows it **+0x98**, cascading into EVERY subclass -- `MechWeapon`, `Emitter`, `PPC`, `Myomers`,
and they ALL model the binary's 0x54 `AlarmIndicator` with 4-byte `ReconAlarm` (`weaponAlarm`) or 8-byte `Sensor` -- which ALL modeled the binary 0x54 `AlarmIndicator` with 4-byte `ReconAlarm`/8-byte `HeatAlarm`
`HeatAlarm` stand-ins, so they are themselves short AND carry phantom/duplicate/misplaced tail fields stand-ins and were short + phantom-tailed. All byte-exacted TOGETHER in one build (an 8-agent read-only
(measured: `MechWeapon` 0x3C4 vs binary 0x3F0 with ~0x24 of phantom tail; `Emitter` 0x4B0 vs binary 0x478 with decomp-mapping workflow decoded every ctor first; full map in scratchpad/alarm_unify_maps.txt), each
a duplicate `outputVoltage`==`rechargeLevel@0x320` + ~0x64 of oversized own fields). Retyping PoweredSubsystem's `static_assert`-locked against its ctor + factory alloc. The verified chain:
alarms WITHOUT also byte-exacting MechWeapon/Emitter/PPC overflowed the Emitter factory alloc (0x478) -> a heap HeatSink 0x1D0 -> **PoweredSubsystem 0x31C** (retype 2 alarms; ctor @004b0f74) ->
corruption during Emitter construction. So PoweredSubsystem was kept on `HeatAlarm(8)` stand-ins (marked in **MechWeapon 0x3F0** (weaponAlarm@0x350 ReconAlarm->GaugeAlarm54; delete 5 phantom tail fields; ctor @004b99a8) ->
powersub.hpp) and the whole tree still fits its allocs. **The real fix is a subsystem-tree ALARM UNIFICATION: **Emitter 0x478** (delete outputVoltage/beamLengthRatio/firingArmed aliases + beamHit*/beamColor/beamHitData/
retype every binary-`AlarmIndicator` stand-in (ReconAlarm/HeatAlarm) to `GaugeAlarm54(0x54)` and de-phantom energyRampTime phantoms; retype beamOrientation EulerAngles->Quaternion 16B; ctor @004bb120) ->
each weapon/power class against its ctor** (MechWeapon @004b99a8, Emitter @004ba478, PPC, Sensor). Each class **PPC 0x478** (no own fields; ctor @004bb888);
needs: alarm retype + phantom/alias-field removal + a `static_assert` sizeof/offset lock vs its factory alloc. PoweredSubsystem -> **Sensor 0x328** (no alarm, 3 own fields; ctor @004b1d18);
This is a multi-class effort (a separate task, bigger than the heat leaf) -- the core heat leaf above is the PoweredSubsystem -> **Myomers 0x358** (no alarm; ctor @004b8fec).
foundational prerequisite and is complete. Same techniques apply (ctor decomp -> offsets, alias/phantom checks, **Non-layout fixes in the same wave:** (a) `outputVoltage`->`rechargeLevel` also in compiled `GAUSS.CPP:74/93`
`friend struct XLayoutCheck` locks). The aux-screen gauge reads + Sensor RadarPercent stay approximate until then. (external readers of the removed Emitter field -- grep EVERY TU); (b) `MechWeapon::GetMuzzlePoint` reimplemented
(the removed `muzzlePoint` collided with Emitter's own fields) via a `BTResolveWeaponMuzzle` void* bridge in
mech4.cpp resolving the weapon's mount segment (`this+0xdc`) through the owner segment table; (c)
`DetachFromVoltageSource` fixed to set `electricalStateAlarm` not `modeAlarm` (raw @004b0e30 writes 0x264).
VERIFIED: combat DESTROYED-in-8, 28 shots, 0 crashes, heat `heatEnergy=1.34e7`, all locks pass, heapcheck-clean
through construction (the exact phase the isolated PoweredSubsystem retype had overflowed). **The vehicleSubSystems
aux-screen gauge raw reads (`btl4gau2.cpp:868/952` at `subsystem+0x2b8`/`+0x278`) + Sensor RadarPercent now read
the CORRECT byte offsets** (garbage under the short layout). ⚠ LESSON: a factory-bridge `runtime Check(sizeof<=alloc)`
does NOT fail the build -- use a `static_assert` sizeof lock to catch alloc overflow at COMPILE time.
+2 -2
View File
@@ -71,7 +71,7 @@ void
// //
// Fire the weapon // Fire the weapon
// //
outputVoltage = 0.0f; rechargeLevel = 0.0f; // was outputVoltage: the Emitter dup was removed (== inherited rechargeLevel@0x320)
Check_Fpu(); Check_Fpu();
} }
@@ -90,7 +90,7 @@ GaussRifle::GaussRifle(
{ {
Check(owner); Check(owner);
Check_Pointer(subsystem_resource); Check_Pointer(subsystem_resource);
outputVoltage = 0.0f; rechargeLevel = 0.0f; // was outputVoltage: the Emitter dup was removed (== inherited rechargeLevel@0x320)
Check_Fpu(); Check_Fpu();
} }
+27 -22
View File
@@ -199,20 +199,20 @@ void
Vector3D beamVector; Vector3D beamVector;
beamVector.Subtract(targetPoint, muzzlePoint); // FUN_00408bf8 beamVector.Subtract(targetPoint, muzzlePoint); // FUN_00408bf8
ComputeAimOrientation(beamHitData, beamVector); // FUN_004b9864 -> aim matrix LinearMatrix aimTransform; // was the phantom beamHitData member
ComputeAimOrientation(aimTransform, beamVector); // FUN_004b9864 -> aim matrix
Vector3D delta; Vector3D delta;
delta.Subtract(targetPoint, muzzlePoint); // FUN_00408644 delta.Subtract(targetPoint, muzzlePoint); // FUN_00408644
Scalar dist = (Scalar)Sqrt(delta.x*delta.x + delta.y*delta.y + delta.z*delta.z); // FUN_004dd138 Scalar dist = (Scalar)Sqrt(delta.x*delta.x + delta.y*delta.y + delta.z*delta.z); // FUN_004dd138
beamLengthRatio = dist / graphicLength; // 0x434 = dist / 0x438 beamScale.z = dist / graphicLength; // 0x434 (== beamScale[2]) = dist / 0x438
if (dist <= effectiveRange) // this+0x328 if (dist <= effectiveRange) // this+0x328
{ {
// record the hit point + impact and register the pip / fire marker // The binary writes the impact point/delta/energy into the inherited Damage
beamHitPoint = targetPoint; // 0xf2 <- targetPoint // damageData (0x3C8/0x3B0/0x3AC); the recon's beamHitPoint/beamImpact/
beamImpact = delta; // 0xec <- delta // beamImpactScalar copies were dead (no readers) -> removed. Register the pip.
beamImpactScalar = damagePortion; // 0xeb <- 0x113 DrawWeaponPip(aimTransform); // FUN_004b9728
DrawWeaponPip(beamHitData); // FUN_004b9728
} }
// stash the beam endpoint for replication (world hit point) // stash the beam endpoint for replication (world hit point)
@@ -285,7 +285,7 @@ void
case 2: // Loaded -- ready; fire on the trigger's rising edge case 2: // Loaded -- ready; fire on the trigger's rising edge
if (fireEdge) if (fireEdge)
{ {
if (firingArmed && HasActiveTarget()) // this+0x3e8 && entity+0x388 if (useConfiguredPip && HasActiveTarget()) // this+0x3E0 (Loaded->Firing gate) && entity+0x388
{ {
weaponAlarm.SetLevel(0); // -> Firing weaponAlarm.SetLevel(0); // -> Firing
FireWeapon(); // (*vtable+0x48)() FireWeapon(); // (*vtable+0x48)()
@@ -317,7 +317,7 @@ void
if (currentLevel < seekVoltage[seekVoltageIndex]) if (currentLevel < seekVoltage[seekVoltageIndex])
currentLevel = seekVoltage[seekVoltageIndex]; // -> ComputeOutputVoltage == 1.0 currentLevel = seekVoltage[seekVoltageIndex]; // -> ComputeOutputVoltage == 1.0
ComputeOutputVoltage(); // (*vtable+0x44)() ComputeOutputVoltage(); // (*vtable+0x44)()
if (outputVoltage == 1.0f) // _DAT_004bac04 -- fully charged if (rechargeLevel == 1.0f) // _DAT_004bac04 -- fully charged (== inherited @0x320)
{ {
weaponAlarm.SetLevel(2); // -> Loaded weaponAlarm.SetLevel(2); // -> Loaded
} }
@@ -496,27 +496,29 @@ Logical
void void
Emitter::ComputeOutputVoltage() Emitter::ComputeOutputVoltage()
{ {
// outputVoltage IS the inherited MechWeapon::rechargeLevel@0x320 (the binary keeps
// no separate Emitter slot; the recon's own `outputVoltage` was a duplicate).
if (Fabs(currentLevel - 0.0f) > 1.0e-4f) // _DAT_004ba818 / _DAT_004ba81c if (Fabs(currentLevel - 0.0f) > 1.0e-4f) // _DAT_004ba818 / _DAT_004ba81c
{ {
outputVoltage = currentLevel / seekVoltage[seekVoltageIndex]; // 0x320 rechargeLevel = currentLevel / seekVoltage[seekVoltageIndex]; // 0x320
} }
else else
{ {
outputVoltage = 0.0f; rechargeLevel = 0.0f;
} }
if (Fabs(outputVoltage - 1.0f) <= 0.01f) // _DAT_004ba820 / _DAT_004ba824 if (Fabs(rechargeLevel - 1.0f) <= 0.01f) // _DAT_004ba820 / _DAT_004ba824
{ {
outputVoltage = 1.0f; rechargeLevel = 1.0f;
} }
if (outputVoltage < 0.0f) // _DAT_004ba818 if (rechargeLevel < 0.0f) // _DAT_004ba818
{ {
outputVoltage = 0.0f; // _DAT_004ba828 rechargeLevel = 0.0f; // _DAT_004ba828
} }
else if (outputVoltage > 1.0f) // _DAT_004ba820 else if (rechargeLevel > 1.0f) // _DAT_004ba820
{ {
outputVoltage = 0.0f; // _DAT_004ba830 (NB: also 0.0f) rechargeLevel = 0.0f; // _DAT_004ba830 (NB: also 0.0f)
} }
} }
@@ -674,9 +676,9 @@ Emitter::Emitter(
SetPerformance(&Emitter::EmitterSimulation); // this[7..9] = {0x004baa88,0,0} SetPerformance(&Emitter::EmitterSimulation); // this[7..9] = {0x004baa88,0,0}
} }
firingArmed = 1; // E4: arm the Loaded->Firing gate (this+0x3e8; // (E4 removed) the former `firingArmed = 1` wrote this+0x3e8 == MechWeapon::recoil,
// real semantic is a weapon-selected flag from // NOT the Loaded->Firing gate: the binary gate reads useConfiguredPip@0x3E0 (set by
// the controls path -- bypassed in bring-up). // the MechWeapon ctor from usesExternalModel), so no separate arming is needed.
weaponAlarm.SetLevel(3); // Loading weaponAlarm.SetLevel(3); // Loading
beamEndpoint = Point3D(0.0f, 0.0f, 0.0f); // 0x460 <- (0,0,0) beamEndpoint = Point3D(0.0f, 0.0f, 0.0f); // 0x460 <- (0,0,0)
beamFlag = 0; // 0x46c beamFlag = 0; // 0x46c
@@ -735,9 +737,12 @@ Emitter::Emitter(
Scalar v = seekVoltage[seekVoltageRecommendedIndex]; Scalar v = seekVoltage[seekVoltageRecommendedIndex];
energyCoefficient = energyTotal / (v * v * /*_DAT_004bb3b4*/ 0.5f); // 0x454 energyCoefficient = energyTotal / (v * v * /*_DAT_004bb3b4*/ 0.5f); // 0x454
// voltage-curve coefficient -> ramp time (expf of the seek curve) // voltage-curve coefficient -> ramp time (expf of the seek curve). The binary
// stores this in the base slot voltageScale@0x310 and never reads it back, so the
// recon's own `energyRampTime` member was a phantom -> computed into a local.
Scalar curve = /*_DAT_004bb3c4 - _DAT_004bb3b8 * v*/ VoltageCurve(v); // best-effort Scalar curve = /*_DAT_004bb3c4 - _DAT_004bb3b8 * v*/ VoltageCurve(v); // best-effort
energyRampTime = (rechargeRate / -curve) / energyCoefficient; // this+0x328 working term Scalar energyRampTime = (rechargeRate / -curve) / energyCoefficient;
(void)energyRampTime;
} }
// damageFraction = damageAmount / (damageAmount + heatCostToFire) // damageFraction = damageAmount / (damageAmount + heatCostToFire)
+46 -42
View File
@@ -263,49 +263,53 @@ class NotationFile;
// Byte offsets into the shipped object; MechWeapon ends at +0x3EF. // Byte offsets into the shipped object; MechWeapon ends at +0x3EF.
// //
protected: protected:
// --- charge curve (seek voltage) --- // Byte-exact own fields, ctor @004bb120 (param_1[N] == byte N*4), 0x3F0..0x478.
int seekVoltageIndex; // @0x3F0 current charge-curve index // The former "additional firing/beam scratch" block (beamHitPoint/beamImpact/
int seekVoltageRecommendedIndex;// @0x3F4 resource recommended index (reset target) // beamImpactScalar/beamColor/beamHitData/firingArmed/energyRampTime) + outputVoltage
int seekStepCounter; // @0x3F8 reset 0 (best-effort) // + beamLengthRatio were phantom/duplicate members that over-sized Emitter to 0x4B0;
int seekVoltageCount; // @0x3FC index modulus / count-1 (best-effort) // removed (see the .cpp for where each really lives / became a local):
Scalar seekVoltage[5]; // @0x400 per-index target voltages (heat-scaled) // * outputVoltage -> inherited MechWeapon::rechargeLevel@0x320 (ComputeOutputVoltage writes it)
Scalar currentLevel; // @0x414 accumulated charge (EMITTER.TCP: currentLevel) // * beamHitData -> a LinearMatrix local in FireWeapon (aim transform)
Scalar outputVoltage; // normalised discharge readiness (EMITTER.TCP: outputVoltage) // * beamHitPoint/beamImpact/beamImpactScalar -> binary writes into the inherited
// Damage damageData (0x3C8/0x3B0/0x3AC); the recon assignments were dead -> deleted
// * beamColor -> never read (WriteUpdateRecord builds colour from targetEntity)
// * firingArmed -> the Loaded->Firing gate reads inherited useConfiguredPip@0x3E0
// * energyRampTime -> binary writes the base slot voltageScale@0x310; recon computes a local
// * beamLengthRatio -> is beamScale.z@0x434
int seekVoltageIndex; // @0x3F0 (param_1[0xfc]) current charge-curve index
int seekVoltageRecommendedIndex;// @0x3F4 (param_1[0xfd]) recommended index
int seekStepCounter; // @0x3F8 (param_1[0xfe]) reset 0
int seekVoltageCount; // @0x3FC (param_1[0xff]) index modulus / count-1
Scalar seekVoltage[5]; // @0x400 (param_1[0x100..0x104]) per-index target voltages
Scalar currentLevel; // @0x414 (param_1[0x105]) accumulated charge
int firingActive; // @0x418 (param_1[0x106]) reset 0; FireWeapon sets 1
Quaternion beamOrientation; // @0x41C (param_1[0x107..0x10a], 16B) aim orientation
Vector3D beamScale; // @0x42C (param_1[0x10b..0x10d]) beam scale; .z = length ratio
Scalar graphicLength; // @0x438 (param_1[0x10e]) resource GraphicLength
Scalar dischargeTime; // @0x43C (param_1[0x10f]) resource DischargeTime
Scalar dischargeTimer; // @0x440 (param_1[0x110]) beam-on countdown
Scalar damageFraction; // @0x444 (param_1[0x111]) damageAmount / (damageAmount + heatCostToFire)
Scalar energyTotal; // @0x448 (param_1[0x112]) (damageAmount + heatCostToFire) * 1e7
Scalar damagePortion; // @0x44C (param_1[0x113]) per-shot damage energy
Scalar heatPortion; // @0x450 (param_1[0x114]) per-shot self-heat energy
Scalar energyCoefficient; // @0x454 (param_1[0x115]) energyTotal / (seekVoltage^2 * k)
Scalar seekReserved; // @0x458 (param_1[0x116]) reset 0
Scalar seekRate; // @0x45C (param_1[0x117]) d(level)/dt working value
Point3D beamEndpoint; // @0x460 (param_1[0x118..0x11a]) last hit / beam-end point
int beamFlag; // @0x46C (param_1[0x11b]) beam-active flag
int targetLocalFlag; // @0x470 (param_1[0x11c]) target-relative vs world
Entity *targetEntity; // @0x474 (param_1[0x11d]) current beam target -- LAST, ends 0x478
// --- additional firing/beam scratch referenced by the recovered bodies --- friend struct EmitterLayoutCheck;
Point3D beamHitPoint; // last hit point };
Vector3D beamImpact; // impact delta
Scalar beamImpactScalar; // per-shot damage energy at impact
RGBColor beamColor; // beam/team colour
LinearMatrix beamHitData; // pip transform for DrawWeaponPip
int firingArmed; // trigger-armed flag
Scalar energyRampTime; // seek-curve ramp time working value
// heatAccumulator removed -- it SHADOWED the inherited HeatSink::pendingHeat
// (@0x1c8, the heat sim's per-frame input); FireWeapon now writes pendingHeat (E5).
// NOTE: outputVoltage is MechWeapon's recharge-level slot @0x320, reused struct EmitterLayoutCheck {
// by Emitter as the normalised discharge readiness (EMITTER.TCP: static_assert(offsetof(Emitter, seekVoltageIndex) == 0x3F0, "Emitter::seekVoltageIndex @0x3F0 (MechWeapon must end 0x3F0)");
// outputVoltage; == 1.0 means "fully charged -> Loaded"). static_assert(offsetof(Emitter, currentLevel) == 0x414, "Emitter::currentLevel @0x414");
static_assert(offsetof(Emitter, beamOrientation) == 0x41C, "Emitter::beamOrientation @0x41C (Quaternion 16B)");
// --- beam / firing transient state (network-replicated) --- static_assert(offsetof(Emitter, beamScale) == 0x42C, "Emitter::beamScale @0x42C");
int firingActive; // @0x418 reset 0; FireWeapon sets 1 (best-effort) static_assert(offsetof(Emitter, targetEntity) == 0x474, "Emitter::targetEntity @0x474 (last)");
EulerAngles beamOrientation; // @0x41C aim orientation (3 floats), zeroed at reset static_assert(sizeof(Emitter) == 0x478, "sizeof(Emitter) 0x478 (== factory alloc)");
Vector3D beamScale; // @0x42C reset (1,1,1) (best-effort) };
Scalar beamLengthRatio; // @0x434 range / graphicLength (best-effort)
Scalar graphicLength; // @0x438 resource GraphicLength
Scalar dischargeTime; // @0x43C resource DischargeTime
Scalar dischargeTimer; // @0x440 beam-on countdown (reset = dischargeTime)
Scalar damageFraction; // @0x444 damageAmount / (damageAmount + heatCostToFire)
Scalar energyTotal; // @0x448 (damageAmount + heatCostToFire) * 1.0e7 (best-effort)
Scalar damagePortion; // @0x44C per-shot damage energy (best-effort)
Scalar heatPortion; // @0x450 per-shot self-heat energy (best-effort)
Scalar energyCoefficient; // @0x454 energyTotal / (seekVoltage^2 * k) (best-effort)
Scalar seekReserved; // @0x458 reset 0 (best-effort)
Scalar seekRate; // @0x45C d(level)/dt working value (best-effort)
Point3D beamEndpoint; // @0x460 last hit / beam-end point (replicated)
int beamFlag; // @0x46C beam-active flag (replicated)
int targetLocalFlag; // @0x470 target-relative vs world (replicated)
Entity *targetEntity; // @0x474 current beam target (replicated)
}; // sizeof ~= 0x478
#endif #endif
+3
View File
@@ -96,6 +96,9 @@ public:
void SetLevel(int n) { level = n; } void SetLevel(int n) { level = n; }
int GetLevel() const { return level; } int GetLevel() const { return level; }
int Level() const { return level; } int Level() const { return level; }
// ReconAlarm/AlarmIndicator API aliases (callers that predate the retype use these):
void SetState(unsigned n){ level = (int)n; }
unsigned GetState() const { return (unsigned)level; }
protected: protected:
// Interior mirrors FUN_0041b9ec: base GaugeAlarm header (+0x00), three count // Interior mirrors FUN_0041b9ec: base GaugeAlarm header (+0x00), three count
// words at +0x0c/+0x10/+0x14, three sub-indicators at +0x18/+0x2c/+0x40. The // words at +0x0c/+0x10/+0x14, three sub-indicators at +0x18/+0x2c/+0x40. The
+23
View File
@@ -655,6 +655,29 @@ Scalar
return 0.0f; return 0.0f;
} }
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// BTResolveWeaponMuzzle -- the faithful FUN_004b9948 (MechWeapon::GetMuzzlePoint)
// muzzle resolve: look up the weapon's mount segment (index, from the subsystem's
// inherited this+0xdc slot) in the owner Mech's segment table and transform it to
// world. Lives here (a complete-Mech TU with the segment API); mechweap.cpp treats
// `owner` as a raw pointer so it calls this via a void* bridge instead of the Mech API.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void
BTResolveWeaponMuzzle(void *ownerMech, int segIndex, Point3D &out)
{
Mech *m = (Mech *)ownerMech;
if (m == 0) { out = Point3D(0.0f, 0.0f, 0.0f); return; }
EntitySegment *seg = m->GetSegment(segIndex); // owner+0x300 table, GetNth(index)
if (seg != 0)
{
AffineMatrix mw;
mw.Multiply(seg->GetSegmentToEntity(), m->localToWorld); // segment -> world (== mech4 gun-port path)
out = mw; // Point3D = matrix W_Axis translation
}
else
out = m->localOrigin.linearPosition; // safe non-garbage fallback (owner origin)
}
void void
Mech::PerformAndWatch(const Time& till, MemoryStream *update_stream) Mech::PerformAndWatch(const Time& till, MemoryStream *update_stream)
{ {
+20 -11
View File
@@ -160,8 +160,9 @@ MechWeapon::MechWeapon(
rangeToTarget = 0.0f; // 0x324 rangeToTarget = 0.0f; // 0x324
targetWithinRange = False; // 0x34C targetWithinRange = False; // 0x34C
segmentReference = segmentPageIndex; // segmentReference/pipSegment were phantom tail members (past the binary 0x3F0 end);
pipSegment = -1; // removed. The muzzle segment index is the inherited base slot this+0xdc (used by
// GetMuzzlePoint); the pip mount is gated by useConfiguredPip in DrawWeaponPip.
// usesExternalModel: True if the model name contains the marker substring // usesExternalModel: True if the model name contains the marker substring
// (DAT_00511aa2); selects whether the configured pip is drawn. // (DAT_00511aa2); selects whether the configured pip is drawn.
@@ -358,17 +359,23 @@ void
char *o = (char *)owner; // inherited MechSubsystem::owner (the Mech) char *o = (char *)owner; // inherited MechSubsystem::owner (the Mech)
if (o != 0) if (o != 0)
position = *(Point3D *)(o + 0x37c); // MECH_TARGET_POS position = *(Point3D *)(o + 0x37c); // MECH_TARGET_POS
else // (no owner -> leave position unchanged; the phantom `targetPoint` member is gone --
position = targetPoint; // binary GetTargetPosition @004b9cbc unconditionally reads the owner target slot.)
} }
// Faithful FUN_004b9948 muzzle resolve, defined in mech4.cpp (a complete-Mech TU);
// mechweap.cpp reaches the owner Mech only as a raw pointer, so it bridges via void*.
extern void BTResolveWeaponMuzzle(void *ownerMech, int segIndex, Point3D &out);
// //
// @004b9cdc -- direction from a supplied point to the weapon mount (entity+0x37c). // @004b9cdc -- direction from a supplied point to the weapon mount (entity+0x37c).
// //
void void
MechWeapon::GetVectorToWeapon(const Point3D &from, Vector3D &direction) MechWeapon::GetVectorToWeapon(const Point3D &from, Vector3D &direction)
{ {
direction.Subtract(muzzlePoint, from); // FUN_00408644 Point3D muzzle;
GetMuzzlePoint(muzzle); // resolve live (phantom muzzlePoint member removed)
direction.Subtract(muzzle, from); // FUN_00408644
} }
// //
@@ -378,11 +385,13 @@ void
void void
MechWeapon::GetMuzzlePoint(Point3D &point) MechWeapon::GetMuzzlePoint(Point3D &point)
{ {
// @004b9948 -- the shipped code resolves segmentReference against the owning // @004b9948 -- resolve the weapon's mount segment (its index is the inherited
// Mech's segment table (SegmentIterator) and transforms the segment origin to // subsystem slot at this+0xdc, == binary param_1+0xdc) in the owning Mech's
// world space. That transform is produced by the owning Mech's per-frame // segment table and transform it to world (FUN_00424da8). Reads live each call
// segment update, which caches it in muzzlePoint; we return the cached value. // (the phantom cached `muzzlePoint` member is removed -- it collided with the
point = muzzlePoint; // FUN_00424da8 result // Emitter subclass's own fields at 0x3F0+).
int segIndex = *(int *)((char *)this + 0xdc); // inherited segment index (byte-exact base)
BTResolveWeaponMuzzle(owner, segIndex, point); // mech4.cpp bridge (null-guarded)
} }
// //
@@ -422,7 +431,7 @@ void
// CROSS-FAMILY (hud): the cockpit HUD manager AddElement entry point lives in // CROSS-FAMILY (hud): the cockpit HUD manager AddElement entry point lives in
// the HUD module; the submission is wired there. We retain the gating and // the HUD module; the submission is wired there. We retain the gating and
// transform reference here. // transform reference here.
if (this->pipSegment != -1) // FUN_0041a1a4 guard if (useConfiguredPip) // this+0x3E0 (weapon draws a configured pip)
{ {
(void)transform; (void)transform;
(void)pipPosition; (void)pipPosition;
+22 -14
View File
@@ -266,7 +266,7 @@ class CockpitHud;
Logical targetWithinRange; // @0x34C PPC.CPP: "if (targetWithinRange)" Logical targetWithinRange; // @0x34C PPC.CPP: "if (targetWithinRange)"
// --- damage / display alarm --- // --- damage / display alarm ---
AlarmIndicator weaponAlarm; // @0x350 alarm indicator (level count from ctor) GaugeAlarm54 weaponAlarm; // @0x350 0x54 AlarmIndicator (ctor FUN_0041b9ec); level @0x364 -> ends 0x3A4
Scalar previousFireImpulse; // @0x3A4 reset 0 (fire-edge history, see @004b9608) Scalar previousFireImpulse; // @0x3A4 reset 0 (fire-edge history, see @004b9608)
Damage damageData; // @0x3A8 damageType @0x3A8, damageAmount @0x3AC Damage damageData; // @0x3A8 damageType @0x3A8, damageAmount @0x3AC
@@ -277,21 +277,29 @@ class CockpitHud;
Logical useConfiguredPip; // @0x3E0 = (usesExternalModel == 0) (best-effort) Logical useConfiguredPip; // @0x3E0 = (usesExternalModel == 0) (best-effort)
ResourceDescription::ResourceID ResourceDescription::ResourceID
explosionResourceID; // @0x3E4 resource ExplosionModelFile explosionResourceID; // @0x3E4 resource ExplosionModelFile
Scalar recoil; // @0x3E8 reset 0 (TODO: name) Scalar recoil; // @0x3E8 reset 0 (Emitter::firingArmed aliases this slot)
int segmentPageIndex; // @0x3EC resource segmentIndex (+0x28) int segmentPageIndex; // @0x3EC resource segmentIndex (+0x28) -- LAST field, binary ends 0x3F0
// --- targeting/segment references (best-effort; used by GetMuzzlePoint / // The binary MechWeapon ends at 0x3F0 (segmentPageIndex is the last ctor write,
// DrawWeaponPip). segmentReference indexes the owning Mech segment // @004b99a8 param_1[0xfb]). The former tail fields segmentReference/pipSegment/
// table; pipSegment is the HUD pip's mount segment (-1 == none). --- // hasTarget/targetPoint/muzzlePoint were PHANTOM (no binary storage) and, worse,
int segmentReference; // muzzle segment lookup index // collided with the Emitter subclass's own fields at 0x3F0+; removed:
int pipSegment; // HUD pip mount segment (-1 == none) // * segmentReference -> the muzzle segment is the inherited base slot this+0xdc
// (GetMuzzlePoint resolves it live via the mech4 BTResolveWeaponMuzzle bridge)
// * pipSegment -> DrawWeaponPip gates on useConfiguredPip
// * hasTarget -> dead (HasActiveTarget reads owner+0x388)
// * targetPoint -> GetTargetPosition reads owner+0x37c
// * muzzlePoint -> resolved live by GetMuzzlePoint
// --- cached targeting kinematics (populated by the owning Mech's friend struct MechWeaponLayoutCheck;
// per-frame targeting/segment update; consumed by UpdateTargeting, };
// GetTargetPosition, GetMuzzlePoint, GetVectorToWeapon). ---
Logical hasTarget; // owning mech has a locked target struct MechWeaponLayoutCheck {
Point3D targetPoint; // current target world position static_assert(offsetof(MechWeapon, weaponAlarm) == 0x350, "MechWeapon::weaponAlarm @0x350 (0x54 alarm)");
Point3D muzzlePoint; // resolved weapon muzzle world position static_assert(offsetof(MechWeapon, previousFireImpulse) == 0x3A4, "MechWeapon::previousFireImpulse @0x3A4 (alarm ends here)");
static_assert(offsetof(MechWeapon, recoil) == 0x3E8, "MechWeapon::recoil @0x3E8");
static_assert(offsetof(MechWeapon, segmentPageIndex) == 0x3EC, "MechWeapon::segmentPageIndex @0x3EC (last field)");
static_assert(sizeof(MechWeapon) == 0x3F0, "sizeof(MechWeapon) 0x3F0");
}; };
#endif #endif
+6 -7
View File
@@ -527,13 +527,12 @@ void Myomers::ToggleSeekVoltage()
//===========================================================================// //===========================================================================//
struct MyomersLayoutCheck struct MyomersLayoutCheck
{ {
// OVERFLOW lock only: the PoweredSubsystem base is NOT byte-exact (its 0x54 alarms // BYTE-EXACT: with PoweredSubsystem byte-exact (ends 0x31C) and the phantom
// are modeled as HeatAlarm(8) pending the subsystem-tree alarm unification -- see // moverConnection tail removed, Myomers' own fields (ctor @004b8fec: speedEffect@0x31C
// powersub.hpp), so Myomers own fields don't land at their binary offsets yet. The // .. accelerationEfficiency@0x354) land exactly at 0x31C..0x358.
// phantom moverConnection tail was removed (it was a write-only base+0x110 shim), so static_assert(offsetof(Myomers, speedEffect) == 0x31C, "Myomers::speedEffect @0x31C (attr 0x12)");
// the object fits the 0x358 factory alloc. Byte-exact offsets return with the P7 static_assert(offsetof(Myomers, seekVoltage) == 0x330, "Myomers::seekVoltage @0x330 (attr 0x17)");
// weapon/power-subtree re-base. static_assert(sizeof(Myomers) == 0x358, "sizeof(Myomers) 0x358 (factory alloc, byte-exact)");
static_assert(sizeof(Myomers) <= 0x358, "sizeof(Myomers) must fit the factory Memory::Allocate(0x358)");
}; };
+1 -1
View File
@@ -482,7 +482,7 @@ void
source->currentTapCount -= 1; // source+0x1e8 -= 1 source->currentTapCount -= 1; // source+0x1e8 -= 1
voltageSource.Clear(); // FUN_00417a5c(this+0x1d0) voltageSource.Clear(); // FUN_00417a5c(this+0x1d0)
} }
modeAlarm.SetLevel(1); // FUN_0041bbd8(this+0x264, 1) TODO: confirm which alarm electricalStateAlarm.SetLevel(NoVoltage); // FUN_0041bbd8(this+0x264, 1) -- 0x264 IS electricalStateAlarm (was wrongly modeAlarm@0x2B8)
} }
// //
+20 -11
View File
@@ -205,20 +205,29 @@ class Generator;
int auxScreenPlacement; // @0x1E0 resource +0x108 int auxScreenPlacement; // @0x1E0 resource +0x108
char auxScreenLabel[64]; // @0x1E4 resource +0x10C char auxScreenLabel[64]; // @0x1E4 resource +0x10C
char engScreenLabel[64]; // @0x224 resource +0x14C char engScreenLabel[64]; // @0x224 resource +0x14C
// ⚠ NOT YET BYTE-EXACT: the binary electricalStateAlarm/modeAlarm are 0x54-byte // The binary alarms are 0x54-byte AlarmIndicators (ctor @004b0f74:
// AlarmIndicators (level @+0x14). Retyping them to GaugeAlarm54 grows PoweredSubsystem // FUN_0041b9ec(param_1+0x99,5) @0x264 and (param_1+0xae,3) @0x2B8; level @+0x14
// +0x98, which cascades into EVERY subclass (MechWeapon/Emitter/PPC/Myomers/Sensor) -- // == 0x278/0x2CC). Now byte-exact via GaugeAlarm54 -- part of the subsystem-tree
// all of which ALSO model the binary alarm with 4-byte ReconAlarm / 8-byte HeatAlarm // alarm unification (the whole PoweredSubsystem subtree byte-exacted together).
// stand-ins and are themselves short + phantom-tailed. Making PoweredSubsystem GaugeAlarm54 electricalStateAlarm; // @0x264 5-level (ElectricalState); level @0x278 -> ends 0x2B8
// byte-exact therefore requires the SYSTEMIC subsystem-tree alarm unification (P7 GaugeAlarm54 modeAlarm; // @0x2B8 3-level (ConnectMode); level @0x2CC -> ends 0x30C
// 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 thermalResistivityCoefficient; // @0x30C resource +0x100
Scalar voltageScale; // @0x310 init 1.0f Scalar voltageScale; // @0x310 init 1.0f
Scalar startTime; // @0x314 resource +0x18C Scalar startTime; // @0x314 resource +0x18C
Scalar startTimer; // @0x318 init = startTime, counts up while Starting Scalar startTimer; // @0x318 init = startTime, counts up while Starting -> ends 0x31C
friend struct PoweredSubsystemLayoutCheck;
};
struct PoweredSubsystemLayoutCheck {
static_assert(offsetof(PoweredSubsystem, voltageSource) == 0x1D0, "PoweredSubsystem::voltageSource @0x1D0");
static_assert(offsetof(PoweredSubsystem, auxScreenNumber) == 0x1DC, "PoweredSubsystem::auxScreenNumber @0x1DC (res +0x104)");
static_assert(offsetof(PoweredSubsystem, auxScreenLabel) == 0x1E4, "PoweredSubsystem::auxScreenLabel @0x1E4");
static_assert(offsetof(PoweredSubsystem, engScreenLabel) == 0x224, "PoweredSubsystem::engScreenLabel @0x224");
static_assert(offsetof(PoweredSubsystem, electricalStateAlarm) == 0x264, "PoweredSubsystem::electricalStateAlarm @0x264");
static_assert(offsetof(PoweredSubsystem, modeAlarm) == 0x2B8, "PoweredSubsystem::modeAlarm @0x2B8");
static_assert(offsetof(PoweredSubsystem, thermalResistivityCoefficient)== 0x30C, "PoweredSubsystem::thermalResistivityCoefficient @0x30C");
static_assert(sizeof(PoweredSubsystem) == 0x31C, "sizeof(PoweredSubsystem) 0x31C");
}; };
//########################################################################### //###########################################################################
+7 -1
View File
@@ -407,7 +407,13 @@ int
//===========================================================================// //===========================================================================//
struct SensorLayoutCheck struct SensorLayoutCheck
{ {
static_assert(sizeof(Sensor) <= 0x328, "sizeof(Sensor) must fit the factory Memory::Allocate(0x328)"); // Now BYTE-EXACT: with PoweredSubsystem byte-exact (ends 0x31C), Sensor's 3 own
// fields (ctor @004b1d18: radarPercent@0x31C / selfTest@0x320 / badVoltage@0x324)
// land exactly at 0x31C..0x328. (Public fields -> namespace-scope offsetof works.)
static_assert(offsetof(Sensor, radarPercent) == 0x31C, "Sensor::radarPercent @0x31C (attr 0x12)");
static_assert(offsetof(Sensor, selfTest) == 0x320, "Sensor::selfTest @0x320");
static_assert(offsetof(Sensor, badVoltage) == 0x324, "Sensor::badVoltage @0x324");
static_assert(sizeof(Sensor) == 0x328, "sizeof(Sensor) 0x328 (factory alloc, byte-exact)");
}; };