diff --git a/CLAUDE.md b/CLAUDE.md index ab3311e..c679d40 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/docs/HARD_PROBLEMS.md b/docs/HARD_PROBLEMS.md index c3a4d1e..62e41f7 100644 --- a/docs/HARD_PROBLEMS.md +++ b/docs/HARD_PROBLEMS.md @@ -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. diff --git a/game/original/BT/GAUSS.CPP b/game/original/BT/GAUSS.CPP index 7c785b2..09cc06a 100644 --- a/game/original/BT/GAUSS.CPP +++ b/game/original/BT/GAUSS.CPP @@ -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(); } diff --git a/game/reconstructed/emitter.cpp b/game/reconstructed/emitter.cpp index b3ed382..85a1775 100644 --- a/game/reconstructed/emitter.cpp +++ b/game/reconstructed/emitter.cpp @@ -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) diff --git a/game/reconstructed/emitter.hpp b/game/reconstructed/emitter.hpp index defee23..e79915b 100644 --- a/game/reconstructed/emitter.hpp +++ b/game/reconstructed/emitter.hpp @@ -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 diff --git a/game/reconstructed/heat.hpp b/game/reconstructed/heat.hpp index 27fffdd..b39fb89 100644 --- a/game/reconstructed/heat.hpp +++ b/game/reconstructed/heat.hpp @@ -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 diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index fafe0a5..e92d21f 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -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) { diff --git a/game/reconstructed/mechweap.cpp b/game/reconstructed/mechweap.cpp index a5f6df3..09f6149 100644 --- a/game/reconstructed/mechweap.cpp +++ b/game/reconstructed/mechweap.cpp @@ -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; diff --git a/game/reconstructed/mechweap.hpp b/game/reconstructed/mechweap.hpp index 627d887..c09cb9a 100644 --- a/game/reconstructed/mechweap.hpp +++ b/game/reconstructed/mechweap.hpp @@ -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 diff --git a/game/reconstructed/myomers.cpp b/game/reconstructed/myomers.cpp index 2481bf0..9540272 100644 --- a/game/reconstructed/myomers.cpp +++ b/game/reconstructed/myomers.cpp @@ -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)"); }; diff --git a/game/reconstructed/powersub.cpp b/game/reconstructed/powersub.cpp index a7105bd..6a6b544 100644 --- a/game/reconstructed/powersub.cpp +++ b/game/reconstructed/powersub.cpp @@ -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) } // diff --git a/game/reconstructed/powersub.hpp b/game/reconstructed/powersub.hpp index bf4c12e..bffa89c 100644 --- a/game/reconstructed/powersub.hpp +++ b/game/reconstructed/powersub.hpp @@ -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"); }; //########################################################################### diff --git a/game/reconstructed/sensor.cpp b/game/reconstructed/sensor.cpp index 29ba77f..9188c85 100644 --- a/game/reconstructed/sensor.cpp +++ b/game/reconstructed/sensor.cpp @@ -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)"); };