# Gyroscope ctor @004b3778 field-initialisation map vs game/reconstructed/gyro.cpp (+ gyro.hpp layout) ## exact_semantics Gyroscope ctor FUN_004b3778 (reference/decomp/all/part_013.c:2717-2954). this=param_1 (int*, so param_1[N] = this+4N), owner=param_2, res=param_4 (SubsystemResource). Resource offsets verified against CreateStreamedSubsystem FUN_004b3eb4 (part_013.c:3068-4300): Exageration=+0xF8, MaxAnimationNoise=+0xFC, MinAnimationNoise=+0x100, RotationPerSecond=+0x104, PercentageOnNormal/Destruction/Degradation/Failure=+0x108/10C/110/114, SpringConstantXYZ=+0x118, DampingConstantXYZ=+0x124, PosSpringXYZ=+0x130, NegSpringXYZ=+0x13C, RotationSpringConstant Roll/Yaw/Pitch=+0x148/14C/150, RotationDampingConstant Roll/Yaw/Pitch=+0x154/158/15C, RotationPosSpring Pitch/Yaw/Roll=+0x160/164/168 (stored *PI/180), RotationNegSpring Pitch/Yaw/Roll=+0x16C/170/174 (*PI/180), EyeJoint name[32]=+0x178, MechJoint name[32]=+0x198, 5 damage multipliers=+0x1B8..0x1C8, 5x{Trans,PitchRoll,Yaw,Vibration}=+0x1CC/1DC/1EC/1FC/20C. Helper truth (part_000.c): FUN_00408440 @8157 = copy 3 dwords (Vector3D=); FUN_00408e90 @8695 = copy 3 dwords too (Rotation triple =, NOT 4 floats); FUN_0040aadc @9773 = 3x4 identity matrix (12 dwords: [0]=[5]=[10]=1.0f, rest 0); FUN_00408744 @8333 = out = vec rotated by matrix rows (with identity => plain copy); FUN_004092fc @8859 = out = vec * scalar; FUN_0040ab44 @9806 = matrix from placement {FUN_0040adec(m, quat@param2+0xC); FUN_0040b0ac(m,3,pos@param2)}; FUN_00409a00 @9207 = quat from rotation-triple via matrix (zero rotation => identity quat {0,0,0,1}); FUN_00408644 = a-b, FUN_004085ec = a+b, FUN_004086d0 = component-wise a*b (NOT cross), FUN_004086ac = vec*scalar. DAT_004e0f74 = global zero Vector3D; DAT_004e0f8c = global zero rotation triple. CTOR STEP-BY-STEP (decomp line -> action): 2780: FUN_004b18a4(this, owner, param_3, res, &DAT_0050fdb0) -- PowerWatcher base ctor; base occupies 0x000..0x1D7. 2781: +0x000 vtable = &PTR_FUN_00510abc. 2793: FUN_0040aadc(this+0x25C) -- IDENTITY 3x4 matrix @0x25C..0x28B. 2807-2811: if ((*(uint*)(owner+0x28) & 0xC)==0 && (*(byte*)(owner+0x29) & 1)!=0) [i.e. master && flags&0x100]: this+0x1C=PTR_FUN_0050fe08, +0x20=DAT_0050fe0c, +0x24=DAT_0050fe10 -- 3-dword Performance/member-fn record -> GyroscopeSimulation FUN_004b275c. Master only. 2812-2820 scalar copies: 0x1D8=res+0xF8 (exageration); 0x3A0=res+0x100 (MIN AnimationNoise); 0x39C=res+0xFC (MAX AnimationNoise); 0x3A4=res+0x104 (rotationPerSecond); 0x3AC=res+0x108; 0x3B0=res+0x10C; 0x3B4=res+0x110; 0x3B8=res+0x114 (the four Percentages). 2821-2833 vec3 copies (FUN_00408440): 0x1E8=res+0x118 SpringConstant; 0x1F4=res+0x124 DampingConstant; 0x218=res+0x130 PosSpring; 0x224=res+0x13C NegSpring; 0x2C0=res+0x148 RotationSpringConstant (Roll,Yaw,Pitch order); 0x2CC=res+0x154 RotationDampingConstant (Roll,Yaw,Pitch). 2834-2835 rotation copies (FUN_00408e90, 3 dwords): 0x2F0=res+0x160 RotationPosSpring (Pitch,Yaw,Roll; radians); 0x2FC=res+0x16C RotationNegSpring. 2836-2840: 0x32C=res+0x1B8, 0x330=res+0x1BC, 0x334=res+0x1C0, 0x338=res+0x1C4, 0x33C=res+0x1C8 (Collision/Ballistic/Explosive/Laser/Energy DamageMultiplier). 2841-2875 five 16-byte block copies: 0x340=res+0x1CC, 0x350=res+0x1DC, 0x360=res+0x1EC, 0x370=res+0x1FC, 0x380=res+0x20C (each {trans,pitchRoll,yaw,vibration}). 2876-2879: stack identity matrix; FUN_00408744(this+0x200, this+0x218, I) => 0x200 = COPY of PosSpring. (0x200 is the eye UPPER clamp: the integrator does min(orientation, 0x200).) 2880-2883: FUN_00408744(this+0x20C, this+0x224, I) => 0x20C = COPY of NegSpring (eye LOWER clamp: max(orientation, 0x20C)). 2884: FUN_004092fc(this+0x2D8, this+0x2F0, 2.0f) => 0x2D8 = 2.0 * RotationPosSpring (body UPPER clamp). 2885: FUN_004092fc(this+0x2E4, this+0x2FC, 2.0f) => 0x2E4 = 2.0 * RotationNegSpring (body LOWER clamp). 2886-2920 joint resolve: build refcounted strings from res+0x178 (EyeJoint) and res+0x198 (MechJoint); iVar3=FUN_00417ab4(owner+0x31C); segEye=FUN_00424b60(owner, eyeName), idEye=*(segEye+0xC0); segMech=FUN_00424b60(owner, mechName), idMech=*(segMech+0xC0); handle=FUN_0041d3b3(local, iVar3+0xE4); node=vfunc(handle+0x34)(id). this+0x3C8 (param_1[0xF2]) = node for EyeJoint name; this+0x3CC (param_1[0xF3]) = node for MechJoint name. NOTE: resolution is name -> segment -> the segment's joint handle at seg+0xC0 -> skeleton node, NOT a direct joint-name lookup (explains 'jointlocal'/'jointeye' apparent crossing). 2921-2927 scalar zero/one: 0x390=0 (param_1[0xE4]); 0x394=1.0f (0xE5); 0x398=0 (0xE6); 0x3BC=0 swayAngle (0xEF); 0x3C0=0 swayTimer (0xF0); 0x3C4=0 swayActive (0xF1); 0x3A8=0 swayBias (0xEA). 2928: FUN_00409a00(this+0x298, &DAT_004e0f8c) => identity quaternion {0,0,0,1} @0x298..0x2A7. 2929: FUN_00408440(this+0x28C, &DAT_004e0f74) => zero vec3 @0x28C (placement position). 2930: FUN_00408e90(this+0x2A8, &DAT_004e0f8c) => zero rotation triple @0x2A8. 2931: this+0x254 = 0 (scalar). 2933: this+0x258 = (int)(this+0x254) -- SELF-POINTER to the 0x254 scalar. 2934: FUN_0040ab44(this+0x25C, this+0x28C) -- rebuild matrix @0x25C from placement {pos@0x28C, quat@0x298} => identity again. 2935-2942 zero vec3s (DAT_004e0f74): 0x1DC eyeOrientation; 0x230 eyeAccel; 0x23C eyeVelocity; 0x248 eyeWork; 0x308 bodyAccel; 0x314 bodyVelocity; 0x320 bodyWork. 2939: FUN_00408e90(this+0x2B4, &DAT_004e0f8c) => bodyOrientation = zero rotation. 2943-2952: release the two temp name strings. RESULT: EVERY byte in 0x1D8..0x3CF is initialised; the binary leaves NOTHING as fill. INTEGRATOR CROSS-CHECK (FUN_004b2ec0 @part_013.c:2360, needed to name the fields): d1 = orient(0x1DC) - NegSpring(0x224); d2 = orient - PosSpring(0x218); accel(0x230) += SpringConstant(0x1E8) (.) d1; accel += SpringConstant (.) d2; work(0x248)=accel; vel(0x23C) += work*dt; accel = DampingConstant(0x1F4) (.) vel [OVERWRITE, component multiply, line 2387 uses param_1+500=0x1F4]; work=accel; vel += work*dt; orient += identity-rotate(vel) [== vel, NOT dt-scaled]; per-axis orient = min(orient, 0x200) then max(orient, 0x20C). Equilibrium: orient -> (PosSpring+NegSpring)/2 (spring constants are negative in data) -- this midpoint is THE steady eye offset. WriteMechJoint FUN_004b34ec @2603: node=*(this+0x3CC), requires *(node+0x10)==5; if *(float**)(node+0xC) differs from this+0x1DC by >1e-4: FUN_0041d11c(node, this+0x1DC) = SetTranslation(eyeOrientation); then if FUN_0041cfa0(node) differs from this+0x2B4: FUN_0041d020(node, this+0x2B4) = SetRotation(bodyOrientation). No null-check on either node in the binary. ## member_map - 0x000: vtable = &PTR_FUN_00510abc (line 2781) - 0x01C/0x020/0x024: 3-dword Performance record = PTR_FUN_0050fe08/DAT_0050fe0c/DAT_0050fe10 -> GyroscopeSimulation FUN_004b275c; written only when ((owner+0x28 & 0xC)==0 && owner flags & 0x100) (lines 2807-2811) - 0x1D8: exageration <- res+0xF8 (line 2812) - 0x1DC: eyeOrientation vec3, ZEROED (line 2935); is the value written as MechJoint node TRANSLATION by FUN_004b34ec - 0x1E8: springConstant vec3 <- res+0x118 SpringConstantXYZ (line 2821) - 0x1F4: dampingConstant vec3 <- res+0x124 DampingConstantXYZ (line 2823); used at FUN_004b2ec0:2387 as accel = damping (.) velocity - 0x200: eye clamp UPPER = copy of posSpring(0x218) via FUN_00408744 with identity (lines 2876-2879) - 0x20C: eye clamp LOWER = copy of negSpring(0x224) (lines 2880-2883) - 0x218: posSpring vec3 <- res+0x130 PosSpringXYZ (line 2825) - 0x224: negSpring vec3 <- res+0x13C NegSpringXYZ (line 2827) - 0x230: eyeAccel vec3 = 0 (line 2936); ApplyDamageImpulse @004b2d8c adds here - 0x23C: eyeVelocity vec3 = 0 (line 2937) [recon calls it eyeForce] - 0x248: eyeWork scratch vec3 = 0 (line 2938) - 0x254: scalar = 0 (line 2931) - 0x258: pointer = &this->0x254 (self-pointer, line 2933) - 0x25C..0x28B: 3x4 matrix = identity (line 2793), rebuilt at line 2934 from placement {pos@0x28C, quat@0x298} via FUN_0040ab44 => identity - 0x28C: placement position vec3 = 0 (line 2929, DAT_004e0f74) - 0x298..0x2A7: quaternion = identity {0,0,0,1} (line 2928, FUN_00409a00 of zero rotation) - 0x2A8: rotation triple = 0 (line 2930, DAT_004e0f8c) - 0x2B4: bodyOrientation rotation = 0 (line 2939); written as MechJoint node ROTATION by FUN_004b34ec - 0x2C0: rotationSpringConstant vec3 (Roll,Yaw,Pitch order!) <- res+0x148 (line 2829) - 0x2CC: rotationDampingConstant vec3 (Roll,Yaw,Pitch) <- res+0x154 (line 2833) - 0x2D8: body clamp UPPER = 2.0 * rotationPosSpring(0x2F0) via FUN_004092fc (line 2884) - 0x2E4: body clamp LOWER = 2.0 * rotationNegSpring(0x2FC) (line 2885) - 0x2F0: rotationPosSpring (Pitch,Yaw,Roll, radians) <- res+0x160 (line 2834) - 0x2FC: rotationNegSpring (Pitch,Yaw,Roll, radians) <- res+0x16C (line 2835) - 0x308: bodyAccel vec3 = 0 (line 2940) - 0x314: bodyVelocity vec3 = 0 (line 2941) [recon: bodyForce] - 0x320: bodyWork scratch vec3 = 0 (line 2942, 'param_1 + 800') - 0x32C/0x330/0x334/0x338/0x33C: Collision/Ballistic/Explosive/Laser/Energy DamageMultiplier <- res+0x1B8/1BC/1C0/1C4/1C8 (lines 2836-2840) - 0x340/0x350/0x360/0x370/0x380: five 16-byte DamageResponse{trans,pitchRoll,yaw,vibration} <- res+0x1CC/1DC/1EC/1FC/20C (lines 2841-2875) - 0x390: animation offset = 0; 0x394: animation scale = 1.0f; 0x398: animation phase = 0 (lines 2921-2923) - 0x39C: maxAnimationNoise <- res+0xFC; 0x3A0: minAnimationNoise <- res+0x100 (lines 2813-2814; MAX at 0x39C, MIN at 0x3A0) - 0x3A4: rotationPerSecond <- res+0x104; 0x3A8: swayBias = 0 (lines 2815, 2927) - 0x3AC/0x3B0/0x3B4/0x3B8: percentageOnNormal/Destruction/Degradation/Failure <- res+0x108/10C/110/114 (lines 2816-2820) - 0x3BC: swayAngle = 0; 0x3C0: swayTimer = 0; 0x3C4: swayActive = 0 (lines 2924-2926) - 0x3C8: eye joint node = skeleton node of the SEGMENT named by res+0x178 EyeJoint (segment->0xC0 joint handle -> skeleton vfunc+0x34) (lines 2886-2915) - 0x3CC: mech joint node = same path for res+0x198 MechJoint (line 2920). Every byte 0x1D8..0x3CF is ctor-initialised; the binary leaves no CD-fill. ## recon_mismatches - gyro.cpp:235 -> 'eyeOrientation = r->springConstant' is the confirmed mislabel: binary copies res+0x118 SpringConstant into this+0x1E8 (part_013.c:2821) and ZEROES eyeOrientation@0x1DC (line 2935). The recon starts the eye AT the spring-constant vector and leaves the real spring constant uninitialised. - gyro.cpp ctor (nowhere) -> dampingConstant (res+0x124 -> this+0x1F4, part_013.c:2823) is never copied. gyro.hpp:274 declares 'eyeSpringConstant // @0x1F4?' but no code initialises it => 0xCDCDCDCD. It is the multiplier of the damping stage (FUN_004b2ec0:2387). - gyro.cpp ctor (nowhere) -> eye clamp seeds missing: binary sets 0x200 = copy of posSpring and 0x20C = copy of negSpring (part_013.c:2876-2883, FUN_00408744 with identity). Recon's eyeLimitLow/eyeLimitHigh are never assigned => the per-axis clamp in IntegrateEyeJoint clamps eyeOrientation to 0xCDCDCDCD = -4.31e8, exactly the garbage observed in WriteMechJoint. Naming also inverted: 0x200 is the UPPER bound (min), 0x20C the LOWER (max). - gyro.cpp ctor (nowhere) -> accumulator zeroing missing: binary zeroes eyeAccel@0x230, eyeVelocity@0x23C, eyeWork@0x248, bodyAccel@0x308, bodyVelocity@0x314, bodyWork@0x320 (part_013.c:2936-2942) and bodyOrientation@0x2B4 (line 2939). None are initialised by the recon ctor => NaN propagation. - gyro.cpp ctor (nowhere) -> body clamp seeds missing: 0x2D8 = 2.0*rotationPosSpring and 0x2E4 = 2.0*rotationNegSpring (part_013.c:2884-2885, FUN_004092fc x2.0). gyro.hpp:282-283 wrongly claims bodyLimitLow '= rotationPosSpring' (no x2) and the ctor never assigns them anyway. - gyro.cpp:246-247 -> comment claims damage multipliers/response curves are 'copied verbatim' but NO code exists and gyro.hpp declares NO members for this+0x32C..0x38F (res+0x1B8..0x21B, part_013.c:2836-2875). - gyro.hpp (missing block) -> the whole 0x254..0x2B3 region is undeclared: 0x254 scalar=0, 0x258 self-pointer, 0x25C identity 3x4 matrix, 0x28C zero pos, 0x298 identity quat, 0x2A8 zero rotation (part_013.c:2793,2928-2934). Because it is absent, every member declared after eyeWork is shifted ~-0x60 versus its claimed offset. - gyro.hpp:268-308 -> declaration order does not match the claimed '@0xNN' comments: actual packed offsets are eyeVelocity@0x1E8 (binary: springConstant there), eyeLimitLow@0x1F4 (binary: dampingConstant), eyeLimitHigh@0x200, eyePosSpring@0x20C, eyeNegSpring@0x218, eyeSpringConstant@0x224, bodyOrientation@0x254, ..., mechJointNode@~0x308. Only exageration@0x1D8 and sizeof<=0x3D0 are static_asserted, so none of this was caught. - gyro.hpp:294-295 comment vs binary: correct as written (MAX@0x39C<-res+0xFC, MIN@0x3A0<-res+0x100) -- confirming PROGRESS_LOG; no change needed, listed to close the open question. - gyro.cpp:428-432 (IntegrateEyeJoint) -> subtract order flipped: binary computes (eyeOrientation - negSpring) and (eyeOrientation - posSpring) (FUN_004b2ec0:2376-2377), recon computes (spring - orientation). Both spring terms multiply by springConstant@0x1E8 (recon OK in shape but the member is the uninitialised one). - gyro.cpp:441 -> 'eyeAccel.Cross(eyeSpringConstant, eyeForce)' is wrong twice: the binary OVERWRITES accel = dampingConstant(0x1F4) COMPONENT-multiplied by velocity (FUN_004086d0 is per-component multiply, NOT a cross product; part_000.c:8294-8300), and the constant is the DAMPING vector, not the spring vector. Same error mirrored at gyro.cpp:482 for the body (there the binary DOES use 0x2CC damping -- recon constant is right but the op must be component multiply). - gyro.cpp:470-471 (IntegrateBody) -> the body force build has an x/z axis SWAP the recon lacks: force.x = k.x*diff.z, force.y = k.y*diff.y, force.z = k.z*diff.x (FUN_004b30ec:2479-2484; k is Roll,Yaw,Pitch ordered while the diffs are Pitch,Yaw,Roll ordered), and the final add is also swapped: orientation.x += delta.z / .z += delta.x (lines 2504-2506). - gyro.cpp:261-262 -> ResolveJoint(owner, name) approximates the binary but the real path is name -> segment (FUN_00424b60) -> the segment's joint handle at seg+0xC0 -> skeleton node via vfunc+0x34 on FUN_00417ab4(owner+0x31C)+0xE4 (part_013.c:2904-2920). The node bound is the JOINT OF THE SEGMENT NAMED by EyeJoint/MechJoint, which is why the names look 'crossed' ('eye' segment owns 'jointeye'). ## fix 1) gyro.hpp -- rewrite the own-field block in BINARY order and add the missing members so offsets are exact (then static_assert-lock each): Scalar exageration@0x1D8; Vector3D eyeOrientation@0x1DC; Vector3D springConstant@0x1E8; Vector3D dampingConstant@0x1F4; Vector3D eyeClampUpper@0x200; Vector3D eyeClampLower@0x20C; Vector3D posSpring@0x218; Vector3D negSpring@0x224; Vector3D eyeAccel@0x230; Vector3D eyeVelocity@0x23C; Vector3D eyeWork@0x248; Scalar spare0@0x254; void* spare0Ptr@0x258; Scalar workMatrix[12]@0x25C; Vector3D placePos@0x28C; Scalar placeQuat[4]@0x298; Vector3D placeRot@0x2A8; Vector3D bodyOrientation@0x2B4; Vector3D rotationSpringConstant@0x2C0; Vector3D rotationDampingConstant@0x2CC; Vector3D bodyClampUpper@0x2D8; Vector3D bodyClampLower@0x2E4; Vector3D rotationPosSpring@0x2F0; Vector3D rotationNegSpring@0x2FC; Vector3D bodyAccel@0x308; Vector3D bodyVelocity@0x314; Vector3D bodyWork@0x320; Scalar damageMultiplier[5]@0x32C; Gyroscope__DamageResponse damageResponse[5]@0x340; Scalar animOffset@0x390; Scalar animScale@0x394; Scalar animPhase@0x398; Scalar maxAnimationNoise@0x39C; Scalar minAnimationNoise@0x3A0; Scalar rotationPerSecond@0x3A4; Scalar swayBias@0x3A8; Scalar percentageOnNormal@0x3AC; ...Destruction@0x3B0; ...Degradation@0x3B4; ...Failure@0x3B8; Scalar swayAngle@0x3BC; Scalar swayTimer@0x3C0; int swayActive@0x3C4; Joint* eyeJointNode@0x3C8; Joint* mechJointNode@0x3CC. static_assert offsetof for at least springConstant==0x1E8, dampingConstant==0x1F4, eyeClampUpper==0x200, bodyOrientation==0x2B4, mechJointNode==0x3CC, sizeof(Gyroscope)==0x3D0. 2) gyro.cpp ctor -- replace line 235 and add every missing init, exactly mirroring @004b3778: springConstant=r->springConstant; dampingConstant=r->dampingConstant; posSpring=r->posSpring; negSpring=r->negSpring; rotationSpringConstant=r->rotationSpringConstant; rotationDampingConstant=r->rotationDampingConstant; rotationPosSpring=r->rotationPosSpring; rotationNegSpring=r->rotationNegSpring; eyeClampUpper=posSpring; eyeClampLower=negSpring; bodyClampUpper=rotationPosSpring*2.0f; bodyClampLower=rotationNegSpring*2.0f; for(i=0..4) damageMultiplier[i]=..., damageResponse[i]=...; eyeOrientation=bodyOrientation=eyeAccel=eyeVelocity=eyeWork=bodyAccel=bodyVelocity=bodyWork=Vector3D(0,0,0); spare0=0; spare0Ptr=&spare0; workMatrix=identity; placePos=0; placeQuat={0,0,0,1}; placeRot=0. Keep the existing scalar/animation inits (already correct). 3) gyro.cpp IntegrateEyeJoint -- make it byte-exact per FUN_004b2ec0: toNeg=eyeOrientation-negSpring; toPos=eyeOrientation-posSpring; eyeAccel += springConstant(.)toNeg; eyeAccel += springConstant(.)toPos; eyeWork=eyeAccel; eyeVelocity += eyeWork*dt; eyeAccel = dampingConstant(.)eyeVelocity (component multiply, overwrite); eyeWork=eyeAccel; eyeVelocity += eyeWork*dt; eyeOrientation += eyeVelocity; per-axis: eyeOrientation = min(eyeOrientation, eyeClampUpper) then max(eyeOrientation, eyeClampLower). 4) gyro.cpp IntegrateBody -- per FUN_004b30ec including the axis swap: dN=bodyOrientation-rotationNegSpring; dP=bodyOrientation-rotationPosSpring; f1=(k.x*dN.z, k.y*dN.y, k.z*dN.x); f2=(k.x*dP.z, k.y*dP.y, k.z*dP.x) with k=rotationSpringConstant; bodyAccel+=f1; bodyAccel+=f2; bodyWork=bodyAccel; bodyVelocity+=bodyWork*dt; bodyAccel=rotationDampingConstant(.)bodyVelocity; bodyWork=bodyAccel; bodyVelocity+=bodyWork*dt; delta=bodyVelocity; bodyOrientation.x+=delta.z; bodyOrientation.y+=delta.y; bodyOrientation.z+=delta.x; then min against bodyClampUpper / max against bodyClampLower per axis (binary clamps y,x,z order -- order irrelevant). 5) Result to expect once live: eyeOrientation settles at (posSpring+negSpring)/2 (the spring constants in the data are negative) and is pushed every frame into the MechJoint node as its TRANSLATION with bodyOrientation as its ROTATION (WriteMechJoint FUN_004b34ec, type-5 gate) -- that midpoint is the authentic steady eye offset, and damage impulses into eyeAccel/bodyAccel produce the transient cockpit bounce.