//===========================================================================// // File: gyro.hpp // // 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 for this class; every declaration below // is inferred from the decompiled bodies and -- crucially -- from the // resource field-name string pool parsed by Gyroscope::CreateStreamedSubsystem // (@004b3eb4). Those "missing !" diagnostics are GROUND TRUTH for the // resource layout; the in-object member names are best-effort (flagged). // // Recovered binary facts (see gyro.cpp for per-method @ADDR evidence): // - Gyroscope IS-A PowerWatcher (powersub.hpp); the ctor @004b3778 chains to // the PowerWatcher base ctor (FUN_004b18a4) passing &Gyroscope::DefaultData // (&DAT_0050fdb0). The full chain is therefore // Gyroscope : PowerWatcher : HeatWatcher : MechSubsystem : Subsystem // (NOT HeatableSubsystem -- the Watcher branch, re-based; see CLAUDE.md §10d) // - vtable @00510abc ctor @004b3778 dtor @004b3e88 // - DefaultData @0050fdb0 // - RegisteredClass classID 0x0BC4 (resource +0x20), streamed model size // 0x21C (resource +0x24) -- both stamped by CreateStreamedSubsystem. // // WHAT THE GYROSCOPE DOES (from the recovered simulation @004b275c + helpers): // It is the mech's balance/orientation model. It carries a damage-dependent // idle "sway" (the PercentageOn{Normal,Degradation,Destruction,Failure} // targets, ramped at RotationPerSecond and clamped to the [MinAnimationNoise, // MaxAnimationNoise] band), a full pitch/yaw/roll spring-damper that tips the // body when off balance, a separate spring-damper that drives the cockpit // "eye" joint, and a set of per-damage-type response curves (Trans / PitchRoll // / Yaw / Vibration) that perturb the gyro when the mech is hit. The result // is written into two skeleton joints (the "EyeJoint" and the "MechJoint"). // When the watched power source is dead (PowerWatcher), the heat model is off, // or the subsystem is in the Failure heat state, it snaps to the destruction // target instead of the normal one (-> visible tip / collapse). // #if !defined(GYRO_HPP) # define GYRO_HPP #if !defined(POWERSUB_HPP) # include "powersub.hpp" #endif //##################### Forward Class Declarations ####################### class Mech; class Joint; // engine skeleton node (JOINT.h); eye/mech joint targets class Damage; // engine damage record (DAMAGE.h); ApplyDamageResponse input //########################################################################### //################# Gyroscope Model Resource ########################## //########################################################################### // // Extends the PowerWatcher resource (which adds MinVoltagePercent @+0xF0). // Field offsets and names are exactly those read by // Gyroscope::CreateStreamedSubsystem @004b3eb4. Every Scalar below is primed // to -1.0f on pass 1 and is mandatory (the parser fails with // " missing !" if absent). The Rotation*Spring* angular // fields are additionally multiplied by PI/180 (deg->rad, _DAT_004b5b24). // // Per-damage-type gyro response curve (4 named Scalars, +0x10 each). The // recon first modelled this as a Vector4D; the engine Vector4D is incomplete // here and the fields are addressed by name (Trans/PitchRoll/Yaw/Vibration), // so a dedicated POD captures the layout exactly. struct Gyroscope__DamageResponse { Scalar trans; // +0x0 Scalar pitchRoll; // +0x4 Scalar yaw; // +0x8 Scalar vibration; // +0xC }; struct Gyroscope__SubsystemResource: public PowerWatcher::SubsystemResource // ends 0xF4 after RESOURCE_AUDIT.md fix B { // +0xF4: a 4-byte record slot between the PowerWatcher base and "Exageration". // The Gyroscope parser/ctor do not read it (verified: ctor reads 0xF8/0xFC/0x100/ // 0x104), but the record reserves it -- omitting it slid every field 4 bytes low // (exageration -> 0xF4 instead of 0xF8). Keep the slot to match the binary. Scalar _reserved_f4; // +0xF4 (unread record slot) Scalar exageration; // +0xF8 "Exageration" Scalar maxAnimationNoise; // +0xFC "MaxAnimationNoise" Scalar minAnimationNoise; // +0x100 "MinAnimationNoise" Scalar rotationPerSecond; // +0x104 "RotationPerSecond" Scalar percentageOnNormal; // +0x108 "PercentageOnNormal" Scalar percentageOnDestruction; // +0x10C "PercentageOnDestruction" Scalar percentageOnDegradation; // +0x110 "PercentageOnDegradation" Scalar percentageOnFailure; // +0x114 "PercentageOnFailure" Vector3D springConstant; // +0x118 "SpringConstantX/Y/Z" Vector3D dampingConstant; // +0x124 "DampingConstantX/Y/Z" Vector3D posSpring; // +0x130 "PosSpringX/Y/Z" Vector3D negSpring; // +0x13C "NegSpringX/Y/Z" // angular spring/damping, ordered Roll(+0x148), Yaw(+0x14C), Pitch(+0x150) Vector3D rotationSpringConstant; // +0x148 "RotationSpringConstantRoll/Yaw/Pitch" Vector3D rotationDampingConstant;// +0x154 "RotationDampingConstantRoll/Yaw/Pitch" // rotation limits, ordered Pitch(.x) / Yaw(.y) / Roll(.z), each *PI/180. // RECON NOTE: originally guessed Point4D (quaternion-ish); the parser only // reads three named components (Pitch/Yaw/Roll) so a Vector3D is exact. Vector3D rotationPosSpring; // +0x160 "RotationPosSpringPitch/Yaw/Roll" Vector3D rotationNegSpring; // +0x16C "RotationNegSpringPitch/Yaw/Roll" char eyeJoint[32]; // +0x178 "EyeJoint" (skeleton node name) char mechJoint[32]; // +0x198 "MechJoint" (skeleton node name) // per-damage-type response: scalar multiplier + {Trans,PitchRoll,Yaw,Vibration} Scalar collisionDamageMultiplier; // +0x1B8 "CollisionDamageMultiplier" Scalar ballisticDamageMultiplier; // +0x1BC "BallisticDamageMultiplier" Scalar explosiveDamageMultiplier; // +0x1C0 "ExplosiveDamageMultiplier" Scalar laserDamageMultiplier; // +0x1C4 "LaserDamageMultiplier" Scalar energyDamageMultiplier; // +0x1C8 "EnergyDamageMultiplier" Gyroscope__DamageResponse collisionDamageResponse;// +0x1CC Trans/PitchRoll/Yaw/Vibration Gyroscope__DamageResponse ballisticDamageResponse;// +0x1DC Gyroscope__DamageResponse explosiveDamageResponse;// +0x1EC Gyroscope__DamageResponse laserDamageResponse; // +0x1FC Gyroscope__DamageResponse energyDamageResponse; // +0x20C }; // sizeof == 0x21C (model size stamp) static_assert(offsetof(Gyroscope__SubsystemResource, exageration) == 0xF8, "Gyroscope exageration must be at 0xF8"); static_assert(offsetof(Gyroscope__SubsystemResource, energyDamageResponse)== 0x20C, "Gyroscope energyDamageResponse must be at 0x20C"); static_assert(sizeof(Gyroscope__SubsystemResource) == 0x21C, "Gyroscope record must be 0x21C"); //########################################################################### //############################# Gyroscope ############################### //########################################################################### class Gyroscope: public PowerWatcher { friend struct GyroLayoutCheck; // compile-time offset lock (gyro.cpp) //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Shared Data Support (DefaultData @0050fdb0, vtable @00510abc) // public: static Derivation ClassDerivations; static Receiver::MessageHandlerSet MessageHandlers; static AttributeIndexSet AttributeIndex; static SharedData DefaultData; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // BT segment / base-state compatibility shims. // // CROSS-FAMILY: these accessors and segment-flag constants logically belong // on the shared bases (Mech/Subsystem::GetSegmentFlags, HeatSink::HeatStateLevel // / HeatModelOff, PoweredSubsystem::ElectricalStateLevel). Gyroscope derives // from PowerWatcher (-> Subsystem), which in these headers exposes none of // them, so they are backed locally here so this module compiles; the backing // fields are written by the (cross-family) base simulation once the real // accessors land. See report "CROSS-FAMILY NEEDS". // #if !defined(BT_SEGMENT_FLAG_CONSTS) # define BT_SEGMENT_FLAG_CONSTS enum { SegmentCopyMask = 0x0C, // (flags & 0xC): 0 == master, 4 == damaged copy SegmentLiveFlag = 0x01, MasterHeatSinkFlag = 0x0100 }; #endif public: // BASE-CHAIN RE-BASE: the 4 shim BACKING FIELDS are removed (they over-sized // the object + duplicated engine-base state); these accessors now read the // REAL inherited base state, exactly like Torso (see CLAUDE.md §10d). // (GetSegmentFlags removed -- unused; the ctor reads owner->simulationFlags.) HeatSink::HeatState HeatStateLevel() const { return (HeatSink::HeatState)heatAlarm.GetLevel(); } // inherited HeatWatcher Logical HeatModelOff() const { return simulationState == 1; /* Destroyed */ } // inherited MechSubsystem PoweredSubsystem::ElectricalState ElectricalStateLevel() const { return (PoweredSubsystem::ElectricalState)watchdogAlarm.GetLevel(); } // inherited PowerWatcher //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Test Class Support // public: static Logical TestClass(Mech&); Logical TestInstance() const; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Simulation Support // public: typedef void (Gyroscope::*Performance)(Scalar time_slice); void SetPerformance(Performance performance) { Check(this); activePerformance = (Simulation::Performance)performance; } // @004b275c -- the registered Performance (PTR @0050fe08). Advances the // idle sway toward the damage-state target then runs the two integrators. void GyroscopeSimulation(Scalar time_slice); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Damage / impulse hooks (called by the Mech damage path to kick the gyro) // public: void ApplyDamageImpulse( // @004b2d8c Scalar x, Scalar y, Scalar z, Scalar magnitude); void ApplyDamageTorque( // @004b2de4 Scalar x, Scalar y, Scalar z, Scalar magnitude); void ApplyVerticalImpulse( // @004b2e50 Scalar pitch, Scalar /*y*/, Scalar /*z*/, Scalar magnitude); // @004b2980 -- the damage->gyro fan-out (task #56): normalize the hit // direction (random horizontal if ~zero), rotate into the torso-twist // frame, scale by the per-damage-type multiplier/response curves, clamp // at 1.3, fire the four Apply* kicks. The binary passes the whole // Damage BY VALUE (12 dwords, caller add esp,0x34) but never mutates it // and never reads surfaceNormal/impactPoint -- const-ref is byte-equal. void ApplyDamageResponse(const Damage &damage); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // 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) // public: Logical HandleDeathMessage(Message &message); // slot 9, @004b2660 -> @004b179c void ResetToInitialState(); // slot 10, @004b2678 //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Construction and Destruction // public: typedef Gyroscope__SubsystemResource SubsystemResource; Gyroscope( // @004b3778 Mech *owner, int subsystem_ID, SubsystemResource *subsystem_resource, SharedData &shared_data = DefaultData ); ~Gyroscope(); // @004b3e88 static int CreateStreamedSubsystem( // @004b3eb4 NotationFile *model_file, const char *model_name, const char *subsystem_name, SubsystemResource *subsystem_resource, NotationFile *subsystem_file, const ResourceDirectories *directories, int passes = 1 ); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Internal model helpers // protected: void IntegrateEyeJoint(Scalar time_slice); // @004b2ec0 void IntegrateBody(Scalar time_slice); // @004b30ec 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 // (this[n] == @0x(4n)). Names are best-effort from usage; the resource // provenance (where a field is copied straight out of the resource) is // noted. PowerWatcher/HeatWatcher/HeatableSubsystem base data occupies the // low offsets and is NOT redeclared here. // protected: // 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 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 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}) // @0x390 (this[0xE4..0xE6]) -- the VIBRATION AXIS: ctor init (0,1,0) = // straight up. Read as ONE Vector3D by ApplyDamageResponse @4b2d2b (the // second ApplyDamageImpulse = the vibration shake). [T1 re-disassembled] // (Was mis-split as scalars animationOffset/animationScale/animationPhase // -- the 0/1/0 "init pattern" was this unit vector all along.) Vector3D vibrationDirection; // @0x390 init (0.0f, 1.0f, 0.0f) Scalar maxAnimationNoise; // @0x39C resource +0xFC Scalar minAnimationNoise; // @0x3A0 resource +0x100 Scalar rotationPerSecond; // @0x3A4 resource +0x104 Scalar swayBias; // @0x3A8 (this[0xEA]) added to target, init 0 Scalar percentageOnNormal; // @0x3AC resource +0x108 Scalar percentageOnDestruction;// @0x3B0 resource +0x10C Scalar percentageOnDegradation;// @0x3B4 resource +0x110 Scalar percentageOnFailure; // @0x3B8 resource +0x114 Scalar swayAngle; // @0x3BC (this[0xEF]) current idle sway value Scalar swayVelocity; // @0x3C0 (this[0xF0]) noise ramp timer int swayActive; // @0x3C4 (this[0xF1]) noise state flag Joint *eyeJointNode; // @0x3C8 (this[0xF2]) resolved EyeJoint Joint *mechJointNode; // @0x3CC (this[0xF3]) resolved MechJoint }; #endif