//===========================================================================// // File: torso.cpp // // 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; see torso.hpp. Each non-trivial method // cites the originating @ADDR. Confidence flags: [CONFIDENT] / [BEST-EFFORT] // / [EXCLUDED] as in gyro.cpp. // // Hex constants converted to decimal (from section_dump.txt): // _DAT_004b6500 = 0.0f _DAT_004b6504 = 0.0001f (zero-vel epsilon) // _DAT_004b6508 = 0.02f _DAT_004b650c = 0.05f (settle tolerances) // _DAT_004b64f8 = 0.5f _DAT_004b64fc = 1.0f (ramp cap) // _DAT_004b6a14 = 0.0f _DAT_004b6a18 = 0.0001f (recenter epsilon) // _DAT_004b6b08 = 10.0f (min slew-window, ms->s) // _DAT_004b6fb8 = 0.0174532925f (PI/180, deg->rad) _DAT_004b6fbc = 0.5f // _DAT_004b76c4 = -1.0f (resource "unset" sentinel) // DAT_0052140c = milliseconds-per-second tick scale (runtime-initialised; // reads 0 in the static image -- used as (now-t0)/scale). // // Helper-function name mapping: // FUN_004b18a4 PowerWatcher base constructor // FUN_004b198c PowerWatcher::CreateStreamedSubsystem // FUN_004b1804 PowerWatcher::ResetToInitialState (slot 10) // FUN_004b179c PowerWatcher slot-9 death/voltage handler // FUN_004b181c PowerWatcher per-frame watch update // FUN_0041bd34 Subsystem::WriteUpdateRecord (base) // FUN_00414b60 Clock::Now() (ms) // FUN_004081e0 Scalar lerp(dst,a,b,t) // FUN_0041cfa0/0041d020/0041d0a8 skeleton-node get/set transform // FUN_00404118 NotationFile::ReadScalar FUN_00404088 ReadString // FUN_004d4b58 stricmp FUN_004dcd00 fabsf // #include #pragma hdrstop #if !defined(TORSO_HPP) # include #endif #if !defined(MECH_HPP) # include // complete Mech -- owner->ResolveJoint #endif #include // Joint, JointSubsystem (fwd shim) #include // EulerAngles, Radian, Hinge (fwd shim) #if !defined(APP_HPP) # include #endif #if !defined(TESTBT_HPP) # include #endif static const Scalar Zero = 0.0f; // _DAT_004b6500 / _DAT_004b6a14 static const Scalar VelEps = 0.0001f; // _DAT_004b6504 / _DAT_004b6a18 static const Scalar TwistEps = 0.02f; // _DAT_004b6508 static const Scalar SettleEps = 0.05f; // _DAT_004b650c static const Scalar RampCap = 1.0f; // _DAT_004b64fc static const Scalar MinSlewMs = 10.0f; // _DAT_004b6b08 static const Scalar DegToRad = 0.0174532925f; // _DAT_004b6fb8 static const Scalar Unset = -1.0f; // _DAT_004b76c4 // DAT_0052140c -- the runtime "ticks-per-second" scale (reads 0 in the static // image; the engine initialises it from the system clock). TODO: confirm the // real value once the clock-init is recovered; 1000.0f (ms->s) is consistent // with the MinSlewMs window above. static const Scalar MsPerSecond = 1000.0f; // DAT_0052140c // // CROSS-FAMILY compile shims for the streamed-subsystem joint validation: the // engine has no `Skeleton` C++ type with FindNode (only SkeletonClassID streams // in VDATA.h), so the real joint-existence check could not be recovered. These // let CreateStreamedSubsystem compile; they conservatively accept any joint. // namespace { struct ReconSkeleton { Logical FindNode(const char * /*node_name*/) const { return True; } }; static ReconSkeleton g_reconSkeleton; inline ReconSkeleton* LoadSkeleton(const ResourceDirectories * /*dirs*/, const char * /*name*/) { return &g_reconSkeleton; } // The Torso update record appends three trailing Scalars after the base // Simulation::UpdateRecord header (recovered offsets +0x10/+0x14/+0x18). inline Scalar RecordField(Simulation::UpdateRecord *record, int byte_offset) { return *(const Scalar*)((const char*)record + byte_offset); } } //########################################################################### // BASE-CHAIN RE-BASE -- compile-time layout locks (STEP 3). // // The Torso is read at RAW absolute offsets externally (the gyro cross-link // stores (char*)sinkSourceSubsystem+0x1D8 == currentTwist, mech.cpp:740; the // damage-slice, HUD ctor and radar read the same +0x1D8; MechControlsMapper // reads +0x1F0/+0x1F4/+0x250/+0x274). These asserts fail the BUILD if the // re-based layout ever drifts, long before any runtime mis-read. //########################################################################### // A friend of Torso so it can offsetof() the protected own-block fields. struct TorsoLayoutCheck { static_assert(sizeof(Torso) == 0x280, "sizeof(Torso) must be 0x280 (factory alloc + gyro cross-link)"); static_assert(offsetof(Torso, currentTwist) == 0x1D8, "Torso currentTwist must be at 0x1D8 (gyro linkTarget +0x1D8)"); static_assert(offsetof(Torso, currentElevation) == 0x1E4, "Torso currentElevation must be at 0x1E4"); static_assert(offsetof(Torso, analogTwistAxis) == 0x1F0, "Torso analogTwistAxis must be at 0x1F0 (mapper +0x1F0)"); static_assert(offsetof(Torso, analogElevationAxis) == 0x1F4, "Torso analogElevationAxis must be at 0x1F4 (mapper +0x1F4)"); static_assert(offsetof(Torso, horizontalEnabled) == 0x250, "Torso horizontalEnabled must be at 0x250 (mapper +0x250)"); static_assert(offsetof(Torso, recenterActive) == 0x274, "Torso recenterActive must be at 0x274 (proves the 0x270 pad)"); static_assert(offsetof(Torso, horizontalShadowJointNode) == 0x27C, "Torso last own field at 0x27C (+4 => sizeof 0x280)"); }; //########################################################################### //########################################################################### // Torso //########################################################################### //########################################################################### //############################################################################# // Shared Data Support (DefaultData @00510af8) // Derivation Torso::ClassDerivations( PowerWatcher::GetClassDerivations(), // returns Derivation* (no &) "Torso" ); Receiver::MessageHandlerSet Torso::MessageHandlers; Torso::AttributeIndexSet Torso::AttributeIndex; Torso::SharedData Torso::DefaultData( &Torso::ClassDerivations, Torso::MessageHandlers, Torso::AttributeIndex, Torso::StateCount ); //############################################################################# // Construction / Destruction // // // @004b6b0c [CONFIDENT] -- chains to the PowerWatcher base ctor (FUN_004b18a4) // with &Torso::DefaultData, installs the Torso vtable (PTR @0051103c). A live // master segment (flags & 0xC == 0 && flags & 1) gets TorsoSimulation as its // Performance and isDamagedCopy=0; any other segment gets TorsoCopySimulation // and isDamagedCopy=1. Angular resource values are converted deg->rad here. // Torso::Torso( 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 7 CROSS-FAMILY shim backing fields were deleted // (they over-sized the object); their accessors now read the real inherited // base state, so there is nothing to prime here. The master/copy selection // below already reads the authoritative owner->simulationFlags. // INTEGRATION (gate reconcile): read OWNER simulationFlags (param_2+0x28) — // the oracle-verified authoritative source — not the local segment shim. if ((owner->simulationFlags & SegmentCopyMask) == 0 && (owner->simulationFlags & MasterHeatSinkFlag) != 0) // owner flags & 0x100 (binary @004b6b0c) { isDamagedCopy = 0; // @0x24C SetPerformance(&Torso::TorsoSimulation); // PTR @00510c10 (-> @004b5cf0) } else { isDamagedCopy = 1; SetPerformance(&Torso::TorsoCopySimulation); // PTR @00510c1c (-> @004b65f8) } statusFlags = 0; // @0x20C buttonAccelerationPerSecond = r->buttonAccelerationPerSecond; // @0x210 <- +0x150 buttonAccelerationStart = r->buttonAccelerationStartValue; // @0x214 <- +0x154 baseTwistRate = r->horizontalRotationPerSecond * DegToRad; // @0x23C <- +0xF4 baseElevationRate = r->verticalRotationPerSecond * DegToRad; // @0x240 <- +0xF8 horizontalLimitRight = r->horizontalLimitRight * DegToRad; // @0x1DC <- +0xFC horizontalLimitLeft = r->horizontalLimitLeft * DegToRad; // @0x1E0 <- +0x100 verticalLimitTop = r->verticalLimitTop * DegToRad; // @0x220 <- +0x104 verticalLimitBottom = r->verticalLimitBottom * DegToRad; // @0x224 <- +0x108 // derived limit copies + half-bottom (used as soft centre / settle band) twistCenterHigh = verticalLimitTop; // @0x230 = @0x220 twistCenterLow = verticalLimitBottom; // @0x234 = @0x224 elevationCenter = verticalLimitTop; // @0x228 = @0x220 elevationHalfBottom = verticalLimitBottom * 0.5f; // @0x22C (_DAT_004b6fbc) buttonRampActive = 0; // @0x268 buttonRamp = 0.0f;// @0x26C horizontalEnabled = r->torsoHorizontalEnabled; // @0x250 <- +0x14C if (horizontalEnabled) { // resolve the two skeleton joints named in the resource: horizontalJointNode = ResolveJoint(r->torsoHorizontalJoint); // @0x278 horizontalShadowJointNode = ResolveJoint(r->torsoHorizontalShadowJoint); // @0x27C // bring-up verification (env BT_TORSO_LOG; default OFF): confirm the // named joints resolved to live nodes and report their joint types. if (getenv("BT_TORSO_LOG")) { DEBUG_STREAM << "[torso] resolve '" << r->torsoHorizontalJoint << "' -> " << (void*)horizontalJointNode; if (horizontalJointNode) DEBUG_STREAM << " type=" << (int)horizontalJointNode->GetJointType(); DEBUG_STREAM << " ; shadow '" << r->torsoHorizontalShadowJoint << "' -> " << (void*)horizontalShadowJointNode; if (horizontalShadowJointNode) DEBUG_STREAM << " type=" << (int)horizontalShadowJointNode->GetJointType(); DEBUG_STREAM << "\n" << std::flush; } } // ---- BRING-UP DEMO (env BT_FORCE_TORSO; default OFF; NOT faithful) -------- // The Blackhawk 0xBC5 record has TorsoHorizontalEnabled=0 + empty joint names // (the binary skips torso joints for this mech, verified). To exercise the // reconstructed twist path end-to-end (ResolveJoint -> TorsoSimulation -> // UpdateJoints -> PushTwist -> Joint::SetRotation), force-enable and resolve // the REAL BLH.SKL torso joints ('jointshakey2' torso body / 'jointtshadow' // = the "apply torso twist to yaw" hinge), widen the limits, and give a slew // rate. TorsoSimulation drives the sweep (below). Remove after verification. if (isDamagedCopy == 0 && getenv("BT_FORCE_TORSO")) { horizontalEnabled = True; // @0x250 const char *mj = getenv("BT_FORCE_TORSO_JOINT"); if (mj == 0 || *mj == '\0') mj = "jointshakey2"; horizontalJointNode = ResolveJoint(mj); // torso body (ball) horizontalShadowJointNode = ResolveJoint("jointtshadow"); // shadow twist (hingey) horizontalLimitLeft = 0.7f; // @0x1E0 ~40 deg horizontalLimitRight = -0.7f; // @0x1DC baseTwistRate = 1.0f; // @0x23C rad/s slew if (getenv("BT_TORSO_LOG")) { DEBUG_STREAM << "[torso] FORCE-ENABLE '" << mj << "' -> " << (void*)horizontalJointNode; if (horizontalJointNode) DEBUG_STREAM << " type=" << (int)horizontalJointNode->GetJointType(); DEBUG_STREAM << " ; shadow 'jointtshadow' -> " << (void*)horizontalShadowJointNode; if (horizontalShadowJointNode) DEBUG_STREAM << " type=" << (int)horizontalShadowJointNode->GetJointType(); DEBUG_STREAM << "\n" << std::flush; } } // -------------------------------------------------------------------------- effectiveTwistRate = baseTwistRate; // @0x244 = @0x23C effectiveElevationRate = baseElevationRate; // @0x248 = @0x240 currentTwist = 0.0f; // @0x1D8 currentElevation = 0.0f; // @0x1E4 twistVelocity = 0.0f; // @0x1E8 twistRate = 0.0f; // @0x238 analogTwistAxis = analogElevationAxis = 0.0f; // @0x1F0 / @0x1F4 elevateUpCommand = elevateDownCommand = 0; // @0x1F8 / @0x1FC twistLeftCommand = twistRightCommand = 0; // @0x200 / @0x204 centerCommand = 0; // @0x208 hitElevTop = hitElevBottom = 0; // @0x258 / @0x25C hitTwistLeft = hitTwistRight = 0; // @0x260 / @0x264 recenterActive = 0; // @0x274 lastUpdateTime = GetCreationTime(); // @0x254 = this[5] // bring-up verification (env BT_TORSO_LOG): the gyro cross-links to // (Torso*)+0x1D8 (== currentTwist) at a RAW offset (mech.cpp:740), so the // compiled layout MUST place currentTwist at 0x1D8 and the object must fit the // 0x280 factory alloc. Log both so a mismatch is caught immediately. if (getenv("BT_TORSO_LOG")) { DEBUG_STREAM << "[torso] ctor this=" << (void*)this << " sizeof(Torso)=" << (unsigned)sizeof(Torso) << " (0x280=" << (unsigned)0x280 << ")" << " currentTwist@" << (unsigned)((char*)¤tTwist - (char*)this) << " (want 0x1D8=" << (unsigned)0x1D8 << ")" << " damagedCopy=" << isDamagedCopy << " horizEnabled=" << (int)horizontalEnabled << "\n" << std::flush; // RESOURCE DIAGNOSTIC: is horizEnabled=0 a genuine content value or a // mis-read? Dump the resource enable flag + joint names (valid strings => // seg is the right record) + the raw int at record+0x14C + the two limit // scalars (to confirm the resource is a plausible Torso record at all). DEBUG_STREAM << "[torso] res enabled=" << (int)r->torsoHorizontalEnabled << " raw@0x14C=" << *(const int*)((const char*)r + 0x14C) << " hJoint='" << r->torsoHorizontalJoint << "'" << " sJoint='" << r->torsoHorizontalShadowJoint << "'" << " hRotPerSec=" << r->horizontalRotationPerSecond << " hLimL=" << r->horizontalLimitLeft << "\n" << std::flush; // Is the record correctly positioned? classID@+0x20 should be 0xBC5 and // modelSize@+0x24 should be 0x158 for a real Torso record; garbage => the // seg pointer / stream position is wrong (not a content issue). DEBUG_STREAM << "[torso] res classID=0x" << std::hex << *(const int*)((const char*)r + 0x20) << " modelSize=0x" << *(const int*)((const char*)r + 0x24) << std::dec << " (want classID=0xBC5 size=0x158)\n" << std::flush; } Check_Fpu(); } // // @004b6fc0 [BEST-EFFORT, prologue not captured] -- reinstalls the vtable, runs // the PowerWatcher teardown and frees on the deleting bit. (Mirrors @004b3e88.) // Torso::~Torso() { Check(this); Check_Fpu(); } Logical Torso::TestClass(Mech &) { return True; } Logical Torso::TestInstance() const { return IsDerivedFrom(ClassDerivations); } //############################################################################# // Subsystem virtual overrides // // // @004b5bf8 (slot 10) [CONFIDENT] -- ResetToInitialState. Chains to // PowerWatcher::ResetToInitialState (FUN_004b1804) first; when (re)powering, // snaps the effective rates back to the base rates and clears the button ramp. // Always clears the command inputs / velocity / latches, then re-pushes the // (centred) twist into the joints via WriteJoints. // void Torso::ResetToInitialState() { WatcherResetToInitialState(); // CROSS-FAMILY: PowerWatcher::ResetToInitialState (FUN_004b1804) // @004b5bf8: on a (re)power (param_2 != 0) restore the effective rates and // clear all FOUR limit latches (0x258/0x25c/0x260/0x264); always clear the // command + analog inputs, the aim state and velocities, then re-push the // centred twist into the joints via WriteJoints. const Logical powered = True; // bring-up: reset always re-powers if (powered) { effectiveTwistRate = baseTwistRate; // @0x244 = @0x23C effectiveElevationRate = baseElevationRate; // @0x248 = @0x240 hitElevTop = hitElevBottom = 0; // @0x258 / @0x25C hitTwistLeft = hitTwistRight = 0; // @0x260 / @0x264 } elevateUpCommand = elevateDownCommand = 0; // @0x1F8 / @0x1FC twistLeftCommand = twistRightCommand = 0; // @0x200 / @0x204 centerCommand = 0; // @0x208 analogTwistAxis = analogElevationAxis = 0.0f;// @0x1F0 / @0x1F4 currentTwist = 0.0f; // @0x1D8 currentElevation = 0.0f; // @0x1E4 twistVelocity = 0.0f; // @0x1E8 elevationVelocity = 0.0f; // @0x1EC twistAtUpdate = 0.0f; // @0x21C targetTwist = 0.0f; // @0x218 twistRate = 0.0f; // @0x238 recenterActive = 0; // @0x274 Scalar verticalOut; WriteJoints(verticalOut); // FUN_004b66b4 } // // @004b5be0 (slot 9) [BEST-EFFORT, prologue not captured] -- forwards to the // PowerWatcher slot-9 handler (FUN_004b179c): on a "destroyed" message it clears // the watched power source's voltage alarm, otherwise chains to the base. // Logical Torso::HandleDeathMessage(Message &message) { return WatcherHandleDeathMessage(message); // CROSS-FAMILY: PowerWatcher::HandleDeathMessage (FUN_004b179c) } // // @004b6a78 (slot 6) [CONFIDENT] -- network/replay update record. Samples the // clock (FUN_00414b60) into lastUpdateTime, biasing it forward by one interval // when the elapsed window is below MinSlewMs, chains to Subsystem::WriteUpdateRecord // (FUN_0041bd34), then writes twistAtUpdate / twistVelocity / twistRate from the // record fields (record +0x10 / +0x14 / +0x18). // void Torso::WriteUpdateRecord(UpdateRecord *message, int update_model) { lastUpdateTime = GetCurrentTime(); // @0x254 if ((Scalar)(lastUpdateTime - GetCreationTime()) / MsPerSecond < MinSlewMs) { lastUpdateTime += (lastUpdateTime - GetCreationTime()); // stretch tiny windows } Subsystem::WriteUpdateRecord(message, update_model); // FUN_0041bd34 twistAtUpdate = RecordField(message, 0x10); // @0x21C twistVelocity = RecordField(message, 0x14); // @0x1E8 twistRate = RecordField(message, 0x18); // @0x238 } //############################################################################# // Per-frame simulation // // // @004b5cf0 [CONFIDENT] -- the live-master Performance (PTR @00510c10). // // 1. PowerWatcher watch update (FUN_004b181c). // 2. Latch the effective rates from the base rates, then ZERO them if the // watched power source is dead (this[0x10]==1), not Ready (this @0x198 != 4), // or in the Failure heat state (this @0x140 == 2); the Degradation heat state // (==1) instead halves the twist rate. // 3. Apply the button-acceleration ramp (start value -> cap) while a command is // held, integrate the per-axis commands into currentTwist / currentElevation, // clamp to the software limits, set the limit latches, and -- if releasing -- // run Recenter. The "moved" dirty bit (this @0x18 |= 1) is raised when the // aim changed beyond TwistEps/SettleEps, and statusFlags reflects which limit // (if any) is being held. // void Torso::TorsoSimulation(Scalar time_slice) { Check(this); WatcherUpdateWatch(); // CROSS-FAMILY: PowerWatcher per-frame watch (FUN_004b181c) Scalar twist0 = currentTwist; // snapshot for velocity calc Scalar elev0 = currentElevation; effectiveTwistRate = baseTwistRate; // @0x244 = @0x23C effectiveElevationRate = baseElevationRate; // @0x248 = @0x240 if (HeatModelOff()) effectiveTwistRate = 0.0f; // this[0x10]==1 if (ElectricalStateLevel() != PoweredSubsystem::Ready) effectiveTwistRate = 0.0f; // @0x198 != 4 switch (HeatStateLevel()) // @0x140 { case HeatSink::DegradationHeat: effectiveTwistRate = baseTwistRate * 0.5f; break; // _DAT_004b64f8 case HeatSink::FailureHeat: effectiveTwistRate = 0.0f; break; default: break; } // BRING-UP DEMO (env BT_FORCE_TORSO): un-gate the slew rate (the forced demo // torso isn't wired to a live power source, so ElectricalStateLevel would zero // it) and drive a left/right analog sweep so the reconstructed twist path is // exercised visibly. Faithful behavior is untouched when the env is unset. static const int s_forceTorso = getenv("BT_FORCE_TORSO") ? 1 : 0; if (s_forceTorso) { effectiveTwistRate = baseTwistRate; // un-gate (ctor set 1.0 rad/s) static int s_sweep = 0; analogTwistAxis = ((++s_sweep / 90) & 1) ? -1.0f : 1.0f; // +/- every ~90 frames } Scalar twistStep = effectiveTwistRate * time_slice; Scalar elevStep = effectiveElevationRate * time_slice; // button-acceleration ramp (forced to RampCap in the shipped build -- see note) if (buttonRampActive == 0) { buttonRamp = buttonAccelerationStart; // @0x26C <- @0x214 } else { buttonRamp += buttonAccelerationPerSecond * time_slice; // ramp up buttonRamp = Min(buttonRamp, RampCap); buttonRampActive = 0; } buttonRamp = 1.0f; // NB: @004b5cf0 unconditionally overwrites @0x26C with 1.0f // ---- digital elevation (pitch) commands @0x1F8 / @0x1FC ---- if (elevateUpCommand > 0) // @0x1F8 { currentElevation += elevStep * buttonRamp; currentElevation = Min(currentElevation, verticalLimitTop); // @0x220 buttonRampActive = 1; } if (elevateDownCommand > 0) // @0x1FC { currentElevation -= elevStep * buttonRamp; currentElevation = Max(currentElevation, verticalLimitBottom); // @0x224 buttonRampActive = 1; } // ---- digital twist (yaw) commands @0x200 / @0x204 ---- if (twistLeftCommand > 0) // @0x200 { currentTwist += twistStep * buttonRamp; currentTwist = Min(currentTwist, horizontalLimitLeft); // @0x1E0 recenterActive = 0; buttonRampActive = 1; } if (twistRightCommand > 0) // @0x204 { currentTwist -= twistStep * buttonRamp; currentTwist = Max(currentTwist, horizontalLimitRight); // @0x1DC recenterActive = 0; buttonRampActive = 1; } if (centerCommand > 0) // @0x208 { recenterActive = 1; buttonRampActive = 0; } // ---- analog (RIO/stick) axes @0x1F0 / @0x1F4: proportional, no button ramp ---- if (analogTwistAxis != Zero) // @0x1F0 { currentTwist += analogTwistAxis * twistStep; currentTwist = Min(currentTwist, horizontalLimitLeft); // @0x1E0 currentTwist = Max(currentTwist, horizontalLimitRight); // @0x1DC recenterActive = 0; } if (analogElevationAxis != Zero) // @0x1F4 { currentElevation += analogElevationAxis * elevStep; currentElevation = Min(currentElevation, verticalLimitTop); // @0x220 currentElevation = Max(currentElevation, verticalLimitBottom); // @0x224 } if (recenterActive != 0) { recenterActive = Recenter(time_slice); // FUN_004b6918 } // ---- derive angular velocities + settle detection ---- Scalar oldTwistRate = twistRate; // local_90: OLD rate, snapshot before recompute if (time_slice > Zero) { twistVelocity = (currentTwist - twist0) / time_slice; // @0x1E8 elevationVelocity = (currentElevation - elev0) / time_slice; // @0x1EC twistRate = twistVelocity; // @0x238 (signed) twistVelocity = fabsf(twistVelocity); elevationVelocity = fabsf(elevationVelocity); } if (fabsf(twistRate) <= VelEps) twistRate = 0.0f; if (fabsf(oldTwistRate) <= VelEps) oldTwistRate = 0.0f; ComputeTargetTwist(); // FUN_004b6510 -> targetTwist (@0x218) // raise the "moved" dirty bit unless we have settled within tolerance. The // aim error is measured against targetTwist (0x218) and the settle test uses // the OLD rate snapshot (not the previous twist angle). Scalar aimError = currentTwist - targetTwist; // 0x1D8 - 0x218 Logical justStopped = (twistRate == Zero) && (oldTwistRate != Zero); Scalar rateDelta = twistRate - oldTwistRate; if (!(fabsf(aimError) <= TwistEps // _DAT_004b6508 && fabsf(rateDelta) <= SettleEps // _DAT_004b650c && !justStopped)) { SetMovedFlag(); // this @0x18 |= 1 } // ---- limit latches (edge-triggered) + statusFlags ---- // Cleared when off the limit; set (with statusFlags=2) only on the FRAME the // limit is newly reached. @0x260/@0x264 latch the twist limits, @0x258/@0x25C // the elevation limits (verified against part_013.c 4658-4692). statusFlags = 0; if (fabsf(currentTwist - horizontalLimitLeft) > VelEps) hitTwistLeft = 0; // @0x260 else if (hitTwistLeft == 0) { hitTwistLeft = 1; statusFlags = 2; } if (fabsf(currentTwist - horizontalLimitRight) > VelEps) hitTwistRight = 0; // @0x264 else if (hitTwistRight == 0) { hitTwistRight = 1; statusFlags = 2; } if (fabsf(currentElevation - verticalLimitTop) > VelEps) hitElevTop = 0; // @0x258 else if (hitElevTop == 0) { hitElevTop = 1; statusFlags = 2; } if (fabsf(currentElevation - verticalLimitBottom) > VelEps) hitElevBottom = 0; // @0x25C else if (hitElevBottom == 0) { hitElevBottom = 1; statusFlags = 2; } if (recenterActive != 0) statusFlags = 1; // Per-frame skeleton write: push currentTwist into the resolved horizontal // joint(s). In the shipped binary this is UpdateJoints (@004b67ec) -- the // out-param-free twin of WriteJoints -- with no DIRECT caller in the recovered // decomp: it was dispatched by the engine's generic per-frame joint pass (an // indirect/virtual call). This port ticks the torso's Performance here, so we // resolve that dispatch to a direct call at the same per-frame cadence, which // is what makes the torso visibly track the aim. UpdateJoints(); // FUN_004b67ec Check_Fpu(); } // // @004b65f8 [CONFIDENT] -- the damaged-copy Performance (PTR @00510c1c). A copy // segment does not take commands; it just eases its twist toward the master's // target. ComputeTargetTwist() reports whether the master is still slewing: if // not, snap currentTwist to the target; otherwise lerp by // dt / ((now - lastUpdateTime)/scale + dt). Result is clamped to the limits. // void Torso::TorsoCopySimulation(Scalar time_slice) { if (!ComputeTargetTwist()) // FUN_004b6510 -> 0 = settled { currentTwist = targetTwist; // @0x1D8 = @0x218 } else { Scalar age = (Scalar)(lastUpdateTime - GetCreationTime()) / MsPerSecond; currentTwist = Lerp(currentTwist, targetTwist, time_slice / (age + time_slice)); // FUN_004081e0 } targetTwist = Min(targetTwist, horizontalLimitLeft); // @0x1E0 targetTwist = Max(targetTwist, horizontalLimitRight); // @0x1DC // Per-frame skeleton write for the replicated (damaged-copy) torso -- same // external joint pass as the master path (see TorsoSimulation for the @004b67ec // note). Harmless in single-player bring-up (no copies); correct for MP. UpdateJoints(); // FUN_004b67ec } //############################################################################# // Internal model helpers // // // @004b6510 [CONFIDENT] -- compute the extrapolated twist target. Picks the // time base (clamped command timestamp vs. clock), forms an elapsed time in // seconds, predicts targetTwist = twistAtUpdate + twistRate * elapsed, clamps to // the limits, and returns 1 while still extrapolating (live), 0 once settled. // Logical Torso::ComputeTargetTwist() { int base; Logical slewing; if (isDamagedCopy == 0 || lastUpdateTime <= GetCurrentTime()) { base = GetCurrentTime() - GetCreationTime(); // this[0x10] - this[0x14] slewing = False; } else { base = lastUpdateTime - GetCreationTime(); // @0x254 - this[0x14] slewing = True; } Scalar elapsed = (Scalar)base / MsPerSecond; targetTwist = twistAtUpdate + twistRate * elapsed; // @0x218 = @0x21C + @0x238*t targetTwist = Min(targetTwist, horizontalLimitLeft); // @0x1E0 targetTwist = Max(targetTwist, horizontalLimitRight); // @0x1DC return slewing; } // // ResolveJoint -- forward to the owning Mech's shared resolver (FUN_00424b60, // inlined by the Torso ctor @004b6b0c). Out-of-line so the complete Mech type // (mech.hpp) is visible; returns NULL for an absent node (ctor guards on it). // Joint* Torso::ResolveJoint(const char *joint_name) { Check(owner); // inherited MechSubsystem::owner (Mech*) return owner->ResolveJoint(joint_name); } // // @004b66b4 (inner block) [CONFIDENT] -- write one twist (yaw) scalar into a // resolved skeleton node, preserving the node's other DOF. Dispatched on the // joint type exactly like AnimationInstance::Animate (JMOVER.cpp:1518-1567): // hinge nodes (types 0..2) take a scalar Radian (FUN_0041d0a8 == // Joint::SetRotation(Radian)); ball nodes (types 4..5) take an EulerAngles whose // YAW carries the twist while pitch/roll are read back and kept (FUN_0041cfa0 == // GetEulerAngles, FUN_0041d020 == SetRotation(EulerAngles)). SetRotation sets // jointModified + ModifyJoints() internally, so we write unconditionally. // void Torso::PushTwist(Joint *node, Scalar twist) { if (node == NULL) // ctor may have failed to resolve the node; { // the binary trusts it -- guard for bring-up return; } Joint::JointType jt = node->GetJointType(); // node+0x10 // bring-up verification (env BT_TORSO_LOG; default OFF): show the first few // joint writes so the per-frame path can be confirmed in a headless run. static const int s_log = getenv("BT_TORSO_LOG") ? 1 : 0; static int s_count = 0; if (s_log && (s_count % 30) == 0 && s_count < 1800) // sample periodically to show the sweep { DEBUG_STREAM << "[torso] PushTwist node=" << (void*)node << " type=" << (int)jt << " twist=" << (float)twist << "\n" << std::flush; } ++s_count; switch (jt) // node+0x10 { case Joint::HingeXJointType: // types 0..2 case Joint::HingeYJointType: case Joint::HingeZJointType: node->SetRotation(Radian(twist)); // FUN_0041d0a8 break; case Joint::BallJointType: // types 4..5 case Joint::BallTranslationJointType: { EulerAngles angles = node->GetEulerAngles(); // FUN_0041cfa0 (keep pitch/roll) EulerAngles twisted(angles.pitch, twist, angles.roll); // yaw <- twist (index 1) node->SetRotation(twisted); // FUN_0041d020 break; } default: // StaticJointType(3) / NULLJointType(-1) break; } } // // @004b66b4 [CONFIDENT] -- write currentTwist into the two horizontal joints // (main @0x278 + shadow @0x27C) when horizontalEnabled, and report the current // elevation through `verticalOut`. Each joint is updated as a scalar channel // (node type < 3) or a vector channel (types 4..5), matching the gyro pattern. // void Torso::WriteJoints(Scalar &verticalOut) { verticalOut = currentElevation; // *param_2 = @0x1E4 if (!horizontalEnabled) // @0x250 { return; } PushTwist(horizontalJointNode, currentTwist); // @0x278 PushTwist(horizontalShadowJointNode, currentTwist); // @0x27C } // // @004b67ec [CONFIDENT] -- identical to WriteJoints minus the elevation output; // used on the paths that only need to refresh the skeleton. // void Torso::UpdateJoints() { if (!horizontalEnabled) { return; } PushTwist(horizontalJointNode, currentTwist); PushTwist(horizontalShadowJointNode, currentTwist); } // // @004b6918 [CONFIDENT] -- ease currentTwist back toward 0 by effectiveTwistRate // * dt, clamping so it does not cross zero, and return True while still off // centre (|currentTwist| > VelEps). // Logical Torso::Recenter(Scalar time_slice) { Scalar step = effectiveTwistRate * time_slice; // @0x244 if (currentTwist > Zero) { currentTwist -= step; if (currentTwist < 0.0f) currentTwist = 0.0f; } if (currentTwist < Zero) { currentTwist += step; if (currentTwist > 0.0f) currentTwist = 0.0f; } return fabsf(currentTwist - Zero) > VelEps; // _DAT_004b6a18 } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // CreateStreamedSubsystem -- Torso // // @004b6fec [CONFIDENT for the field list / classID / size; body linearised]. // Chains to PowerWatcher::CreateStreamedSubsystem (FUN_004b198c), stamps the // resource (classID 0x0BC5 @+0x20, model size 0x158 @+0x24), reads the optional // "TorsoHorizontalEnabled" flag (default True; the string "false" -> False), the // six mandatory angular/limit scalars and the two acceleration scalars, then -- // only when enabled -- the two joint names, which must resolve in the model's // "skeleton" file. // int Torso::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); // +0x24 = 0x158 r->classID = RegisteredClass::TorsoClassID; // +0x20 = 0x0BC5 if (passes == 1) { // "TorsoHorizontalEnabled": absent => True; the literal "false" => False. const char *flag = 0; if (!model_file->GetEntry(subsystem_name, "TorsoHorizontalEnabled", &flag)) { r->torsoHorizontalEnabled = True; } else { r->torsoHorizontalEnabled = (stricmp(flag, "false") == 0) ? False : True; // FUN_004d4b58 } } #define REQ_SCALAR(NAME, FIELD) \ if (!model_file->GetEntry(subsystem_name, NAME, &r->FIELD) \ && r->FIELD == Unset) \ { DebugStream << subsystem_name << " missing " << NAME << "!"; return False; } REQ_SCALAR("ButtonAccelerationPerSecond", buttonAccelerationPerSecond) // +0x150 REQ_SCALAR("ButtonAccelerationStartValue", buttonAccelerationStartValue)// +0x154 REQ_SCALAR("HorizontalRotationPerSecond", horizontalRotationPerSecond) // +0xF4 REQ_SCALAR("VerticalRotationPerSecond", verticalRotationPerSecond) // +0xF8 REQ_SCALAR("HorizontalLimitRight", horizontalLimitRight) // +0xFC REQ_SCALAR("HorizontalLimitLeft", horizontalLimitLeft) // +0x100 REQ_SCALAR("VerticalLimitTop", verticalLimitTop) // +0x104 REQ_SCALAR("VerticalLimitBottom", verticalLimitBottom) // +0x108 if (r->torsoHorizontalEnabled) { const char *hj = "Unspecified"; if (!model_file->GetEntry(subsystem_name, "TorsoHorizontalJoint", &hj) && strcmp(hj, "Unspecified") == 0) { DebugStream << subsystem_name << " missing TorsoHorizontalJoint!"; return False; } if (strcmp(hj, "Unspecified") != 0) strcpy(r->torsoHorizontalJoint, hj); // +0x10C const char *sj = "Unspecified"; if (!model_file->GetEntry(subsystem_name, "TorsoHorizontalShadowJoint", &sj) && strcmp(sj, "Unspecified") == 0) { DebugStream << subsystem_name << " missing TorsoHorizontalShadowJoint!"; return False; } if (strcmp(sj, "Unspecified") != 0) strcpy(r->torsoHorizontalShadowJoint, sj); // +0x12C const char *skeleton = 0; if (!model_file->GetEntry("video", "skeleton", &skeleton)) { DebugStream << model_name << " is missing skeleton file!"; return -1; } ReconSkeleton *skl = LoadSkeleton(directories, skeleton); if (!skl->FindNode(r->torsoHorizontalJoint)) { DebugStream << r->torsoHorizontalJoint << " not found in " << skeleton; return -1; } // (shadow-joint lookup follows the same pattern) } #undef REQ_SCALAR Check_Fpu(); return True; } //===========================================================================// // WAVE 4 factory bridge -- Torso (factory case 0xBC5, "SinkSource" label). // The real class at 0xBC5 (ctor @004b6b0c) is Torso; the factory built a // HeatSinkSource RECON_SUBSYS stub in its place. Constructing the real Torso // here is what lets the reconstructed twist -> skeleton path run. The object // is read at RAW offsets externally (the gyro cross-link -> currentTwist@0x1D8, // mech.cpp:740), so the compiled layout MUST match the binary 0x280 -- now // compile-time proven by TorsoLayoutCheck (sizeof(Torso)==0x280) above. //===========================================================================// Subsystem *CreateTorsoSubsystem(Mech *owner, int id, void *seg) { return (Subsystem *) new (Memory::Allocate(0x280)) Torso(owner, id, (Torso::SubsystemResource *)seg, Torso::DefaultData); } // // STEP 6 bridge -- expose the live torso twist (Torso::currentTwist == the binary // torso+0x1d8) to mech.cpp / the cylinder damage table without pulling the Torso // header (which collides with mech.cpp's local subsystem stubs) into mech.cpp. // The roster torso @mech+0x438 is a Torso (ClassID 0xBC5); 0 when absent. // Scalar BTGetTorsoTwist(Subsystem *torso) { return torso ? ((Torso *)torso)->CurrentTwist() : 0.0f; }