From cb85517ede2069247de8507b057415f524e87d7a Mon Sep 17 00:00:00 2001 From: arcattack Date: Sat, 11 Jul 2026 06:47:22 -0500 Subject: [PATCH] Gyro: byte-exact re-enable -- ctor map, integrators, jointeye writers (task #56) The Gyroscope subsystem is LIVE again (was a stub since the NaN revert). Reconstructed byte-exact from @004b3778 / @004b2ec0 / @004b30ec / @004b33e0 / @004b34ec + the byte-verified dispatch in the unexported Mech performance FUN_004a9b5c (calls @0x4aaf74/0x4aaf83): - ctor field map corrected: springConstant@0x1E8 / dampingConstant@0x1F4 were mislabelled (the old 'eyeOrientation = r->springConstant' poison line), the 0x254-0x2B3 block (externalPitchPtr, work matrix, placement) was missing so everything after was mis-offset, and clamps/accumulators were 0xCD fill -- every field now initialised per the binary; 33 offsets static_assert-locked, sizeof == 0x3D0 exact - integrators: state-minus-target displacements, componentwise damping OVERWRITE (FUN_004086d0 is a component multiply, not a cross), position step without dt, clamp to [posSpring, negSpring]; IntegrateBody's X/Z-crossed force + integration terms - writers: WriteMechJoint drives ONE node ('jointeye', type 5): TRANSLATION = eyePosition spring, ROTATION = bodyOrientation spring; WriteEyeJoint is a multiplicative sway attenuator on 'jointlocal' (post-anim only) - dispatch moved out of GyroscopeSimulation into the Mech performance tail (GyroFrameJointWrite bridge, mech4.cpp) with the binary gates; gyro<->torso pitch link wired (gyro+0x258 = &torso currentTwist) via complete-type-TU bridges, retiring the SubProxy::linkTarget landmine - Mech ctor: deathAnimationLatched/legResetLatch were never initialised -- 0xCDCDCDCD gated the writers off silently Runtime-verified: joints resolve (both BallTranslation), Performance installs, WriteMechJoint finite from frame 0 (no NaN), Madcat cockpit un-regressed, combat targeting healthy. Empirical: spring targets are SYMMETRIC -> eye equilibrium (0,0,0), clamps +/-0.1-0.15u -- the gyro is the hit-BOUNCE mechanism, not a steady eye offset (hypothesis disproven, recorded in KB). Pending (task #56 tail): the damage->gyro fan-out FUN_004b2980 (unexported gap) so hits actually kick the springs; the mech+0x3F0 overspeed sway model; the torso-pitch EyepointRotation writer (FUN_004b66b4) + glance-look states. Co-Authored-By: Claude Fable 5 --- context/open-questions.md | 31 +++- game/reconstructed/gyro.cpp | 324 ++++++++++++++++++++++++----------- game/reconstructed/gyro.hpp | 79 ++++++--- game/reconstructed/mech.cpp | 50 +++--- game/reconstructed/mech4.cpp | 33 ++++ game/reconstructed/torso.cpp | 11 ++ game/reconstructed/torso.hpp | 3 + 7 files changed, 373 insertions(+), 158 deletions(-) diff --git a/context/open-questions.md b/context/open-questions.md index 5cde9c8..23fc52b 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -69,12 +69,14 @@ authentic path scoped. - **Cockpit HUD leftovers (tasks #35-#38 residue, 2026-07-08)** — the reticle + 7 weapon pips + ALL the instrument dynamics are LIVE (the Execute @004cdcf0 gap was RECOVERED via capstone disasm, task #37; the authentic Lock producer + the simple-X mode landed in #38 — see - [[gauges-hud]] for the full instrument map). Remaining: (a) the **blx_cop canopy shell**: it is - the torso segment's SkeletonType_A mesh and fully encloses the eye; it renders as a black box - because the skeleton-branch render path isn't giving it its texture + the PUNCH cutout (the - canopy WINDOWS are punch texels — with punch live the black pane texels become holes and the - world shows through the frame). Fix lives in the [[rendering]] skeleton/punch branch, not the - HUD; hidden by name filter until then (`BT_INSIDE_COCKPIT=1` shows it). (b) the **3D marker + [[gauges-hud]] for the full instrument map). Remaining: (a) the **`*_cop` canopy shell** — + SOLVED for the Madcat (task #55, see [[cockpit-view]]): authentic eye (baseOffset + parent-DCS + + inverse view) + single-sided inward-wound lattice + dark `softer` ramp = the dark frame with the + world through the openings, verified vs gameplay footage; shows by DEFAULT now + (`BT_HIDE_COCKPIT=1` hides). NOTE the old claim here ("canopy windows are punch texels") was + WRONG — the shell has no texture; the openings are geometry. STILL OPEN: the non-Madcat mechs' + views are blocked because the resting eye sits ~0.6u inside the canopy wall — the missing + gyro eye-joint steady offset (task #56, [[cockpit-view]] §gyro). (b) the **3D marker chain** ([0x31c]/[0x320] dpl objects + the PNAME1-8.bgf player-name meshes over the locked target — matters for MP). (c) the compass rotation SIGN is derived-not-verified [T3] (turn left → the stem should swing right; flip the sin sign in Draw if footage disagrees). @@ -82,9 +84,20 @@ authentic path scoped. BTPlayer+0x1dc is never populated → MessageBoard is empty (authentic for bring-up). What resolves it: wire the pool + decode `AddStatusMessage @0042e580` + the `Player__StatusMessage` record ({messageId@+0xc, nameEntity@+0x10}). -- **Gyroscope (0xBC4)** — layout + joint-I/O reconstructed, but the ctor field-init + the two - integrators (IntegrateEyeJoint @004b2ec0 / IntegrateBody @004b30ec) are incomplete → writes NaN - to the root joint. Reverted to a stub. A full subsystem-sim reconstruction (bigger than Torso). +- **Gyroscope (0xBC4) — RE-ENABLED LIVE (task #56, 2026-07-10).** The NaN revert's root causes + are fixed byte-exact (ctor field map @004b3778 — springConstant@0x1E8/dampingConstant@0x1F4 + were mislabelled, the 0x254-0x2B3 block was missing, clamps/accumulators uninitialised; the + integrators' state-minus-target/componentwise-damping-overwrite/X-Z-crossing semantics; the + writers' one-node-two-channels form; dispatch from the MECH performance tail, not the gyro + Performance). Layout locked (sizeof==0x3D0); runtime clean (joints type 5, finite state, no + NaN). Empirical: spring targets are SYMMETRIC → eye equilibrium (0,0,0) → the gyro is a + ±0.1-0.15u BOUNCE mechanism, NOT a steady offset (the earlier hypothesis is disproven). + REMAINING (task #56 tail): (a) the damage→gyro fan-out **FUN_004b2980** (0x4b2980-0x4b2d8b, + unexported gap; called @0x4a02fb + direct crunch kicks) — without it no hit-bounce fires; + (b) the mech+0x3F0 overspeed sway model (swayBias fed 0); (c) the per-frame + `EyepointRotation.pitch = torso pitch` writer (FUN_004b66b4) + mapper glance-look states + (FUN_004afd10 — yaw/pitch from mech+0x564..0x570). Also: `deathAnimationLatched`/`legResetLatch` + were never ctor-initialised (0xCDCDCDCD gate bug, fixed in mech.cpp ctor). See [[cockpit-view]]. - **MechControlsMapper look/eyepoint commit** — reconstructed but its offsets collide with declared members; arbitrate before enabling (no port consumer yet). The fire-trigger-through-mapper is still the `gBTWeaponTrigger` bring-up. diff --git a/game/reconstructed/gyro.cpp b/game/reconstructed/gyro.cpp index 7ec37ec..7851f65 100644 --- a/game/reconstructed/gyro.cpp +++ b/game/reconstructed/gyro.cpp @@ -150,8 +150,38 @@ static GyroSkeleton *LoadSkeleton(const ResourceDirectories * /*dirs*/, const ch //########################################################################### struct GyroLayoutCheck { - static_assert(offsetof(Gyroscope, exageration) == 0x1D8, "Gyroscope exageration must be at 0x1D8 after re-base"); - static_assert(sizeof(Gyroscope) <= 0x3D0, "Gyroscope must fit the 0x3D0 factory alloc"); + // BINARY-EXACT own-block lock (ctor @004b3778 field map, task #56). + static_assert(offsetof(Gyroscope, exageration) == 0x1D8, "exageration @0x1D8"); + static_assert(offsetof(Gyroscope, eyePosition) == 0x1DC, "eyePosition @0x1DC"); + static_assert(offsetof(Gyroscope, springConstant) == 0x1E8, "springConstant @0x1E8"); + static_assert(offsetof(Gyroscope, dampingConstant) == 0x1F4, "dampingConstant @0x1F4"); + static_assert(offsetof(Gyroscope, eyeClampUpper) == 0x200, "eyeClampUpper @0x200"); + static_assert(offsetof(Gyroscope, eyeClampLower) == 0x20C, "eyeClampLower @0x20C"); + static_assert(offsetof(Gyroscope, posSpring) == 0x218, "posSpring @0x218"); + static_assert(offsetof(Gyroscope, negSpring) == 0x224, "negSpring @0x224"); + static_assert(offsetof(Gyroscope, eyeForce) == 0x230, "eyeForce @0x230"); + static_assert(offsetof(Gyroscope, eyeVelocity) == 0x23C, "eyeVelocity @0x23C"); + static_assert(offsetof(Gyroscope, eyeWork) == 0x248, "eyeWork @0x248"); + static_assert(offsetof(Gyroscope, spare0) == 0x254, "spare0 @0x254"); + static_assert(offsetof(Gyroscope, externalPitchPtr) == 0x258, "externalPitchPtr @0x258"); + static_assert(offsetof(Gyroscope, workMatrix) == 0x25C, "workMatrix @0x25C"); + static_assert(offsetof(Gyroscope, bodyOrientation) == 0x2B4, "bodyOrientation @0x2B4"); + static_assert(offsetof(Gyroscope, rotationSpringConstant) == 0x2C0, "rotationSpringConstant @0x2C0"); + static_assert(offsetof(Gyroscope, rotationDampingConstant) == 0x2CC, "rotationDampingConstant @0x2CC"); + static_assert(offsetof(Gyroscope, bodyClampUpper) == 0x2D8, "bodyClampUpper @0x2D8"); + static_assert(offsetof(Gyroscope, bodyClampLower) == 0x2E4, "bodyClampLower @0x2E4"); + static_assert(offsetof(Gyroscope, rotationPosSpring) == 0x2F0, "rotationPosSpring @0x2F0"); + static_assert(offsetof(Gyroscope, rotationNegSpring) == 0x2FC, "rotationNegSpring @0x2FC"); + static_assert(offsetof(Gyroscope, bodyForce) == 0x308, "bodyForce @0x308"); + static_assert(offsetof(Gyroscope, bodyVelocity) == 0x314, "bodyVelocity @0x314"); + static_assert(offsetof(Gyroscope, bodyWork) == 0x320, "bodyWork @0x320"); + static_assert(offsetof(Gyroscope, damageMultiplier) == 0x32C, "damageMultiplier @0x32C"); + static_assert(offsetof(Gyroscope, damageResponse) == 0x340, "damageResponse @0x340"); + static_assert(offsetof(Gyroscope, swayBias) == 0x3A8, "swayBias @0x3A8"); + static_assert(offsetof(Gyroscope, swayAngle) == 0x3BC, "swayAngle @0x3BC"); + static_assert(offsetof(Gyroscope, eyeJointNode) == 0x3C8, "eyeJointNode @0x3C8"); + static_assert(offsetof(Gyroscope, mechJointNode) == 0x3CC, "mechJointNode @0x3CC"); + static_assert(sizeof(Gyroscope) == 0x3D0, "Gyroscope must be exactly the 0x3D0 factory alloc"); }; @@ -232,22 +262,50 @@ Gyroscope::Gyroscope( percentageOnDegradation = r->percentageOnDegradation; // @0x3B4 <- +0x110 percentageOnFailure = r->percentageOnFailure; // @0x3B8 <- +0x114 - eyeOrientation = r->springConstant; // @0x1DC (+0x118 -> spring work) - // the eye spring/damping + pos/neg-spring vectors are copied into the - // integrator's working set (see IntegrateEyeJoint): - eyePosSpring = r->posSpring; // @0x218 <- +0x130 - eyeNegSpring = r->negSpring; // @0x224 <- +0x13C + // BINARY-EXACT copies + inits (ctor @004b3778 lines 2812-2942 -- the OLD recon's + // single line `eyeOrientation = r->springConstant` was the NaN poison: it left + // springConstant/dampingConstant uninitialised AND seeded the eye state with a + // spring constant. Every accumulator below is zeroed in the binary; nothing is + // left as MSVC 0xCD fill.) + springConstant = r->springConstant; // @0x1E8 <- +0x118 (:2821) + dampingConstant = r->dampingConstant; // @0x1F4 <- +0x124 (:2823) + posSpring = r->posSpring; // @0x218 <- +0x130 (:2825) + negSpring = r->negSpring; // @0x224 <- +0x13C (:2827) + eyeClampUpper = posSpring; // @0x200 identity transform = copy (:2876-2879) + eyeClampLower = negSpring; // @0x20C (:2880-2883) - rotationSpringConstant = r->rotationSpringConstant; // @0x2C0 <- +0x148 - rotationDampingConstant = r->rotationDampingConstant; // @0x2CC <- +0x154 - bodyPosSpring = r->rotationPosSpring; // @0x2F0 <- +0x160 - bodyNegSpring = r->rotationNegSpring; // @0x2FC <- +0x16C + rotationSpringConstant = r->rotationSpringConstant; // @0x2C0 <- +0x148 (:2830) + rotationDampingConstant = r->rotationDampingConstant; // @0x2CC <- +0x154 (:2833) + rotationPosSpring = r->rotationPosSpring; // @0x2F0 <- +0x160 (:2834, already deg->rad) + rotationNegSpring = r->rotationNegSpring; // @0x2FC <- +0x16C (:2835) + bodyClampUpper.Multiply(rotationPosSpring, 2.0f); // @0x2D8 = 2.0 * rotPosSpring (:2884) + bodyClampLower.Multiply(rotationNegSpring, 2.0f); // @0x2E4 = 2.0 * rotNegSpring (:2885) - // per-damage-type response multipliers and {Trans,PitchRoll,Yaw,Vibration} - // curves are stored at this[0xCB..0xE0] (@0x32C..0x380); copied verbatim. + // per-damage-type response multipliers + {Trans,PitchRoll,Yaw,Vibration} quads (:2836-2875) + damageMultiplier[0] = r->collisionDamageMultiplier; // @0x32C + damageMultiplier[1] = r->ballisticDamageMultiplier; + damageMultiplier[2] = r->explosiveDamageMultiplier; + damageMultiplier[3] = r->laserDamageMultiplier; + damageMultiplier[4] = r->energyDamageMultiplier; + damageResponse[0] = r->collisionDamageResponse; // @0x340..0x38F + damageResponse[1] = r->ballisticDamageResponse; + damageResponse[2] = r->explosiveDamageResponse; + damageResponse[3] = r->laserDamageResponse; + damageResponse[4] = r->energyDamageResponse; - // identity-init the two orientation work matrices (FUN_0040aadc) and seed - // the eye/body negative-spring axes (FUN_00408744 clamp-box) -- see @004b3778. + // integrator state + placement scratch: ALL zeroed by the binary (:2921-2942) + { + const Vector3D vzero(0.0f, 0.0f, 0.0f); + eyePosition = eyeForce = eyeVelocity = eyeWork = vzero; // @0x1DC/0x230/0x23C/0x248 + bodyOrientation = bodyForce = bodyVelocity = bodyWork = vzero; // @0x2B4/0x308/0x314/0x320 + placePos = placeRot = vzero; // @0x28C/0x2A8 + placeQuat[0] = placeQuat[1] = placeQuat[2] = 0.0f; placeQuat[3] = 1.0f; // @0x298 identity quat (:2928) + for (int i = 0; i < 12; ++i) workMatrix[i] = 0.0f; // @0x25C identity 3x4 (:2793/:2934) + workMatrix[0] = workMatrix[5] = workMatrix[10] = 1.0f; + spare0 = 0.0f; // @0x254 (:2931) + externalPitchPtr = &spare0; // @0x258 self-pointer (:2933); + // bt_mech tail re-points to &torso pitch + } animationOffset = 0.0f; // @0x390 animationScale = 1.0f; // @0x394 animationPhase = 0.0f; // @0x398 @@ -273,6 +331,21 @@ Gyroscope::Gyroscope( DEBUG_STREAM << " ; mech '" << r->mechJoint << "' -> " << (void*)mechJointNode; if (mechJointNode) DEBUG_STREAM << " type=" << (int)mechJointNode->GetJointType(); DEBUG_STREAM << "\n" << std::flush; + // spring data (captures the eye equilibrium = (posSpring+negSpring)/2 -- the + // numeric values live only in the archived .MDL configs, not the EXE): + DEBUG_STREAM << "[gyro] springK=(" << (float)springConstant.x << "," << (float)springConstant.y + << "," << (float)springConstant.z << ") dampK=(" << (float)dampingConstant.x << "," + << (float)dampingConstant.y << "," << (float)dampingConstant.z << ")\n"; + DEBUG_STREAM << "[gyro] posSpring=(" << (float)posSpring.x << "," << (float)posSpring.y + << "," << (float)posSpring.z << ") negSpring=(" << (float)negSpring.x << "," + << (float)negSpring.y << "," << (float)negSpring.z << ") -> eye equilibrium=(" + << (float)((posSpring.x+negSpring.x)*0.5f) << "," << (float)((posSpring.y+negSpring.y)*0.5f) + << "," << (float)((posSpring.z+negSpring.z)*0.5f) << ")\n" << std::flush; + DEBUG_STREAM << "[gyro] simulationFlags=0x" << std::hex << (unsigned)owner->simulationFlags + << std::dec << " performance " + << ((((owner->simulationFlags & SegmentCopyMask) == 0) + && ((owner->simulationFlags & MasterHeatSinkFlag) != 0)) ? "INSTALLED" : "NOT installed") + << "\n" << std::flush; } Check_Fpu(); @@ -318,12 +391,17 @@ void const Vector3D zero(0.0f, 0.0f, 0.0f); - eyeOrientation = zero; // FUN_00408440(this+0x77, &DAT_004e0f74) - eyeForce = zero; - eyeAccel = zero; - bodyPosSpring = zero; // FUN_00408e90(this+0xAD, &DAT_004e0f8c) -- zero rotation - bodyAccel = zero; - bodyForce = zero; + // @004b2678 zeroes the EIGHT integrator vectors (:2190-2197): eye pos/force/ + // velocity/work + body orientation/force/velocity/work. (The old recon zeroed + // the SPRING TARGET bodyPosSpring instead of bodyOrientation -- wrong member.) + eyePosition = zero; // @0x1DC FUN_00408440(this+0x77, &DAT_004e0f74) + eyeForce = zero; // @0x230 + eyeVelocity = zero; // @0x23C + eyeWork = zero; // @0x248 + bodyOrientation = zero; // @0x2B4 FUN_00408e90(this+0xAD, &DAT_004e0f8c) + bodyForce = zero; // @0x308 + bodyVelocity = zero; // @0x314 + bodyWork = zero; // @0x320 // CROSS-FAMILY: PowerWatcher (PowerWatcher : Subsystem in these headers) has // no slot-10 ResetToInitialState() to chain to; the base reset (FUN_004b1804) @@ -404,95 +482,94 @@ void IntegrateEyeJoint(time_slice); // FUN_004b2ec0 IntegrateBody(time_slice); // FUN_004b30ec - // Per-frame skeleton write: push the integrated eye/body orientation into the - // resolved joints. Like the torso's UpdateJoints, the binary's WriteEyeJoint - // (@004b33e0, via thunk @004b2eac) + WriteMechJoint (@004b34ec) have NO direct - // caller in the decomp -- they were dispatched by the engine's generic per-frame - // joint pass. This port ticks the gyro's Performance here, so resolve that - // dispatch to a direct call at the same cadence (== what makes the eye/body move). - WriteEyeJoint(); // FUN_004b33e0 - WriteMechJoint(); // FUN_004b34ec - + // NOTE (task #56, byte-verified): the binary Performance @004b275c ENDS here. + // WriteEyeJoint/WriteMechJoint are NOT called from the gyro -- they are called + // from the MECH master performance tail (FUN_004a9b5c, calls @0x4aaf74/0x4aaf83) + // after the animation pass, with the death/leg-anim gates. See + // GyroFrameJointWrite (bridge below) + the mech4.cpp dispatch. Check_Fpu(); } // -// @004b2ec0 [BEST-EFFORT] -- integrate the cockpit "eye" orientation with a -// spring toward (posSpring/negSpring) and damping, scaled by time_slice, then -// clamp each axis into [eyeLimitLow, eyeLimitHigh]. Vector ops are the engine -// SIMD-ish helpers; member names are inferred from the access pattern. +// @004b2ec0 [T1 byte-exact, task #56] -- the eye TRANSLATION spring integrator. +// Displacements are STATE minus TARGET (the config spring constants are negative +// = restoring), both terms use springConstant, the damping step OVERWRITES the +// force accumulator with dampingConstant (.) velocity (FUN_004086d0 is a +// COMPONENT-WISE multiply, not a cross), and the position step has NO dt (the +// binary passes velocity through an identity matrix). eyeForce is NOT cleared +// per frame -- it carries last frame's damping term + damage impulses by design. +// Equilibrium: eyePosition -> (posSpring+negSpring)/2 per axis = the authentic +// STEADY EYE OFFSET (written to 'jointeye' translation by WriteMechJoint). // void Gyroscope::IntegrateEyeJoint(Scalar time_slice) { - Vector3D toPos; toPos.Subtract(eyePosSpring, eyeOrientation); // FUN_00408644 - Vector3D toNeg; toNeg.Subtract(eyeNegSpring, eyeOrientation); // (using @0x218 / @0x224) + Vector3D d1; d1.Subtract(eyePosition, negSpring); // :2376 state - target + Vector3D d2; d2.Subtract(eyePosition, posSpring); // :2377 - Vector3D springForce; springForce.Multiply(eyeSpringConstant, toPos); // FUN_004086d0 (this+0x1E8) - Vector3D dampingForce; dampingForce.Multiply(eyeSpringConstant, toNeg); + Vector3D f; + f.Multiply(springConstant, d1); eyeForce += f; // :2378-2381 componentwise + f.Multiply(springConstant, d2); eyeForce += f; - eyeAccel += springForce; // FUN_004085ec (this+0x230) - eyeAccel += dampingForce; + eyeWork = eyeForce; // :2383 + f.Multiply(eyeWork, time_slice); eyeVelocity += f; // :2384-2385 - eyeWork = eyeAccel; // this+0x248 - Vector3D step; step.Multiply(eyeWork, time_slice); // FUN_004086ac - eyeForce += step; // FUN_004085ec (this+0x23C) + eyeForce.Multiply(dampingConstant, eyeVelocity); // :2387 OVERWRITE (componentwise) + eyeWork = eyeForce; + f.Multiply(eyeWork, time_slice); eyeVelocity += f; // :2388-2390 - eyeAccel.Cross(eyeSpringConstant, eyeForce); // FUN_004086d0 cross - eyeWork = eyeAccel; - step.Multiply(eyeWork, time_slice); - eyeForce += step; + eyePosition += eyeVelocity; // :2391-2393 identity pass-through, NO dt - // the original clamped eyeForce against an identity orientation box - // (FUN_00408744 with a just-built identity matrix) -- a pass-through -- then - // integrated it into eyeOrientation. - Vector3D delta = eyeForce; // FUN_00408744 (identity box) - eyeOrientation += delta; // FUN_004085ec (this+0x1DC) - - // per-axis clamp into [eyeLimitLow @0x200, eyeLimitHigh @0x20C] - if (eyeOrientation.x > eyeLimitLow.x) eyeOrientation.x = eyeLimitLow.x; - if (eyeOrientation.y > eyeLimitLow.y) eyeOrientation.y = eyeLimitLow.y; - if (eyeOrientation.z > eyeLimitLow.z) eyeOrientation.z = eyeLimitLow.z; - if (eyeOrientation.x < eyeLimitHigh.x) eyeOrientation.x = eyeLimitHigh.x; - if (eyeOrientation.y < eyeLimitHigh.y) eyeOrientation.y = eyeLimitHigh.y; - if (eyeOrientation.z < eyeLimitHigh.z) eyeOrientation.z = eyeLimitHigh.z; + // per-axis clamp: min against eyeClampUpper(0x200 = posSpring), then + // max against eyeClampLower(0x20C = negSpring) + if (eyePosition.x > eyeClampUpper.x) eyePosition.x = eyeClampUpper.x; + if (eyePosition.y > eyeClampUpper.y) eyePosition.y = eyeClampUpper.y; + if (eyePosition.z > eyeClampUpper.z) eyePosition.z = eyeClampUpper.z; + if (eyePosition.x < eyeClampLower.x) eyePosition.x = eyeClampLower.x; + if (eyePosition.y < eyeClampLower.y) eyePosition.y = eyeClampLower.y; + if (eyePosition.z < eyeClampLower.z) eyePosition.z = eyeClampLower.z; } // -// @004b30ec [BEST-EFFORT] -- the body tip integrator. Same structure as -// IntegrateEyeJoint but driven by rotationSpringConstant / rotationDampingConstant -// (this+0x2C0/0x2CC) against the rotationPos/NegSpring limits, accumulating into -// bodyOrientation (this+0x2B4) and clamping into [bodyLimitLow, bodyLimitHigh]. +// @004b30ec [T1 byte-exact, task #56] -- the body ROTATION spring integrator. +// Same shape as the eye integrator with three verified quirks: (1) BOTH spring +// terms use rotationSpringConstant (never damping); (2) the spring force +// components are X/Z-CROSSED: force = (k.x*d.z, k.y*d.y, k.z*d.x); (3) the +// integration back into bodyOrientation is crossed again (ori.x += vel.z, +// ori.z += vel.x). Damping is componentwise and UNcrossed. Clamps go min +// against bodyClampUpper (2*rotPosSpring) then max against bodyClampLower +// (2*rotNegSpring), in y,x,z order. // void Gyroscope::IntegrateBody(Scalar time_slice) { - Vector3D toNeg; toNeg.Subtract(bodyOrientation, bodyNegSpring); // (this+0x2B4 - this+0x2FC) - Vector3D toPos; toPos.Subtract(bodyOrientation, bodyPosSpring); // (this+0x2B4 - this+0x2F0) + Vector3D dN; dN.Subtract(bodyOrientation, rotationNegSpring); // state - target + Vector3D dP; dP.Subtract(bodyOrientation, rotationPosSpring); - Vector3D springForce; springForce.Multiply(rotationSpringConstant, toNeg); // (this+0x2C0) - Vector3D dampingForce; dampingForce.Multiply(rotationDampingConstant, toPos); // (this+0x2CC) + const Vector3D &k = rotationSpringConstant; + Vector3D f; + f.x = k.x * dN.z; f.y = k.y * dN.y; f.z = k.z * dN.x; // :2479-2481 X/Z crossed + bodyForce += f; + f.x = k.x * dP.z; f.y = k.y * dP.y; f.z = k.z * dP.x; // :2482-2484 + bodyForce += f; - bodyAccel += springForce; // this+0x308 - bodyAccel += dampingForce; - bodyWork = bodyAccel; // this+0x320 - Vector3D step; step.Multiply(bodyWork, time_slice); - bodyForce += step; // this+0x314 + bodyWork = bodyForce; + f.Multiply(bodyWork, time_slice); bodyVelocity += f; - bodyAccel.Cross(rotationDampingConstant, bodyForce); // cross (this+0x2CC) - bodyWork = bodyAccel; - step.Multiply(bodyWork, time_slice); - bodyForce += step; + bodyForce.Multiply(rotationDampingConstant, bodyVelocity); // :2498 componentwise, uncrossed, OVERWRITE + bodyWork = bodyForce; + f.Multiply(bodyWork, time_slice); bodyVelocity += f; - Vector3D delta = bodyForce; // FUN_00408744 (identity box) - bodyOrientation += delta; + bodyOrientation.x += bodyVelocity.z; // :2504-2506 crossed again + bodyOrientation.y += bodyVelocity.y; + bodyOrientation.z += bodyVelocity.x; - if (bodyOrientation.y > bodyLimitLow.y) bodyOrientation.y = bodyLimitLow.y; // @0x2DC - if (bodyOrientation.x > bodyLimitLow.x) bodyOrientation.x = bodyLimitLow.x; // @0x2D8 - if (bodyOrientation.z > bodyLimitLow.z) bodyOrientation.z = bodyLimitLow.z; // @0x2E0 - if (bodyOrientation.y < bodyLimitHigh.y) bodyOrientation.y = bodyLimitHigh.y; // @0x2E8 - if (bodyOrientation.x < bodyLimitHigh.x) bodyOrientation.x = bodyLimitHigh.x; // @0x2E4 - if (bodyOrientation.z < bodyLimitHigh.z) bodyOrientation.z = bodyLimitHigh.z; // @0x2EC + if (bodyOrientation.y > bodyClampUpper.y) bodyOrientation.y = bodyClampUpper.y; // @0x2DC + if (bodyOrientation.x > bodyClampUpper.x) bodyOrientation.x = bodyClampUpper.x; // @0x2D8 + if (bodyOrientation.z > bodyClampUpper.z) bodyOrientation.z = bodyClampUpper.z; // @0x2E0 + if (bodyOrientation.y < bodyClampLower.y) bodyOrientation.y = bodyClampLower.y; // @0x2E8 + if (bodyOrientation.x < bodyClampLower.x) bodyOrientation.x = bodyClampLower.x; // @0x2E4 + if (bodyOrientation.z < bodyClampLower.z) bodyOrientation.z = bodyClampLower.z; // @0x2EC } // @@ -528,9 +605,11 @@ void } // -// @004b34ec [CONFIDENT] -- push the integrated orientations into the MechJoint -// node (type 5 only): write eyeOrientation as the node rotation and bodyOrientation -// as the node translation, again only when they differ by more than QuantiseEps. +// @004b34ec [T1 byte-exact, task #56] -- push the integrated state into the +// MechJoint node ('jointeye', type 5 BallTranslation ONLY): TRANSLATION <- +// eyePosition (the steady eye offset + hit bounce; siteeyepoint rides this +// joint), ROTATION <- bodyOrientation (the body tip), each written only when +// it differs by more than QuantiseEps. ONE node, both channels. // void Gyroscope::WriteMechJoint() @@ -546,14 +625,16 @@ void static int s_gc = 0; if (s_glog && (s_gc % 60) == 0 && s_gc < 1200) { - DEBUG_STREAM << "[gyro] WriteMechJoint eye=(" << (float)eyeOrientation.x << "," << (float)eyeOrientation.y - << "," << (float)eyeOrientation.z << ") body=(" << (float)bodyOrientation.x << "," + DEBUG_STREAM << "[gyro] WriteMechJoint eyePos=(" << (float)eyePosition.x << "," << (float)eyePosition.y + << "," << (float)eyePosition.z << ") body=(" << (float)bodyOrientation.x << "," << (float)bodyOrientation.y << "," << (float)bodyOrientation.z << ")\n" << std::flush; + if (!(eyePosition.x == eyePosition.x) || !(bodyOrientation.x == bodyOrientation.x)) + DEBUG_STREAM << "[gyro] *** NaN DETECTED in integrator state ***\n" << std::flush; } ++s_gc; - if (!NodeRotationEquals(mechJointNode, eyeOrientation, QuantiseEps)) // FUN_004084fc + if (!NodeRotationEquals(mechJointNode, eyePosition, QuantiseEps)) // FUN_004084fc vs value+0 { - SetNodeRotation(mechJointNode, eyeOrientation); // FUN_0041d11c + SetNodeRotation(mechJointNode, eyePosition); // FUN_0041d11c = SetTRANSLATION } if (!NodeVectorEquals(mechJointNode, bodyOrientation, QuantiseEps)) // FUN_004091f4 { @@ -612,13 +693,14 @@ void Vector3D dir(-x, -y, -z); magnitude *= exageration; // *(this+0x1D8) dir *= magnitude; // FUN_004086ac - eyeAccel += dir; // FUN_004085ec (this+0x230) + eyeForce += dir; // FUN_004085ec (this+0x230) } // -// @004b2de4 [CONFIDENT] -- as above but adds into the body accumulator -// (bodyAccel @0x308), then forces bodyForce.x (this+0x30C) to 0 and flips the -// sign of bodyAccel -- a one-shot torque kick. +// @004b2de4 [CONFIDENT] -- as above but adds into the body force accumulator +// (bodyForce @0x308), then zeroes its .y (this+0x30C) and flips the sign of +// the whole vector -- a one-shot torque kick. (The old recon wrote a member +// at the WRONG offset here; 0x30C is bodyForce.y in the binary layout.) // void Gyroscope::ApplyDamageTorque(Scalar x, Scalar y, Scalar z, Scalar magnitude) @@ -626,15 +708,15 @@ void Vector3D dir(-x, -y, -z); magnitude *= exageration; dir *= magnitude; - bodyAccel += dir; // FUN_004085ec (this+0x308) - bodyForce.x = 0.0f; // *(this+0x30C) = 0 - bodyAccel.Negate(bodyAccel); // *(this+0x308) = -*(this+0x308) + bodyForce += dir; // FUN_004085ec (this+0x308) + bodyForce.y = 0.0f; // *(this+0x30C) = 0 + bodyForce.Negate(bodyForce); // *(this+0x308) = -*(this+0x308) } // // @004b2e50 [CONFIDENT] -- a purely-vertical (pitch) kick: only the first -// component survives (y,z forced to 0), scaled by exageration, added to bodyAccel -// and mirrored into bodyForce (this+0x30C = this+0x308). +// component survives (y,z forced to 0), scaled by exageration, added to bodyForce +// and mirrored into its .y (this+0x30C = this+0x308). // void Gyroscope::ApplyVerticalImpulse(Scalar pitch, Scalar, Scalar, Scalar magnitude) @@ -642,8 +724,8 @@ void Vector3D dir(pitch, 0.0f, 0.0f); magnitude *= exageration; dir *= magnitude; - bodyAccel += dir; // FUN_004085ec (this+0x308) - bodyForce.x = bodyAccel.x; // *(this+0x30C) = *(this+0x308) + bodyForce += dir; // FUN_004085ec (this+0x308) + bodyForce.y = bodyForce.x; // *(this+0x30C) = *(this+0x308) } @@ -787,3 +869,39 @@ Subsystem *CreateGyroSubsystem(Mech *owner, int id, void *seg) return (Subsystem *) new (Memory::Allocate(0x3D0)) Gyroscope(owner, id, (Gyroscope::SubsystemResource *)seg, Gyroscope::DefaultData); } + +//===========================================================================// +// Per-frame joint-write dispatch bridge (task #56, byte-verified): the binary +// Mech master performance FUN_004a9b5c calls WriteEyeJoint @0x4aaf74 / +// WriteMechJoint @0x4aaf83 each frame AFTER the animation pass, preceded by +// gyro->swayBias = mech sway accumulator, gated !deathAnimationLatched (both) +// and legAnimationState != 0 (eye writer only; WriteEyeJoint is MULTIPLICATIVE +// on the joint's animated rotation, so it must never run on a joint the +// animation pass didn't just re-write). mech4.cpp calls this bridge from the +// same spot in the port's master performance (complete-type TU rule). +//===========================================================================// +void GyroFrameJointWrite(Subsystem *gyro, Scalar sway_bias, + int leg_animation_state, int death_animation_latched) +{ + if (gyro == 0) + return; + Gyroscope *g = static_cast(gyro); + g->SetSwayBias(sway_bias); // gyro+0x3A8 = mech+0x3F0 (bytes 0x4aaf18-3c) + if (death_animation_latched != 0) // mech+0x650 gate + return; + if (leg_animation_state != 0) // mech+0x3B0 gate (+ mech+0x57C, always 0) + g->WriteEyeJoint(); // @0x4aaf74 + g->WriteMechJoint(); // @0x4aaf83 +} + +//===========================================================================// +// Post-stream gyro<->torso link (task #56, binary bt_mech tail): the binary +// re-points gyro+0x258 to &torso pitch (torso+0x1D8) after subsystem streaming; +// the damage-response path reads it. (Replaces the old SubProxy::linkTarget +// landmine, which wrote through an engine base field at gyro+4.) +//===========================================================================// +void GyroBindExternalPitch(Subsystem *gyro, Scalar *pitch) +{ + if (gyro != 0) + static_cast(gyro)->BindExternalPitch(pitch); +} diff --git a/game/reconstructed/gyro.hpp b/game/reconstructed/gyro.hpp index 4cb3e7b..f850319 100644 --- a/game/reconstructed/gyro.hpp +++ b/game/reconstructed/gyro.hpp @@ -213,6 +213,20 @@ class Joint; // engine skeleton node (JOINT.h); eye/mech joint targets void ApplyVerticalImpulse( // @004b2e50 Scalar pitch, Scalar /*y*/, Scalar /*z*/, Scalar magnitude); + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Per-frame joint-write dispatch (called from the MECH master performance + // tail, NOT from GyroscopeSimulation -- byte-verified: the binary calls + // WriteEyeJoint @0x4aaf74 / WriteMechJoint @0x4aaf83 inside the unexported + // Mech performance FUN_004a9b5c, after the animation pass, gated on + // !deathLatched (+ legAnimState!=0 for the eye writer), preceded by + // swayBias = mech sway accumulator). Bridge fns in gyro.cpp forward here. + // + public: + void SetSwayBias(Scalar v) { swayBias = v; } + void BindExternalPitch(Scalar *p) { if (p) externalPitchPtr = p; } // bt_mech tail: &torso pitch + void WriteEyeJoint(); // @004b33e0 (@004b2eac thunks here) + void WriteMechJoint(); // @004b34ec + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Subsystem virtual overrides (slots on vtable @00510abc) // @@ -251,9 +265,7 @@ class Joint; // engine skeleton node (JOINT.h); eye/mech joint targets protected: void IntegrateEyeJoint(Scalar time_slice); // @004b2ec0 void IntegrateBody(Scalar time_slice); // @004b30ec - void WriteEyeJoint(); // @004b33e0 (@004b2eac thunks here) - void WriteMechJoint(); // @004b34ec - Logical UpdateAnimationNoise(Scalar time_slice);// @004b357c + Logical UpdateAnimationNoise(Scalar time_slice);// @004b357c (DEAD in the binary: zero callers -- kept unwired) //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Local data. Byte offsets are those observed in the shipped object @@ -263,30 +275,51 @@ class Joint; // engine skeleton node (JOINT.h); eye/mech joint targets // low offsets and is NOT redeclared here. // protected: - Scalar exageration; // @0x1D8 resource +0xF8 (impulse scale) + // BINARY-EXACT layout (ctor @004b3778 field map, task #56 — every byte + // 0x1D8..0x3CF is ctor-initialised in the binary; the old recon layout + // mislabelled 0x1DC/0x1E8/0x1F4 and omitted 0x254..0x2B3 entirely, which + // mis-offset everything after it AND left the integrator state as 0xCD fill + // (the NaN-to-root-joint revert). See context/cockpit-view.md + the + // wf_f52abaff synthesis for the per-line decomp evidence. + Scalar exageration; // @0x1D8 res +0xF8 (impulse scale) - Vector3D eyeOrientation; // @0x1DC integrated cockpit "eye" angles - Vector3D eyeVelocity; // @0x1E8 d(eyeOrientation)/dt - Vector3D eyeLimitLow; // @0x200 = posSpring (clamp lo, +0x200/204/208) - Vector3D eyeLimitHigh; // @0x20C = negSpring (clamp hi, +0x20C/210/214) - Vector3D eyePosSpring; // @0x218 resource posSpring (+0x130) - Vector3D eyeNegSpring; // @0x224 resource negSpring (+0x13C) - Vector3D eyeSpringConstant; // @0x1F4? eyeSpring work (see ctor 0408744) - Vector3D eyeAccel; // @0x230 spring-damper accumulator - Vector3D eyeForce; // @0x23C scaled by time_slice - Vector3D eyeWork; // @0x248 scratch (cross/normalise) + Vector3D eyePosition; // @0x1DC eye TRANSLATION spring state -> jointeye + // translation (steady offset + hit bounce); ZERO + Vector3D springConstant; // @0x1E8 res +0x118 SpringConstantX/Y/Z + Vector3D dampingConstant; // @0x1F4 res +0x124 DampingConstantX/Y/Z + Vector3D eyeClampUpper; // @0x200 = posSpring (min-clamped against) + Vector3D eyeClampLower; // @0x20C = negSpring (max-clamped against) + Vector3D posSpring; // @0x218 res +0x130 + Vector3D negSpring; // @0x224 res +0x13C + Vector3D eyeForce; // @0x230 accumulator (damage-impulse target; NOT + // cleared per frame -- carries by design) + Vector3D eyeVelocity; // @0x23C + Vector3D eyeWork; // @0x248 scratch - Vector3D bodyOrientation; // @0x2B4 integrated body tip angles - Vector3D rotationSpringConstant; // @0x2C0 resource +0x148 (roll/yaw/pitch) - Vector3D rotationDampingConstant;// @0x2CC resource +0x154 - Vector3D bodyLimitLow; // @0x2D8 = rotationPosSpring - Vector3D bodyLimitHigh; // @0x2E4 = rotationNegSpring - Vector3D bodyPosSpring; // @0x2F0 resource rotationPosSpring (+0x160) - Vector3D bodyNegSpring; // @0x2FC resource rotationNegSpring (+0x16C) - Vector3D bodyAccel; // @0x308 spring-damper accumulator - Vector3D bodyForce; // @0x314 scaled by time_slice + Scalar spare0; // @0x254 = 0 + Scalar *externalPitchPtr; // @0x258 ctor: &spare0; post-stream re-pointed to + // &torso pitch (bt_mech tail); read by the + // damage-response @004b2980 + Scalar workMatrix[12]; // @0x25C 3x4 work matrix (identity; damage frame) + Vector3D placePos; // @0x28C placement pos (zero) + Scalar placeQuat[4]; // @0x298 placement quat {0,0,0,1} + Vector3D placeRot; // @0x2A8 placement rot scratch (zero) + + Vector3D bodyOrientation; // @0x2B4 body ROTATION spring state -> jointeye rotation; ZERO + Vector3D rotationSpringConstant; // @0x2C0 res +0x148 (Roll,Yaw,Pitch order) + Vector3D rotationDampingConstant;// @0x2CC res +0x154 + Vector3D bodyClampUpper; // @0x2D8 = 2.0 * rotationPosSpring + Vector3D bodyClampLower; // @0x2E4 = 2.0 * rotationNegSpring + Vector3D rotationPosSpring; // @0x2F0 res +0x160 (Pitch,Yaw,Roll; radians) + Vector3D rotationNegSpring; // @0x2FC res +0x16C + Vector3D bodyForce; // @0x308 accumulator (torque/vertical impulse target) + Vector3D bodyVelocity; // @0x314 Vector3D bodyWork; // @0x320 scratch + Scalar damageMultiplier[5]; // @0x32C res +0x1B8..0x1C8 (Coll/Ball/Expl/Laser/Energy) + Gyroscope__DamageResponse + damageResponse[5]; // @0x340 res +0x1CC..0x21B (5 x {trans,pitchRoll,yaw,vibration}) + Scalar animationOffset; // @0x390 (this[0xE4]) idle-noise phase, init 0 Scalar animationScale; // @0x394 (this[0xE5]) init 1.0f Scalar animationPhase; // @0x398 (this[0xE6]) init 0 diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index f60d24a..7788c59 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -213,9 +213,10 @@ extern Subsystem *CreateMyomersSubsystem(Mech *, int, void *); // 0xBC6 (WAVE 6 extern Subsystem *CreateProjectileWeaponSubsystem(Mech *, int, void *); // 0xBCD (WAVE 7 -- ammoBinLink 0xC, layout locked; FireWeapon no-spawn Phase A) extern Subsystem *CreateMissileLauncherSubsystem(Mech *, int, void *); // 0xBD0 (WAVE 7 -- shadows deleted, layout locked; FireWeapon no-spawn Phase A) extern Subsystem *CreateGaussRifleSubsystem(Mech *, int, void *); // 0xBCE (WAVE 7 -- Emitter subclass, no-op FireWeapon; dedicated bridge) -// CreateGyroSubsystem (0xBC4) is reconstructed + ready in gyro.cpp (layout re-based, -// joint I/O real), but NOT wired -- the gyro ctor/integrator reconstruction is -// incomplete (garbage output); see the 0xBC4 case below. +extern Subsystem *CreateGyroSubsystem(Mech *, int, void *); // 0xBC4 (task #56 -- ctor/integrators byte-exact, ENABLED) +extern void GyroFrameJointWrite(Subsystem *, Scalar, int, int); // task #56 -- mech-performance joint-write dispatch (mech4.cpp) +extern void GyroBindExternalPitch(Subsystem *, Scalar *); // task #56 -- post-stream gyro+0x258 = &torso twist +extern Scalar *BTGetTorsoTwistAddr(Subsystem *); // torso.cpp bridge (complete-type TU) // Post-stream entity glue + cosmetic objects. struct StandingAnimation { template StandingAnimation(A&&...) {} }; @@ -839,6 +840,10 @@ Mech::Mech( hudSubsystem = 0; // this[0x16d] (0xBD6 -> HUD; was misnamed mechTechSubsystem) forwardThrottleScale = 1.0f; // @0x5c0 mapper speed scale (writer not in decomp; neutral) sensorSubsystem = 0; // this[0x1f7] (0xBBE) -- same hazard, zero it too + deathAnimationLatched = 0; // @0x650 -- same 0xCDCDCDCD hazard: the gyro joint-write + // dispatch (task #56) gates on it every frame; garbage + // nonzero silently disabled the writers until death anims ran + legResetLatch = 0; // @0x654 -- same latch family, same fill hazard attrPad = 0.0f; // shared read pad for un-populated gauge attributes linearSpeed = 0.0f; // live forward ground speed (LinearSpeed gauge); set per frame radarRange = 1000.0f; // radar display scale (SetTargetRange is stubbed; @@ -1017,17 +1022,17 @@ Mech::Mech( break; case GyroClassID: // 0xBC4 -> real class Gyroscope (ctor @004b3778) - // WAVE 5 DEFERRED: the layout re-base + joint I/O ARE done (gyro - // constructs, resolves real joints jointlocal/jointeye, WriteMechJoint - // fires -- CreateGyroSubsystem is ready in gyro.cpp). BUT the gyro's - // ctor field-init + integrator field-MAPPING are incomplete in the - // reconstruction (binary ctor @004b3778 puts springConstant@0x1E8, - // dampingConstant@0x1F4, etc.; the recon mislabels these and leaves the - // accumulators uninitialised) -> live output is 0xCDCDCDCD/NaN written - // to the ROOT joint. Keep the stub until the ctor+integrator are - // faithfully reconstructed from @004b3778. See CLAUDE.md §10d. - subsystemArray[id] = (Subsystem *) - new (Memory::Allocate(0x3d0)) Gyro(this, id, seg); // FUN_004b3778 (stub) + // RE-ENABLED (task #56). The old NaN revert's root causes are fixed + // byte-exact against @004b3778/@004b2ec0/@004b30ec: the ctor field map + // (springConstant@0x1E8 / dampingConstant@0x1F4 were mislabelled, the + // 0x254-0x2B3 block was missing so everything after was mis-offset, + // clamps/accumulators were left as 0xCD fill) and the integrator + // semantics (state-minus-target displacements, componentwise damping + // OVERWRITE, IntegrateBody X/Z crossings) -- all static_assert-locked. + // The joint writers now dispatch from the MECH performance tail + // (GyroFrameJointWrite bridge; binary calls @0x4aaf74/0x4aaf83), NOT + // from GyroscopeSimulation. + subsystemArray[id] = CreateGyroSubsystem(this, id, seg); // ctor @004b3778 (real) gyroSubsystem = subsystemArray[id]; // Wword(0x14a) break; @@ -1139,16 +1144,15 @@ Mech::Mech( weaponCount = 2; // Wword(0x112) reset to base index (sentinels 0,1) // - // Cross-link the gyro to the torso twist (sinkSourceSubsystem+0x1D8 == - // Torso::currentTwist) so the gyro can fold torso yaw into the mech joint. - // ⚠ DEFERRED (WAVE 5): the recon SubProxy::linkTarget sits at gyro+4 (an engine - // base field), NOT the real gyro torso-link offset (which the gyro layout has - // not yet mapped) -- writing here would STOMP the live gyro base. Skipped until - // the gyro's torso-link field is recovered; harmless meanwhile (the Blackhawk - // torso is disabled, so there is no twist to couple). See CLAUDE.md §10d. + // Cross-link the gyro to the torso twist (task #56, binary bt_mech tail: + // gyro+0x258 = torso+0x1D8 == Torso::currentTwist; the gyro damage-response + // @004b2980 reads it per hit). Done through the two complete-type-TU bridges + // (GyroBindExternalPitch / BTGetTorsoTwistAddr) -- the old raw + // SubProxy::linkTarget write landed at gyro+4 (an engine base field) and would + // have stomped the live gyro base; that landmine is retired. // - // if (sinkSourceSubsystem != 0 && gyroSubsystem != 0) - // ((SubProxy *)gyroSubsystem)->linkTarget = (char *)sinkSourceSubsystem + 0x1d8; + if (sinkSourceSubsystem != 0 && gyroSubsystem != 0) + GyroBindExternalPitch(gyroSubsystem, BTGetTorsoTwistAddr(sinkSourceSubsystem)); // // Build the capability sub-rosters by IsDerivedFrom() class tests. diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index f107f26..ffe5d8e 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -3147,6 +3147,39 @@ void << " present / roster " << subsystemCount << "\n" << std::flush; } + // + // GYRO JOINT-WRITE DISPATCH (task #56, byte-verified @0x4aaf18-0x4aaf89): + // the binary Mech master performance tail pushes the gyro's integrated state + // into the skeleton every frame AFTER the animation pass + subsystem tick: + // gyro->swayBias = mech sway accumulator (the mech+0x3F0 overspeed model is + // NOT yet reconstructed -- feed 0, flagged); + // if (!deathAnimationLatched) { if (legAnimationState != 0) WriteEyeJoint(); + // WriteMechJoint(); } + // WriteMechJoint drives 'jointeye' (translation = the eye spring offset + + // hit bounce, rotation = body tip); siteeyepoint rides that joint, so this + // IS what moves the cockpit eye. Runs after the roster tick above (the gyro + // Performance integrated this frame) and after the gait channel wrote the + // joints (WriteEyeJoint is multiplicative on the animated rotation). + // + if (GetInstance() == MasterInstance) + { + extern void GyroFrameJointWrite(Subsystem *, Scalar, int, int); + static int s_gyrodbg = 0; + if (getenv("BT_GYRO_LOG") && s_gyrodbg++ == 0) + DEBUG_STREAM << "[gyro] dispatch: gyro=" << (void *)gyroSubsystem + << " legAnim=" << legAnimationState + << " death=" << deathAnimationLatched << "\n" << std::flush; + GyroFrameJointWrite(gyroSubsystem, 0.0f /* mech+0x3F0 model TBD */, + legAnimationState, deathAnimationLatched); + } + else if (getenv("BT_GYRO_LOG")) + { + static int s_gyrodbg2 = 0; + if (s_gyrodbg2++ == 0) + DEBUG_STREAM << "[gyro] dispatch SKIPPED: instance=" << (int)GetInstance() + << " != master\n" << std::flush; + } + // Keep the simulation/networking bookkeeping consistent (this is exactly // what the base "no time / stasis" early-out does). WriteSimulationUpdate(update_stream); diff --git a/game/reconstructed/torso.cpp b/game/reconstructed/torso.cpp index ee1afc2..cdb0a46 100644 --- a/game/reconstructed/torso.cpp +++ b/game/reconstructed/torso.cpp @@ -900,3 +900,14 @@ Scalar BTGetTorsoTwist(Subsystem *torso) { return torso ? ((Torso *)torso)->CurrentTwist() : 0.0f; } + +// +// Task #56 bridge -- ADDRESS of the live torso twist for the gyro's external- +// pitch pointer (binary bt_mech stream tail: gyro+0x258 = torso+0x1D8; the gyro +// damage-response @004b2980 reads it per hit). Same complete-type-TU pattern +// as BTGetTorsoTwist above. +// +Scalar *BTGetTorsoTwistAddr(Subsystem *torso) +{ + return torso ? ((Torso *)torso)->CurrentTwistAddr() : 0; +} diff --git a/game/reconstructed/torso.hpp b/game/reconstructed/torso.hpp index 5f5bb51..f44cdb1 100644 --- a/game/reconstructed/torso.hpp +++ b/game/reconstructed/torso.hpp @@ -140,6 +140,9 @@ class Joint; // engine skeleton node (JOINT.h); the twist target // Current horizontal (yaw) torso aim in radians (binary torso+0x1d8); the // damage-table PieSlice wheel reads this when RotateWithTorso is set. Scalar CurrentTwist() const { return currentTwist; } + // Address of the twist scalar (task #56: the binary bt_mech stream tail + // re-points gyro+0x258 to torso+0x1D8; the gyro damage-response reads it). + Scalar* CurrentTwistAddr() { return ¤tTwist; } protected: // Resolve a named skeleton node to its live Joint* via the owning Mech // (the ctor @004b6b0c inlines Mech::ResolveJoint == FUN_00424b60). Defined