diff --git a/CLAUDE.md b/CLAUDE.md index 6740a9f..0370c6d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -997,6 +997,71 @@ pre-init the seek table in the ctor so it's valid when the ctor src is null); E8 but never set `owner`** (mechsub.cpp) -> every subsystem's `owner` was garbage once heat de-shadowed; now set in both MechSubsystem ctors. Also: the Mech ctor now zeroes the target slots (+0x37c/+0x388/+0x38c) so an un-targeted mech (spawned enemy) reads "no target" (else its weapons fire at a garbage pointer). +**✅ WAVE 4 (standalone readouts) DONE — Sensor / Searchlight / ThermalSight / AmmoBin un-stubbed + ticking** +(verified: **BLH tick 20→26 executable**, Mad Cat 24, combat **DESTROYED** un-regressed on both, 0 crashes). +The 4 readout classes were reconstructed but built as `RECON_SUBSYS` stubs; each is now wired via a +`CreateSubsystem` bridge (in its own .cpp) at the mislabel-correct factory case: 0xBC3→**Sensor** +(`MyomersClassID` label), 0xBD8→**Searchlight** (`LegSubsystem`), 0xBDE→**ThermalSight** (`MechDisplay`), +0xBCB→**AmmoBin** (`JumpJet`). Each got the Torso de-shim: delete the cross-family shadow fields, redirect +accessors to the real inherited base, add a `friend struct LayoutCheck` with `static_assert` +offset/sizeof locks. Redirects by branch: **Sensor** (PoweredSubsystem:HeatSink leaf) → `heatAlarm`/ +`electricalStateAlarm`/`simulationState`/`heatEnergy`/`simulationFlags|=8`; **Searchlight/ThermalSight** +(PowerWatcher) → `watchdogAlarm`/`heatAlarm`/`simulationState`/`ForceUpdate()`/`simulationFlags`; **AmmoBin** +(HeatWatcher) → `simulationState` (deleted the `statusState`@0x40 shadow). +- **⭐ FUNCTIONAL BUG FIXED (Searchlight + ThermalSight): the ctor gate read the shadow `segmentFlags` (=0), so + `((0&0xC)==0 && (0&0x100)!=0)` was ALWAYS FALSE → their Performance NEVER installed (dead since + reconstruction).** The de-shim switches the gate to `owner->simulationFlags` (binary-exact part_013.c: + 6012/6140; mirrors torso.cpp:181) → they now tick. (Sensor uses the simpler `!=4` gate; AmmoBin's gate was + already correct.) +- **AmmoBin layout: `ammoAlarm` retyped `HeatAlarm`(8B)→`WatcherGaugeAlarm`(0x54) + the `statusState` shadow + deleted → `ammoModelFile` lands at the binary 0x1E8, `sizeof==0x22C` exact (locked, the load-bearing lock).** + ⚠ CORRECTION to an earlier assumption: the all-laser Blackhawk is NOT AmmoBin-free — it DOES carry AmmoBins + (the un-stub exposed the crash below ON THE BLH, and BLH tick rose 20→26 = Sensor+Searchlight+Thermal+AmmoBins). +- **⚠ SHARED LATENT BUG EXPOSED + GUARDED (`HeatWatcher::WatchSimulation`, heatfamily_reslice.cpp:305):** the + un-stub made `AmmoBinSimulation` (and via `PowerWatcher::Simulation`, the light/thermal sims) call the + HeatWatcher base `WatchSimulation`, which derefs `watchedLink.Resolve()->currentTemperature` — the watch + link isn't resolved in bring-up (the "WatchedSubsystem" name→roster-segment bind doesn't happen), so + `Resolve()` returns null → AV (`AmmoBinSimulation`→`WatchSimulation`, caught under cdb). The binary never + null-checks (its link always resolves at load). GUARDED (marked): `if (watched==0){ heatAlarm.SetLevel(0); + return; }` — same pattern as `Emitter::TrackSeekVoltage`. FAITHFUL FOLLOW-UP = resolve `watchedLink` so the + heat-watch alarm tracks the real watched temperature (it gates cook-off / light-blank / thermal-blank — + secondary behaviors, inert until then). +- **LAYOUT-LOCK FINDING (a new systemic fact): the heat-leaf branch (PoweredSubsystem:HeatSink) is NOT + byte-exact to the binary** — Sensor's exact-offset asserts (radarPercent@0x31C) FAILED at compile time, + unlike the re-based WATCHER branch (Searchlight/ThermalSight/AmmoBin offsets ARE exact and locked). So + Sensor uses only the OVERFLOW lock (`sizeof<=0x328`); its own fields don't land at the binary offsets — + fine until the attribute-table RAW read (the RadarPercent gauge) is wired (a Phase-4 HUD follow-up needing + the heat-leaf branch re-based, same surgery the Watcher branch got). Nothing reads these subsystems at raw + offsets today, so named-member access (compiled-consistent) is correct meanwhile. +- **Factory now: 14 of 20 cases wired to real ticking classes** (was 10). Remaining stubs: Myomers (0xBC6, + WAVE 6), ProjectileWeapon/MissileLauncher + the unresolved 0xBCE (0xBCD/0xBD0, WAVE 7), SubsystemMessageManager + (0xBD3, WAVE 8), Gyroscope (0xBC4, deferred — NaN integrator). Kept DEFAULT-ON (no env gate — pure readouts, + no bring-up fallback needed). +**✅ WAVE 6 (Myomers 0xBC6, mover-coupled) — STRUCTURAL un-stub DONE (gated `BT_MYOMERS`, default ON; verified: +BLH tick 26→27, locomotion + combat un-regressed, 0 crashes; `BT_MYOMERS=0` falls back to the Actuator stub → +26).** The real Myomers (the mech's artificial-muscle drive — voltage→`speedEffect`, throttled by heat + +damage; ctor @4b8fec, `PoweredSubsystem:HeatSink` leaf) is now built at factory case 0xBC6 ("Actuator" +mislabel) via `CreateMyomersSubsystem`. **KEY: it is wired INERT** — it constructs + ticks its real Performance +but does NOT affect gameplay, which is exactly what makes a mover-coupled subsystem safe to land: (a) +`MyomersSimulation` early-returns on `OwnerAdvancedDamage()` (the advanced-damage sim gate; the real source is +the still-stubbed SubsystemMessageManager 0xBD3, so it's a `False` constant → no motion-heat); (b) the mover +feed (`ConnectToMover`→`MoverAttach`) + `SetOwnerMaxSpeed` are no-op/local stubs → **the live JointedMover is +never touched → the gait/drive cannot regress** (confirmed: the mech walks normally, pos advances, adv ramps +0.13→1.05). The ctor DOES do real work: resolves the powering Generator (`ResolveVoltageSource`) + builds the +`seekVoltage[]` gear table from the resource. **De-shim:** the ~10 cross-family Mech/Mover/DamageZone shim +BACKING fields (ownerBaseSpeed/…/damageStructureLevel + segmentFlags) were DROPPED and their accessors return +the neutral defaults the ctor primed — necessary because the real object is EXACTLY 0x358 (zero headroom) and +the shim fields would overflow the alloc; locked `static_assert(sizeof(Myomers)<=0x358)` (heat-leaf branch not +byte-exact → overflow-lock only, like Sensor). The `Myomers` **name collision** is already handled in +myomers.hpp (`#define Myomers Myomers__powersub_stale` around the powersub.hpp include, since powersub.hpp +still carries the OLD Sensor-mislabel "Myomers" — that stale class should eventually be deleted from +powersub.hpp). **⚠ AUTHENTIC COUPLING = the real Wave-6 payoff, DEFERRED:** making the mech actually SLOW when +damaged/overheated + motion GENERATE heat needs (1) the advanced-damage gate from a real messmgr (0xBD3, +WAVE 8), (2) the `owner*` shims redirected to real Mech motion/mass/motion-gain accessors, and (3) +`MoverAttach` actually routing `speedEffect@0x31C` into the JointedMover roster — which **drives the LIVE +mover**, so it must be reconciled with the gait cutover FIRST (both would feed the mover) before enabling. +Factory now: **15 of 20 cases wired to real classes** (remaining: ProjectileWeapon/0xBCE/MissileLauncher +0xBCD/0xBD0 [WAVE 7], SubsystemMessageManager 0xBD3 [WAVE 8], Gyroscope 0xBC4 [deferred]). **✅ HEAT-FLOW LINKAGE CONNECTED — weapon heat now conducts to the central sink** (verified: `[heat] conduct self.E=.. -> other.E`, sustained fire `FIRED #1..#21+`, combat intact, no crash). THREE fixes, each faithful to the decomp (no stand-ins): @@ -1796,8 +1861,10 @@ brightness is correct — the bug was saturation, not the lack of fade. **Remaining (real weapon fidelity):** per-weapon color from the beam model (blue PPC, yellow standard lasers) once weapon enumeration is wired (factory class IDs, not simulationFlags); the throttled real-fire beam (drive off Emitter::FireWeapon, heat); damage by the aimed zone (cylinder lookup, STEP 6). -Remaining subsystem families (gyro/torso/myomers motion, sensor/searchlight/thermalsight readouts, -projectile/missile weapons 0xBCD/0xBCE/0xBD0) per SUBSYS_PLAN.md WAVEs 4-8. +Remaining subsystem families (projectile/missile weapons 0xBCD/0xBCE/0xBD0 [WAVE 7]; SubsystemMessageManager +0xBD3 [WAVE 8]; Gyroscope 0xBC4 deferred — NaN integrator) per SUBSYS_PLAN.md. DONE: Torso (WAVE 5) + +Sensor/Searchlight/ThermalSight/AmmoBin readouts (WAVE 4) + Myomers structural un-stub (WAVE 6, INERT — +authentic mover/heat coupling deferred). **✅ TORSO twist/elevation aim — joint-I/O + simulation RECONSTRUCTED (dormant; blocked on the WAVE-4 un-stub).** The torso's aim→skeleton path is now fully reconstructed and builds green with zero combat regression, but is diff --git a/docs/SUBSYS_PLAN.md b/docs/SUBSYS_PLAN.md index a209439..8483e1e 100644 --- a/docs/SUBSYS_PLAN.md +++ b/docs/SUBSYS_PLAN.md @@ -88,7 +88,7 @@ The whole roster sits on the BT base chain `engine Subsystem → MechSubsystem - **WAVE 3 — Energy weapons (user: "emitter/PPC second").** - **3a (dependency):** power bus — Generator (0xBC1) + PoweredSubsystem (0xBC2). Low-risk, foundational; energy weapons need it to charge (`GetVoltageState()==4`). - **3b:** Emitter (0xBC8) + PPC (0xBD4). Construct+tick+fire underneath; **keep the explosion stand-in for the visual** (beam renderable deferred — see §4). -- **WAVE 4 — Standalone readouts (low risk, DONE classes):** Sensor (0xBC3), Searchlight (0xBD8), ThermalSight (0xBDE), AmmoBin (0xBCB). Pure power/heat-gated readouts; run standalone. +- **WAVE 4 — Standalone readouts: ✅ DONE + WIRED (live, verified).** Sensor (0xBC3), Searchlight (0xBD8), ThermalSight (0xBDE), AmmoBin (0xBCB) all un-stubbed via `CreateSubsystem` bridges + de-shim + `LayoutCheck` static_asserts. BLH tick 20→26, Mad Cat 24, combat DESTROYED un-regressed, 0 crashes. KEY finds: (a) Searchlight/ThermalSight ctor gate read the shadow `segmentFlags`(=0) → Performance NEVER installed → fixed to `owner->simulationFlags`; (b) AmmoBin `ammoAlarm` retyped `HeatAlarm`→`WatcherGaugeAlarm`(0x54) + `statusState`@0x40 shadow deleted; (c) the heat-leaf branch (Sensor's PoweredSubsystem:HeatSink) is NOT layout-exact → overflow-lock only (offset re-base deferred to Phase-4 HUD); (d) shared `HeatWatcher::WatchSimulation` null-deref on the unresolved `watchedLink` GUARDED (faithful follow-up = resolve the watch link). See CLAUDE.md §10d "WAVE 4". - **WAVE 5 — Torso (0xBC5): ✅ DONE (live, binary-exact layout, zero regression).** The joint I/O (`Mech::ResolveJoint`/`Torso::PushTwist` + corrected `TorsoSimulation` + per-frame `UpdateJoints()`) AND the base-chain re-base are complete. Hierarchy is `Torso : PowerWatcher : HeatWatcher : MechSubsystem` (a SEPARATE @@ -108,7 +108,7 @@ The whole roster sits on the BT base chain `engine Subsystem → MechSubsystem dampingConstant@0x1F4, ...; recon mislabels + leaves accumulators uninit). Reverted to the stub (no NaN to the root joint). Remaining = a full ctor+integrator reconstruction from @004b3778 (bigger than the torso). Full detail: CLAUDE.md §10d "WAVE-5 GYROSCOPE". Also: the mech.cpp gyro↔torso cross-link write is broken (SubProxy::linkTarget→gyro+4) — commented out. -- **WAVE 6 — Mover-coupled:** Myomers (0xBC6). Routes `SpeedEffect` into the Mech mover (`Mech+0x128`), writes `maxSpeed@0x7A0`, loops the myomer list `@0x1EB`. Verify those Mech offsets are valid before enabling (Risk 7), else no-op/wild writes. +- **WAVE 6 — Mover-coupled: ◐ STRUCTURAL un-stub DONE + WIRED (gated `BT_MYOMERS` default ON; verified INERT, no locomotion/combat regression).** Myomers (0xBC6) constructs + ticks its real Performance via `CreateMyomersSubsystem`, but is INERT: `MyomersSimulation` early-returns on `OwnerAdvancedDamage()==False` (real gate = messmgr 0xBD3), and the mover feed (`MoverAttach`) + `SetOwnerMaxSpeed` are no-op stubs so the live JointedMover is never touched. De-shim dropped the `owner*`/`segmentFlags` shim fields (accessors return neutral defaults) to fit the exact-0x358 alloc (`sizeof` lock). ⚠ AUTHENTIC COUPLING DEFERRED: routing `SpeedEffect@0x31C` into the mover + real owner-motion accessors + the advanced-damage heat gate — this DRIVES THE LIVE MOVER, so reconcile with the gait cutover FIRST. BLH tick 26→27; `BT_MYOMERS=0` → Actuator stub. See CLAUDE.md §10d "WAVE 6". - **WAVE 7 — Projectile/missile weapons:** ProjectileWeapon (0xBCD), MissileLauncher (0xBD0). **PARTIALLY BLOCKED:** case 0xBCE calls `@4bdcb4`, which has no CLASSMAP entry — decompile it first and resolve the 0xBCD/0xBCE/0xBD0 label tangle (Risk 1) before swapping these. - **WAVE 8 — Message hub:** SubsystemMessageManager (0xBD3). Real, but **verify the cache-slot/tick-bypass interaction** (Risk 4) before enabling its tick. diff --git a/game/reconstructed/ammobin.cpp b/game/reconstructed/ammobin.cpp index 1acd026..22890a7 100644 --- a/game/reconstructed/ammobin.cpp +++ b/game/reconstructed/ammobin.cpp @@ -126,7 +126,8 @@ AmmoBin::AmmoBin( { // vtable installed by the compiler (PTR_FUN_0051286c) - statusState = 0; + // (WAVE 4) statusState=0 removed -- the base MechSubsystem ctor owns + // simulationState@0x40; the reads below use the inherited member. ammoAlarm.Initialize(6); // @0x194 FUN_0041b9ec(this+0x65, 6) InitCookOffHeatSource(&cookOffHeatSource); // @0x1F0 FUN_0041db7c(this+0x7c) reserved = 0; // @0x228 @@ -171,7 +172,7 @@ AmmoBin::~AmmoBin() // int AmmoBin::FeedAmmo() { - if (statusState == 1) // this+0x40 == 1 (destroyed) + if (simulationState == 1) // this+0x40 == 1 (destroyed) return 0; switch (ammoAlarm.Level()) // this+0x1A8 @@ -243,7 +244,7 @@ void AmmoBin::AmmoBinSimulation(Scalar time_slice) { WatchSimulation(time_slice); // FUN_004aeac4(this) -- drive this+0x140 - if (statusState == 1) // this+0x40 (destroyed) + if (simulationState == 1) // this+0x40 (destroyed) ammoAlarm.SetLevel(Empty); // feed timer (only while a round is in transit) @@ -313,9 +314,9 @@ Logical AmmoBin::HandleMessage(int message) // void AmmoBin::ReadUpdateRecord(UpdateRecord *update) { - int previousState = statusState; // this+0x40 + int previousState = simulationState; // this+0x40 (inherited MechSubsystem) HeatWatcher::ReadUpdateRecord(update); // FUN_004aea84 - if (statusState == 1 && previousState != 1) + if (simulationState == 1 && previousState != 1) { ammoCount = 0; // this+0x180 ammoAlarm.SetLevel(Empty); // 2 @@ -430,3 +431,42 @@ Logical AmmoBin::TestClass(Mech &) { return True; } + + +//############################################################################# +// WAVE 4 -- compile-time layout locks (ZERO-headroom: sizeof MUST be 0x22C). +// Own fields are protected -> the friend AmmoBinLayoutCheck grants offsetof. +// The `ammoModelFile == 0x1E8` lock is LOAD-BEARING: it fails the build if +// either the ammoAlarm retype (HeatAlarm 8B -> WatcherGaugeAlarm 0x54) or the +// statusState deletion is missed. HeatWatcher is locked at 0x180. +//############################################################################# +struct AmmoBinLayoutCheck +{ + static_assert(sizeof(AmmoBin) == 0x22C, "sizeof(AmmoBin) must be 0x22C (factory alloc, part_012.c:10091)"); + static_assert(offsetof(AmmoBin, ammoCount) == 0x180, "AmmoBin own block starts at 0x180 (HeatWatcher ends 0x180)"); + static_assert(offsetof(AmmoBin, ammoAlarm) == 0x194, "AmmoBin ammoAlarm @0x194 (0x54-byte WatcherGaugeAlarm)"); + static_assert(offsetof(AmmoBin, ammoModelFile) == 0x1E8, "AmmoBin ammoModelFile @0x1E8 (proves ammoAlarm==0x54 AND statusState deleted)"); + static_assert(offsetof(AmmoBin, explosionModelFile) == 0x1EC, "AmmoBin explosionModelFile @0x1EC"); + static_assert(offsetof(AmmoBin, heatPerRound) == 0x1F4, "AmmoBin heatPerRound @0x1F4"); + static_assert(offsetof(AmmoBin, initialAmmoCount) == 0x220, "AmmoBin initialAmmoCount @0x220"); + static_assert(offsetof(AmmoBin, feedRate) == 0x224, "AmmoBin feedRate @0x224"); + static_assert(offsetof(AmmoBin, reserved) == 0x228, "AmmoBin reserved @0x228 (+4 => sizeof 0x22C)"); +}; + + +//===========================================================================// +// Factory bridge -- AmmoBin (factory case 0xBCB, "JumpJet" label). +// The real class at 0xBCB (ctor @004bd5c4) is AmmoBin; the factory built a +// JumpJet RECON_SUBSYS stub. alloc 0x22c, ctor(owner,id,resource) -- the ctor +// supplies AmmoBin::DefaultData to the HeatWatcher base itself. No cache +// write; not a weapon (raw part_012.c:10090-10099 stores roster-only). +// NOTE: the Blackhawk (all-laser) carries NO 0xBCB; exercise with a ballistic/ +// missile mech (MECH2.EGG = Mad Cat, LRMs). Its FeedAmmo consumer +// (ProjectileWeapon/MissileLauncher 0xBCD/0xBD0) is still stubbed, so full +// effect couples to the weapons wave -- but the bin's own feed/cook-off ticks. +//===========================================================================// +Subsystem *CreateAmmoBinSubsystem(Mech *owner, int id, void *seg) +{ + return (Subsystem *) new (Memory::Allocate(0x22c)) + AmmoBin(owner, id, (AmmoBin::SubsystemResource *)seg); +} diff --git a/game/reconstructed/ammobin.hpp b/game/reconstructed/ammobin.hpp index 8f8580d..915c5ab 100644 --- a/game/reconstructed/ammobin.hpp +++ b/game/reconstructed/ammobin.hpp @@ -77,6 +77,7 @@ class AmmoBin: public HeatWatcher { + friend struct AmmoBinLayoutCheck; // compile-time offset locks (ammobin.cpp) //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Shared Data Support // @@ -203,8 +204,11 @@ int ammoClassID; // @0x188 (word 0x62) entity class spawned per round int cookOffArmed; // @0x18C (word 0x63) cook-off timer running int cookOffTime; // @0x190 (word 0x64) game clock at which it detonates - HeatAlarm ammoAlarm; // @0x194 (word 0x65) 6-level feed alarm (level @0x1A8) - int statusState; // base subsystem damage-state mirror (this+0x40) + WatcherGaugeAlarm ammoAlarm; // @0x194 (0x54) 6-level feed alarm (level @0x1A8) + // (WAVE 4) `int statusState` DELETED -- it was a this+0x40 SHADOW of the + // inherited MechSubsystem::simulationState; the reads now use that base + // member. The 0x54 ammoAlarm (was an 8-byte HeatAlarm) lands ammoModelFile + // at 0x1E8 -- exact binary layout, locked by AmmoBinLayoutCheck. int ammoModelFile; // @0x1E8 (word 0x7A) round model index int explosionModelFile; // @0x1EC (word 0x7B) cook-off explosion model index // @0x1F0 (word 0x7C) cook-off heat-injection descriptor, inited by diff --git a/game/reconstructed/heatfamily_reslice.cpp b/game/reconstructed/heatfamily_reslice.cpp index 4d2d119..d8629d0 100644 --- a/game/reconstructed/heatfamily_reslice.cpp +++ b/game/reconstructed/heatfamily_reslice.cpp @@ -302,6 +302,23 @@ void HeatWatcher::WatchSimulation(Scalar /*time_slice*/) // FUN_004aeac4 { HeatableSubsystem *watched = (HeatableSubsystem *)watchedLink.Resolve(); // FUN_00417ab4(this+0x114) + + // BRING-UP GUARD (marked deviation): in the shipped game watchedLink ALWAYS + // resolves -- the HeatWatcher resource's "WatchedSubsystem" name binds to a + // roster segment at load -- so the binary (FUN_004aeac4) derefs `watched` + // unconditionally. Our port does not yet resolve the watch link, so Resolve() + // returns null; the deref AVs the moment a HeatWatcher-derived subsystem's + // Performance actually runs (exposed by the WAVE 4 un-stub: AmmoBinSimulation + // calls this directly; PowerWatcher::Simulation for Searchlight/ThermalSight). + // With no resolved source, hold the heat alarm at Normal (same established + // pattern as Emitter::TrackSeekVoltage's null-source guard). FAITHFUL + // FOLLOW-UP = resolve watchedLink so the alarm tracks the real temperature. + if (watched == 0) + { + heatAlarm.SetLevel(0); // NormalHeat (no watched source yet) + return; + } + Scalar temp = watched->currentTemperature; // *(watched + 0x114) if (temp > failureTemperature) // > this+0x124 diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index 4ba074a..aca1cbb 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -196,6 +196,11 @@ extern Subsystem *CreateGeneratorSubsystem(Mech *, int, void *); // 0xBC1 (WAVE extern Subsystem *CreatePoweredSubsystem(Mech *, int, void *); // 0xBC2 (WAVE 3a) extern Subsystem *CreateEmitterSubsystem(Mech *, int, void *); // 0xBC8/0xBD4 (WAVE 3b) extern Subsystem *CreateTorsoSubsystem(Mech *, int, void *); // 0xBC5 (WAVE 4 -- base re-based, layout locked) +extern Subsystem *CreateSensorSubsystem(Mech *, int, void *); // 0xBC3 (WAVE 4 readouts -- de-shimmed, layout locked) +extern Subsystem *CreateSearchlightSubsystem(Mech *, int, void *); // 0xBD8 (WAVE 4 readouts -- de-shimmed + gate fixed) +extern Subsystem *CreateThermalSightSubsystem(Mech *, int, void *); // 0xBDE (WAVE 4 readouts -- de-shimmed + gate fixed) +extern Subsystem *CreateAmmoBinSubsystem(Mech *, int, void *); // 0xBCB (WAVE 4 -- AmmoBin : HeatWatcher, layout locked) +extern Subsystem *CreateMyomersSubsystem(Mech *, int, void *); // 0xBC6 (WAVE 6 -- mover-coupled drive; INERT, gated BT_MYOMERS) // CreateGyroSubsystem (0xBC4) is reconstructed + ready in gyro.cpp (layout re-based, // joint I/O real), but NOT wired -- the gyro ctor/integrator reconstruction is // incomplete (garbage output); see the 0xBC4 case below. @@ -689,9 +694,11 @@ Mech::Mech( subsystemArray[id] = CreatePoweredSubsystem(this, id, seg); // FUN_004b0f74 break; - case MyomersClassID: // 0xBC3 - subsystemArray[id] = (Subsystem *) - new (Memory::Allocate(0x328)) Myomers(this, id, seg); // FUN_004b1d18 + case MyomersClassID: // 0xBC3 -> real class Sensor (ctor @004b1d18, "Myomers" mislabel) + // WAVE 4: the real class at 0xBC3 is Sensor (string pool @0050fae0 + + // SENSOR.HPP); MyomersClassID is a factory-enum mislabel (real Myomers + // is 0xBC6). De-shimmed + layout-locked (radarPercent@0x31C, sizeof==0x328). + subsystemArray[id] = CreateSensorSubsystem(this, id, seg); // FUN_004b1d18 break; case GyroClassID: // 0xBC4 -> real class Gyroscope (ctor @004b3778) @@ -717,9 +724,19 @@ Mech::Mech( sinkSourceSubsystem = subsystemArray[id]; // Wword(0x10e) -- gyro links to +0x1D8 break; - case ActuatorClassID: // 0xBC6 - subsystemArray[id] = (Subsystem *) - new (Memory::Allocate(0x358)) Actuator(this, id, seg); // FUN_004b8fec + case ActuatorClassID: // 0xBC6 -> real class Myomers (ctor @004b8fec, "Actuator" stub) + // WAVE 6: the real class at 0xBC6 is Myomers (the artificial-muscle + // drive). GATED BT_MYOMERS (default ON): the real class constructs + + // ticks but is INERT w.r.t. locomotion/heat (no-op mover feed + the + // advanced-damage sim gate is False), so it can't regress the gait. + // BT_MYOMERS=0 falls back to the Actuator stub. Authentic mover/heat + // coupling is a follow-up (needs real Mech accessors + messmgr 0xBD3, + // and must reconcile the mover feed with the gait cutover first). + if (BTEnvOn("BT_MYOMERS", 1)) + subsystemArray[id] = CreateMyomersSubsystem(this, id, seg); // FUN_004b8fec + else + subsystemArray[id] = (Subsystem *) + new (Memory::Allocate(0x358)) Actuator(this, id, seg); break; case WeaponEmitterClassID: // 0xBC8 -> real Emitter beam weapon @@ -727,9 +744,11 @@ Mech::Mech( ++weaponCount; break; - case JumpJetClassID: // 0xBCB - subsystemArray[id] = (Subsystem *) - new (Memory::Allocate(0x22c)) JumpJet(this, id, seg); // FUN_004bd5c4 + case JumpJetClassID: // 0xBCB -> real class AmmoBin (ctor @004bd5c4, mislabeled) + // WAVE 4: AmmoBin : HeatWatcher : MechSubsystem; own block @0x180, + // sizeof 0x22C compile-time locked. Feeds a ProjectileWeapon/ + // MissileLauncher (0xBCD/0xBD0) via a SharedData connection (still stubbed). + subsystemArray[id] = CreateAmmoBinSubsystem(this, id, seg); // FUN_004bd5c4 break; case MechWeaponClassID: // 0xBCD @@ -766,9 +785,11 @@ Mech::Mech( hudSubsystem = subsystemArray[id]; // raw part_012.c:10164 param_1[0x16d] = HUD break; - case LegSubsystemClassID: // 0xBD8 - subsystemArray[id] = (Subsystem *) - new (Memory::Allocate(0x238)) LegSubsystem(this, id, seg); // FUN_004b84dc + case LegSubsystemClassID: // 0xBD8 -> real class Searchlight (ctor @004b84dc, mislabeled) + // WAVE 4: de-shimmed + GATE FIXED (was gating on the shadow + // segmentFlags -> Performance never installed). Searchlight : + // PowerWatcher; own fields layout-locked at 0x1D8+. + subsystemArray[id] = CreateSearchlightSubsystem(this, id, seg); // FUN_004b84dc break; case HeatableClassID: // 0xBDC -> real class MechTech (mislabeled). @@ -778,9 +799,11 @@ Mech::Mech( // the 0x5b4 cache belongs to case 0xbd6 (the HUD) -- earlier recon had it swapped. break; - case DisplayClassID: // 0xBDE - subsystemArray[id] = (Subsystem *) - new (Memory::Allocate(0x234)) MechDisplay(this, id, seg); // FUN_004b8718 + case DisplayClassID: // 0xBDE -> real class ThermalSight (ctor @004b8718, "MechDisplay" stub) + // WAVE 4: de-shimmed + GATE FIXED. ThermalSight : PowerWatcher; + // own fields layout-locked at 0x1D8+. The cockpit IR view-mode + // render hooks (pvision/viewport) are clearly-marked no-ops. + subsystemArray[id] = CreateThermalSightSubsystem(this, id, seg); // FUN_004b8718 break; default: diff --git a/game/reconstructed/myomers.cpp b/game/reconstructed/myomers.cpp index 5fb3d84..595fc91 100644 --- a/game/reconstructed/myomers.cpp +++ b/game/reconstructed/myomers.cpp @@ -142,18 +142,10 @@ Myomers::Myomers( // (heat-base default coefficient @0x150 = 0.15f; inherited HeatSink slot) // (Subsystem flags @0x28 |= 8; optional graphic-state hook -- see header note) - // CROSS-FAMILY shim backing fields -- neutral defaults; the real values are - // supplied by the owning Mech / its mover / this segment's DamageZone. - ownerBaseSpeed = 1.0f; - ownerMaxSpeed = 0.0f; - ownerVelocityMag = 0.0f; - ownerDriveMag = 0.0f; - ownerVelocityY = 0.0f; - ownerMotionGain = 0.0f; - ownerMass = 0.0f; - ownerAdvancedDamage = False; - damageStructureLevel = 0.0f; - moverConnection = -1; // @0x110 + // (WAVE 6 de-shim) the owner*/damageStructure shim backing fields are gone -- + // their accessors return neutral defaults directly. Only moverConnection + // (used by ConnectToMover) remains a member. + moverConnection = -1; // @0x110 (detached) // Register the per-tick Performance. The binary ctor @004b8fec stores the // MyomersSimulation pointer-to-member into activePerformance (this[7..9] = @@ -163,11 +155,8 @@ Myomers::Myomers( // torso this[7..9]=PTR_FUN_00510c10). The earlier reconstruction mislabeled // this store a "graphic-state hook" and omitted the registration, leaving // activePerformance at the DoNothingOnce default (drops out after frame 1). - // segmentFlags is the CROSS-FAMILY Mech-segment field, primed to the live- - // master value so the drive/heat integrator actually ticks. - segmentFlags = MasterHeatSinkFlag; - // INTEGRATION (gate reconcile): read OWNER simulationFlags (param_2+0x28) — - // the oracle-verified authoritative source — not the local primed shim. + // (WAVE 6) the segmentFlags shim is removed; the gate reads OWNER + // simulationFlags (param_2+0x28) directly -- the oracle-verified source. if ((owner->simulationFlags & SegmentCopyMask) == 0 && (owner->simulationFlags & MasterHeatSinkFlag) != 0) // owner flags & 0x100 (binary @004b8fec) { @@ -527,3 +516,33 @@ void Myomers::ToggleSeekVoltage() { // cycle currentSeekVoltageIndex within [minSeekVoltageIndex, maxSeekVoltageIndex] } + + +//===========================================================================// +// WAVE 6 -- compile-time OVERFLOW lock (the real class must fit the 0x358 +// factory alloc). Like Sensor, the heat-leaf base (PoweredSubsystem:HeatSink) +// is NOT byte-exact to the binary, so we lock only the overflow bound, not the +// exact field offsets -- nothing reads Myomers at a raw offset today (the mover +// feed + gauge readouts are inert) and MyomersSimulation uses named members. +// The de-shim dropped the owner*/segmentFlags backing fields to make room. +//===========================================================================// +struct MyomersLayoutCheck +{ + static_assert(sizeof(Myomers) <= 0x358, "sizeof(Myomers) must fit the factory Memory::Allocate(0x358)"); +}; + + +//===========================================================================// +// WAVE 6 factory bridge -- Myomers (factory case 0xBC6, "Actuator" stub). +// The real class at 0xBC6 (ctor @004b8fec) is Myomers (the mech's artificial- +// muscle drive); the factory built an Actuator RECON_SUBSYS stub. alloc 0x358. +// The subsystem constructs (resolves its Generator + builds the seek-voltage +// gear table) and ticks its real Performance, but is currently INERT w.r.t. +// locomotion/heat (no-op mover feed + the OwnerAdvancedDamage()==False sim gate) +// -- so it cannot regress the gait/drive. Authentic coupling = a follow-up. +//===========================================================================// +Subsystem *CreateMyomersSubsystem(Mech *owner, int id, void *seg) +{ + return (Subsystem *) new (Memory::Allocate(0x358)) + Myomers(owner, id, (Myomers::SubsystemResource *)seg); +} diff --git a/game/reconstructed/myomers.hpp b/game/reconstructed/myomers.hpp index 45856ae..66960fc 100644 --- a/game/reconstructed/myomers.hpp +++ b/game/reconstructed/myomers.hpp @@ -282,43 +282,41 @@ class Mech; // belong on Mech / JointedMover / DamageZone -- see report CROSS-FAMILY NEEDS. // protected: - Scalar OwnerBaseSpeed() const { return ownerBaseSpeed; } // Mech +0x34C base-speed scalar - Scalar OwnerMaxSpeed() const { return ownerMaxSpeed; } // Mech +0x7A0 top speed - void SetOwnerMaxSpeed(Scalar s) { ownerMaxSpeed = s; } - Scalar OwnerVelocityMag() const { return ownerVelocityMag; } // |Mech velocity| (+0x1C4..) - Scalar OwnerDriveMag() const { return ownerDriveMag; } // |Mech drive/accel| (+0x82C..) - Scalar OwnerVelocityY() const { return ownerVelocityY; } // Mech velocity.y (+0x1C8) - Scalar OwnerMotionGain() const { return ownerMotionGain; } // Mech +0x20C - Scalar OwnerMass() const { return ownerMass; } // *Mech +0x250 - Logical OwnerAdvancedDamage() const { return ownerAdvancedDamage; } // FUN_004ad7d4 sim-control gate - Scalar DamageStructureLevel() const{ return damageStructureLevel; }// DamageZone +0x158 + // WAVE 6 un-stub -- CROSS-FAMILY Mech/Mover/DamageZone couplings. These + // logically read the owning Mech's motion state, its JointedMover roster, + // and this segment's DamageZone -- none reachable from a PoweredSubsystem + // here (owner is an incomplete Mech*, JointedMover exposes no roster API, + // DamageZone is not in this hierarchy). To let the real class FIT its 0x358 + // factory alloc, the shim BACKING FIELDS are dropped and the accessors + // return the neutral defaults the ctor used to prime, so the subsystem + // CONSTRUCTS + TICKS but is INERT w.r.t. locomotion/heat: the mover feed is + // a no-op and MyomersSimulation early-returns on OwnerAdvancedDamage()==False. + // ⚠ FAITHFUL FOLLOW-UP: redirect these to real Mech/JointedMover/DamageZone + // accessors + the advanced-damage gate (SubsystemMessageManager 0xBD3) so the + // mech actually slows when damaged/overheated and motion generates heat -- + // that path DOES drive the live mover, so reconcile it with the gait cutover + // FIRST (both would feed the mover). + Scalar OwnerBaseSpeed() const { return 1.0f; } // Mech +0x34C (neutral) + Scalar OwnerMaxSpeed() const { return 0.0f; } // Mech +0x7A0 + void SetOwnerMaxSpeed(Scalar) { } // no-op (real: write Mech top speed) + Scalar OwnerVelocityMag() const { return 0.0f; } // |Mech velocity| + Scalar OwnerDriveMag() const { return 0.0f; } // |Mech drive/accel| + Scalar OwnerVelocityY() const { return 0.0f; } // Mech velocity.y + Scalar OwnerMotionGain() const { return 0.0f; } // Mech +0x20C + Scalar OwnerMass() const { return 0.0f; } // *Mech +0x250 + Logical OwnerAdvancedDamage() const { return False; } // advanced-damage gate (keeps sim INERT) + Scalar DamageStructureLevel() const{ return 0.0f; } // DamageZone +0x158 - void MoverAttach(int index, Scalar *feed) { (void)index; (void)feed; } // JointedMover vtable +0x38 - void MoverDetach(int index) { (void)index; } // JointedMover vtable +0x3C - void MoverRelease(int handle, Scalar *feed) { (void)handle; (void)feed; } // JointedMover vtable +0x44 + void MoverAttach(int index, Scalar *feed) { (void)index; (void)feed; } // JointedMover vtable +0x38 (no-op) + void MoverDetach(int index) { (void)index; } // JointedMover vtable +0x3C (no-op) + void MoverRelease(int handle, Scalar *feed) { (void)handle; (void)feed; } // JointedMover vtable +0x44 (no-op) - Scalar ownerBaseSpeed; - Scalar ownerMaxSpeed; - Scalar ownerVelocityMag; - Scalar ownerDriveMag; - Scalar ownerVelocityY; - Scalar ownerMotionGain; - Scalar ownerMass; - Logical ownerAdvancedDamage; - Scalar damageStructureLevel; - int moverConnection; // @0x110 mover attachment handle (-1 == detached) - - // CROSS-FAMILY: owning-Mech segment flags (read at owner+0x28 in the - // binary ctor @004b8fec). The Performance is registered only for the - // live MASTER segment: (flags & 0xC)==0 && (flags & 0x100). Backed - // locally and primed to the live-master value so MyomersSimulation is - // installed and the drive/heat integrator ticks; the real flags belong - // on the Mech segment. See report CROSS-FAMILY NEEDS. + // Live-master gate constants (the ctor reads owner->simulationFlags). enum { SegmentCopyMask = 0x0C, // (flags & 0xC): 0 == master, 4 == damaged copy MasterHeatSinkFlag = 0x0100 // live-master segment bit }; - Word segmentFlags; + int moverConnection; // @0x110 mover attachment handle (-1 == detached; set by ConnectToMover) //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Local data. Offsets are byte offsets into the shipped object. diff --git a/game/reconstructed/searchlight.cpp b/game/reconstructed/searchlight.cpp index 8b00ae4..72bfddb 100644 --- a/game/reconstructed/searchlight.cpp +++ b/game/reconstructed/searchlight.cpp @@ -82,21 +82,20 @@ Searchlight::Searchlight( Check(owner); Check_Pointer(subsystem_resource); - segmentFlags = 0; - hostShutDown = False; - watchedVoltageLevel = 4; // Ready - heatStateLevel = 0; // NormalHeat - controlsAllowLights = True; - graphicsDirty = False; - instanceFlags = 0; - + // (WAVE 4 de-shim) the 7 cross-family shim fields are gone; the accessors + // read the inherited base state directly. Only the own fields init here. lightState = 0; // @0x1D8 requestedOn = 0; // @0x1E0 stateAlarm.Initialize(2); // FUN_0041b9ec(this+0x1E4, 2) commandedOn = subsystem_resource->segmentIndex; // @0x1DC <- inherited resource +0x28 - if (((GetSegmentFlags() & 0xC) == 0) && - ((GetSegmentFlags() & 0x100) != 0)) + // GATE FIX (the functional bug the de-shim unblocks): the binary reads OWNER + // simulationFlags (param_2+0x28, raw part_013.c:6012), NOT the old shadow + // `segmentFlags` -- which was seeded 0, so the gate was ALWAYS FALSE and + // SearchlightSimulation NEVER installed. A live master segment arms it; a + // copy/replicant gets DontExecute (flag 2). Mirrors torso.cpp:181-182. + if (((owner->simulationFlags & 0xC) == 0) && + ((owner->simulationFlags & 0x100) != 0)) { SetPerformance(&Searchlight::SearchlightSimulation); // this[7..9] <- PTR_FUN_00511200 } @@ -256,3 +255,34 @@ int return True; } + + +//===========================================================================// +// WAVE 4 -- compile-time layout locks. PowerWatcher is Torso-proven at 0x1D8, +// so the own fields land at their EXACT binary offsets; sizeof is bounded to +// the factory alloc. (BtAlarm is a 4-byte stand-in for the binary's 0x54 +// GaugeAlarm -- immaterial: nothing reads Searchlight at a raw offset past its +// own fields, and the shared BT_LOCAL_ALARM_SHIM stays 4 bytes because +// thermalsight.hpp shares that guard.) +//===========================================================================// +struct SearchlightLayoutCheck +{ + static_assert(offsetof(Searchlight, lightState) == 0x1D8, "Searchlight lightState @0x1D8 (PowerWatcher ends 0x1D8)"); + static_assert(offsetof(Searchlight, commandedOn) == 0x1DC, "Searchlight commandedOn @0x1DC (res+0x28)"); + static_assert(offsetof(Searchlight, requestedOn) == 0x1E0, "Searchlight requestedOn @0x1E0"); + static_assert(offsetof(Searchlight, stateAlarm) == 0x1E4, "Searchlight stateAlarm @0x1E4 (proves base ends 0x1D8)"); + static_assert(sizeof(Searchlight) <= 0x238, "sizeof(Searchlight) must fit the factory Memory::Allocate(0x238)"); +}; + + +//===========================================================================// +// WAVE 4 factory bridge -- Searchlight (factory case 0xBD8, "LegSubsystem" label). +// The real class at 0xBD8 (ctor @004b84dc) is Searchlight; the factory built a +// LegSubsystem RECON_SUBSYS stub. No cache write; not a weapon (raw +// part_012.c:10166-10175 stores roster-only). +//===========================================================================// +Subsystem *CreateSearchlightSubsystem(Mech *owner, int id, void *seg) +{ + return (Subsystem *) new (Memory::Allocate(0x238)) + Searchlight(owner, id, (Searchlight::SubsystemResource *)seg, Searchlight::DefaultData); +} diff --git a/game/reconstructed/searchlight.hpp b/game/reconstructed/searchlight.hpp index c0d7741..b5f9c1d 100644 --- a/game/reconstructed/searchlight.hpp +++ b/game/reconstructed/searchlight.hpp @@ -85,6 +85,7 @@ class Searchlight: public PowerWatcher { + friend struct SearchlightLayoutCheck; // compile-time offset locks (searchlight.cpp) //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Shared Data Support // @@ -131,23 +132,31 @@ // are backed by local members so this module compiles; see report. // public: - Word GetSegmentFlags() const { return segmentFlags; } - Logical HostShutDown() const { return hostShutDown; } - int WatchedVoltageLevel() const { return watchedVoltageLevel; } // PowerWatcher API - int HeatStateLevel() const { return heatStateLevel; } // HeatSink API - Logical ControlsAllowLights() const { return controlsAllowLights; } // Mech controls mapper - void SetGraphicsDirty() { graphicsDirty = True; } // this[0x18] |= 1 - void SetInstanceFlag(int bits) { instanceFlags |= bits; } // this[10] |= bits + // De-shimmed (WAVE 4 un-stub): the 7 CROSS-FAMILY backing fields + // (segmentFlags/hostShutDown/watchedVoltageLevel/heatStateLevel/ + // controlsAllowLights/graphicsDirty/instanceFlags) are DELETED. They + // duplicated engine-base state AND -- via the old shadow `segmentFlags` + // (seeded 0) -- made the ctor gate ((0&0xC)==0 && (0&0x100)!=0) FALSE, so + // SearchlightSimulation was NEVER installed. Accessors now read the REAL + // inherited base members (mirrors torso.hpp/torso.cpp): + // HostShutDown <- MechSubsystem::simulationState == Destroyed (this+0x40) + // WatchedVoltageLevel <- PowerWatcher::watchdogAlarm level (this+0x198) + // HeatStateLevel <- HeatWatcher::heatAlarm level (this+0x140) + // SetGraphicsDirty -> engine Simulation::ForceUpdate() (this+0x18) + // SetInstanceFlag -> engine Simulation::simulationFlags |= bits (this+0x28) + // GetSegmentFlags() removed (the ctor gate now reads owner->simulationFlags). + // ControlsAllowLights(): no ported base member (the real read is the + // owner controls-mapper lightsEnabled via an unsafe raw owner-offset + // chain) -> return True for bring-up; the only consumer (ToggleLamp) is + // dormant. Faithful follow-up = a real Mech::ControlsAllowLights(). + Logical HostShutDown() const { return simulationState == 1; /* Destroyed */ } + int WatchedVoltageLevel() const { return watchdogAlarm.GetLevel(); } // @0x198 + int HeatStateLevel() const { return heatAlarm.GetLevel(); } // @0x140 + Logical ControlsAllowLights() const { return True; } // bring-up (see above) + void SetGraphicsDirty() { ForceUpdate(); } // this[0x18] |= 1 + void SetInstanceFlag(int bits) { simulationFlags |= bits; } // this[10] |= bits // The "ToggleLamp" command arg is carried at message+0xC (decomp). static int MessageArg(Message &m) { return *(const int *)((const char *)&m + 0xC); } - protected: - Word segmentFlags; // Mech-supplied segment flags - Logical hostShutDown; // this[0x40] host-power shutdown flag - int watchedVoltageLevel; // @0x198 watched-voltage alarm level - int heatStateLevel; // @0x140 heat-state alarm level - Logical controlsAllowLights; // owner controls-mapper lightsEnabled - Logical graphicsDirty; // this[0x18] graphics-dirty bit - int instanceFlags; // this[10] instance flags //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Simulation Support diff --git a/game/reconstructed/sensor.cpp b/game/reconstructed/sensor.cpp index e052c76..4e043cb 100644 --- a/game/reconstructed/sensor.cpp +++ b/game/reconstructed/sensor.cpp @@ -136,12 +136,8 @@ Sensor::Sensor( Check(owner); Check_Pointer(subsystem_resource); - segmentFlags = 0; - hasActivePerformance = False; - heatStateLevel = NormalHeat; - heatModelOff = False; - electricalState = Ready; - + // (WAVE 4 de-shim) the 5 cross-family shim fields are gone; the accessors + // now read the inherited base state directly. Only the 3 own fields init here. radarPercent = RadarBaseline; // @0x31C = 1.0f selfTest = False; // @0x320 = 0 badVoltage = False; // @0x324 = 0 @@ -363,3 +359,37 @@ int Check_Fpu(); return True; } + + +//===========================================================================// +// WAVE 4 -- compile-time OVERFLOW lock (the property that matters for the +// placement-new into the 0x328 factory alloc). +// NOTE: the reconstructed HEAT-LEAF base chain (PoweredSubsystem : HeatSink : +// HeatableSubsystem) is NOT byte-exact to the binary (unlike the re-based +// WATCHER branch Torso sits on), so radarPercent does NOT land at the binary's +// 0x31C. That is fine here: SensorSimulation reads every field through NAMED +// members (compiled-consistent), and NOTHING reads Sensor at a raw binary +// offset yet. The one raw-offset consumer is the attribute table +// (RadarPercent -> 0x31C), used only by the (un-drawn) cockpit gauge -- so the +// exact-offset re-base of the heat-leaf branch is a Phase-4 HUD follow-up. +// Until then, no-overflow (sizeof <= alloc) is the correct, sufficient lock. +//===========================================================================// +struct SensorLayoutCheck +{ + static_assert(sizeof(Sensor) <= 0x328, "sizeof(Sensor) must fit the factory Memory::Allocate(0x328)"); +}; + + +//===========================================================================// +// WAVE 4 factory bridge -- Sensor (factory case 0xBC3, "Myomers" mislabel). +// The real class at 0xBC3 (ctor @004b1d18) is Sensor (string pool @0050fae0 + +// surviving SENSOR.HPP); MyomersClassID is a factory-enum mislabel (the real +// Myomers is 0xBC6/@4b8fec). Object is 0x328 == the factory alloc (zero +// headroom; locked by SensorLayoutCheck above). Sensor's ctor is 3-arg +// (DefaultData applied internally) -- no shared_data argument. +//===========================================================================// +Subsystem *CreateSensorSubsystem(Mech *owner, int id, void *seg) +{ + return (Subsystem *) new (Memory::Allocate(0x328)) + Sensor(owner, id, (Sensor::SubsystemResource *)seg); +} diff --git a/game/reconstructed/sensor.hpp b/game/reconstructed/sensor.hpp index a27199a..9b5db12 100644 --- a/game/reconstructed/sensor.hpp +++ b/game/reconstructed/sensor.hpp @@ -111,24 +111,27 @@ }; #endif public: - Word - GetSegmentFlags() const { return segmentFlags; } + // De-shimmed (WAVE 4 un-stub): the 5 CROSS-FAMILY backing fields + // (segmentFlags/hasActivePerformance/heatStateLevel/heatModelOff/ + // electricalState) are DELETED -- they over-sized the object past its + // zero-headroom 0x328 factory alloc AND duplicated engine-base state. + // The accessors now read the REAL inherited base members: + // HeatStateLevel <- HeatSink::heatAlarm level (this+0x170) + // ElectricalState <- PoweredSubsystem::electricalStateAlarm level (this+0x278) + // HeatModelOff <- MechSubsystem::simulationState == Destroyed (this+0x40) + // HeatMasterEnergy <- inherited HeatSink::heatEnergy (this+0x158) + // SetHasPerformanceFlag -> engine Simulation::simulationFlags |= 8 (this+0x28) + // GetSegmentFlags() removed (unused; the ctor gate reads owner->simulationFlags). HeatSink::HeatState - HeatStateLevel() const { return heatStateLevel; } + HeatStateLevel() const { return (HeatSink::HeatState)heatAlarm.GetLevel(); } Logical - HeatModelOff() const { return heatModelOff; } + HeatModelOff() const { return simulationState == 1; /* MechSubsystem::DestroyedState */ } Scalar - HeatMasterEnergy() const { return heatEnergy; } // own inherited sink + HeatMasterEnergy() const { return heatEnergy; } // inherited HeatSink sink PoweredSubsystem::ElectricalState - ElectricalStateLevel() const { return electricalState; } + ElectricalStateLevel() const { return (PoweredSubsystem::ElectricalState)electricalStateAlarm.GetLevel(); } void - SetHasPerformanceFlag() { hasActivePerformance = True; } - protected: - Word segmentFlags; // @this[0x28]/segment flags (Mech-supplied) - Logical hasActivePerformance; - HeatSink::HeatState heatStateLevel; // heatAlarm level (cross-family base) - Logical heatModelOff; // thermal model disabled flag - PoweredSubsystem::ElectricalState electricalState; // electricalStateAlarm level + SetHasPerformanceFlag() { simulationFlags |= 8; } // engine Simulation flag bit 3 //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Attribute Support diff --git a/game/reconstructed/thermalsight.cpp b/game/reconstructed/thermalsight.cpp index 6f26b83..52d91af 100644 --- a/game/reconstructed/thermalsight.cpp +++ b/game/reconstructed/thermalsight.cpp @@ -83,17 +83,18 @@ ThermalSight::ThermalSight( Check(owner); Check_Pointer(subsystem_resource); - segmentFlags = 0; - hostShutDown = False; - watchedVoltageLevel = 4; // Ready - heatStateLevel = 0; // NormalHeat - + // (WAVE 4 de-shim) the 4 cross-family shim fields are gone; the accessors + // read the inherited base state directly. Only the own fields init here. thermalActive = 0; // @0x1D8 requestedOn = 0; // @0x1DC stateAlarm.Initialize(2); // FUN_0041b9ec(this+0x1E0, 2) - if (((GetSegmentFlags() & 0xC) == 0) && - ((GetSegmentFlags() & 0x100) != 0)) + // GATE FIX (the functional bug the de-shim unblocks): the binary reads OWNER + // simulationFlags (param_2+0x28, raw part_013.c:6140), NOT the old shadow + // `segmentFlags` (seeded 0 -> gate always FALSE -> Performance never + // installed). No else branch (matches the binary; unlike Searchlight/Torso). + if (((owner->simulationFlags & 0xC) == 0) && + ((owner->simulationFlags & 0x100) != 0)) { SetPerformance(&ThermalSight::ThermalSightSimulation); // this[7..9] <- PTR_FUN_00511290 } @@ -166,9 +167,9 @@ void stateAlarm.SetLevel(thermalActive); // FUN_0041bbd8(this+0x1E0, thermalActive) - if ((previous != thermalActive) && (LocalViewport()->viewingThisMech != 0)) + if ((previous != thermalActive) && IsLocallyViewed()) // was LocalViewport()->viewingThisMech { - ToggleGlobalThermalVision(); // FUN_0045fe44 + ToggleGlobalThermalVision(); // FUN_0045fe44 -- bring-up no-op (pvision unported) } Check_Fpu(); @@ -207,3 +208,33 @@ int return True; } + + +//===========================================================================// +// WAVE 4 -- compile-time layout locks. PowerWatcher is Torso-proven at 0x1D8, +// so the own fields land at their EXACT binary offsets; sizeof is bounded to +// the factory alloc. (BtAlarm is a 4-byte stand-in for the binary's 0x54 +// GaugeAlarm -- immaterial: nothing reads ThermalSight at a raw offset past +// its own fields, and the shared BT_LOCAL_ALARM_SHIM stays 4 bytes because +// searchlight.hpp shares that guard.) +//===========================================================================// +struct ThermalSightLayoutCheck +{ + static_assert(offsetof(ThermalSight, thermalActive) == 0x1D8, "ThermalSight thermalActive @0x1D8 (PowerWatcher ends 0x1D8)"); + static_assert(offsetof(ThermalSight, requestedOn) == 0x1DC, "ThermalSight requestedOn @0x1DC"); + static_assert(offsetof(ThermalSight, stateAlarm) == 0x1E0, "ThermalSight stateAlarm @0x1E0 (proves base ends 0x1D8)"); + static_assert(sizeof(ThermalSight) <= 0x234, "sizeof(ThermalSight) must fit the factory Memory::Allocate(0x234)"); +}; + + +//===========================================================================// +// WAVE 4 factory bridge -- ThermalSight (factory case 0xBDE, "MechDisplay" stub). +// The real class at 0xBDE (ctor @004b8718) is ThermalSight; the factory built +// a MechDisplay RECON_SUBSYS stub. No cache write; not a weapon (raw +// part_012.c:10186-10195 stores roster-only). +//===========================================================================// +Subsystem *CreateThermalSightSubsystem(Mech *owner, int id, void *seg) +{ + return (Subsystem *) new (Memory::Allocate(0x234)) + ThermalSight(owner, id, (ThermalSight::SubsystemResource *)seg, ThermalSight::DefaultData); +} diff --git a/game/reconstructed/thermalsight.hpp b/game/reconstructed/thermalsight.hpp index 70877fb..0b18811 100644 --- a/game/reconstructed/thermalsight.hpp +++ b/game/reconstructed/thermalsight.hpp @@ -83,6 +83,7 @@ class ThermalSight: public PowerWatcher { + friend struct ThermalSightLayoutCheck; // compile-time offset locks (thermalsight.cpp) //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Shared Data Support // @@ -128,20 +129,28 @@ // module compiles; see report. // public: - struct BtViewport { int viewingThisMech; BtViewport(): viewingThisMech(0) {} }; - - Word GetSegmentFlags() const { return segmentFlags; } - Logical HostShutDown() const { return hostShutDown; } - int WatchedVoltageLevel() const { return watchedVoltageLevel; } // PowerWatcher API - int HeatStateLevel() const { return heatStateLevel; } // HeatSink API - BtViewport* LocalViewport() { return &localViewport; } // PTR_DAT_004fd4b8 - void ToggleGlobalThermalVision() {} // FUN_0045fe44 - protected: - Word segmentFlags; // Mech-supplied segment flags - Logical hostShutDown; // this[0x40] host-power shutdown flag - int watchedVoltageLevel; // @0x198 watched-voltage alarm level - int heatStateLevel; // @0x140 heat-state alarm level - BtViewport localViewport; // active L4 viewport (+0x48 viewingThisMech) + // De-shimmed (WAVE 4 un-stub): the 5 CROSS-FAMILY backing fields + // (segmentFlags/hostShutDown/watchedVoltageLevel/heatStateLevel/ + // localViewport) + struct BtViewport are DELETED. They duplicated + // engine-base state AND -- via the old shadow `segmentFlags` (seeded 0) -- + // made the ctor gate ((0&0xC)==0 && (0&0x100)!=0) FALSE, so + // ThermalSightSimulation was NEVER installed. Accessors now read the REAL + // inherited base members (mirrors torso.hpp / searchlight.hpp): + // HostShutDown <- MechSubsystem::simulationState == Destroyed (this+0x40) + // WatchedVoltageLevel <- PowerWatcher::watchdogAlarm level (this+0x198) + // HeatStateLevel <- HeatWatcher::heatAlarm level (this+0x140) + // The cockpit IR view-mode RENDER hooks have no ported target and stay + // clearly-marked no-ops for bring-up (thermalActive/stateAlarm are still + // computed faithfully): + // IsLocallyViewed() -- was localViewport->viewingThisMech (L4 viewport + // global PTR_DAT_004fd4b8+0x48); no ported accessor. + // ToggleGlobalThermalVision() -- was FUN_0045fe44 (global "pvision" + // palette flip); the D3D9 renderer has no pvision. + Logical HostShutDown() const { return simulationState == 1; /* Destroyed */ } + int WatchedVoltageLevel() const { return watchdogAlarm.GetLevel(); } // @0x198 + int HeatStateLevel() const { return heatAlarm.GetLevel(); } // @0x140 + Logical IsLocallyViewed() const { return False; } // bring-up no-op (see above) + void ToggleGlobalThermalVision() {} // bring-up no-op (pvision unported) //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Simulation Support