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
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.
`massScale@0x160`; Reservoir `coolantCapacity`==`thermalCapacity@0x128`; Emitter `outputVoltage`==inherited
`rechargeLevel@0x320`; Emitter `beamLengthRatio`==`beamScale.z@0x434`; Emitter `firingArmed`==inherited
`useConfiguredPip@0x3E0`) → delete the member, use the inherited/existing 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; MechWeapon `segmentReference/pipSegment/hasTarget/targetPoint/muzzlePoint` past 0x3F0; Emitter
`beamHitPoint/beamImpact/beamImpactScalar/beamColor/beamHitData/energyRampTime` — binary writes into inherited
`damageData`/`voltageScale` or the value is a method local) → remove it, read the real source or use a local.
**✅✅ SUBSYSTEM-TREE ALARM UNIFICATION — DONE: the WHOLE PoweredSubsystem weapon/power subtree is now byte-exact
(P7 CLOSED; full per-class map from the 8-agent `alarm-unification-map` workflow, saved scratchpad/alarm_unify_maps.txt).**
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`).**
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
@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.
**✅✅ SUBSYSTEM-TREE ALARM UNIFICATION -- DONE (P7 CLOSED; the whole PoweredSubsystem weapon/power subtree is
byte-exact).** Making **`PoweredSubsystem`** byte-exact (its two 0x54 alarms `electricalStateAlarm@0x264`/
`modeAlarm@0x2B8`) grows it **+0x98**, cascading into EVERY subclass -- `MechWeapon`, `Emitter`, `PPC`, `Myomers`,
`Sensor` -- which ALL modeled the binary 0x54 `AlarmIndicator` with 4-byte `ReconAlarm`/8-byte `HeatAlarm`
stand-ins and were short + phantom-tailed. All byte-exacted TOGETHER in one build (an 8-agent read-only
decomp-mapping workflow decoded every ctor first; full map in scratchpad/alarm_unify_maps.txt), each
`static_assert`-locked against its ctor + factory alloc. The verified chain:
HeatSink 0x1D0 -> **PoweredSubsystem 0x31C** (retype 2 alarms; ctor @004b0f74) ->
**MechWeapon 0x3F0** (weaponAlarm@0x350 ReconAlarm->GaugeAlarm54; delete 5 phantom tail fields; ctor @004b99a8) ->
**Emitter 0x478** (delete outputVoltage/beamLengthRatio/firingArmed aliases + beamHit*/beamColor/beamHitData/
energyRampTime phantoms; retype beamOrientation EulerAngles->Quaternion 16B; ctor @004bb120) ->
**PPC 0x478** (no own fields; ctor @004bb888);
PoweredSubsystem -> **Sensor 0x328** (no alarm, 3 own fields; ctor @004b1d18);
PoweredSubsystem -> **Myomers 0x358** (no alarm; ctor @004b8fec).
**Non-layout fixes in the same wave:** (a) `outputVoltage`->`rechargeLevel` also in compiled `GAUSS.CPP:74/93`
(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
//
outputVoltage = 0.0f;
rechargeLevel = 0.0f; // was outputVoltage: the Emitter dup was removed (== inherited rechargeLevel@0x320)
Check_Fpu();
}
@@ -90,7 +90,7 @@ GaussRifle::GaussRifle(
{
Check(owner);
Check_Pointer(subsystem_resource);
outputVoltage = 0.0f;
rechargeLevel = 0.0f; // was outputVoltage: the Emitter dup was removed (== inherited rechargeLevel@0x320)
Check_Fpu();
}
+27 -22
View File
@@ -199,20 +199,20 @@ void
Vector3D beamVector;
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;
delta.Subtract(targetPoint, muzzlePoint); // FUN_00408644
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
{
// record the hit point + impact and register the pip / fire marker
beamHitPoint = targetPoint; // 0xf2 <- targetPoint
beamImpact = delta; // 0xec <- delta
beamImpactScalar = damagePortion; // 0xeb <- 0x113
DrawWeaponPip(beamHitData); // FUN_004b9728
// The binary writes the impact point/delta/energy into the inherited Damage
// damageData (0x3C8/0x3B0/0x3AC); the recon's beamHitPoint/beamImpact/
// beamImpactScalar copies were dead (no readers) -> removed. Register the pip.
DrawWeaponPip(aimTransform); // FUN_004b9728
}
// 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
if (fireEdge)
{
if (firingArmed && HasActiveTarget()) // this+0x3e8 && entity+0x388
if (useConfiguredPip && HasActiveTarget()) // this+0x3E0 (Loaded->Firing gate) && entity+0x388
{
weaponAlarm.SetLevel(0); // -> Firing
FireWeapon(); // (*vtable+0x48)()
@@ -317,7 +317,7 @@ void
if (currentLevel < seekVoltage[seekVoltageIndex])
currentLevel = seekVoltage[seekVoltageIndex]; // -> ComputeOutputVoltage == 1.0
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
}
@@ -496,27 +496,29 @@ Logical
void
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
{
outputVoltage = currentLevel / seekVoltage[seekVoltageIndex]; // 0x320
rechargeLevel = currentLevel / seekVoltage[seekVoltageIndex]; // 0x320
}
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}
}
firingArmed = 1; // E4: arm the Loaded->Firing gate (this+0x3e8;
// real semantic is a weapon-selected flag from
// the controls path -- bypassed in bring-up).
// (E4 removed) the former `firingArmed = 1` wrote this+0x3e8 == MechWeapon::recoil,
// NOT the Loaded->Firing gate: the binary gate reads useConfiguredPip@0x3E0 (set by
// the MechWeapon ctor from usesExternalModel), so no separate arming is needed.
weaponAlarm.SetLevel(3); // Loading
beamEndpoint = Point3D(0.0f, 0.0f, 0.0f); // 0x460 <- (0,0,0)
beamFlag = 0; // 0x46c
@@ -735,9 +737,12 @@ Emitter::Emitter(
Scalar v = seekVoltage[seekVoltageRecommendedIndex];
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
energyRampTime = (rechargeRate / -curve) / energyCoefficient; // this+0x328 working term
Scalar energyRampTime = (rechargeRate / -curve) / energyCoefficient;
(void)energyRampTime;
}
// damageFraction = damageAmount / (damageAmount + heatCostToFire)
+46 -42
View File
@@ -263,49 +263,53 @@ class NotationFile;
// Byte offsets into the shipped object; MechWeapon ends at +0x3EF.
//
protected:
// --- charge curve (seek voltage) ---
int seekVoltageIndex; // @0x3F0 current charge-curve index
int seekVoltageRecommendedIndex;// @0x3F4 resource recommended index (reset target)
int seekStepCounter; // @0x3F8 reset 0 (best-effort)
int seekVoltageCount; // @0x3FC index modulus / count-1 (best-effort)
Scalar seekVoltage[5]; // @0x400 per-index target voltages (heat-scaled)
Scalar currentLevel; // @0x414 accumulated charge (EMITTER.TCP: currentLevel)
Scalar outputVoltage; // normalised discharge readiness (EMITTER.TCP: outputVoltage)
// Byte-exact own fields, ctor @004bb120 (param_1[N] == byte N*4), 0x3F0..0x478.
// The former "additional firing/beam scratch" block (beamHitPoint/beamImpact/
// beamImpactScalar/beamColor/beamHitData/firingArmed/energyRampTime) + outputVoltage
// + beamLengthRatio were phantom/duplicate members that over-sized Emitter to 0x4B0;
// removed (see the .cpp for where each really lives / became a local):
// * outputVoltage -> inherited MechWeapon::rechargeLevel@0x320 (ComputeOutputVoltage writes it)
// * beamHitData -> a LinearMatrix local in FireWeapon (aim transform)
// * 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 ---
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).
friend struct EmitterLayoutCheck;
};
// NOTE: outputVoltage is MechWeapon's recharge-level slot @0x320, reused
// by Emitter as the normalised discharge readiness (EMITTER.TCP:
// outputVoltage; == 1.0 means "fully charged -> Loaded").
// --- beam / firing transient state (network-replicated) ---
int firingActive; // @0x418 reset 0; FireWeapon sets 1 (best-effort)
EulerAngles beamOrientation; // @0x41C aim orientation (3 floats), zeroed at reset
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
struct EmitterLayoutCheck {
static_assert(offsetof(Emitter, seekVoltageIndex) == 0x3F0, "Emitter::seekVoltageIndex @0x3F0 (MechWeapon must end 0x3F0)");
static_assert(offsetof(Emitter, currentLevel) == 0x414, "Emitter::currentLevel @0x414");
static_assert(offsetof(Emitter, beamOrientation) == 0x41C, "Emitter::beamOrientation @0x41C (Quaternion 16B)");
static_assert(offsetof(Emitter, beamScale) == 0x42C, "Emitter::beamScale @0x42C");
static_assert(offsetof(Emitter, targetEntity) == 0x474, "Emitter::targetEntity @0x474 (last)");
static_assert(sizeof(Emitter) == 0x478, "sizeof(Emitter) 0x478 (== factory alloc)");
};
#endif
+3
View File
@@ -96,6 +96,9 @@ public:
void SetLevel(int n) { level = n; }
int GetLevel() 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:
// Interior mirrors FUN_0041b9ec: base GaugeAlarm header (+0x00), three count
// words at +0x0c/+0x10/+0x14, three sub-indicators at +0x18/+0x2c/+0x40. The
+23
View File
@@ -655,6 +655,29 @@ Scalar
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
Mech::PerformAndWatch(const Time& till, MemoryStream *update_stream)
{
+20 -11
View File
@@ -160,8 +160,9 @@ MechWeapon::MechWeapon(
rangeToTarget = 0.0f; // 0x324
targetWithinRange = False; // 0x34C
segmentReference = segmentPageIndex;
pipSegment = -1;
// segmentReference/pipSegment were phantom tail members (past the binary 0x3F0 end);
// 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
// (DAT_00511aa2); selects whether the configured pip is drawn.
@@ -358,17 +359,23 @@ void
char *o = (char *)owner; // inherited MechSubsystem::owner (the Mech)
if (o != 0)
position = *(Point3D *)(o + 0x37c); // MECH_TARGET_POS
else
position = targetPoint;
// (no owner -> leave position unchanged; the phantom `targetPoint` member is gone --
// 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).
//
void
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
MechWeapon::GetMuzzlePoint(Point3D &point)
{
// @004b9948 -- the shipped code resolves segmentReference against the owning
// Mech's segment table (SegmentIterator) and transforms the segment origin to
// world space. That transform is produced by the owning Mech's per-frame
// segment update, which caches it in muzzlePoint; we return the cached value.
point = muzzlePoint; // FUN_00424da8 result
// @004b9948 -- resolve the weapon's mount segment (its index is the inherited
// subsystem slot at this+0xdc, == binary param_1+0xdc) in the owning Mech's
// segment table and transform it to world (FUN_00424da8). Reads live each call
// (the phantom cached `muzzlePoint` member is removed -- it collided with the
// 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
// the HUD module; the submission is wired there. We retain the gating and
// transform reference here.
if (this->pipSegment != -1) // FUN_0041a1a4 guard
if (useConfiguredPip) // this+0x3E0 (weapon draws a configured pip)
{
(void)transform;
(void)pipPosition;
+22 -14
View File
@@ -266,7 +266,7 @@ class CockpitHud;
Logical targetWithinRange; // @0x34C PPC.CPP: "if (targetWithinRange)"
// --- 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)
Damage damageData; // @0x3A8 damageType @0x3A8, damageAmount @0x3AC
@@ -277,21 +277,29 @@ class CockpitHud;
Logical useConfiguredPip; // @0x3E0 = (usesExternalModel == 0) (best-effort)
ResourceDescription::ResourceID
explosionResourceID; // @0x3E4 resource ExplosionModelFile
Scalar recoil; // @0x3E8 reset 0 (TODO: name)
int segmentPageIndex; // @0x3EC resource segmentIndex (+0x28)
Scalar recoil; // @0x3E8 reset 0 (Emitter::firingArmed aliases this slot)
int segmentPageIndex; // @0x3EC resource segmentIndex (+0x28) -- LAST field, binary ends 0x3F0
// --- targeting/segment references (best-effort; used by GetMuzzlePoint /
// DrawWeaponPip). segmentReference indexes the owning Mech segment
// table; pipSegment is the HUD pip's mount segment (-1 == none). ---
int segmentReference; // muzzle segment lookup index
int pipSegment; // HUD pip mount segment (-1 == none)
// The binary MechWeapon ends at 0x3F0 (segmentPageIndex is the last ctor write,
// @004b99a8 param_1[0xfb]). The former tail fields segmentReference/pipSegment/
// hasTarget/targetPoint/muzzlePoint were PHANTOM (no binary storage) and, worse,
// collided with the Emitter subclass's own fields at 0x3F0+; removed:
// * 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
// per-frame targeting/segment update; consumed by UpdateTargeting,
// GetTargetPosition, GetMuzzlePoint, GetVectorToWeapon). ---
Logical hasTarget; // owning mech has a locked target
Point3D targetPoint; // current target world position
Point3D muzzlePoint; // resolved weapon muzzle world position
friend struct MechWeaponLayoutCheck;
};
struct MechWeaponLayoutCheck {
static_assert(offsetof(MechWeapon, weaponAlarm) == 0x350, "MechWeapon::weaponAlarm @0x350 (0x54 alarm)");
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
+6 -7
View File
@@ -527,13 +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)");
// BYTE-EXACT: with PoweredSubsystem byte-exact (ends 0x31C) and the phantom
// moverConnection tail removed, Myomers' own fields (ctor @004b8fec: speedEffect@0x31C
// .. accelerationEfficiency@0x354) land exactly at 0x31C..0x358.
static_assert(offsetof(Myomers, speedEffect) == 0x31C, "Myomers::speedEffect @0x31C (attr 0x12)");
static_assert(offsetof(Myomers, seekVoltage) == 0x330, "Myomers::seekVoltage @0x330 (attr 0x17)");
static_assert(sizeof(Myomers) == 0x358, "sizeof(Myomers) 0x358 (factory alloc, byte-exact)");
};
+1 -1
View File
@@ -482,7 +482,7 @@ void
source->currentTapCount -= 1; // source+0x1e8 -= 1
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
char auxScreenLabel[64]; // @0x1E4 resource +0x10C
char engScreenLabel[64]; // @0x224 resource +0x14C
// ⚠ 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
// The binary alarms are 0x54-byte AlarmIndicators (ctor @004b0f74:
// FUN_0041b9ec(param_1+0x99,5) @0x264 and (param_1+0xae,3) @0x2B8; level @+0x14
// == 0x278/0x2CC). Now byte-exact via GaugeAlarm54 -- part of the subsystem-tree
// alarm unification (the whole PoweredSubsystem subtree byte-exacted together).
GaugeAlarm54 electricalStateAlarm; // @0x264 5-level (ElectricalState); level @0x278 -> ends 0x2B8
GaugeAlarm54 modeAlarm; // @0x2B8 3-level (ConnectMode); level @0x2CC -> ends 0x30C
Scalar thermalResistivityCoefficient; // @0x30C resource +0x100
Scalar voltageScale; // @0x310 init 1.0f
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
{
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)");
};