//===========================================================================// // File: torso.hpp // // Project: BattleTech Brick: Entity Manager // // Contents: Torso subsystem -- torso twist (yaw) and elevation (pitch) aim // //---------------------------------------------------------------------------// // 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; declarations are inferred from the // decompiled bodies and from the resource field-name string pool parsed by // Torso::CreateStreamedSubsystem (@004b6fec) -- the "missing !" // diagnostics are GROUND TRUTH for the resource layout. In-object member names // are best-effort (flagged in torso.cpp). // // Recovered binary facts (see torso.cpp for per-method @ADDR evidence): // - Torso IS-A PowerWatcher (powersub.hpp); the ctor @004b6b0c chains to the // PowerWatcher base ctor (FUN_004b18a4) passing &Torso::DefaultData // (&DAT_00510af8). Full chain: // Torso : PowerWatcher : HeatWatcher : MechSubsystem : Subsystem // (NOT HeatableSubsystem -- the Watcher branch is separate from the // HeatSink/PoweredSubsystem heat leaves; they share only MechSubsystem.) // - vtable @0051103c ctor @004b6b0c dtor @004b6fc0 // - DefaultData @00510af8 // - RegisteredClass classID 0x0BC5 (resource +0x20), streamed model size // 0x158 (resource +0x24). // // WHAT THE TORSO DOES (from the recovered simulation @004b5cf0 + helpers): // It models torso twist (horizontal / yaw) and torso elevation (vertical / // pitch) aim. Player turn/elevate commands ramp an angular rate (with a // button-acceleration curve from a start value up to a cap), integrate into // horizontal/vertical aim angles that are clamped to per-axis software limits // (HorizontalLimitLeft/Right, VerticalLimitTop/Bottom), auto-recenter the // twist when released, and write the result into two skeleton joints // (TorsoHorizontalJoint and its TorsoHorizontalShadowJoint). All angular // resource values are stored in radians (deg * PI/180). When the watched // power source is dead, not Ready, or in the Failure heat state, the effective // rate is zeroed (the torso freezes). // #if !defined(TORSO_HPP) # define TORSO_HPP #if !defined(POWERSUB_HPP) # include "powersub.hpp" #endif //##################### Forward Class Declarations ####################### class Mech; class Joint; // engine skeleton node (JOINT.h); the twist target //########################################################################### //################# Torso Model Resource ############################## //########################################################################### // // Extends the PowerWatcher resource (MinVoltagePercent @+0xF0). Field offsets // and names are exactly those read by Torso::CreateStreamedSubsystem @004b6fec. // The angular Scalars are stored in radians (the ctor multiplies the parsed // degree values by PI/180); they are primed to -1.0f on pass 1 and mandatory. // struct Torso__SubsystemResource: public PowerWatcher::SubsystemResource { Scalar horizontalRotationPerSecond; // +0xF4 "HorizontalRotationPerSecond" Scalar verticalRotationPerSecond; // +0xF8 "VerticalRotationPerSecond" Scalar horizontalLimitRight; // +0xFC "HorizontalLimitRight" Scalar horizontalLimitLeft; // +0x100 "HorizontalLimitLeft" Scalar verticalLimitTop; // +0x104 "VerticalLimitTop" Scalar verticalLimitBottom; // +0x108 "VerticalLimitBottom" char torsoHorizontalJoint[32]; // +0x10C "TorsoHorizontalJoint" char torsoHorizontalShadowJoint[32]; // +0x12C "TorsoHorizontalShadowJoint" Logical torsoHorizontalEnabled; // +0x14C "TorsoHorizontalEnabled" (default True) Scalar buttonAccelerationPerSecond; // +0x150 "ButtonAccelerationPerSecond" Scalar buttonAccelerationStartValue; // +0x154 "ButtonAccelerationStartValue" }; // sizeof == 0x158 (model size stamp) static_assert(offsetof(Torso__SubsystemResource, horizontalRotationPerSecond) == 0xF4, "Torso horizontalRotationPerSecond must be at 0xF4"); static_assert(offsetof(Torso__SubsystemResource, buttonAccelerationStartValue)== 0x154, "Torso buttonAccelerationStartValue must be at 0x154"); static_assert(sizeof(Torso__SubsystemResource) == 0x158, "Torso record must be 0x158"); //########################################################################### //############################# Torso ################################### //########################################################################### class Torso: public PowerWatcher { friend struct TorsoLayoutCheck; // compile-time offset locks (torso.cpp STEP 3) //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Shared Data Support (DefaultData @00510af8, vtable @0051103c) // public: static Derivation ClassDerivations; static SharedData DefaultData; static Receiver::MessageHandlerSet MessageHandlers; static AttributeIndexSet AttributeIndex; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // BT segment / base-state / joint compatibility shims. // // CROSS-FAMILY: these accessors, the segment-flag constants, the joint // helpers and the PowerWatcher base-method forwards logically belong on the // shared bases (Mech::GetSegmentFlags / Mech::ResolveJoint, HeatSink:: // HeatStateLevel / HeatModelOff, PoweredSubsystem::ElectricalStateLevel, the // skeleton-node writer, the Simulation dirty bit + creation/current time // accessors, and PowerWatcher::ResetToInitialState / HandleDeathMessage / // UpdateWatch). Until those families expose them they are backed locally so // this module compiles; see report "CROSS-FAMILY NEEDS". // #if !defined(BT_SEGMENT_FLAG_CONSTS) # define BT_SEGMENT_FLAG_CONSTS enum { SegmentCopyMask = 0x0C, // (flags & 0xC): 0 == primary, 4 == damaged copy SegmentLiveFlag = 0x01, MasterHeatSinkFlag = 0x0100 }; #endif public: // BASE-CHAIN RE-BASE: the 7 shim BACKING FIELDS are removed so the object // matches the binary 0x280 layout (currentTwist@0x1D8); these accessors now // read the REAL inherited base state: // HeatStateLevel <- HeatWatcher::heatAlarm level (this+0x140) // ElectricalState <- PowerWatcher::watchdogAlarm level (this+0x198) // HeatModelOff <- MechSubsystem::simulationState==Destroyed (this+0x40) // Creation/Current <- engine Simulation::lastPerformance (this+0x14/0x10) // the net/copy timing is dormant in single-player; // equal reads give a 0 window == the shipped master path. // SetMovedFlag -> engine Simulation::ForceUpdate() dirty bit (this+0x18) // (GetSegmentFlags removed -- unused; the ctor reads owner->simulationFlags.) HeatSink::HeatState HeatStateLevel() const { return (HeatSink::HeatState)heatAlarm.GetLevel(); } Logical HeatModelOff() const { return simulationState == 1; /* Destroyed */ } PoweredSubsystem::ElectricalState ElectricalStateLevel() const{ return (PoweredSubsystem::ElectricalState)watchdogAlarm.GetLevel(); } LWord GetCreationTime() const { return (LWord)(long)lastPerformance; } LWord GetCurrentTime() const { return (LWord)(long)lastPerformance; } // Current horizontal (yaw) torso aim in radians (binary torso+0x1d8); the // damage-table PieSlice wheel reads this when RotateWithTorso is set. Scalar CurrentTwist() const { return currentTwist; } // Address of the twist scalar (task #56: the binary bt_mech stream tail // re-points gyro+0x258 to torso+0x1D8; the gyro damage-response reads it). Scalar* CurrentTwistAddr() { return ¤tTwist; } protected: // Resolve a named skeleton node to its live Joint* via the owning Mech // (the ctor @004b6b0c inlines Mech::ResolveJoint == FUN_00424b60). Defined // out-of-line in torso.cpp -- needs the complete Mech type. Joint* ResolveJoint(const char *joint_name); // Write one twist (yaw) scalar into a resolved skeleton node, preserving its // other DOF -- the @004b66b4 inner block (hinge -> SetRotation(Radian); // ball -> SetRotation(EulerAngles) with yaw <- twist). void PushTwist(Joint *node, Scalar value); // Simulation dirty / replicate bit -> real engine Simulation::ForceUpdate(). void SetMovedFlag() { ForceUpdate(); } // Local helper: "is value pinned at this software limit?". Logical AtLimit(Scalar value, Scalar limit) const { return fabsf(value - limit) <= 0.0001f; } // CROSS-FAMILY delegate shims (STEP-4): UpdateWatch now chains to the real // PowerWatcher per-tick watch body (@004b181c, task #57) -- this is what // raises watchdogAlarm to Ready and un-gates the twist rate. The other // two remain no-ops (no layout impact). void WatcherResetToInitialState() {} Logical WatcherHandleDeathMessage(Message & /*m*/) { return False; } void WatcherUpdateWatch() { UpdateWatch(); } // FUN_004b181c (body: powersub.cpp) //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Test Class Support // public: static Logical TestClass(Mech&); Logical TestInstance() const; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Command interface (set by MechControlsMapper each frame -- inputs @0x1F8) // public: // Command roles verified against the raw decomp @004b5cf0 (part_013.c // 4522-4571): @0x1F8/@0x1FC drive ELEVATION (0x1e4, clamp vertTop/Bottom); // @0x200/@0x204 drive TWIST (0x1d8, clamp horizLimitLeft/Right); @0x208 // recenters twist. (The earlier draft had elevation/twist swapped.) enum { ElevateUpCommand, // @0x1F8 elevation + (clamp verticalLimitTop) ElevateDownCommand, // @0x1FC elevation - (clamp verticalLimitBottom) TwistLeftCommand, // @0x200 twist + (clamp horizontalLimitLeft) TwistRightCommand, // @0x204 twist - (clamp horizontalLimitRight) CenterCommand // @0x208 recenter twist }; // Analog (RIO/stick) aim demand written by MechControlsMapper::Interpret- // Controls (@0x1F0 twist, @0x1F4 elevation); proportional, no button ramp. void SetAnalogTwistAxis(Scalar v) { analogTwistAxis = v; } void SetAnalogElevationAxis(Scalar v) { analogElevationAxis = v; } void CommandRecenter() { centerCommand = 1; } // @0x208 (Basic-mode re-center) void ClearRecenterCommand() { centerCommand = 0; } // button released (writer-owned state) Logical GetHorizontalEnabled() const { return horizontalEnabled; } // @0x250 (mapper free-aim gate @004afd10) // Reachable horizontal (yaw) half-arc the guns can be brought to bear by // twisting the torso: the wider of the two software limits (radians), or 0 // when horizontal twist is disabled (e.g. the Blackhawk, TorsoHorizontalEnabled=0). // This is the AUTHENTIC, data-driven weapon traverse -- weapons carry no arc // field; the torso mount is what lets a mech point its guns off dead-ahead. Scalar GetHorizontalReach() const { if (!horizontalEnabled) return 0.0f; Scalar l = fabsf(horizontalLimitLeft), r = fabsf(horizontalLimitRight); return (l > r) ? l : r; } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Simulation Support // public: typedef void (Torso::*Performance)(Scalar time_slice); void SetPerformance(Performance performance) { Check(this); activePerformance = (Simulation::Performance)performance; } void TorsoSimulation(Scalar time_slice); // @004b5cf0 (master, PTR @00510c10) void TorsoCopySimulation(Scalar time_slice); // @004b65f8 (damaged copy, PTR @00510c1c) //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Subsystem virtual overrides (slots on vtable @0051103c) // public: // Update-record pair (task #57 direction fix): the ENGINE's semantics // [T0 SIMULATE.cpp] are Write = PRODUCE the outgoing record (master), // Read = APPLY the incoming record (replicant). @004b6a78 (slot 6, // base chain 41bd34 = ReadUpdateRecord) CONSUMES record+0x10/14/18 -- // it is the READ side, previously mislabeled Write; the real WRITE // side is @004b6a1c (slot 7, base chain 41c500, 3 args), which Ghidra // missed -- recovered from raw disasm. With the old flip the master // never serialized twist and clobbered its own fields from the // uninitialized stream buffer (the replicant's 0xCDCDCDCD twist pin). void ReadUpdateRecord(UpdateRecord *message); // slot 6, @004b6a78 (apply) void WriteUpdateRecord(UpdateRecord *message, int update_model); // slot 7, @004b6a1c (serialize) Logical HandleDeathMessage(Message &message); // slot 9, @004b5be0 -> @004b179c void ResetToInitialState(); // slot 10, @004b5bf8 //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Construction and Destruction // public: typedef Torso__SubsystemResource SubsystemResource; Torso( // @004b6b0c Mech *owner, int subsystem_ID, SubsystemResource *subsystem_resource, SharedData &shared_data = DefaultData ); ~Torso(); // @004b6fc0 static int CreateStreamedSubsystem( // @004b6fec 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: Logical ComputeTargetTwist(); // @004b6510 (returns "still slewing") void WriteJoints(Scalar &verticalOut); // @004b66b4 void UpdateJoints(); // @004b67ec Logical Recenter(Scalar time_slice); // @004b6918 (returns "still off-centre") //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Local data. Byte offsets are those observed in the shipped object // (this[n] == @0x(4n)); names are best-effort. Angular values are radians. // PowerWatcher/HeatWatcher/MechSubsystem base data occupies the low // offsets and is NOT redeclared here. // protected: Scalar currentTwist; // @0x1D8 current horizontal (yaw) aim Scalar horizontalLimitRight; // @0x1DC resource +0xFC (rad) Scalar horizontalLimitLeft; // @0x1E0 resource +0x100 (rad) Scalar currentElevation; // @0x1E4 current vertical (pitch) aim Scalar twistVelocity; // @0x1E8 d(currentTwist)/dt Scalar elevationVelocity; // @0x1EC d(currentElevation)/dt Scalar analogTwistAxis; // @0x1F0 RIO/stick twist demand Scalar analogElevationAxis; // @0x1F4 RIO/stick elevation demand int elevateUpCommand; // @0x1F8 digital elevation-up (>0 = active) int elevateDownCommand; // @0x1FC digital elevation-down int twistLeftCommand; // @0x200 digital twist-left int twistRightCommand; // @0x204 digital twist-right int centerCommand; // @0x208 digital recenter int statusFlags; // @0x20C 0/1/2 (limit/active state) Scalar buttonAccelerationPerSecond;// @0x210 resource +0x150 Scalar buttonAccelerationStart; // @0x214 resource +0x154 Scalar targetTwist; // @0x218 ComputeTargetTwist output Scalar twistAtUpdate; // @0x21C twist snapshot at last net update Scalar verticalLimitTop; // @0x220 resource +0x104 (rad) Scalar verticalLimitBottom; // @0x224 resource +0x108 (rad) Scalar elevationCenter; // @0x228 derived limit copy Scalar elevationHalfBottom; // @0x22C = verticalLimitBottom * 0.5f Scalar twistCenterHigh; // @0x230 derived limit copy Scalar twistCenterLow; // @0x234 derived limit copy Scalar twistRate; // @0x238 net-synced angular rate Scalar baseTwistRate; // @0x23C resource +0xF4 (rad/s) Scalar baseElevationRate; // @0x240 resource +0xF8 (rad/s) Scalar effectiveTwistRate; // @0x244 baseTwistRate, gated by power/heat Scalar effectiveElevationRate; // @0x248 baseElevationRate, gated by power/heat int isDamagedCopy; // @0x24C 0 = live master, 1 = copy Logical horizontalEnabled; // @0x250 resource +0x14C LWord lastUpdateTime; // @0x254 ms timestamp of last command sample int hitElevTop; // @0x258 elevation-top limit latch (vs verticalLimitTop) int hitElevBottom; // @0x25C elevation-bottom limit latch (vs verticalLimitBottom) int hitTwistLeft; // @0x260 twist-left limit latch (vs horizontalLimitLeft) int hitTwistRight; // @0x264 twist-right limit latch (vs horizontalLimitRight) int buttonRampActive; // @0x268 Scalar buttonRamp; // @0x26C current button-accel multiplier int _reserved0x270; // @0x270 binary reserves a 4-byte slot here // (cleared by ResetToInitialState @004b5bf8); // present so recenterActive lands at 0x274. int recenterActive; // @0x274 Joint *horizontalJointNode; // @0x278 resolved TorsoHorizontalJoint Joint *horizontalShadowJointNode; // @0x27C resolved TorsoHorizontalShadowJoint }; // sizeof == 0x280 (own block 0x1D8..0x280); locked by static_asserts below. // // BASE-CHAIN RE-BASE: the 7 CROSS-FAMILY shim backing fields that used to sit // here (segmentFlags/heatModelOff/heatStateLevel/electricalState/creationTime/ // currentTime/movedFlag) are DELETED -- they over-sized the object by 28 bytes // and duplicated engine-base state. Their accessors now read the real inherited // members (HeatWatcher::heatAlarm, PowerWatcher::watchdogAlarm, MechSubsystem:: // simulationState, engine Simulation::lastPerformance/ForceUpdate). See above. // #endif