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 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-11 06:47:22 -05:00
co-authored by Claude Fable 5
parent abaa145b6f
commit cb85517ede
7 changed files with 373 additions and 158 deletions
+221 -103
View File
@@ -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<Gyroscope *>(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<Gyroscope *>(gyro)->BindExternalPitch(pitch);
}
+56 -23
View File
@@ -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
+27 -23
View File
@@ -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<class...A> 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.
+33
View File
@@ -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);
+11
View File
@@ -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;
}
+3
View File
@@ -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 &currentTwist; }
protected:
// Resolve a named skeleton node to its live Joint* via the owning Mech
// (the ctor @004b6b0c inlines Mech::ResolveJoint == FUN_00424b60). Defined