subsystems: un-stub Wave 4 readouts + Wave 6 Myomers (10->15 factory cases)

WAVE 4 (standalone readouts) -- Sensor(0xBC3)/Searchlight(0xBD8)/
ThermalSight(0xBDE)/AmmoBin(0xBCB) un-stubbed via Create<Class>Subsystem
bridges + Torso-style de-shim (drop cross-family shadow fields, redirect
accessors to the real inherited base, static_assert layout locks).
- FIX: Searchlight/ThermalSight ctors gated their Performance on the shadow
  segmentFlags(=0) so it NEVER installed; switched to owner->simulationFlags.
- AmmoBin: retype ammoAlarm HeatAlarm->WatcherGaugeAlarm(0x54) + drop the
  statusState@0x40 shadow -> exact 0x22C layout.
- Guard HeatWatcher::WatchSimulation against the unresolved watchedLink
  (null-deref exposed once these sims run; faithful fix = resolve the link).
- Heat-leaf branch (Sensor) is not byte-exact -> overflow-lock only.

WAVE 6 (Myomers 0xBC6, mover-coupled) -- structural un-stub, gated
BT_MYOMERS (default on; =0 -> Actuator stub). Wired INERT: MyomersSimulation
early-returns (advanced-damage gate stubbed) + no-op mover feed, so the live
JointedMover is untouched and the gait cannot regress. De-shim drops the
owner*/segmentFlags shims to fit the exact-0x358 alloc. Authentic mover/heat
coupling deferred (needs messmgr 0xBD3 + reconciling the mover feed with the
gait cutover).

Verified: BLH tick 20->27, Mad Cat 24, combat DESTROYED un-regressed,
locomotion un-regressed, 0 crashes, 0 heap detections under BT_HEAPCHECK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-05 23:01:31 -05:00
co-authored by Claude Opus 4.8
parent 5aa791b245
commit 8b36440d05
14 changed files with 421 additions and 141 deletions
+69 -2
View File
@@ -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 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 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). 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
`Create<Class>Subsystem` 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 <Class>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] **✅ 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, conduct self.E=.. -> other.E`, sustained fire `FIRED #1..#21+`, combat intact, no crash). THREE fixes,
each faithful to the decomp (no stand-ins): 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) **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 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). off Emitter::FireWeapon, heat); damage by the aimed zone (cylinder lookup, STEP 6).
Remaining subsystem families (gyro/torso/myomers motion, sensor/searchlight/thermalsight readouts, Remaining subsystem families (projectile/missile weapons 0xBCD/0xBCE/0xBD0 [WAVE 7]; SubsystemMessageManager
projectile/missile weapons 0xBCD/0xBCE/0xBD0) per SUBSYS_PLAN.md WAVEs 4-8. 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).** **✅ 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 The torso's aim→skeleton path is now fully reconstructed and builds green with zero combat regression, but is
+2 -2
View File
@@ -88,7 +88,7 @@ The whole roster sits on the BT base chain `engine Subsystem → MechSubsystem
- **WAVE 3 — Energy weapons (user: "emitter/PPC second").** - **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`). - **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). - **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 `Create<Class>Subsystem` bridges + de-shim + `<Class>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 - **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 (`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 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 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 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. §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 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. - **WAVE 8 — Message hub:** SubsystemMessageManager (0xBD3). Real, but **verify the cache-slot/tick-bypass interaction** (Risk 4) before enabling its tick.
+45 -5
View File
@@ -126,7 +126,8 @@ AmmoBin::AmmoBin(
{ {
// vtable installed by the compiler (PTR_FUN_0051286c) // 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) ammoAlarm.Initialize(6); // @0x194 FUN_0041b9ec(this+0x65, 6)
InitCookOffHeatSource(&cookOffHeatSource); // @0x1F0 FUN_0041db7c(this+0x7c) InitCookOffHeatSource(&cookOffHeatSource); // @0x1F0 FUN_0041db7c(this+0x7c)
reserved = 0; // @0x228 reserved = 0; // @0x228
@@ -171,7 +172,7 @@ AmmoBin::~AmmoBin()
// //
int AmmoBin::FeedAmmo() int AmmoBin::FeedAmmo()
{ {
if (statusState == 1) // this+0x40 == 1 (destroyed) if (simulationState == 1) // this+0x40 == 1 (destroyed)
return 0; return 0;
switch (ammoAlarm.Level()) // this+0x1A8 switch (ammoAlarm.Level()) // this+0x1A8
@@ -243,7 +244,7 @@ void AmmoBin::AmmoBinSimulation(Scalar time_slice)
{ {
WatchSimulation(time_slice); // FUN_004aeac4(this) -- drive this+0x140 WatchSimulation(time_slice); // FUN_004aeac4(this) -- drive this+0x140
if (statusState == 1) // this+0x40 (destroyed) if (simulationState == 1) // this+0x40 (destroyed)
ammoAlarm.SetLevel(Empty); ammoAlarm.SetLevel(Empty);
// feed timer (only while a round is in transit) // feed timer (only while a round is in transit)
@@ -313,9 +314,9 @@ Logical AmmoBin::HandleMessage(int message)
// //
void AmmoBin::ReadUpdateRecord(UpdateRecord *update) void AmmoBin::ReadUpdateRecord(UpdateRecord *update)
{ {
int previousState = statusState; // this+0x40 int previousState = simulationState; // this+0x40 (inherited MechSubsystem)
HeatWatcher::ReadUpdateRecord(update); // FUN_004aea84 HeatWatcher::ReadUpdateRecord(update); // FUN_004aea84
if (statusState == 1 && previousState != 1) if (simulationState == 1 && previousState != 1)
{ {
ammoCount = 0; // this+0x180 ammoCount = 0; // this+0x180
ammoAlarm.SetLevel(Empty); // 2 ammoAlarm.SetLevel(Empty); // 2
@@ -430,3 +431,42 @@ Logical AmmoBin::TestClass(Mech &)
{ {
return True; 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);
}
+6 -2
View File
@@ -77,6 +77,7 @@
class AmmoBin: class AmmoBin:
public HeatWatcher public HeatWatcher
{ {
friend struct AmmoBinLayoutCheck; // compile-time offset locks (ammobin.cpp)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Shared Data Support // Shared Data Support
// //
@@ -203,8 +204,11 @@
int ammoClassID; // @0x188 (word 0x62) entity class spawned per round int ammoClassID; // @0x188 (word 0x62) entity class spawned per round
int cookOffArmed; // @0x18C (word 0x63) cook-off timer running int cookOffArmed; // @0x18C (word 0x63) cook-off timer running
int cookOffTime; // @0x190 (word 0x64) game clock at which it detonates int cookOffTime; // @0x190 (word 0x64) game clock at which it detonates
HeatAlarm ammoAlarm; // @0x194 (word 0x65) 6-level feed alarm (level @0x1A8) WatcherGaugeAlarm ammoAlarm; // @0x194 (0x54) 6-level feed alarm (level @0x1A8)
int statusState; // base subsystem damage-state mirror (this+0x40) // (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 ammoModelFile; // @0x1E8 (word 0x7A) round model index
int explosionModelFile; // @0x1EC (word 0x7B) cook-off explosion model index int explosionModelFile; // @0x1EC (word 0x7B) cook-off explosion model index
// @0x1F0 (word 0x7C) cook-off heat-injection descriptor, inited by // @0x1F0 (word 0x7C) cook-off heat-injection descriptor, inited by
+17
View File
@@ -302,6 +302,23 @@ void
HeatWatcher::WatchSimulation(Scalar /*time_slice*/) // FUN_004aeac4 HeatWatcher::WatchSimulation(Scalar /*time_slice*/) // FUN_004aeac4
{ {
HeatableSubsystem *watched = (HeatableSubsystem *)watchedLink.Resolve(); // FUN_00417ab4(this+0x114) 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) Scalar temp = watched->currentTemperature; // *(watched + 0x114)
if (temp > failureTemperature) // > this+0x124 if (temp > failureTemperature) // > this+0x124
+38 -15
View File
@@ -196,6 +196,11 @@ extern Subsystem *CreateGeneratorSubsystem(Mech *, int, void *); // 0xBC1 (WAVE
extern Subsystem *CreatePoweredSubsystem(Mech *, int, void *); // 0xBC2 (WAVE 3a) extern Subsystem *CreatePoweredSubsystem(Mech *, int, void *); // 0xBC2 (WAVE 3a)
extern Subsystem *CreateEmitterSubsystem(Mech *, int, void *); // 0xBC8/0xBD4 (WAVE 3b) 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 *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, // CreateGyroSubsystem (0xBC4) is reconstructed + ready in gyro.cpp (layout re-based,
// joint I/O real), but NOT wired -- the gyro ctor/integrator reconstruction is // joint I/O real), but NOT wired -- the gyro ctor/integrator reconstruction is
// incomplete (garbage output); see the 0xBC4 case below. // incomplete (garbage output); see the 0xBC4 case below.
@@ -689,9 +694,11 @@ Mech::Mech(
subsystemArray[id] = CreatePoweredSubsystem(this, id, seg); // FUN_004b0f74 subsystemArray[id] = CreatePoweredSubsystem(this, id, seg); // FUN_004b0f74
break; break;
case MyomersClassID: // 0xBC3 case MyomersClassID: // 0xBC3 -> real class Sensor (ctor @004b1d18, "Myomers" mislabel)
subsystemArray[id] = (Subsystem *) // WAVE 4: the real class at 0xBC3 is Sensor (string pool @0050fae0 +
new (Memory::Allocate(0x328)) Myomers(this, id, seg); // FUN_004b1d18 // 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; break;
case GyroClassID: // 0xBC4 -> real class Gyroscope (ctor @004b3778) case GyroClassID: // 0xBC4 -> real class Gyroscope (ctor @004b3778)
@@ -717,9 +724,19 @@ Mech::Mech(
sinkSourceSubsystem = subsystemArray[id]; // Wword(0x10e) -- gyro links to +0x1D8 sinkSourceSubsystem = subsystemArray[id]; // Wword(0x10e) -- gyro links to +0x1D8
break; break;
case ActuatorClassID: // 0xBC6 case ActuatorClassID: // 0xBC6 -> real class Myomers (ctor @004b8fec, "Actuator" stub)
subsystemArray[id] = (Subsystem *) // WAVE 6: the real class at 0xBC6 is Myomers (the artificial-muscle
new (Memory::Allocate(0x358)) Actuator(this, id, seg); // FUN_004b8fec // 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; break;
case WeaponEmitterClassID: // 0xBC8 -> real Emitter beam weapon case WeaponEmitterClassID: // 0xBC8 -> real Emitter beam weapon
@@ -727,9 +744,11 @@ Mech::Mech(
++weaponCount; ++weaponCount;
break; break;
case JumpJetClassID: // 0xBCB case JumpJetClassID: // 0xBCB -> real class AmmoBin (ctor @004bd5c4, mislabeled)
subsystemArray[id] = (Subsystem *) // WAVE 4: AmmoBin : HeatWatcher : MechSubsystem; own block @0x180,
new (Memory::Allocate(0x22c)) JumpJet(this, id, seg); // FUN_004bd5c4 // 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; break;
case MechWeaponClassID: // 0xBCD case MechWeaponClassID: // 0xBCD
@@ -766,9 +785,11 @@ Mech::Mech(
hudSubsystem = subsystemArray[id]; // raw part_012.c:10164 param_1[0x16d] = HUD hudSubsystem = subsystemArray[id]; // raw part_012.c:10164 param_1[0x16d] = HUD
break; break;
case LegSubsystemClassID: // 0xBD8 case LegSubsystemClassID: // 0xBD8 -> real class Searchlight (ctor @004b84dc, mislabeled)
subsystemArray[id] = (Subsystem *) // WAVE 4: de-shimmed + GATE FIXED (was gating on the shadow
new (Memory::Allocate(0x238)) LegSubsystem(this, id, seg); // FUN_004b84dc // segmentFlags -> Performance never installed). Searchlight :
// PowerWatcher; own fields layout-locked at 0x1D8+.
subsystemArray[id] = CreateSearchlightSubsystem(this, id, seg); // FUN_004b84dc
break; break;
case HeatableClassID: // 0xBDC -> real class MechTech (mislabeled). 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. // the 0x5b4 cache belongs to case 0xbd6 (the HUD) -- earlier recon had it swapped.
break; break;
case DisplayClassID: // 0xBDE case DisplayClassID: // 0xBDE -> real class ThermalSight (ctor @004b8718, "MechDisplay" stub)
subsystemArray[id] = (Subsystem *) // WAVE 4: de-shimmed + GATE FIXED. ThermalSight : PowerWatcher;
new (Memory::Allocate(0x234)) MechDisplay(this, id, seg); // FUN_004b8718 // 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; break;
default: default:
+36 -17
View File
@@ -142,18 +142,10 @@ Myomers::Myomers(
// (heat-base default coefficient @0x150 = 0.15f; inherited HeatSink slot) // (heat-base default coefficient @0x150 = 0.15f; inherited HeatSink slot)
// (Subsystem flags @0x28 |= 8; optional graphic-state hook -- see header note) // (Subsystem flags @0x28 |= 8; optional graphic-state hook -- see header note)
// CROSS-FAMILY shim backing fields -- neutral defaults; the real values are // (WAVE 6 de-shim) the owner*/damageStructure shim backing fields are gone --
// supplied by the owning Mech / its mover / this segment's DamageZone. // their accessors return neutral defaults directly. Only moverConnection
ownerBaseSpeed = 1.0f; // (used by ConnectToMover) remains a member.
ownerMaxSpeed = 0.0f; moverConnection = -1; // @0x110 (detached)
ownerVelocityMag = 0.0f;
ownerDriveMag = 0.0f;
ownerVelocityY = 0.0f;
ownerMotionGain = 0.0f;
ownerMass = 0.0f;
ownerAdvancedDamage = False;
damageStructureLevel = 0.0f;
moverConnection = -1; // @0x110
// Register the per-tick Performance. The binary ctor @004b8fec stores the // Register the per-tick Performance. The binary ctor @004b8fec stores the
// MyomersSimulation pointer-to-member into activePerformance (this[7..9] = // 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 // torso this[7..9]=PTR_FUN_00510c10). The earlier reconstruction mislabeled
// this store a "graphic-state hook" and omitted the registration, leaving // this store a "graphic-state hook" and omitted the registration, leaving
// activePerformance at the DoNothingOnce default (drops out after frame 1). // activePerformance at the DoNothingOnce default (drops out after frame 1).
// segmentFlags is the CROSS-FAMILY Mech-segment field, primed to the live- // (WAVE 6) the segmentFlags shim is removed; the gate reads OWNER
// master value so the drive/heat integrator actually ticks. // simulationFlags (param_2+0x28) directly -- the oracle-verified source.
segmentFlags = MasterHeatSinkFlag;
// INTEGRATION (gate reconcile): read OWNER simulationFlags (param_2+0x28) —
// the oracle-verified authoritative source — not the local primed shim.
if ((owner->simulationFlags & SegmentCopyMask) == 0 if ((owner->simulationFlags & SegmentCopyMask) == 0
&& (owner->simulationFlags & MasterHeatSinkFlag) != 0) // owner flags & 0x100 (binary @004b8fec) && (owner->simulationFlags & MasterHeatSinkFlag) != 0) // owner flags & 0x100 (binary @004b8fec)
{ {
@@ -527,3 +516,33 @@ void Myomers::ToggleSeekVoltage()
{ {
// cycle currentSeekVoltageIndex within [minSeekVoltageIndex, maxSeekVoltageIndex] // 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);
}
+29 -31
View File
@@ -282,43 +282,41 @@ class Mech;
// belong on Mech / JointedMover / DamageZone -- see report CROSS-FAMILY NEEDS. // belong on Mech / JointedMover / DamageZone -- see report CROSS-FAMILY NEEDS.
// //
protected: protected:
Scalar OwnerBaseSpeed() const { return ownerBaseSpeed; } // Mech +0x34C base-speed scalar // WAVE 6 un-stub -- CROSS-FAMILY Mech/Mover/DamageZone couplings. These
Scalar OwnerMaxSpeed() const { return ownerMaxSpeed; } // Mech +0x7A0 top speed // logically read the owning Mech's motion state, its JointedMover roster,
void SetOwnerMaxSpeed(Scalar s) { ownerMaxSpeed = s; } // and this segment's DamageZone -- none reachable from a PoweredSubsystem
Scalar OwnerVelocityMag() const { return ownerVelocityMag; } // |Mech velocity| (+0x1C4..) // here (owner is an incomplete Mech*, JointedMover exposes no roster API,
Scalar OwnerDriveMag() const { return ownerDriveMag; } // |Mech drive/accel| (+0x82C..) // DamageZone is not in this hierarchy). To let the real class FIT its 0x358
Scalar OwnerVelocityY() const { return ownerVelocityY; } // Mech velocity.y (+0x1C8) // factory alloc, the shim BACKING FIELDS are dropped and the accessors
Scalar OwnerMotionGain() const { return ownerMotionGain; } // Mech +0x20C // return the neutral defaults the ctor used to prime, so the subsystem
Scalar OwnerMass() const { return ownerMass; } // *Mech +0x250 // CONSTRUCTS + TICKS but is INERT w.r.t. locomotion/heat: the mover feed is
Logical OwnerAdvancedDamage() const { return ownerAdvancedDamage; } // FUN_004ad7d4 sim-control gate // a no-op and MyomersSimulation early-returns on OwnerAdvancedDamage()==False.
Scalar DamageStructureLevel() const{ return damageStructureLevel; }// DamageZone +0x158 // ⚠ 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 MoverAttach(int index, Scalar *feed) { (void)index; (void)feed; } // JointedMover vtable +0x38 (no-op)
void MoverDetach(int index) { (void)index; } // JointedMover vtable +0x3C void MoverDetach(int index) { (void)index; } // JointedMover vtable +0x3C (no-op)
void MoverRelease(int handle, Scalar *feed) { (void)handle; (void)feed; } // JointedMover vtable +0x44 void MoverRelease(int handle, Scalar *feed) { (void)handle; (void)feed; } // JointedMover vtable +0x44 (no-op)
Scalar ownerBaseSpeed; // Live-master gate constants (the ctor reads owner->simulationFlags).
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.
enum { enum {
SegmentCopyMask = 0x0C, // (flags & 0xC): 0 == master, 4 == damaged copy SegmentCopyMask = 0x0C, // (flags & 0xC): 0 == master, 4 == damaged copy
MasterHeatSinkFlag = 0x0100 // live-master segment bit 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. // Local data. Offsets are byte offsets into the shipped object.
+40 -10
View File
@@ -82,21 +82,20 @@ Searchlight::Searchlight(
Check(owner); Check(owner);
Check_Pointer(subsystem_resource); Check_Pointer(subsystem_resource);
segmentFlags = 0; // (WAVE 4 de-shim) the 7 cross-family shim fields are gone; the accessors
hostShutDown = False; // read the inherited base state directly. Only the own fields init here.
watchedVoltageLevel = 4; // Ready
heatStateLevel = 0; // NormalHeat
controlsAllowLights = True;
graphicsDirty = False;
instanceFlags = 0;
lightState = 0; // @0x1D8 lightState = 0; // @0x1D8
requestedOn = 0; // @0x1E0 requestedOn = 0; // @0x1E0
stateAlarm.Initialize(2); // FUN_0041b9ec(this+0x1E4, 2) stateAlarm.Initialize(2); // FUN_0041b9ec(this+0x1E4, 2)
commandedOn = subsystem_resource->segmentIndex; // @0x1DC <- inherited resource +0x28 commandedOn = subsystem_resource->segmentIndex; // @0x1DC <- inherited resource +0x28
if (((GetSegmentFlags() & 0xC) == 0) && // GATE FIX (the functional bug the de-shim unblocks): the binary reads OWNER
((GetSegmentFlags() & 0x100) != 0)) // 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 SetPerformance(&Searchlight::SearchlightSimulation); // this[7..9] <- PTR_FUN_00511200
} }
@@ -256,3 +255,34 @@ int
return True; 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);
}
+24 -15
View File
@@ -85,6 +85,7 @@
class Searchlight: class Searchlight:
public PowerWatcher public PowerWatcher
{ {
friend struct SearchlightLayoutCheck; // compile-time offset locks (searchlight.cpp)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Shared Data Support // Shared Data Support
// //
@@ -131,23 +132,31 @@
// are backed by local members so this module compiles; see report. // are backed by local members so this module compiles; see report.
// //
public: public:
Word GetSegmentFlags() const { return segmentFlags; } // De-shimmed (WAVE 4 un-stub): the 7 CROSS-FAMILY backing fields
Logical HostShutDown() const { return hostShutDown; } // (segmentFlags/hostShutDown/watchedVoltageLevel/heatStateLevel/
int WatchedVoltageLevel() const { return watchedVoltageLevel; } // PowerWatcher API // controlsAllowLights/graphicsDirty/instanceFlags) are DELETED. They
int HeatStateLevel() const { return heatStateLevel; } // HeatSink API // duplicated engine-base state AND -- via the old shadow `segmentFlags`
Logical ControlsAllowLights() const { return controlsAllowLights; } // Mech controls mapper // (seeded 0) -- made the ctor gate ((0&0xC)==0 && (0&0x100)!=0) FALSE, so
void SetGraphicsDirty() { graphicsDirty = True; } // this[0x18] |= 1 // SearchlightSimulation was NEVER installed. Accessors now read the REAL
void SetInstanceFlag(int bits) { instanceFlags |= bits; } // this[10] |= bits // 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). // The "ToggleLamp" command arg is carried at message+0xC (decomp).
static int MessageArg(Message &m) { return *(const int *)((const char *)&m + 0xC); } 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 // Simulation Support
+36 -6
View File
@@ -136,12 +136,8 @@ Sensor::Sensor(
Check(owner); Check(owner);
Check_Pointer(subsystem_resource); Check_Pointer(subsystem_resource);
segmentFlags = 0; // (WAVE 4 de-shim) the 5 cross-family shim fields are gone; the accessors
hasActivePerformance = False; // now read the inherited base state directly. Only the 3 own fields init here.
heatStateLevel = NormalHeat;
heatModelOff = False;
electricalState = Ready;
radarPercent = RadarBaseline; // @0x31C = 1.0f radarPercent = RadarBaseline; // @0x31C = 1.0f
selfTest = False; // @0x320 = 0 selfTest = False; // @0x320 = 0
badVoltage = False; // @0x324 = 0 badVoltage = False; // @0x324 = 0
@@ -363,3 +359,37 @@ int
Check_Fpu(); Check_Fpu();
return True; 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);
}
+16 -13
View File
@@ -111,24 +111,27 @@
}; };
#endif #endif
public: public:
Word // De-shimmed (WAVE 4 un-stub): the 5 CROSS-FAMILY backing fields
GetSegmentFlags() const { return segmentFlags; } // (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 HeatSink::HeatState
HeatStateLevel() const { return heatStateLevel; } HeatStateLevel() const { return (HeatSink::HeatState)heatAlarm.GetLevel(); }
Logical Logical
HeatModelOff() const { return heatModelOff; } HeatModelOff() const { return simulationState == 1; /* MechSubsystem::DestroyedState */ }
Scalar Scalar
HeatMasterEnergy() const { return heatEnergy; } // own inherited sink HeatMasterEnergy() const { return heatEnergy; } // inherited HeatSink sink
PoweredSubsystem::ElectricalState PoweredSubsystem::ElectricalState
ElectricalStateLevel() const { return electricalState; } ElectricalStateLevel() const { return (PoweredSubsystem::ElectricalState)electricalStateAlarm.GetLevel(); }
void void
SetHasPerformanceFlag() { hasActivePerformance = True; } SetHasPerformanceFlag() { simulationFlags |= 8; } // engine Simulation flag bit 3
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
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Attribute Support // Attribute Support
+40 -9
View File
@@ -83,17 +83,18 @@ ThermalSight::ThermalSight(
Check(owner); Check(owner);
Check_Pointer(subsystem_resource); Check_Pointer(subsystem_resource);
segmentFlags = 0; // (WAVE 4 de-shim) the 4 cross-family shim fields are gone; the accessors
hostShutDown = False; // read the inherited base state directly. Only the own fields init here.
watchedVoltageLevel = 4; // Ready
heatStateLevel = 0; // NormalHeat
thermalActive = 0; // @0x1D8 thermalActive = 0; // @0x1D8
requestedOn = 0; // @0x1DC requestedOn = 0; // @0x1DC
stateAlarm.Initialize(2); // FUN_0041b9ec(this+0x1E0, 2) stateAlarm.Initialize(2); // FUN_0041b9ec(this+0x1E0, 2)
if (((GetSegmentFlags() & 0xC) == 0) && // GATE FIX (the functional bug the de-shim unblocks): the binary reads OWNER
((GetSegmentFlags() & 0x100) != 0)) // 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 SetPerformance(&ThermalSight::ThermalSightSimulation); // this[7..9] <- PTR_FUN_00511290
} }
@@ -166,9 +167,9 @@ void
stateAlarm.SetLevel(thermalActive); // FUN_0041bbd8(this+0x1E0, thermalActive) 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(); Check_Fpu();
@@ -207,3 +208,33 @@ int
return True; 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);
}
+23 -14
View File
@@ -83,6 +83,7 @@
class ThermalSight: class ThermalSight:
public PowerWatcher public PowerWatcher
{ {
friend struct ThermalSightLayoutCheck; // compile-time offset locks (thermalsight.cpp)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Shared Data Support // Shared Data Support
// //
@@ -128,20 +129,28 @@
// module compiles; see report. // module compiles; see report.
// //
public: public:
struct BtViewport { int viewingThisMech; BtViewport(): viewingThisMech(0) {} }; // De-shimmed (WAVE 4 un-stub): the 5 CROSS-FAMILY backing fields
// (segmentFlags/hostShutDown/watchedVoltageLevel/heatStateLevel/
Word GetSegmentFlags() const { return segmentFlags; } // localViewport) + struct BtViewport are DELETED. They duplicated
Logical HostShutDown() const { return hostShutDown; } // engine-base state AND -- via the old shadow `segmentFlags` (seeded 0) --
int WatchedVoltageLevel() const { return watchedVoltageLevel; } // PowerWatcher API // made the ctor gate ((0&0xC)==0 && (0&0x100)!=0) FALSE, so
int HeatStateLevel() const { return heatStateLevel; } // HeatSink API // ThermalSightSimulation was NEVER installed. Accessors now read the REAL
BtViewport* LocalViewport() { return &localViewport; } // PTR_DAT_004fd4b8 // inherited base members (mirrors torso.hpp / searchlight.hpp):
void ToggleGlobalThermalVision() {} // FUN_0045fe44 // HostShutDown <- MechSubsystem::simulationState == Destroyed (this+0x40)
protected: // WatchedVoltageLevel <- PowerWatcher::watchdogAlarm level (this+0x198)
Word segmentFlags; // Mech-supplied segment flags // HeatStateLevel <- HeatWatcher::heatAlarm level (this+0x140)
Logical hostShutDown; // this[0x40] host-power shutdown flag // The cockpit IR view-mode RENDER hooks have no ported target and stay
int watchedVoltageLevel; // @0x198 watched-voltage alarm level // clearly-marked no-ops for bring-up (thermalActive/stateAlarm are still
int heatStateLevel; // @0x140 heat-state alarm level // computed faithfully):
BtViewport localViewport; // active L4 viewport (+0x48 viewingThisMech) // 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 // Simulation Support