//===========================================================================// // File: gyro.cpp // // Project: BattleTech Brick: Entity Manager // // Contents: Gyroscope subsystem -- balance / orientation / tip-over model // //---------------------------------------------------------------------------// // Date Who Modification // // -------- --- ---------------------------------------------------------- // // --/--/95 ?? Initial coding. // //---------------------------------------------------------------------------// // Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved // // PROPRIETARY AND CONFIDENTIAL // //===========================================================================// // // RECONSTRUCTED from the shipped binary (Ghidra pseudo-C, recovered shard // part_013.c). No header survived; see gyro.hpp for the class story. Each // non-trivial method cites the originating @ADDR. Confidence is flagged per // method: [CONFIDENT] body matches the decomp closely; [BEST-EFFORT] the // shape is recovered but names/details are inferred; [EXCLUDED] not emitted. // // Hex constants converted to decimal: // 0x3f800000 = 1.0f 0x3f000000 = 0.5f 0x40000000 = 2.0f // _DAT_004b5b20 = -1.0f (resource "unset" sentinel) // _DAT_004b5b24 = 0.0174532925f (PI/180, deg->rad) // _DAT_004b297c = _DAT_004b3774 = 0.0f // _DAT_004b34e8 = 0.0001f (quantise epsilon) // // Helper-function name mapping (engine internals referenced by the decomp): // FUN_004b18a4 PowerWatcher base constructor (powersub.cpp) // FUN_004b198c PowerWatcher::CreateStreamedSubsystem (powersub.cpp) // FUN_004b1804 PowerWatcher::ResetToInitialState (slot10) (powersub.cpp) // FUN_004b179c PowerWatcher slot-9 death/voltage handler (powersub.cpp) // FUN_004b181c PowerWatcher per-frame watch update (powersub.cpp) // FUN_00404118 NotationFile::ReadScalar(model,name,&dst) // FUN_00404088 NotationFile::ReadString(model,name,&dst) // FUN_004dbb24 DebugStream insert (error reporting) // FUN_00408440 Vector3D::operator= / copy(3) // FUN_00408e90 Point4D::operator= / copy(4) // FUN_0040aadc Matrix/quat identity-init // FUN_004085ec Vector3D += ; FUN_004086d0 Vector3D cross/mul // FUN_004086ac Vector3D *= scalar ; FUN_00408644 Vector3D - // FUN_00408744 Vector3D clamp-to-box ; FUN_004092fc Vector3D *= scalar // FUN_0041cfa0/0041d020/0041d0a8/0041d11c skeleton-node get/set transform // FUN_004dcd00 fabsf() // #include #pragma hdrstop #if !defined(GYRO_HPP) # include #endif #if !defined(MECH_HPP) # include // complete Mech -- owner->ResolveJoint #endif #include // Joint, JointSubsystem (fwd shim) #include // EulerAngles, Radian (fwd shim) #if !defined(APP_HPP) # include #endif #if !defined(TESTBT_HPP) # include #endif static const Scalar Zero = 0.0f; // _DAT_004b297c / _DAT_004b3774 static const Scalar QuantiseEps = 0.0001f; // _DAT_004b34e8 static const Scalar Unset = -1.0f; // _DAT_004b5b20 static const Scalar DegToRad = 0.0174532925f; // _DAT_004b5b24 //############################################################################# // Reconstruction shims (file-local). // // The DebugStream trace sink (FUN_004dbb24) is unavailable in this build (the // heat.hpp/mechrecon.hpp diagnostic globals collide), so the "missing !" // resource diagnostics route through a local no-op sink here. // static struct GyroDebugSink { template GyroDebugSink &operator<<(const T &) { return *this; } } DebugSink; // // Skeleton-node accessors -- now backed by the REAL engine Joint API (JOINT.h), // no longer stubs. The gyro writes the integrated sway/orientation into two // resolved skeleton joints (eyeJointNode / mechJointNode, Joint*). Mapping to // the recovered helper addresses: FUN_0041cfa0 = GetEulerAngles, FUN_0041d0a8 = // SetRotation(Radian) [hinge], FUN_0041d020 = SetRotation(EulerAngles) [ball], // FUN_0041d11c = SetTranslation [BallTranslation]. All callers guard the joint // TYPE before use (hinge<3 vs ball 4/5); NULL-guarded for bring-up (an unresolved // joint simply skips the write instead of dereferencing). // static int NodeType(Joint *node) { return node ? (int)node->GetJointType() : (int)Joint::NULLJointType; } static Scalar NodeScalar(Joint *node) { return node ? (Scalar)node->GetRadians() : 0.0f; } // FUN_0041cf.. (hinge) static void SetNodeScalar(Joint *node, Scalar value) { if (node) node->SetRotation(Radian(value)); } // FUN_0041d0a8 static Vector3D NodeVector(Joint *node) { if (node == 0) return Vector3D(0.0f, 0.0f, 0.0f); const EulerAngles &e = node->GetEulerAngles(); // FUN_0041cfa0 return Vector3D((Scalar)e.pitch, (Scalar)e.yaw, (Scalar)e.roll); } static void SetNodeVector(Joint *node, const Vector3D &v) { if (node) node->SetRotation(EulerAngles(v.x, v.y, v.z)); } // FUN_0041d020 static Logical NodeVectorEquals(Joint *node, const Vector3D &v, Scalar eps) { if (node == 0) return True; // no node -> skip the set const EulerAngles &e = node->GetEulerAngles(); return fabsf((Scalar)e.pitch - v.x) <= eps && fabsf((Scalar)e.yaw - v.y) <= eps && fabsf((Scalar)e.roll - v.z) <= eps; } static void SetNodeRotation(Joint *node, const Vector3D &v) { if (node) node->SetTranslation(Point3D(v.x, v.y, v.z)); } // FUN_0041d11c (BallTranslation) static Logical NodeRotationEquals(Joint *node, const Vector3D &v, Scalar eps) { if (node == 0) return True; const Point3D &t = node->GetTranslation(); return fabsf(t.x - v.x) <= eps && fabsf(t.y - v.y) <= eps && fabsf(t.z - v.z) <= eps; } // // Joint resolution -- forward to the owning Mech's shared resolver (Mech:: // ResolveJoint == binary FUN_00424b60, the engine GetSegment->GetJoint path). // static Joint * ResolveJoint(Mech *owner, const char *node_name) { return owner ? owner->ResolveJoint(node_name) : 0; } // // CROSS-FAMILY skeleton load/lookup used only by CreateStreamedSubsystem to // verify the EyeJoint / MechJoint names exist in the model's skeleton file // (FUN_004064fc / FUN_00403e84 / FUN_00403f84). Stubbed. // struct GyroSkeleton { Logical FindNode(const char * /*node_name*/) const { return True; } }; static GyroSkeleton g_gyroSkeleton; static GyroSkeleton *LoadSkeleton(const ResourceDirectories * /*dirs*/, const char * /*name*/) { return &g_gyroSkeleton; } //########################################################################### // BASE-CHAIN RE-BASE -- compile-time layout lock (friend of Gyroscope so it can // offsetof the protected own-block). After the Watcher-branch re-base + the shim // delete, the gyro's first own field (exageration) must land at 0x1D8 (right after // the shared PowerWatcher base), and the object must fit the 0x3D0 factory alloc. // (The interior own-field layout is not yet fully binary-exact -- see CLAUDE.md -- // but it is self-consistent and fits, which is all the scoped un-stub needs.) //########################################################################### struct GyroLayoutCheck { // 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, vibrationDirection) == 0x390, "vibrationDirection @0x390"); 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"); }; //########################################################################### //########################################################################### // Gyroscope //########################################################################### //########################################################################### //############################################################################# // Shared Data Support (DefaultData @0050fdb0) // Derivation Gyroscope::ClassDerivations( PowerWatcher::GetClassDerivations(), // returns Derivation* (no &) "Gyroscope" ); Receiver::MessageHandlerSet Gyroscope::MessageHandlers; Gyroscope::AttributeIndexSet Gyroscope::AttributeIndex; Gyroscope::SharedData Gyroscope::DefaultData( &Gyroscope::ClassDerivations, Gyroscope::MessageHandlers, Gyroscope::AttributeIndex, Gyroscope::StateCount ); //############################################################################# // Construction / Destruction // // // @004b3778 [CONFIDENT] -- chains to the PowerWatcher base ctor (FUN_004b18a4) // with &Gyroscope::DefaultData, installs the Gyroscope vtable (PTR @00510abc), // registers GyroscopeSimulation as the Performance for a live master segment // (flags & 0xC == 0 && flags & 1), copies the parsed scalars/vectors out of the // resource, identity-inits the work matrices, resolves the two skeleton joints // named by the resource (EyeJoint @+0x178, MechJoint @+0x198) into eyeJointNode // /mechJointNode, and primes the animation-noise state. // Gyroscope::Gyroscope( Mech *owner, int subsystem_ID, SubsystemResource *r, SharedData &shared_data ): PowerWatcher(owner, subsystem_ID, r, shared_data) { Check(owner); Check_Pointer(r); // BASE-CHAIN RE-BASE: the 4 CROSS-FAMILY shim backing fields were deleted; their // accessors now read the real inherited base state, so nothing to prime here. // The master/copy gate below reads the authoritative owner->simulationFlags. // INTEGRATION (gate reconcile): read OWNER simulationFlags (param_2+0x28), // the oracle-verified authoritative source, instead of the local segment-flag // shim. GetSegmentFlags() backs a per-subsystem shim; the binary gate at // @004b3778 reads owner+0x28. if ((owner->simulationFlags & SegmentCopyMask) == 0 // (owner flags & 0xC)==0 && (owner->simulationFlags & MasterHeatSinkFlag) != 0) // owner flags & 0x100 { SetPerformance(&Gyroscope::GyroscopeSimulation); // this[7..9] = PTR @0050fe08 } exageration = r->exageration; // @0x1D8 <- +0xF8 minAnimationNoise = r->minAnimationNoise; // @0x3A0 <- +0x100 maxAnimationNoise = r->maxAnimationNoise; // @0x39C <- +0xFC rotationPerSecond = r->rotationPerSecond; // @0x3A4 <- +0x104 percentageOnNormal = r->percentageOnNormal; // @0x3AC <- +0x108 percentageOnDestruction = r->percentageOnDestruction; // @0x3B0 <- +0x10C percentageOnDegradation = r->percentageOnDegradation; // @0x3B4 <- +0x110 percentageOnFailure = r->percentageOnFailure; // @0x3B8 <- +0x114 // 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 (: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 + {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; // 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 } vibrationDirection = Vector3D(0.0f, 1.0f, 0.0f); // @0x390 the up vibration axis (:2921-2923) swayAngle = 0.0f; // @0x3BC swayVelocity = 0.0f; // @0x3C0 swayActive = 0; // @0x3C4 swayBias = 0.0f; // @0x3A8 // EyeJoint / MechJoint were resolved against the skeleton during streaming; // the ctor binds the two SkeletonConnection handles to live joint nodes: eyeJointNode = ResolveJoint(owner, r->eyeJoint); // @0x3C8 this[0xF2] mechJointNode = ResolveJoint(owner, r->mechJoint); // @0x3CC this[0xF3] // bring-up verification (env BT_GYRO_LOG; default OFF): layout + joint resolve. if (getenv("BT_GYRO_LOG")) { DEBUG_STREAM << "[gyro] ctor this=" << (void*)this << " sizeof=" << (unsigned)sizeof(Gyroscope) << " exageration@" << (unsigned)((char*)&exageration - (char*)this) << " (want 0x1D8=" << (unsigned)0x1D8 << ")\n"; DEBUG_STREAM << "[gyro] eye '" << r->eyeJoint << "' -> " << (void*)eyeJointNode; if (eyeJointNode) DEBUG_STREAM << " type=" << (int)eyeJointNode->GetJointType(); 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(); } // // @004b3e88 [CONFIDENT] -- reinstalls the vtable, runs the PowerWatcher // teardown (FUN_004b1930 path) and frees the block when the deleting bit is set. // Gyroscope::~Gyroscope() { Check(this); Check_Fpu(); } //########################################################################### // TestClass / TestInstance -- Gyroscope // // Standard subsystem convention (cf. HeatSink/Sensor). BEST-EFFORT: no // distinct Gyroscope bodies were captured. // Logical Gyroscope::TestClass(Mech &) { return True; } Logical Gyroscope::TestInstance() const { return IsDerivedFrom(ClassDerivations); } //############################################################################# // Subsystem virtual overrides // // // @004b2678 (slot 10) [CONFIDENT] -- ResetToInitialState. Zeroes the sway and // orientation accumulators, re-zeroes the eye/body work vectors (copies the // engine zero-vector &DAT_004e0f74 / zero-quat &DAT_004e0f8c) and chains to // PowerWatcher::ResetToInitialState (FUN_004b1804). // void Gyroscope::ResetToInitialState() { swayAngle = 0.0f; // this[0xEF] swayVelocity = 0.0f; // this[0xF0] swayBias = 0.0f; // this[0xEA] swayActive = 0; // this[0xF1] const Vector3D zero(0.0f, 0.0f, 0.0f); // @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) // belongs on the PowerWatcher/Subsystem family. See "CROSS-FAMILY NEEDS". } // // @004b2660 (slot 9) [CONFIDENT] -- forwards to the PowerWatcher slot-9 handler // (FUN_004b179c). That handler, on a "destroyed" message (msg->type == 4), // resolves the watched power source and clears its voltage alarm; otherwise it // chains to the base. Exact message semantics are inherited from PowerWatcher. // Logical Gyroscope::HandleDeathMessage(Message &/*message*/) { // CROSS-FAMILY: forwards to the PowerWatcher slot-9 death/voltage handler // (FUN_004b179c). PowerWatcher in these headers exposes HandleMessage(int), // not a HandleDeathMessage(Message&), so there is no compatible base method // to chain to here; pass the message through as handled. See "CROSS-FAMILY // NEEDS". return True; } //############################################################################# // Per-frame simulation // // // @004b275c [CONFIDENT] -- the registered Performance (PTR @0050fe08). // // Runs the PowerWatcher watch update first (FUN_004b181c), then walks the idle // "sway" toward a damage-state-dependent target at rotationPerSecond * dt: // * if the watched power source is dead (this[0x10]==1), the subsystem is not // electrically Ready (this @0x198 != 4), OR it is in the Failure heat state // (this @0x140 == 2): aim at (percentageOnDestruction + swayBias); // * otherwise aim at (percentageOnNormal + swayBias). // The step is sign-corrected so it never overshoots the target, then swayAngle // is clamped into [minAnimationNoise, maxAnimationNoise]. Finally the two // spring-damper integrators run. // void Gyroscope::GyroscopeSimulation(Scalar time_slice) { Check(this); PowerWatcher::Simulation(time_slice); // FUN_004b181c -- base per-frame watch update const Logical impaired = HeatModelOff() // this[0x10] == 1 || ElectricalStateLevel() != PoweredSubsystem::Ready // this @0x198 != 4 || HeatStateLevel() == HeatSink::FailureHeat; // this @0x140 == 2 Scalar target = (impaired ? percentageOnDestruction : percentageOnNormal) + swayBias; Scalar step = rotationPerSecond * time_slice; if (target < swayAngle) // moving down toward target { step = -step; } swayAngle += step; // clamp so we do not pass the target this frame if (step <= Zero) { if (step < Zero && swayAngle < target) swayAngle = target; } else { if (swayAngle > target) swayAngle = target; } // clamp into the [min, max] animation-noise band if (swayAngle > maxAnimationNoise) swayAngle = maxAnimationNoise; if (swayAngle < minAnimationNoise) swayAngle = minAnimationNoise; IntegrateEyeJoint(time_slice); // FUN_004b2ec0 IntegrateBody(time_slice); // FUN_004b30ec // 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 [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 d1; d1.Subtract(eyePosition, negSpring); // :2376 state - target Vector3D d2; d2.Subtract(eyePosition, posSpring); // :2377 Vector3D f; f.Multiply(springConstant, d1); eyeForce += f; // :2378-2381 componentwise f.Multiply(springConstant, d2); eyeForce += f; eyeWork = eyeForce; // :2383 f.Multiply(eyeWork, time_slice); eyeVelocity += f; // :2384-2385 eyeForce.Multiply(dampingConstant, eyeVelocity); // :2387 OVERWRITE (componentwise) eyeWork = eyeForce; f.Multiply(eyeWork, time_slice); eyeVelocity += f; // :2388-2390 eyePosition += eyeVelocity; // :2391-2393 identity pass-through, NO dt // 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 [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 dN; dN.Subtract(bodyOrientation, rotationNegSpring); // state - target Vector3D dP; dP.Subtract(bodyOrientation, rotationPosSpring); 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; bodyWork = bodyForce; f.Multiply(bodyWork, time_slice); bodyVelocity += f; bodyForce.Multiply(rotationDampingConstant, bodyVelocity); // :2498 componentwise, uncrossed, OVERWRITE bodyWork = bodyForce; f.Multiply(bodyWork, time_slice); bodyVelocity += f; bodyOrientation.x += bodyVelocity.z; // :2504-2506 crossed again bodyOrientation.y += bodyVelocity.y; bodyOrientation.z += bodyVelocity.x; 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 } // // @004b33e0 [CONFIDENT] -- push swayAngle into the EyeJoint skeleton node. // Depending on the node's parameterisation (type field @+0x10) it either scales // the node's scalar channel by swayAngle (types 0..2) or interpolates the node's // vector channel toward the swayed value (types 4..5), writing back only when // the change exceeds QuantiseEps. (FUN_004b2eac @004b2eac thunks to here.) // void Gyroscope::WriteEyeJoint() { Scalar value = swayAngle; int type = NodeType(eyeJointNode); // *(node + 0x10) if (type < 3) { Scalar base = NodeScalar(eyeJointNode); // *(*(node+0xC)+4) Scalar out = value * base; if (fabsf(base - out) > QuantiseEps) { SetNodeScalar(eyeJointNode, out); // FUN_0041d0a8 } } else if (type == 4 || type == 5) { Vector3D out = NodeVector(eyeJointNode); out *= value; // FUN_004092fc (scale) if (!NodeVectorEquals(eyeJointNode, out, QuantiseEps)) // FUN_004091f4 { SetNodeVector(eyeJointNode, out); // FUN_0041d020 } } } // // @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() { if (NodeType(mechJointNode) != 5) { return; } // bring-up verification (env BT_GYRO_LOG): confirm the per-frame write fires // with the integrated eye/body orientation (sampled periodically). static const int s_glog = getenv("BT_GYRO_LOG") ? 1 : 0; static int s_gc = 0; if (s_glog && (s_gc % 60) == 0 && s_gc < 1200) { 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; // bounce visibility probe: log the first N frames where the eye is actually // displaced (the periodic sampler above misses short oscillations). static int s_bounceSeen = 0; if (s_glog && s_bounceSeen < 40 && (eyePosition.x > 1e-3f || eyePosition.x < -1e-3f || eyePosition.y > 1e-3f || eyePosition.y < -1e-3f || eyePosition.z > 1e-3f || eyePosition.z < -1e-3f)) { ++s_bounceSeen; DEBUG_STREAM << "[gyro-bounce] eyePos=(" << (float)eyePosition.x << "," << (float)eyePosition.y << "," << (float)eyePosition.z << ")\n" << std::flush; } // BOUNCE TRACE (task #56 verification): BT_GYRO_TRACE=1 logs this gyro's // integrated state EVERY frame while displaced, uncapped, tagged with the // instance + a running frame index -- enough to PLOT the oscillation and // check the damped-spring behaviour against the authored constants. static const int s_gtrace = getenv("BT_GYRO_TRACE") ? 1 : 0; if (s_gtrace) { static int s_traceFrame = 0; ++s_traceFrame; const float m2 = eyePosition.x*eyePosition.x + eyePosition.y*eyePosition.y + eyePosition.z*eyePosition.z + bodyOrientation.x*bodyOrientation.x + bodyOrientation.y*bodyOrientation.y + bodyOrientation.z*bodyOrientation.z; if (m2 > 1e-9f) DEBUG_STREAM << "[gtrace] g=" << (void *)this << " f=" << s_traceFrame << " eye=" << (float)eyePosition.x << " " << (float)eyePosition.y << " " << (float)eyePosition.z << " body=" << (float)bodyOrientation.x << " " << (float)bodyOrientation.y << " " << (float)bodyOrientation.z << "\n" << std::flush; } if (!NodeRotationEquals(mechJointNode, eyePosition, QuantiseEps)) // FUN_004084fc vs value+0 { SetNodeRotation(mechJointNode, eyePosition); // FUN_0041d11c = SetTRANSLATION } if (!NodeVectorEquals(mechJointNode, bodyOrientation, QuantiseEps)) // FUN_004091f4 { SetNodeVector(mechJointNode, bodyOrientation); // FUN_0041d020 } } // // @004b357c [BEST-EFFORT] -- procedural animation-noise generator. Lazily // (re)arms a 2.0s noise window (swayVelocity timer @0x3C0, swayActive @0x3C4), // interpolates the EyeJoint node toward a randomised pose proportional to the // remaining window, and returns True while the window is still open. The switch // over the node type mirrors WriteEyeJoint. Returns whether the timer expired. // Logical Gyroscope::UpdateAnimationNoise(Scalar time_slice) { if (swayActive == 0) { swayActive = 1; swayVelocity = 2.0f; // arm a 2.0s window (0x40000000) } Scalar phase = time_slice / swayVelocity; switch (NodeType(eyeJointNode)) { case 0: case 1: case 2: // scalar-channel noise injection (FUN_00408dd4 + FUN_0041d0a8) break; case 4: case 5: // vector-channel slerp toward random target (FUN_00409390 + FUN_0041d020) break; } swayVelocity -= time_slice; if (swayVelocity <= Zero) { swayActive = 0; } return (swayVelocity <= Zero); } //############################################################################# // Damage / impulse hooks // // // @004b2d8c [CONFIDENT] -- negate the (x,y,z) hit direction, scale the supplied // magnitude by `exageration`, and add the resulting impulse into eyeAccel. // void Gyroscope::ApplyDamageImpulse(Scalar x, Scalar y, Scalar z, Scalar magnitude) { Vector3D dir(-x, -y, -z); magnitude *= exageration; // *(this+0x1D8) dir *= magnitude; // FUN_004086ac eyeForce += dir; // FUN_004085ec (this+0x230) } // // @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) { Vector3D dir(-x, -y, -z); magnitude *= exageration; dir *= magnitude; 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 bodyForce // and mirrored into its .y (this+0x30C = this+0x308). // void Gyroscope::ApplyVerticalImpulse(Scalar pitch, Scalar, Scalar, Scalar magnitude) { Vector3D dir(pitch, 0.0f, 0.0f); magnitude *= exageration; dir *= magnitude; bodyForce += dir; // FUN_004085ec (this+0x308) bodyForce.y = bodyForce.x; // *(this+0x30C) = *(this+0x308) } // // @004b2980 [T1 -- re-disassembled byte-exact, task #56] -- the damage->gyro // fan-out (the cockpit hit-BOUNCE). Constants from the dump: eps 1e-4 // (@0x38d1b717), sign threshold 0.5f (@0x4b2d84), clamp 1.3f (@0x4b2d88). // Collision (type 0) and zero damage no-op. Direction: the Damage record's // damageForce, or a RANDOM horizontal direction when ~zero (per component a // sign roll then a value roll); rotated by the yaw-only torso-twist frame // (placeRot=(0, *externalPitchPtr, 0) -> matrix @workMatrix, the engine // euler->matrix conversion == the binary's euler->quat->matrix FUN_00409a00 + // FUN_0040ab44) then re-normalized. Per-type scaling: amount / multiplier * // response{trans,pitchRoll,yaw,vibration}; the Explosive case alone multiplies // by burstCount. Four kicks: directional impulse (trans), torque (pitchRoll), // a vibration shake along the FIXED member axis vibrationDirection@0x390 (up), // and the vertical impulse fed by the row's third field ("yaw"). // void Gyroscope::ApplyDamageResponse(const Damage &damage) { if (damage.damageAmount == 0.0f) // @4b298c (0.0f @0x4b2d80) return; if (damage.damageType == Damage::CollisionDamageType) // @4b299e -- collisions never bounce here return; Scalar trans = 0.0f; // ebp-0x10 Scalar pitchRoll = 0.0f; // ebp-0x0C Scalar yaw = 0.0f; // ebp-0x08 Scalar vibration = 0.0f; // ebp-0x04 Vector3D dir; // ebp-0x1C if (Close_Enough(damage.damageForce, Vector3D(0.0f, 0.0f, 0.0f), 1e-4f)) // @4b29bd FUN_004084fc { // random horizontal: sign roll (>= 0.5 keeps +) then value roll, y=0 dir.x = (RandomUnit() >= 0.5f) ? RandomUnit() : -RandomUnit(); // @4b29d7 dir.z = (RandomUnit() >= 0.5f) ? RandomUnit() : -RandomUnit(); // @4b2a0d dir.y = 0.0f; // @4b2a43 } else { dir = damage.damageForce; // @4b2a4a FUN_00408440 } dir.Normalize(dir); // @4b2a62 FUN_004087f4 (unguarded in the binary too) // @4b2a67-4b2af5: yaw-only body frame from the torso twist; rotate the // WORLD hit direction into it (FUN_00408744 raw row-dot on entries[0..2]/ // [4..6]/[8..10] -- reproduced verbatim below) and re-normalize. The // placement state @0x28C/0x298/0x2A8 is written for member fidelity. placeRot = Vector3D(0.0f, *externalPitchPtr, 0.0f); // gyro+0x2A8..0x2B0 placePos = Vector3D(0.0f, 0.0f, 0.0f); // @4b2aa8 { AffineMatrix m; m = EulerAngles(Radian(placeRot.x), Radian(placeRot.y), Radian(placeRot.z)); for (int i = 0; i < 12; ++i) workMatrix[i] = m.entries[i]; // @4b2ac2 FUN_0040ab44 Vector3D tmp = dir; // ebp-0x28 @4b2ac7 dir.x = tmp.x*workMatrix[0] + tmp.y*workMatrix[1] + tmp.z*workMatrix[2]; // FUN_00408744 dir.y = tmp.x*workMatrix[4] + tmp.y*workMatrix[5] + tmp.z*workMatrix[6]; dir.z = tmp.x*workMatrix[8] + tmp.y*workMatrix[9] + tmp.z*workMatrix[10]; } dir.Normalize(dir); // @4b2aed // @4b2afd: per-type scaling (jump table @0x4b2b10; type > 4 leaves all // four at zero). Each term computed independently, matching the binary's // per-term fld/fdiv/fmul. switch (damage.damageType) { case Damage::BallisticDamageType: // @4b2b3d mult@0x330 resp@0x350 trans = damage.damageAmount / damageMultiplier[1] * damageResponse[1].trans; pitchRoll = damage.damageAmount / damageMultiplier[1] * damageResponse[1].pitchRoll; yaw = damage.damageAmount / damageMultiplier[1] * damageResponse[1].yaw; vibration = damage.damageAmount / damageMultiplier[1] * damageResponse[1].vibration; break; case Damage::ExplosiveDamageType: // @4b2b8a the ONLY case reading burstCount trans = (Scalar)damage.burstCount * damage.damageAmount / damageMultiplier[2] * damageResponse[2].trans; pitchRoll = (Scalar)damage.burstCount * damage.damageAmount / damageMultiplier[2] * damageResponse[2].pitchRoll; yaw = (Scalar)damage.burstCount * damage.damageAmount / damageMultiplier[2] * damageResponse[2].yaw; vibration = (Scalar)damage.burstCount * damage.damageAmount / damageMultiplier[2] * damageResponse[2].vibration; break; case Damage::LaserDamageType: // @4b2be3 mult@0x338 resp@0x370 trans = damage.damageAmount / damageMultiplier[3] * damageResponse[3].trans; pitchRoll = damage.damageAmount / damageMultiplier[3] * damageResponse[3].pitchRoll; yaw = damage.damageAmount / damageMultiplier[3] * damageResponse[3].yaw; vibration = damage.damageAmount / damageMultiplier[3] * damageResponse[3].vibration; break; case Damage::EnergyDamageType: // @4b2c2d mult@0x33C resp@0x380 trans = damage.damageAmount / damageMultiplier[4] * damageResponse[4].trans; pitchRoll = damage.damageAmount / damageMultiplier[4] * damageResponse[4].pitchRoll; yaw = damage.damageAmount / damageMultiplier[4] * damageResponse[4].yaw; vibration = damage.damageAmount / damageMultiplier[4] * damageResponse[4].vibration; break; default: break; } // @4b2c75-4b2ce2: upper-clamp each at 1.3f; NO lower clamp. if (trans > 1.3f) trans = 1.3f; if (pitchRoll > 1.3f) pitchRoll = 1.3f; if (yaw > 1.3f) yaw = 1.3f; if (vibration > 1.3f) vibration = 1.3f; if (getenv("BT_GYRO_LOG")) DEBUG_STREAM << "[gyro-dmg] type=" << (int)damage.damageType << " amt=" << (float)damage.damageAmount << " burst=" << (int)damage.burstCount << " dir=(" << (float)dir.x << "," << (float)dir.y << "," << (float)dir.z << ") t/p/y/v=" << (float)trans << "/" << (float)pitchRoll << "/" << (float)yaw << "/" << (float)vibration << "\n" << std::flush; if (getenv("BT_GYRO_TRACE")) DEBUG_STREAM << "[gtrace] g=" << (void *)this << " HIT t=" << (float)trans << " p=" << (float)pitchRoll << " y=" << (float)yaw << " v=" << (float)vibration << " dir=" << (float)dir.x << " " << (float)dir.y << " " << (float)dir.z << "\n" << std::flush; ApplyDamageImpulse (dir.x, dir.y, dir.z, trans); // @4b2d00 the directional knock ApplyDamageTorque (dir.x, dir.y, dir.z, pitchRoll); // @4b2d23 ApplyDamageImpulse (vibrationDirection.x, vibrationDirection.y, vibrationDirection.z, vibration); // @4b2d4b the up-shake ApplyVerticalImpulse(dir.x, dir.y, dir.z, yaw); // @4b2d6e if (getenv("BT_GYRO_LOG")) DEBUG_STREAM << "[gyro-dmg] post-kick exag=" << (float)exageration << " eyeForce=(" << (float)eyeForce.x << "," << (float)eyeForce.y << "," << (float)eyeForce.z << ") bodyForce=(" << (float)bodyForce.x << "," << (float)bodyForce.y << "," << (float)bodyForce.z << ")\n" << std::flush; } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // CreateStreamedSubsystem -- Gyroscope // // @004b3eb4 [CONFIDENT for the field list / classID / size; the body below is // the heat.cpp-style linearisation of the deeply-nested original]. Chains to // PowerWatcher::CreateStreamedSubsystem (FUN_004b198c), stamps the resource, and // reads every mandatory field in the exact order the binary does. On pass 1 // each Scalar is primed to -1.0f (Unset); a field is "missing" if the read fails // AND the slot still equals -1.0f. The angular Rotation*Spring* fields are // multiplied by PI/180 after reading. The EyeJoint / MechJoint names are // resolved against the model's "skeleton" file. // int Gyroscope::CreateStreamedSubsystem( NotationFile *model_file, const char *model_name, const char *subsystem_name, SubsystemResource *r, NotationFile *subsystem_file, const ResourceDirectories *directories, int passes ) { if ( !PowerWatcher::CreateStreamedSubsystem( // FUN_004b198c model_file, model_name, subsystem_name, r, subsystem_file, directories, passes ) ) { return False; } r->subsystemModelSize = sizeof(*r); // resource +0x24 = 0x21C r->classID = RegisteredClass::GyroscopeClassID; // resource +0x20 = 0x0BC4 #define REQ_SCALAR(NAME, FIELD) \ if (!model_file->GetEntry(subsystem_name, NAME, &r->FIELD) \ && r->FIELD == Unset) \ { DebugSink << subsystem_name << " missing " << NAME << "!"; return False; } REQ_SCALAR("PercentageOnNormal", percentageOnNormal) // +0x108 REQ_SCALAR("PercentageOnDestruction", percentageOnDestruction) // +0x10C REQ_SCALAR("PercentageOnDegradation", percentageOnDegradation) // +0x110 REQ_SCALAR("PercentageOnFailure", percentageOnFailure) // +0x114 REQ_SCALAR("RotationPerSecond", rotationPerSecond) // +0x104 REQ_SCALAR("Exageration", exageration) // +0xF8 REQ_SCALAR("MaxAnimationNoise", maxAnimationNoise) // +0xFC REQ_SCALAR("MinAnimationNoise", minAnimationNoise) // +0x100 REQ_SCALAR("SpringConstantX", springConstant.x) REQ_SCALAR("SpringConstantY", springConstant.y) REQ_SCALAR("SpringConstantZ", springConstant.z) REQ_SCALAR("DampingConstantX", dampingConstant.x) REQ_SCALAR("DampingConstantY", dampingConstant.y) REQ_SCALAR("DampingConstantZ", dampingConstant.z) REQ_SCALAR("NegSpringX", negSpring.x) REQ_SCALAR("NegSpringY", negSpring.y) REQ_SCALAR("NegSpringZ", negSpring.z) REQ_SCALAR("PosSpringX", posSpring.x) REQ_SCALAR("PosSpringY", posSpring.y) REQ_SCALAR("PosSpringZ", posSpring.z) // rotationSpringConstant / rotationDampingConstant are Vector3D ordered // Roll(.x,+0x148) / Yaw(.y,+0x14C) / Pitch(.z,+0x150): REQ_SCALAR("RotationSpringConstantPitch", rotationSpringConstant.z) // +0x150 REQ_SCALAR("RotationSpringConstantYaw", rotationSpringConstant.y) // +0x14C REQ_SCALAR("RotationSpringConstantRoll", rotationSpringConstant.x) // +0x148 REQ_SCALAR("RotationDampingConstantPitch", rotationDampingConstant.z) // +0x15C REQ_SCALAR("RotationDampingConstantYaw", rotationDampingConstant.y) // +0x158 REQ_SCALAR("RotationDampingConstantRoll", rotationDampingConstant.x) // +0x154 // the eight Rotation{Neg,Pos}Spring{Pitch,Yaw,Roll} fields read into a temp // (default -1.0f) and, when present, are stored * DegToRad: #define REQ_ANGLE(NAME, FIELD) \ { Scalar a = Unset; \ if (!model_file->GetEntry(subsystem_name, NAME, &a) && r->FIELD == Unset) \ { DebugSink << subsystem_name << " missing " << NAME << "!"; return False; } \ if (a != Unset) r->FIELD = a * DegToRad; } // rotationPos/NegSpring are Vector3D ordered Pitch(.x) / Yaw(.y) / Roll(.z): REQ_ANGLE("RotationNegSpringPitch", rotationNegSpring.x) // +0x16C REQ_ANGLE("RotationNegSpringYaw", rotationNegSpring.y) // +0x170 REQ_ANGLE("RotationNegSpringRoll", rotationNegSpring.z) // +0x174 REQ_ANGLE("RotationPosSpringPitch", rotationPosSpring.x) // +0x160 REQ_ANGLE("RotationPosSpringYaw", rotationPosSpring.y) // +0x164 REQ_ANGLE("RotationPosSpringRoll", rotationPosSpring.z) // +0x168 REQ_SCALAR("CollisionDamageMultiplier", collisionDamageMultiplier) // +0x1B8 REQ_SCALAR("BallisticDamageMultiplier", ballisticDamageMultiplier) // +0x1BC REQ_SCALAR("ExplosiveDamageMultiplier", explosiveDamageMultiplier) // +0x1C0 REQ_SCALAR("LaserDamageMultiplier", laserDamageMultiplier) // +0x1C4 REQ_SCALAR("EnergyDamageMultiplier", energyDamageMultiplier) // +0x1C8 // each damage type then reads Trans / PitchRoll / Yaw / Vibration: REQ_SCALAR("CollisionDamageTrans", collisionDamageResponse.trans) REQ_SCALAR("CollisionDamagePitchRoll", collisionDamageResponse.pitchRoll) REQ_SCALAR("CollisionDamageYaw", collisionDamageResponse.yaw) REQ_SCALAR("CollisionDamageVibration", collisionDamageResponse.vibration) REQ_SCALAR("BallisticDamageTrans", ballisticDamageResponse.trans) REQ_SCALAR("BallisticDamagePitchRoll", ballisticDamageResponse.pitchRoll) REQ_SCALAR("BallisticDamageYaw", ballisticDamageResponse.yaw) REQ_SCALAR("BallisticDamageVibration", ballisticDamageResponse.vibration) REQ_SCALAR("ExplosiveDamageTrans", explosiveDamageResponse.trans) REQ_SCALAR("ExplosiveDamagePitchRoll", explosiveDamageResponse.pitchRoll) REQ_SCALAR("ExplosiveDamageYaw", explosiveDamageResponse.yaw) REQ_SCALAR("ExplosiveDamageVibration", explosiveDamageResponse.vibration) REQ_SCALAR("LaserDamageTrans", laserDamageResponse.trans) REQ_SCALAR("LaserDamagePitchRoll", laserDamageResponse.pitchRoll) REQ_SCALAR("LaserDamageYaw", laserDamageResponse.yaw) REQ_SCALAR("LaserDamageVibration", laserDamageResponse.vibration) REQ_SCALAR("EnergyDamageTrans", energyDamageResponse.trans) REQ_SCALAR("EnergyDamagePitchRoll", energyDamageResponse.pitchRoll) REQ_SCALAR("EnergyDamageYaw", energyDamageResponse.yaw) REQ_SCALAR("EnergyDamageVibration", energyDamageResponse.vibration) // optional joint names default to "Unspecified"; if specified they must // resolve in the skeleton file (else "missing !"). const char *eye = "Unspecified"; model_file->GetEntry(subsystem_name, "EyeJoint", &eye); if (strcmp(eye, "Unspecified") != 0) strcpy(r->eyeJoint, eye); // -> +0x178 const char *mech = "Unspecified"; model_file->GetEntry(subsystem_name, "MechJoint", &mech); if (strcmp(mech, "Unspecified") != 0) strcpy(r->mechJoint, mech); // -> +0x198 // load the model's "skeleton" file and verify both joints exist in it. // CROSS-FAMILY: LoadSkeleton/Skeleton::FindNode are stubbed here (see top of // file); the real skeleton lookup belongs to the Skeleton/Mech family. const char *skeleton = 0; if (!model_file->GetEntry("video", "skeleton", &skeleton)) { DebugSink << model_name << " is missing skeleton file!"; return -1; } GyroSkeleton *skl = LoadSkeleton(directories, skeleton); // FUN_004064fc / FUN_00403e84 if (!skl->FindNode(r->eyeJoint)) // FUN_00403f84 { DebugSink << r->eyeJoint << " not found in " << skeleton; return -1; } if (!skl->FindNode(r->mechJoint)) { DebugSink << r->mechJoint << " not found in " << skeleton; return -1; } #undef REQ_SCALAR #undef REQ_ANGLE Check_Fpu(); return True; } //===========================================================================// // WAVE 5 factory bridge -- Gyroscope (factory case 0xBC4). Constructs the real // Gyroscope (ctor @004b3778) in the binary's 0x3D0 alloc. The Watcher base is // re-based (exageration@0x1D8, locked above) so it shares only MechSubsystem with // the heat leaves -- no heat-roster interaction (Gyroscope is not a HeatSink). //===========================================================================// Subsystem *CreateGyroSubsystem(Mech *owner, int id, void *seg) { return (Subsystem *) new (Memory::Allocate(0x3D0)) Gyroscope(owner, id, (Gyroscope::SubsystemResource *)seg, Gyroscope::DefaultData); } //===========================================================================// // Per-frame joint-write dispatch bridge (task #56, byte-verified): the binary // Mech master performance FUN_004a9b5c calls WriteEyeJoint @0x4aaf74 / // WriteMechJoint @0x4aaf83 each frame AFTER the animation pass, preceded by // gyro->swayBias = mech sway accumulator, gated !deathAnimationLatched (both) // and legAnimationState != 0 (eye writer only; WriteEyeJoint is MULTIPLICATIVE // on the joint's animated rotation, so it must never run on a joint the // animation pass didn't just re-write). mech4.cpp calls this bridge from the // same spot in the port's master performance (complete-type TU rule). //===========================================================================// void GyroFrameJointWrite(Subsystem *gyro, Scalar sway_bias, int leg_animation_state, int death_animation_latched) { if (gyro == 0) return; Gyroscope *g = static_cast(gyro); g->SetSwayBias(sway_bias); // gyro+0x3A8 = mech+0x3F0 (bytes 0x4aaf18-3c) if (death_animation_latched != 0) // mech+0x650 gate return; if (leg_animation_state != 0) // mech+0x3B0 gate (+ mech+0x57C, always 0) g->WriteEyeJoint(); // @0x4aaf74 g->WriteMechJoint(); // @0x4aaf83 } //===========================================================================// // Post-stream gyro<->torso link (task #56, binary bt_mech tail): the binary // re-points gyro+0x258 to &torso pitch (torso+0x1D8) after subsystem streaming; // the damage-response path reads it. (Replaces the old SubProxy::linkTarget // landmine, which wrote through an engine base field at gyro+4.) //===========================================================================// void GyroBindExternalPitch(Subsystem *gyro, Scalar *pitch) { if (gyro != 0) static_cast(gyro)->BindExternalPitch(pitch); } //===========================================================================// // Damage->gyro bridges (task #56 fan-out; binary call sites: the take-damage // hub @0x4a02fb, the performance crunch/jolt/rumble kicks @0x4aa254/0x4aa288/ // 0x4aa342/0x4aa81e/0x4aa86c, and the firing-recoil kick @0x4bc194). // Complete-type-TU pattern: callers hold only Subsystem*. //===========================================================================// void GyroApplyDamage(Subsystem *gyro, const Damage &damage) { if (gyro != 0) static_cast(gyro)->ApplyDamageResponse(damage); } void GyroApplyDamageImpulse(Subsystem *gyro, Scalar x, Scalar y, Scalar z, Scalar magnitude) { if (gyro != 0) static_cast(gyro)->ApplyDamageImpulse(x, y, z, magnitude); } void GyroApplyDamageTorque(Subsystem *gyro, Scalar x, Scalar y, Scalar z, Scalar magnitude) { if (gyro != 0) static_cast(gyro)->ApplyDamageTorque(x, y, z, magnitude); }