diff --git a/context/open-questions.md b/context/open-questions.md index 44deaab..d53af30 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -161,6 +161,16 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR (task #8 "landed" vs the kShotDamage=12 bring-up residue) — audit before reworking. ## Deferred subsystems / feeds (authentic path scoped, marked in code) +- **Environment gravity unwired — the myomer CLIMB-heat term is inert (found 2026-07-31, #85).** + `Mover::localEnvironment` (MOVER.h:271) is declared and never populated anywhere in the port; + `GetEnvironment()->gravityConstant` is the named analog of the 1995 Mover's gravity POINTER at + `+0x250` (`FUN_00421e2c` does `vy -= **(+0x250)` per tick), which the myomer drive-heat + integrator uses for its `m·g·|vy|·dt` climb-work term. The bridge (`BTMechMyomerMotionSample`) + null-guards it, so climbing currently generates no myomer heat (flat maps: moot; cavern slopes: + a missing cost). EnvironmentZone resources (RES type 23) exist in BTL4.RES — wiring them (or at + least a constant-gravity Environment on mover birth) revives the term. Also relevant to any + future airborne/jump physics (`ApplyAirResistanceAndGravity` reads the same member). + [[subsystems]] - **Revolving-door Phase 2 (true mid-mission drop-in) — PLANNED, gated on a spike** (`docs/REVOLVING_DOOR_PLAN.md`, 2026-07-24). Open questions before any engine work: (a) can an `Entity::MakeMessage` be REGENERATED post-birth (serializing current state), or diff --git a/context/subsystems.md b/context/subsystems.md index 957c384..d0ac3c8 100644 --- a/context/subsystems.md +++ b/context/subsystems.md @@ -66,7 +66,29 @@ Making a base byte-exact GROWS every subclass — they must be re-based TOGETHER change needs its own verification pass). - **WAVE 5** — Torso (aim twist/elevation; joint-I/O reconstructed; the BLH record disables torso → faithfully no visible twist). -- **WAVE 6** — Myomers (mover-coupled; structural un-stub is INERT — no live mover/heat coupling). +- **WAVE 6** — Myomers (mover-coupled; the mover feed is still inert — WAVE 6 cutover — but **DRIVE + HEAT IS LIVE**, #85 fix 2026-07-31, see below). + **✅ MYOMER DRIVE HEAT LIVE (#85, 2026-07-31) [T2].** The five Mech motion operands of the + drive-heat integrator `@004b8d18` were `return 0.0f` cross-family shims, so the (fully + reconstructed) integrator accumulated `ratio² · damageGain · 0` every tick — myomers ice cold at + any seek (Oracle's night-7 panel-confirmed report). Operands re-grounded from the raw disasm + + the decomp and mapped to named members via the complete-TU bridge `BTMechMyomerMotionSample` + (mech4.cpp): `+0x1C4` vec = `localVelocity.linearMotion`; `+0x82C` vec = + `localAcceleration.linearMotion` (the authentic 15-ring smoothed AccelerationLastFrame); + `+0x20C` = `moverMass` (the collision divert's cell — the old "motion gain" label was wrong); + `*(+0x250)` = the environment **gravity pointer** (`FUN_00421e2c` does `vy -= **(+0x250)`/tick). + Physics: `heat += ratio²·(1+dmg)·[0.005·½mv² + 0.2·m·g·|vy|·dt + 0.2·m|v||a|·dt]` — kinetic work + + climb power + acceleration power. The binary `fabs`es v.y (the port's old draft didn't). + **Authored tuning extracted from BTL4.RES** (all 18 mech variants share one record): + VelocityEfficiency=0.995, AccelerationEfficiency=0.8, seek gears 0.3/0.5/0.7/0.9999 rec idx 2; + myomer thermal profile startT=77 / degradation=1000 / failure=2000 / conductance=1.9e5 / + thermalMass=2.5e5. Live-verified (solo arena, BT_MYO_LOG): standstill ≈ 0 generation, hard + circling drove T 77→1225 (past degradation) and the coolant loop pulled it back to ~520 + equilibrium on slowing. ⚠ Two carried caveats: (1) the kinetic work term has **no dt** in the + binary (per-tick accumulate, a 1995 fixed-frame assumption) → heat/s scales mildly with frame + rate; kept verbatim, field-calibrate against pod veterans. (2) the climb term is wired but + inert: `Mover::localEnvironment` is never populated in the port (EnvironmentZone res type 23 + unwired), so g reads 0 — see [[open-questions]]. ⚠ **The registered Performance is the WRAPPER @004b8b9c, not the inner integrator @004b8d18** [T1, fixed 2026-07-28]. The ctor @004b8fec stores `[0x511620]` into activePerformance, and that pointer resolves to `0x4b8b9c`, whose first instruction is `call 0x4b0bd0` = diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 038ad81..345a7fd 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -1606,6 +1606,36 @@ static void } } +//########################################################################### +// BTMechMyomerMotionSample -- the five Mech motion operands of the myomer +// drive-heat integrator @004b8d18 (issue #85), read here in a complete-Mech +// TU and mapped to NAMED engine members per the databinding rule: +// binary mech+0x1C4 vec -> localVelocity.linearMotion (|v|, v.y -- +// the same vector the crash block's iv2 squares, operand-confirmed) +// binary mech+0x82C vec -> localAcceleration.linearMotion (the published +// "AccelerationLastFrame" attribute, decomp-reference entity table) +// binary mech+0x20C -> moverMass (same cell the collision divert +// divides by -- NOT a "motion gain") +// binary *(mech+0x250) -> GetEnvironment()->gravityConstant (the 1995 +// Mover holds a POINTER to the environment gravity: FUN_00421e2c does +// vy -= **(+0x250) per tick. So the integrator's velTerm is m*g*|vy| +// = climb work, and the heat physics reads: kinetic energy + climb +// power + acceleration power, scaled by the authored inefficiencies.) +//########################################################################### +void BTMechMyomerMotionSample(void *mech_v, + float *vel_mag, float *acc_mag, float *vel_y, float *mass, float *gravity) +{ + Mech *m = (Mech *)mech_v; + const Vector3D &v = m->localVelocity.linearMotion; + const Vector3D &a = m->localAcceleration.linearMotion; + *vel_mag = (float)Sqrt(v.x * v.x + v.y * v.y + v.z * v.z); + *acc_mag = (float)Sqrt(a.x * a.x + a.y * a.y + a.z * a.z); + *vel_y = (float)v.y; + *mass = (float)m->moverMass; + Environment *env = m->GetEnvironment(); + *gravity = (env != 0) ? (float)env->gravityConstant : 0.0f; +} + //########################################################################### // MechDeathHandler effect-spawn bridge (mechdmg.cpp calls this) // diff --git a/game/reconstructed/myomers.cpp b/game/reconstructed/myomers.cpp index ccdb385..ed13fd3 100644 --- a/game/reconstructed/myomers.cpp +++ b/game/reconstructed/myomers.cpp @@ -659,30 +659,62 @@ void Myomers::MyomersSimulation(Scalar time_slice) // void Myomers::MyomersDriveHeat(Scalar time_slice) { - if (!OwnerAdvancedDamage()) // FUN_004ad7d4 gate - return; + if (!OwnerAdvancedDamage()) // FUN_004ad7d4 gate (binary + return; // gates the ACCUMULATE; same outcome) - Scalar velMag = OwnerVelocityMag(); // |Mech velocity| (+0x1C4/0x1C8/0x1CC) - Scalar accMag = OwnerDriveMag(); // |Mech drive vector| (+0x82C/0x830/0x834) - Scalar vy = OwnerVelocityY(); // Mech +0x1C8 - Scalar k = OwnerMotionGain(); // Mech +0x20C - Scalar m = OwnerMass(); // *Mech[0x250] + // #85 fix (2026-07-31): the five Mech motion operands were `return 0.0f` + // cross-family shims, so this integrator faithfully accumulated ratio^2 * + // damageGain * ZERO every tick -- the myomers stayed ice cold at any seek + // (Oracle's night-7 panel-confirmed report). Now sampled for real via the + // complete-Mech-TU bridge (mech4.cpp), operands re-grounded against the + // raw disasm of @004b8d18 + the authored resource (BTL4.RES 0x0BC6 records: + // VelocityEfficiency=0.995, AccelerationEfficiency=0.8, gears + // 0.3/0.5/0.7/0.9999 rec idx 2 -- one shared tuning across all 18 mechs). + float velMag, accMag, vy, mass, gravity; + { + extern void BTMechMyomerMotionSample(void *mech, + float *vel_mag, float *acc_mag, float *vel_y, + float *mass, float *gravity); + BTMechMyomerMotionSample(owner, &velMag, &accMag, &vy, &mass, &gravity); + } - Scalar velComplement = 1.0f - accelerationEfficiency; // @0x354 - Scalar workComplement = 1.0f - velocityEfficiency; // @0x350 + Scalar velComplement = 1.0f - accelerationEfficiency; // @0x354 (= 0.2 authored) + Scalar workComplement = 1.0f - velocityEfficiency; // @0x350 (= 0.005 authored) Scalar damageGain = 1.0f + DamageStructureLevel(); // this[0xE0]+0x158 Scalar ratio = seekVoltage[currentSeekVoltageIndex] // @0x330[@0x320] / seekVoltage[recommendedSeekVoltageIndex];// @0x330[@0x324] if (ratio < 1.0f) ratio = 1.0f; - Scalar work = k * (velMag * velMag) * 0.5f; // dot(v,v)*0.5 == |v|^2*0.5 (_DAT_004b8ee4) + // The binary fabs()es all three motion operands (the jbe/fchs pairs + // @4b8dd5/@4b8df0/@4b8e16); |v| and |a| are sqrt-positive already, but + // v.y is signed -- descending must heat like climbing, not cool. + if (vy < 0.0f) vy = -vy; + + // Physics (operands nailed 2026-07-31): mass@0x20C, gravity=**(0x250) -- + // work = 1/2 m v^2 (kinetic energy; NO dt in the + // binary -- the accumulate is per-TICK, an authentic 1995 + // fixed-frame assumption, so heat/s scales with frame rate; + // kept verbatim, field-calibrate if the pod veterans object) + // velTerm = m g |vy| dt (climb power) + // accTerm = m |v| |a| dt (acceleration power) + Scalar work = mass * (velMag * velMag) * 0.5f; // dot(v,v)*0.5 (_DAT_004b8ee4) pendingHeat /* @0x1C8 */ += ratio * ratio * damageGain * - ( velComplement * vy * k * m * time_slice + ( velComplement * vy * mass * gravity * time_slice + workComplement * work - + velComplement * velMag * accMag * k * time_slice ); + + velComplement * velMag * accMag * mass * time_slice ); + + if (getenv("BT_MYO_LOG")) + { + static int s_n = 0; + if ((s_n++ % 120) == 0) + DEBUG_STREAM << "[myoheat] v=" << velMag << " a=" << accMag + << " m=" << mass << " g=" << gravity + << " ratio=" << ratio << " dmgGain=" << damageGain + << " pending=" << pendingHeat << "\n" << std::flush; + } } @@ -807,9 +839,11 @@ struct MyomersLayoutCheck // 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. +// gear table) and ticks its real Performance. DRIVE HEAT IS LIVE (#85 fix +// 2026-07-31: the motion operands are sampled from the real Mech). Still +// inert: the LOCOMOTION coupling (no-op mover feed -- speedEffect is computed +// but nothing consumes it; WAVE 6 cutover) and the climb heat term (the +// environment gravity is unwired in the port, so g reads 0). //===========================================================================// Subsystem *CreateMyomersSubsystem(Mech *owner, int id, void *seg) { diff --git a/game/reconstructed/myomers.hpp b/game/reconstructed/myomers.hpp index f7d9562..c7607a0 100644 --- a/game/reconstructed/myomers.hpp +++ b/game/reconstructed/myomers.hpp @@ -331,11 +331,10 @@ class Mech; 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 + // (The five drive-heat motion shims are GONE -- #85 fix 2026-07-31: + // MyomersDriveHeat samples the real Mech via the complete-TU bridge + // BTMechMyomerMotionSample (mech4.cpp). The mover-feed trio below is + // still the WAVE-6 remainder.) // FUN_004ad7d4 [T1] -- the SAME player+0x260 heat-model experience gate // as HeatSink::HeatModelActive() (the "advanced damage" name was a // misread; the old `return False` stub kept movement heat off for