From 819772f974be28771d6d2e256ec96615bfdaee0b Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Tue, 28 Jul 2026 16:06:56 -0500 Subject: [PATCH] the rest of the Myomers wrapper: two live outputs recovered, and one branch that never fires Follow-on to b70654d, which chained the base sim and stopped there. Decoding the remaining 0x17c bytes of @004b8b9c turned up four more blocks the port had dropped along with it. In binary order the registered Performance is: @004b8bab chain PoweredSubsystemSimulation (fixed in b70654d) @004b8bb9 un-powered self-repair of this myomer's own zone (dead -- see below) @004b8c1e republish outputVoltage@0x344 from the source @004b8c5a republish speedEffect@0x31C, the drive fed to the mover @004b8ceb run the inner integrator, ONLY when outputVoltage > 0 speedEffect is the interesting one. AvailableOutput scales by the Mech base speed and the wrapper divides by it again, so the two cancel and what lands in +0x31C is a 0..1 FRACTION of full drive carrying the gear ratio, the thermal degradation curve and the accumulated zone damage. Neither it nor outputVoltage was ever written before: outputVoltage sat wherever the ctor left it and speedEffect stayed pinned at its ctor 1.0f. Un-stubbed DamageStructureLevel() while in here. It was `return 0.0f`, which held AvailableOutput's (1 - damage) factor at 1, so a shot-up myomer drove exactly as well as a fresh one. Routed to the base's GetSubsystemDamageLevel() bridge, the same cell sensor.cpp:312 already reads. Measured dmg=0.6 -> speed=0.4. @004b8bb9 does not work, and did not work in 1995 either. It builds a Damage (Explosive, amount 0xbc343958 ~= -0.011f, impactPoint from owner+0x100, burst 1) and calls the ZONE's TakeDamage -- vtable +0x18, not the subsystem's +0x24 -- so the plain `damageLevel += amount * damageScale[type]`. Read on its own that is "a myomer you power down slowly heals". But a subsystem's private zone is built by the 2-arg `new DamageZone(this, 0)`, and DAMAGE.cpp:187-190 zeroes all five damageScale entries; Reset never touches them and the only other writer is Mech__DamageZone, the mech's streamed zones. The sum is always `+= amount * 0`. Seeded a zone to 0.6, held it at NoVoltage for ~1500 ticks: damageLevel never moved. Reconstructed and deliberately not "fixed" -- a working repair here would be behavior we invented. The crit path reaches subsystem damage by writing damageLevel directly, which is why subsystems still die. That generalises, so it is written up in context/combat-damage.md on its own: you cannot damage a subsystem's own zone through DamageZone::TakeDamage at all. Anything reconstructed later that means to hurt a subsystem has to go the way the crit path goes, or it will silently do nothing. Verified, self-damage runs across two death/respawn cycles: torso 96/96 samples elec=4, zero dips myomers 96/97 elec=4 (the odd one is the first tick, before the machine runs) healthy outV=10000 speed=1 un-powered outV=0 speed=0 BT_MYOMERS_LOG probes the four outputs; BT_MYOMERS_REPAIR_TEST= seeds the zone and drops the subsystem into Manual so the repair branch can be watched without the AutoConnect hunt restoring power a frame later. Co-Authored-By: Claude Fable 5 --- context/combat-damage.md | 15 +++ context/subsystems.md | 26 +++- game/reconstructed/mech4.cpp | 13 ++ game/reconstructed/mechsub.cpp | 16 +++ game/reconstructed/mechsub.hpp | 8 ++ game/reconstructed/myomers.cpp | 209 ++++++++++++++++++++++++++------- game/reconstructed/myomers.hpp | 18 ++- 7 files changed, 256 insertions(+), 49 deletions(-) diff --git a/context/combat-damage.md b/context/combat-damage.md index 02be611..e3a1ce5 100644 --- a/context/combat-damage.md +++ b/context/combat-damage.md @@ -318,6 +318,21 @@ death). ALL EIGHT proxy-view sites in mechsub.cpp swept to the engine view `GetStatusFlags` (read the vptr as a float → always "intact") and `ApplyDamageAndMeasure` (the crit cascade's damage-measure read garbage). Gotcha §5 (alias fields), new archetype. +## ⚠ A subsystem's PRIVATE zone cannot be damaged via `TakeDamage` (2026-07-28) [T0] +Each `MechSubsystem` owns a private `DamageZone` at `+0xE0`, built by the **2-arg trivial ctor** +`new DamageZone(this, 0)`. That ctor (`engine/MUNGA/DAMAGE.cpp:187-190`) zeroes **all five** +`damageScale[]` entries, `Reset()` never touches them, and the only other writer in the tree is +`Mech__DamageZone` — the mech's *streamed* zones (`mechdmg.cpp:246-253`), a different class. Since +`DamageZone::TakeDamage` is `damageLevel += damageAmount * damageScale[damageType]`, **any** +`TakeDamage` against a subsystem's own zone is arithmetically a no-op, whatever the amount or type. +Verified by experiment: a zone seeded to 0.6 and fed −0.011f for ~1500 ticks never moved. +The crit path works because it **bypasses this entirely** and writes `damageLevel` directly +(`DistributeCriticalHit` pins `*(this[0x38]+0x158) = 1.0f`; `ForceCriticalFailure` sets the state). +Practical consequence: if you are reconstructing something that "damages a subsystem", route it the +way the crit path does — a `TakeDamage` call there will silently do nothing. This is also why +Myomers' Performance `@004b8bb9` (an un-powered self-repair) is dead code in the 1995 binary itself: +see [[subsystems]] WAVE 6. + ## Damage delivery + the real damage model `Entity::TakeDamageMessage(id, size, inflictingEntityID, zone, Damage&)` → `target->Dispatch`. **Base handler IGNORES zone==−1** (`Entity::TakeDamageMessageHandler`, ENTITY.cpp:878 — returns on diff --git a/context/subsystems.md b/context/subsystems.md index bb9f69a..277609e 100644 --- a/context/subsystems.md +++ b/context/subsystems.md @@ -84,9 +84,29 @@ Making a base byte-exact GROWS every subclass — they must be re-based TOGETHER does not match the function you reconstructed, check whether the real target is a wrapper that chains the base sim — the whole family does (`Generator`/`PoweredSubsystem` lead with `HeatSink::HeatSinkSimulation`; the Torso's own perf @004b5cf0 leads with `UpdateWatch`). - Still unreconstructed [T4]: @004b8bb9-0x4b8c1e, an effect when `electricalStateAlarm == 1` and the - myomer's damage zone is < 1.0, building a small vector (const `0xbc343958` ≈ -0.011f) against the - owner's `localOrigin@0x100` — almost certainly the un-powered movement penalty. + **The full wrapper is now reconstructed** [T1/T2, 2026-07-28] — five blocks in binary order: + `@004b8bab` chain `PoweredSubsystemSimulation`; `@004b8bb9` the un-powered self-repair (see below); + `@004b8c1e` republish `outputVoltage@0x344` from the resolved source when the alarm is Ready; + `@004b8c5a` republish `speedEffect@0x31C` = `AvailableOutput(clamped V) / OwnerBaseSpeed()` — the + Mech base speed CANCELS, so `speedEffect` is a **0..1 fraction** of full drive carrying gear ratio, + thermal curve and zone damage; `@004b8ceb` run the inner integrator **only when `outputVoltage > 0`**. + Verified live: healthy `outV=10000 speed=1`, un-powered `outV=0 speed=0`, and 96/96 torso samples at + `elec=4` across two death/respawn cycles. + ⚠ **`@004b8bb9` is DEAD CODE — in the original too** [T0]. It builds a `Damage` + (type=`Explosive`, amount=`0xbc343958`≈-0.011f, impactPoint=`owner+0x100`, burst=1) and calls the + **zone's** `TakeDamage` (zone vtable `+0x18`, *not* the subsystem's `+0x24`). Reading it as + "an un-powered myomer heals" is the evident intent, but it can never fire: a subsystem's private zone + comes from the 2-arg `new DamageZone(this,0)`, and `engine/MUNGA/DAMAGE.cpp:187-190` zeroes **all five** + `damageScale[]` entries; `Reset()` never touches them and the only other writer is `Mech__DamageZone` + (the mech's *streamed* zones — `mechdmg.cpp:246-253`). Since `TakeDamage` is + `damageLevel += amount * damageScale[type]`, the sum is always `+= amount * 0`. Confirmed by + experiment: seeded to 0.6 and held at NoVoltage ~1500 ticks, `damageLevel` never moved. + Reconstructed and deliberately NOT "fixed" — inventing a working repair would be made-up behavior. + **General rule:** you cannot damage a subsystem's own zone through `DamageZone::TakeDamage`; the crit + path writes `damageLevel` directly (`DistributeCriticalHit` pins `*(this[0x38]+0x158) = 1.0f`). + Also un-stubbed here: `Myomers::DamageStructureLevel()` returned a hardcoded `0.0f`, which pinned + `AvailableOutput`'s `(1 - damage)` factor at 1 — a shot-up myomer drove exactly as well as a fresh + one. Now routed to the base bridge `GetSubsystemDamageLevel()`; measured `dmg=0.6 → speed=0.4`. - **WAVE 7** — projectile/missile weapons (byte-exact; flying projectiles are a PORT reconstruction — the 2007 Entity is 0x1BC vs the binary's 0x300, so raw base-offset reads fail). - **TASK #56 (2026-07-11)** — Gyroscope LIVE byte-exact (ctor @004b3778, sizeof 0x3D0 locked; diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 4e28719..5ce8db9 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -715,6 +715,19 @@ int BTMechTargetZone(void *mech) return MECH_TARGET_SUBIDX(mech); } +// Myomers bridge: the owning entity's world position, the binary's raw +// `owner + 0x100`. Myomers' Performance @004b8be3 copies it into the Damage it +// hands its own zone; `owner` is an incomplete Mech* over in myomers.cpp, so the +// read happens here where Mover is complete (same idiom as BTPilotPosition). +int BTMechEntityPosition(void *entity, float *out_xyz) +{ + if (entity == 0 || out_xyz == 0) + return 0; + const Point3D &p = ((Mover *)entity)->localOrigin.linearPosition; // entity+0x100 + out_xyz[0] = p.x; out_xyz[1] = p.y; out_xyz[2] = p.z; + return 1; +} + // Bring-up body-animation player (file scope so OnBodyAnimFinished can re-arm it). // The engine AnimationInstance walks the mech's joint subsystem from a baked .ani // clip in btl4.res; we advance it each moving frame so the legs CYCLE. diff --git a/game/reconstructed/mechsub.cpp b/game/reconstructed/mechsub.cpp index 846228d..cca9264 100644 --- a/game/reconstructed/mechsub.cpp +++ b/game/reconstructed/mechsub.cpp @@ -421,6 +421,22 @@ Scalar return ((DamageZone *)damageZone)->damageLevel - before; } +// +// Straight to the zone's own virtual -- see the header note. Myomers' +// Performance (@004b8c0f-0x4b8c1b) does exactly: +// eax = this[0xE0]; (*(*eax + 0x18))(eax, &damage) +// i.e. DamageZone::TakeDamage, whose T0 body (engine/MUNGA/DAMAGE.cpp:374) is +// damageLevel += damageAmount * damageScale[damageType]; +// Clamp(damageLevel, 0.0f, 1.0f); SetDamageLevelChangedFlag(); +// so a NEGATIVE damageAmount walks the zone back toward intact, and the clamp +// is what stops it below 0. +// +void MechSubsystem::ApplyZoneDamage(Damage &damage) +{ + if (damageZone != 0) + ((DamageZone *)damageZone)->TakeDamage(damage); // zone vtable +0x18 +} + // // @0x4ac274 -- the AMMO-EXPLOSION fan-out (Gitea #46, re-read from the raw // decomp). The old reconstruction here was wrong in kind: it "re-applied the diff --git a/game/reconstructed/mechsub.hpp b/game/reconstructed/mechsub.hpp index c9146a7..55ff2d0 100644 --- a/game/reconstructed/mechsub.hpp +++ b/game/reconstructed/mechsub.hpp @@ -276,6 +276,14 @@ class Damage; // @0x4ac07c -- run the virtual TakeDamage and return the resulting // rise in the subsystem's own damageLevel (the crit allotment used). Scalar ApplyDamageAndMeasure(Damage &damage); + // Apply a Damage straight to this subsystem's OWN zone -- the engine + // DamageZone::TakeDamage virtual (zone vtable +0x18), NOT the subsystem's + // own TakeDamage (this vtable +0x24, which is what ApplyDamageAndMeasure + // runs and which additionally cascades crits/vital checks). Myomers' + // Performance @004b8c16 calls the ZONE virtual directly, so it gets the + // plain `damageLevel += amount * damageScale[type]` and nothing else. + // Lives here because only a complete-DamageZone TU may deref the proxy. + void ApplyZoneDamage(Damage &damage); // The destroyed-side effects SendSubsystemDamage applies when the // subsystem's own zone hits 1.0: statusAlarm Destroyed(1) + the // PrintState debug gate + the zone's state valve. [T1 @0049c9a8] diff --git a/game/reconstructed/myomers.cpp b/game/reconstructed/myomers.cpp index 148ad6b..a037849 100644 --- a/game/reconstructed/myomers.cpp +++ b/game/reconstructed/myomers.cpp @@ -453,14 +453,173 @@ Logical Myomers::HasVoltage(Subsystem *source) } +// The un-powered recovery rate the Performance hands its own damage zone each +// tick, as a NEGATIVE damageAmount: the literal 0xbc343958 stored at @004b8bf0. +static const Scalar MyomerRecoveryPerTick = -0.010999999940395355f; // 0xbc343958 + + //*************************************************************************** -// Myomers::MyomersSimulation Performance @004b8d18 +// Myomers::MyomersSimulation Performance @004b8b9c //*************************************************************************** // -// Per-tick drive/heat integration. BEST-EFFORT (only reachable via the -// activePerformance function pointer; the SetPerformance call site was not -// captured in the shards -- registration is presumed in the un-decompiled -// ToggleSeekVoltage/activation path). +// THE REGISTERED PERFORMANCE -- the outer wrapper, decoded in full 2026-07-28. +// The ctor's [0x511620] resolves to 0x4b8b9c, NOT 0x4b8d18; the port had +// registered the inner drive-heat integrator and dropped this whole wrapper. +// Five blocks, in binary order: +// +// @004b8bab chain PoweredSubsystem::PoweredSubsystemSimulation (0x4b0bd0) +// @004b8bb9 un-powered self-repair of this myomer's own zone (INERT -- see +// the block comment; the original is dead code here too) +// @004b8c1e republish outputVoltage@0x344 from the resolved source +// @004b8c5a republish speedEffect@0x31C, the drive handed to the mover +// @004b8ceb run the inner integrator -- ONLY when outputVoltage > 0 +// +// Restoring the first block alone fixed Gitea #70 (torso twist dead after a +// respawn): the Torso is a PowerWatcher that MIRRORS this subsystem's +// electrical level, and with the base machine never running, a Myomers that +// lost power in the death/reset window sat at NoVoltage forever. The other +// four are the rest of that same dropped wrapper. +// +void Myomers::MyomersSimulation(Scalar time_slice) +{ + // 1. @004b8bab -- the base electrical state machine. MUST come first, and + // must precede any heat-model gate: OwnerAdvancedDamage() is off below + // veteran, so gating ahead of this denied the machine to most pilots. + PoweredSubsystemSimulation(time_slice); // call 0x4b0bd0 + + // 2. @004b8bb9-0x4b8c1b -- the INTENDED un-powered self-repair. While this + // myomer has NO VOLTAGE and its own zone is not yet destroyed, hand that + // zone a NEGATIVE explosive Damage every tick. DamageZone::TakeDamage is + // T0 (engine/MUNGA/DAMAGE.cpp:374) and reads only damageAmount/damageType: + // damageLevel += damageAmount * damageScale[damageType]; + // Clamp(damageLevel, 0.0f, 1.0f); + // so the READING is "a myomer you power down slowly heals, and the `< 1.0` + // test stops a destroyed zone being resurrected". + // + // ⚠ BUT IT IS INERT -- IN THE ORIGINAL TOO. [T0, measured 2026-07-28] + // A subsystem's private zone is built by the 2-arg trivial ctor + // `new DamageZone(this, 0)`, and DAMAGE.cpp:187-190 zeroes ALL FIVE + // damageScale[] entries; DamageZone::Reset never touches them, and the only + // other writer in the codebase is Mech__DamageZone (the MECH's streamed + // zones, a different class -- mechdmg.cpp:246-253). So damageScale stays + // {0,0,0,0,0} for the life of the zone and the sum above is always + // `damageLevel += amount * 0.0f` == no change. Confirmed live: seeded to + // 0.6, held at NoVoltage for ~1500 ticks, damageLevel never moved off 0.6 + // (BT_MYOMERS_REPAIR_TEST below). The crit path reaches subsystem damage a + // different way -- it writes damageLevel DIRECTLY (mechsub.cpp + // DistributeCriticalHit pins *(this[0x38]+0x158) = 1.0f) -- which is why + // subsystems can still be destroyed even though this route cannot. + // + // Reconstructed anyway, and deliberately NOT "fixed": this is what the 1995 + // binary does, and inventing a working repair here would be a behavior we + // made up. Kept so the dead branch is visible rather than silently missing. + // (impactPoint/burstCount are set for fidelity; the callee ignores both.) + if (electricalStateAlarm.GetLevel() == PoweredSubsystem::NoVoltage // this+0x278 == 1 + && DamageStructureLevel() < 1.0f) // zone+0x158 < _DAT_004b8d10 + { + extern int BTMechEntityPosition(void *entity, float *out_xyz); // mech4.cpp + float ownerXYZ[3] = { 0.0f, 0.0f, 0.0f }; + BTMechEntityPosition(owner, ownerXYZ); // owner+0x100 + + Damage repair; // FUN_0041db7c (Damage::Damage) + repair.damageType = Damage::ExplosiveDamageType; // +0x00 = 2 + repair.damageAmount = MyomerRecoveryPerTick; // +0x04 = 0xbc343958 + repair.impactPoint.x = ownerXYZ[0]; // +0x20 <- FUN_00408440 + repair.impactPoint.y = ownerXYZ[1]; + repair.impactPoint.z = ownerXYZ[2]; + repair.burstCount = 1; // +0x2C + ApplyZoneDamage(repair); // zone vtable +0x18 + } + + // 3. @004b8c1e-0x4b8c5a -- republish the input voltage. (The binary also + // zeroes +0x344 when simulationState == Destroyed just above this, then + // overwrites it unconditionally here; that store is dead in the original + // too, so it is not reproduced.) + Generator *source = (Generator *)ResolveVoltageSource(); // FUN_00417ab4(this+0x1D0) + outputVoltage = (electricalStateAlarm.GetLevel() == PoweredSubsystem::Ready + && source != 0) + ? source->MeasuredVoltage() // source+0x1DC + : 0.0f; + + // 4. @004b8c5a-0x4b8ce8 -- republish speedEffect, the live drive fed to the + // mover. Clamp the input voltage to the SELECTED gear and then to the top + // gear, then normalise AvailableOutput by the same Mech base speed it was + // scaled by -- so speedEffect is a 0..1 FRACTION of full drive carrying the + // gear ratio, the thermal-degradation curve and the accumulated zone damage. + if (!HasVoltage(0)) // vtable +0x40 (slot 16) + { + speedEffect = 0.0f; + } + else + { + Scalar drive = outputVoltage; // @0x344 + if (drive >= seekVoltage[currentSeekVoltageIndex]) // @0x330[@0x320] + drive = seekVoltage[currentSeekVoltageIndex]; + if (drive > seekVoltage[maxSeekVoltageIndex]) // @0x330[@0x32C] + drive = seekVoltage[maxSeekVoltageIndex]; + speedEffect = AvailableOutput(drive) / OwnerBaseSpeed(); // @004b8ac0 / (owner+0x34C) + } + + // BRING-UP SCAFFOLDING (temporary, env-gated, off by default). + // + // BT_MYOMERS_REPAIR_TEST= seeds this myomer's OWN zone to once, + // at frame 300, so the un-powered self-repair above has something to walk + // back. Nothing else can damage that zone on a bench: unaimed self-damage + // goes through the mech's hit-location table and never crits a myomer, so + // block 2 of the Performance would otherwise stay unexercised. Pair it with + // BT_POWER_DETACH_TEST=Myomers to open the NoVoltage window. + // Self-contained: it also cuts the voltage link and drops the subsystem into + // MANUAL, so the AutoConnect hunt does NOT put it straight back (which is + // what BT_POWER_DETACH_TEST does -- that one re-attaches within a frame or + // two, far too short a window to watch a repair). + if (getenv("BT_MYOMERS_REPAIR_TEST")) + { + static int s_rtFrame = 0; + if (++s_rtFrame == 300) + { + Scalar seed = (Scalar)atof(getenv("BT_MYOMERS_REPAIR_TEST")); + if (seed <= 0.0f) seed = 0.5f; + SetSubsystemDamageLevel(seed); // mechsub.cpp bridge + DetachFromVoltageSource(); // @004b0e30 + modeAlarm.SetLevel(ManualConnect); // stay dark (no auto-hunt) + DEBUG_STREAM << "[myo] SEED zone damage = " << seed + << ", detached + forced Manual" << std::endl << std::flush; + } + } + + // PROBE (BT_MYOMERS_LOG): the wrapper's outputs. Sample every frame while + // the electrical state is anything but Ready -- the NoVoltage window is the + // self-repair window and it only lasts about a second, so a once-a-second + // probe would step right over the transient. + if (getenv("BT_MYOMERS_LOG")) + { + static int s_ml = 0; + int notReady = (electricalStateAlarm.GetLevel() != PoweredSubsystem::Ready); + if (notReady || (s_ml % 120) == 0) + { + DEBUG_STREAM << "[myo] " << (GetName() ? GetName() : "?") + << " elec=" << (int)electricalStateAlarm.GetLevel() + << " outV=" << outputVoltage + << " speed=" << speedEffect + << " dmg=" << DamageStructureLevel() + << " gear=" << currentSeekVoltageIndex + << std::endl << std::flush; + } + ++s_ml; + } + + // 5. @004b8ceb -- the drive-heat integrator runs ONLY with voltage present. + if (outputVoltage > 0.0f) // vs _DAT_004b8d14 (0.0f) + MyomersDriveHeat(time_slice); // call 0x4b8d18 +} + + +//*************************************************************************** +// Myomers::MyomersDriveHeat inner @004b8d18 +//*************************************************************************** +// +// Per-tick drive/heat integration. Reached only from the wrapper above, and +// only while outputVoltage > 0. // // Reads the owner Mech's motion state and the seek-voltage ratio, then -- when // the sim-control "advanced damage" gate is on (FUN_004ad7d4: *(Mech[0xD0]+0x190) @@ -482,46 +641,8 @@ Logical Myomers::HasVoltage(Subsystem *source) // inefficiency complements of VelocityEfficiency/AccelerationEfficiency -- is // faithful to the decompiled arithmetic.) // -void Myomers::MyomersSimulation(Scalar time_slice) +void Myomers::MyomersDriveHeat(Scalar time_slice) { - // - // CHAIN THE BASE FIRST (2026-07-28). The registered Performance in the - // binary is @004b8b9c, NOT @004b8d18: the ctor @004b8fec stores - // [0x511620] into activePerformance, and that pointer resolves to - // 0x4b8b9c, whose FIRST act is `call 0x4b0bd0` = - // PoweredSubsystem::PoweredSubsystemSimulation. @004b8d18 is the INNER - // drive-heat integrator it goes on to run. The port had registered the - // inner function and dropped the wrapper, so Myomers never advanced its - // own electrical state machine. - // - // WHY IT MATTERS: NoVoltage is only ever entered by that machine - // (`source == 0`), and only that machine walks it back - // (NoVoltage -> Starting -> Ready). With the machine never running, a - // Myomers that lost power during the death/reset window stayed at - // NoVoltage forever -- and the Torso is a PowerWatcher that MIRRORS the - // watched subsystem's electrical level, so torso.cpp:570 held - // effectiveTwistRate at 0. That is Gitea #70, "torso twist stops working - // after a death/respawn". Measured: 0 dips in 48 samples on a pristine - // mission, dips lasting seconds after a respawn, with the generator at a - // full 10000V and the voltage link resolving fine -- a stale state, not a - // live one. Every sibling chains its base the same way (Generator and - // PoweredSubsystem both call HeatSink::HeatSinkSimulation first; the - // Torso's own registered perf @004b5cf0 leads with UpdateWatch). - // - // ORDER IS LOAD-BEARING: the base call precedes the heat-model gate in the - // binary. Gating first (as this did) also denied the electrical machine - // to every NON-EXPERT pilot, since OwnerAdvancedDamage() is the +0x260 - // heat-model flag and that is off below veteran. - // - PoweredSubsystemSimulation(time_slice); // @004b8bab -> call 0x4b0bd0 - - // UNRECONSTRUCTED [T4], flagged not guessed: @004b8bb9-0x4b8c1e runs an - // additional effect when electricalStateAlarm == 1 (NoVoltage) and the - // myomer's own damage zone is below 1.0 -- it builds a small vector - // (constant 0xbc343958 ~= -0.011f) against the owner's localOrigin@0x100. - // Almost certainly the un-powered myomer movement penalty. Not written - // here; it needs its own decode. - if (!OwnerAdvancedDamage()) // FUN_004ad7d4 gate return; diff --git a/game/reconstructed/myomers.hpp b/game/reconstructed/myomers.hpp index c35acf5..c868fe4 100644 --- a/game/reconstructed/myomers.hpp +++ b/game/reconstructed/myomers.hpp @@ -234,8 +234,15 @@ class Mech; activePerformance = (Simulation::Performance)performance; } + // The REGISTERED Performance is the wrapper @004b8b9c (the ctor's + // [0x511620] resolves there, not to @004b8d18). It chains the base + // electrical machine, runs the un-powered self-repair, republishes + // outputVoltage/speedEffect, and only then -- and only when there IS + // voltage -- runs the inner drive-heat integrator @004b8d18 below. void - MyomersSimulation(Scalar time_slice); // @004b8d18 (Performance) + MyomersSimulation(Scalar time_slice); // @004b8b9c (Performance) + void + MyomersDriveHeat(Scalar time_slice); // @004b8d18 (inner, voltage-gated) //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Subsystem virtual overrides (vtable @005117dc) @@ -340,7 +347,14 @@ class Mech; extern int BTPlayerExperienceHeatModelOn(void *owner_mech); // btplayer.cpp (FUN_004ad7d4) return BTPlayerExperienceHeatModelOn(owner) ? True : False; } - Scalar DamageStructureLevel() const{ return 0.0f; } // DamageZone +0x158 + // This subsystem's OWN zone damage [0,1] (0 intact, 1 destroyed) -- the + // binary reads *(this[0xE0] + 0x158) here. Was a `return 0.0f` stand-in, + // which pinned damage to zero and so made AvailableOutput's (1 - damage) + // factor and the drive-heat damageGain permanently ideal: a shot-up + // myomer drove exactly as well as a fresh one. GetSubsystemDamageLevel() + // is the base's complete-type bridge to that same cell (mechsub.cpp:234), + // which sensor.cpp:312 already reads the same way. + Scalar DamageStructureLevel() const{ return GetSubsystemDamageLevel(); } // DamageZone +0x158 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)