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:
co-authored by
Claude Fable 5
parent
abaa145b6f
commit
cb85517ede
@@ -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.
|
||||
|
||||
+221
-103
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user