# Gyroscope integrators: FUN_004b2ec0 (IntegrateEyeJoint) + FUN_004b30ec (IntegrateBody), their vector helpers, ctor value sources, and equilibrium semantics ## exact_semantics HELPERS (all verified by reading their bodies, reference/decomp/all/part_000.c): - FUN_00408440 (:8157) vec3 copy dst=src - FUN_004085ec (:8229) vec3 add dst=a+b - FUN_00408644 (:8255) vec3 sub dst=a-b - FUN_004086ac (:8281) vec3 scale dst=v*s - FUN_004086d0 (:8294) vec3 COMPONENT-WISE MULTIPLY dst=(a.x*b.x, a.y*b.y, a.z*b.z) -- NOT a cross product - FUN_00408744 (:8333) 3x4 row-matrix * vec: out[i]=dot(v, M.row[i]) (M rows at f[0..3],[4..7],[8..11], col 3 ignored) - FUN_0040aadc (:9773) builds an IDENTITY 3x4 matrix -> FUN_00408744(out, v, identity) is a pass-through copy of v - FUN_004092fc (:8859) vec3 scale; FUN_004091f4 (:8828)/FUN_004084fc (:8188) per-component |a-b|<=eps test - DAT_004e0f74 = DAT_004e0f8c = (0,0,0) (section_dump.txt 4e0f70-4e0f90: all zero bytes) FUN_004b2ec0 IntegrateEyeJoint(this, dt) @004b2ec0 (part_013.c:2362-2437) -- operates on the eye TRANSLATION state: 1. d_neg = eyePos(0x1DC) - negSpring(0x224) [:2376, sub] 2. d_pos = eyePos(0x1DC) - posSpring(0x218) [:2377] 3. f1 = springK(0x1E8) o d_neg (component-wise) [:2378] 4. f2 = springK(0x1E8) o d_pos [:2379] 5. force(0x230) += f1; force(0x230) += f2 [:2381-2382] NOTE: 0x230 is NOT cleared per frame -- it still holds last frame's damping term (step 9) plus any external hit impulses (FUN_004b2d8c @004b2d8c adds -hitDir * exageration(0x1D8) * mag into 0x230 -- this is the hit-bounce injection point) 6. work(0x248) = force [:2383, copy] 7. vel(0x23C) += work * dt [:2384-2386] 8. force(0x230) = dampingK(0x1F4) o vel (OVERWRITE; "param_1+500"=0x1F4; component-wise) [:2387] 9. work = force; vel += work * dt [:2388-2390] 10. M = identity (stack, FUN_0040aadc); step = M*vel = vel [:2391-2392] 11. eyePos(0x1DC) += step -- NO dt at this stage (velocity is per-frame displacement) [:2393] 12. per-axis clamp: eyePos = min(eyePos, 0x200/0x204/0x208) then max(eyePos, 0x20C/0x210/0x214) [:2394-2435]; ctor sets 0x200-block = posSpring and 0x20C-block = negSpring, so the state is boxed into [negSpring, posSpring]. FUN_004b30ec IntegrateBody(this, dt) @004b30ec (part_013.c:2443-2549) -- same 2-target spring-damper on the body ROTATION state, but the force/velocity accumulators live in REVERSED component order (x<->z swap) relative to the state: 1. d1 = bodyRot(0x2B4/0x2B8/0x2BC) - negTarget(0x2FC/0x300/0x304) [:2473-2475] 2. d2 = bodyRot - posTarget(0x2F0/0x2F4/0x2F8) [:2476-2478] 3. f1 = (K.x(0x2C0)*d1.z, K.y(0x2C4)*d1.y, K.z(0x2C8)*d1.x) [:2479-2481] <- x/z SWAP: slot0 pairs K.x with the state's z-delta 4. f2 = (K.x*d2.z, K.y*d2.y, K.z*d2.x) [:2482-2484] (:2485-2490 stores dead copies of d1/d2 -- unused) 5. force(0x308) += f1; force(0x308) += f2 [:2491-2493] (0x308 also NOT cleared per frame; torque kicks FUN_004b2de4/FUN_004b2e50 add here, scaled by exageration 0x1D8) 6. work(0x320) = force; vel(0x314) += work * dt [:2494-2497] 7. force(0x308) = dampingK(0x2CC) o vel (component-wise, overwrite) [:2498] 8. work = force; vel += work * dt [:2499-2501] 9. out = identity * vel = vel; out=(local_7c,local_78,local_74) [:2502-2503] 10. bodyRot.x(0x2B4) += out[2]; bodyRot.y(0x2B8) += out[1]; bodyRot.z(0x2BC) += out[0] [:2504-2506] <- second x/z swap. Net: state.z channel is sprung by K.x/damped by D.x through vel[0]; state.x by K.z/D.z through vel[2]; y straight. Consistent per-axis spring-dampers with the K/D vectors applied in reversed component order (the constants are read Roll/Yaw/Pitch while state+targets are Pitch/Yaw/Roll). 11. clamps in order y,x,z: min vs 0x2DC/0x2D8/0x2E0 then max vs 0x2E8/0x2E4/0x2EC [:2507-2548]; ctor sets 0x2D8-block = 2.0*posTarget(0x2F0) and 0x2E4-block = 2.0*negTarget(0x2FC). CTOR @004b3778 (part_013.c:2717-2953) inits everything the integrators touch: springK(0x1E8)<-res+0x118, dampingK(0x1F4)<-res+0x124, posSpring(0x218)<-res+0x130, negSpring(0x224)<-res+0x13C [:2819-2827]; rotK(0x2C0)<-res+0x148, rotD(0x2CC)<-res+0x154, posTarget(0x2F0)<-res+0x160, negTarget(0x2FC)<-res+0x16C [:2829-2835]; eye clamps 0x200=identity*posSpring, 0x20C=identity*negSpring [:2876-2883]; body clamps 0x2D8=2*posTarget, 0x2E4=2*negTarget [:2884-2885]; ZEROES eyePos(0x1DC), force(0x230), vel(0x23C), work(0x248) [:2935-2938], bodyRot(0x2B4) [:2939], body force/vel/work (0x308/0x314/0x320) [:2940-2942] via DAT_004e0f74/DAT_004e0f8c = (0,0,0). Joint nodes: 0x3C8 <- resource string @+0x178 (EyeJoint), 0x3CC <- resource string @+0x198 (MechJoint) [:2886-2920]. WRITERS (consumers, confirms what each state IS): FUN_004b34ec WriteMechJoint (:2605-2624): node 0x3CC (the MechJoint-named joint), type must be 5 (BallTranslation); sets node TRANSLATION = eyePos(0x1DC) via FUN_0041d11c and node rotation VECTOR = bodyRot(0x2B4) via FUN_0041d020, each only when it differs by >0.0001. So 0x1DC is a TRANSLATION (the cockpit eye bounce offset) and 0x2B4 a rotation. FUN_004b33e0 WriteEyeJoint (:2558-2599): node 0x3C8 gets only the scalar sway 0x3BC (scale of its scalar/vector channel). The 'crossed' naming is real: the joint named by the resource's MechJoint field carries the eye translation + body rotation. EQUILIBRIUM: at rest (vel=0, no impulse) the carried-over damping term is 0, so per-axis accel ~ K*((x-pos)+(x-neg)) = 2K*(x - (pos+neg)/2). With restoring constants the eye state settles at the per-axis MIDPOINT (posSpring+negSpring)/2 (inside the [negSpring,posSpring] clamp box), and likewise the body rotation settles at (posTarget+negTarget)/2. THEREFORE the authentic eye carries a STEADY translation offset = midpoint of PosSpring/NegSpring whenever they are not symmetric (posSpring != -negSpring), written every frame as the MechJoint node's translation -- replacing, not adding to, the raw skeleton baseOffset of that joint. (If the loaded constants were non-restoring the state would instead pin exactly at a clamp boundary = posSpring or negSpring; either way the rest pose is derived from the two spring vectors, never from the raw segment baseOffset.) Also note two byte-exact quirks the port must keep: (a) position += velocity with NO dt (step 11), (b) the force accumulators 0x230/0x308 retain the damping term across frames and are the external-impulse injection points. ## member_map - 0x1D8 exageration (scalar) <- resource+0xF8; scales all external impulses (FUN_004b2d8c/2de4/2e50) - 0x1DC-0x1E4 eye TRANSLATION state; ctor zero (DAT_004e0f74=(0,0,0)); written as MechJoint node translation by FUN_004b34ec - 0x1E8-0x1F0 springConstant vec3 <- resource+0x118; multiplies BOTH eye deltas (part_013.c:2378-2379) - 0x1F4-0x1FC dampingConstant vec3 <- resource+0x124; component-wise multiplies eye velocity (part_013.c:2387, 'param_1+500'=0x1F4) - 0x200-0x208 eye upper clamp = posSpring (ctor part_013.c:2876-2879, identity-transformed copy of 0x218) - 0x20C-0x214 eye lower clamp = negSpring (ctor :2880-2883) - 0x218-0x220 posSpring vec3 <- resource+0x130 - 0x224-0x22C negSpring vec3 <- resource+0x13C - 0x230-0x238 eye force accumulator; ctor zero; NOT cleared per frame (retains damping term); hit impulses add here (FUN_004b2d8c: -hitDir*exageration*mag) - 0x23C-0x244 eye velocity; ctor zero; recon calls it 'eyeForce' - 0x248-0x250 eye work copy; ctor zero - 0x254 int=0; 0x258 ptr to this+0x254; 0x25C-0x288 identity 3x4 matrix; 0x28C zero vec3; 0x298 matrix from zero rotation (FUN_00409a00); 0x2A8 zero vec3 -- 0x60-byte block MISSING from the recon layout (everything after eyeWork is shifted -0x60) - 0x2B4-0x2BC body ROTATION state; ctor zero (DAT_004e0f8c=(0,0,0)); written as MechJoint node rotation vector by FUN_004b34ec - 0x2C0-0x2C8 rotationSpringConstant vec3 <- resource+0x148 (applied to the state in REVERSED x<->z component order) - 0x2CC-0x2D4 rotationDampingConstant vec3 <- resource+0x154 (damps the reversed-order velocity) - 0x2D8-0x2E0 body upper clamp = 2.0 * (0x2F0) (ctor :2884) - 0x2E4-0x2EC body lower clamp = 2.0 * (0x2FC) (ctor :2885) - 0x2F0-0x2F8 rotation pos target vec3 <- resource+0x160 - 0x2FC-0x304 rotation neg target vec3 <- resource+0x16C - 0x308-0x310 body force accumulator (REVERSED component order vs state); ctor zero; torque kicks (FUN_004b2de4/FUN_004b2e50) add here - 0x314-0x31C body velocity (reversed order); ctor zero - 0x320-0x328 body work copy; ctor zero - 0x32C-0x33C five damage multipliers <- resource+0x1B8..0x1C8; 0x340-0x38C five 4-float response tables <- resource+0x1CC..0x21C (missing from recon) - 0x390=0 / 0x394=1.0f / 0x398=0 anim state; 0x39C max <- res+0xFC; 0x3A0 min <- res+0x100; 0x3A4 rate <- res+0x104; 0x3A8 bias=0; 0x3AC/0x3B0/0x3B4/0x3B8 <- res+0x108/0x10C/0x110/0x114; 0x3BC sway=0; 0x3C0=0; 0x3C4=0 - 0x3C8 joint node resolved from resource string +0x178 (EyeJoint) -- receives ONLY the sway scalar 0x3BC (FUN_004b33e0) - 0x3CC joint node resolved from resource string +0x198 (MechJoint) -- receives eye translation (0x1DC) + body rotation (0x2B4) (FUN_004b34ec, type-5 only) ## recon_mismatches - gyro.cpp:428-429 -> eye spring deltas are SIGN-REVERSED: recon computes (target - state); binary computes (state - target) (part_013.c:2376-2377: FUN_00408644(out, this+0x1DC, this+0x224/0x218)). Flips the spring force direction. - gyro.cpp:431-432 -> both eye force terms must use springConstant@0x1E8 (part_013.c:2378-2379); the recon uses 'eyeSpringConstant', a member that the ctor NEVER initializes (=0xCDCDCDCD -> the observed -4.31e8/NaN) and which sits at recon offset 0x224 (binary negSpring slot), not 0x1E8. - gyro.cpp:441 -> damping stage is wrong twice: binary does force(0x230) = dampingConstant(0x1F4) COMPONENT-WISE-MULTIPLY velocity(0x23C) (part_013.c:2387; FUN_004086d0 body at part_000.c:8294 is per-component multiply, NOT cross); recon does eyeAccel.Cross(eyeSpringConstant, eyeForce) -- cross product AND the spring constant instead of the damping constant. - gyro.cpp:453-458 -> clamp structure matches (min vs 0x200-block, max vs 0x20C-block) but eyeLimitLow/eyeLimitHigh are never initialized anywhere; binary ctor sets 0x200=posSpring, 0x20C=negSpring (part_013.c:2876-2883). - gyro.cpp:473-474 -> body force terms: binary multiplies BOTH deltas by rotationSpringConstant(0x2C0) (part_013.c:2479-2484); recon multiplies the second delta by rotationDampingConstant -- wrong constant (damping only enters at the velocity stage, :2498). - gyro.cpp:470-496 -> the x<->z component SWAP is missing entirely: binary pairs K.x with delta.z / K.z with delta.x (part_013.c:2479-2484) and adds vel[2]/vel[1]/vel[0] into bodyRot.x/y/z (:2504-2506); recon uses straight component pairing, so pitch gets roll's spring/damping constants and vice versa. - gyro.cpp:482 -> same cross-vs-component-multiply defect as the eye damping stage (binary :2498 is FUN_004086d0 component multiply). - gyro.cpp:490-495 -> bodyLimitLow/High never initialized; binary ctor sets 0x2D8=2.0*posTarget(0x2F0) and 0x2E4=2.0*negTarget(0x2FC) (part_013.c:2884-2885), not '= rotationPos/NegSpring' as gyro.hpp:282-283 claims. - gyro.cpp:235 -> ctor 'eyeOrientation = r->springConstant' is a double error: the binary zeroes the eye state 0x1DC (part_013.c:2935, copy of DAT_004e0f74=(0,0,0)) and copies springConstant into 0x1E8 (:2819-2821). - gyro.cpp:201-279 (ctor) -> missing inits present in the binary: dampingConstant@0x1F4<-r+0x124 (:2823), eye clamps 0x200/0x20C (:2876-2883), body clamps 0x2D8/0x2E4 (:2884-2885), and zeroing of ALL integrator accumulators 0x230/0x23C/0x248 (:2936-2938), 0x2B4 (:2939), 0x308/0x314/0x320 (:2940-2942) -- the direct cause of the 0xCDCDCDCD garbage that forced the revert. Damage multipliers/response tables 0x32C-0x38C (:2836-2875) also uncopied. - gyro.cpp:324 (ResetToInitialState) -> 'bodyPosSpring = zero' is mislabeled: FUN_00408e90(this+0xAD*4=0x2B4, ...) targets bodyOrientation (the state), not bodyPosSpring(0x2F0); as written it wipes the spring target and leaves the state dirty. - gyro.cpp:39-41 (helper comment) + 449/487 -> FUN_004086d0 is ONLY a component-wise multiply (part_000.c:8294), never cross; FUN_00408744 is a 3x4 row-matrix*vector (part_000.c:8333), not a 'clamp-to-box' -- with the freshly built stack identity (FUN_0040aadc) it is a pass-through, which the recon's line 449 pass-through happens to get right. - gyro.hpp:268-288 -> member ORDER/layout diverges from the binary: eyeVelocity@0x1E8 (binary: springConstant), eyeLimitLow@0x1F4 (binary: dampingConstant), eyePosSpring actually lands at 0x20C / eyeNegSpring at 0x218 / eyeSpringConstant at 0x224 (comments claim other offsets), and the 0x60-byte block 0x254-0x2B3 (int, ptr, identity matrix, vec, matrix, vec) is absent so bodyOrientation actually sits at 0x254, not 0x2B4 -- every offset comment from there on is fiction relative to the compiled layout. - gyro.hpp:276 / naming -> 0x23C is the eye VELOCITY and 0x230 the persistent force accumulator; the recon's eyeAccel/eyeForce naming inverts the roles (cosmetic, but it caused the wrong-constant bugs above). ## fix 1) gyro.hpp: re-lay the own-block to binary truth and rename by role: Vector3D eyePosition@0x1DC; Vector3D springConstant@0x1E8; Vector3D dampingConstant@0x1F4; Vector3D eyeMaxLimit@0x200; Vector3D eyeMinLimit@0x20C; Vector3D posSpring@0x218; Vector3D negSpring@0x224; Vector3D eyeForce@0x230; Vector3D eyeVelocity@0x23C; Vector3D eyeWork@0x248; then the missing 0x254-0x2B3 block (int scratchFlag; void* scratchPtr; float workMatrix[12]; Vector3D workEuler; float workMatrix2[?]; Vector3D workVec -- or an opaque byte pad if the 0x298 block type is left to the other angle) so bodyRotation lands at 0x2B4; bodyRotation@0x2B4; rotationSpringConstant@0x2C0; rotationDampingConstant@0x2CC; bodyMaxLimit@0x2D8; bodyMinLimit@0x2E4; rotationPosTarget@0x2F0; rotationNegTarget@0x2FC; bodyForce@0x308; bodyVelocity@0x314; bodyWork@0x320; Scalar damageMultiplier[5]@0x32C; Scalar damageResponse[5][4]@0x340. static_assert every offset (0x1DC,0x1E8,0x1F4,0x200,0x20C,0x218,0x224,0x230,0x23C,0x248,0x2B4,0x2C0,0x2CC,0x2D8,0x2E4,0x2F0,0x2FC,0x308,0x314,0x320,0x3BC,0x3C8,0x3CC).\n\n2) gyro.cpp ctor: replace line 235 with the real copies and inits --\n eyePosition = Vector3D(0,0,0);\n springConstant = r->springConstant; // 0x1E8 <- +0x118\n dampingConstant = r->dampingConstant; // 0x1F4 <- +0x124\n posSpring = r->posSpring; negSpring = r->negSpring;\n eyeMaxLimit = posSpring; eyeMinLimit = negSpring; // ctor :2876-2883 (identity transform = copy)\n rotationSpringConstant = r->rotationSpringConstant; rotationDampingConstant = r->rotationDampingConstant;\n rotationPosTarget = r->rotationPosSpring; rotationNegTarget = r->rotationNegSpring;\n bodyMaxLimit = rotationPosTarget * 2.0f; bodyMinLimit = rotationNegTarget * 2.0f; // :2884-2885\n eyeForce = eyeVelocity = eyeWork = Vector3D(0,0,0);\n bodyRotation = bodyForce = bodyVelocity = bodyWork = Vector3D(0,0,0);\n (plus copy the 5 multipliers and 5x4 response tables from the resource).\n\n3) IntegrateEyeJoint -- byte-exact body:\n Vector3D dNeg(eyePosition.x-negSpring.x, ...); Vector3D dPos(eyePosition.x-posSpring.x, ...);\n eyeForce += CompMul(springConstant, dNeg); eyeForce += CompMul(springConstant, dPos); // do NOT clear eyeForce first\n eyeWork = eyeForce; eyeVelocity += eyeWork * dt;\n eyeForce = CompMul(dampingConstant, eyeVelocity); // overwrite -- persists into next frame\n eyeWork = eyeForce; eyeVelocity += eyeWork * dt;\n eyePosition += eyeVelocity; // NO dt here\n eyePosition = per-axis min(eyePosition, eyeMaxLimit) then per-axis max(eyePosition, eyeMinLimit);\n where CompMul(a,b) = (a.x*b.x, a.y*b.y, a.z*b.z).\n\n4) IntegrateBody -- byte-exact body with the reversed pairing:\n Vector3D d1 = bodyRotation - rotationNegTarget; Vector3D d2 = bodyRotation - rotationPosTarget;\n bodyForce.x += rotationSpringConstant.x*d1.z; bodyForce.y += rotationSpringConstant.y*d1.y; bodyForce.z += rotationSpringConstant.z*d1.x;\n bodyForce.x += rotationSpringConstant.x*d2.z; bodyForce.y += rotationSpringConstant.y*d2.y; bodyForce.z += rotationSpringConstant.z*d2.x;\n bodyWork = bodyForce; bodyVelocity += bodyWork * dt;\n bodyForce = CompMul(rotationDampingConstant, bodyVelocity);\n bodyWork = bodyForce; bodyVelocity += bodyWork * dt;\n bodyRotation.x += bodyVelocity.z; bodyRotation.y += bodyVelocity.y; bodyRotation.z += bodyVelocity.x;\n then clamp: y min bodyMaxLimit.y / x min .x / z min .z, then y max bodyMinLimit.y / x max .x / z max .z.\n\n5) ResetToInitialState: change line 324 to zero bodyRotation (0x2B4), not bodyPosSpring; zero eyePosition/eyeForce/eyeVelocity (+ eyeWork/bodyForce/bodyVelocity/bodyWork for parity).\n\n6) ApplyDamageImpulse/Torque/Vertical already add into the right accumulators (0x230 / 0x308) -- after the rename they must target eyeForce and bodyForce respectively.\n\n7) WriteMechJoint stays as-is semantically (translation <- eyePosition, rotation vector <- bodyRotation, type-5 gate, eps 0.0001) -- with the fixed integrators the previous NaN/garbage writes disappear, and the authentic steady eye offset appears automatically as (posSpring+negSpring)/2 per axis. Fix the file-top helper comment: FUN_004086d0 = component-wise multiply, FUN_00408744 = 3x4 matrix*vector (identity pass-through here).