The MadCat torso twists, the view turns with it, and targeting follows. Three reconstruction fronts closed: THE ELECTRICAL WATCHDOG CHAIN (why the torso never powered up): - PowerWatcher::UpdateWatch reconstructed (@004b181c, the REAL registered Performance -- PTR @0050f5fc; Ghidra missed the fn start): the watchdog MIRRORS the watched subsystem's electrical level (+0x278), brownout downgrade when gen output <= minVoltage% x rated. @004b1804 relabeled ResetToInitialState (slot 10) -- the old "Simulation" tag was wrong. - The factory watcher-CONNECT pass reconstructed (vtable slot +0x38, @004aee2c/@004b1a40 byte-identical, recovered from raw exe bytes): watchedLink.Add(roster[watchedSubsystem]) on the master node. Was the SubProxy::Start() no-op -- every watchdog sat at 0 forever. - MinVoltageScale = 0.01 (a 10-byte x87 literal @0x4b1924; was 1.0f = permanent brownout) and PowerWatcher's Derivation chains its REAL base HeatWatcher (the HeatableSubsystem stand-in broke IsDerivedFrom for the whole Torso/Searchlight/ThermalSight family). - KB correction swept: derivation tag 0x50e604 = HEATWATCHER (not "HeatSink"); the btl4gaug heat-widget gate now tests it via the BTIsHeatWatcher bridge. THE CROSSHAIR (task #58 forensics, 6-agent workflow + live probes): - The VIEW is TORSO-MOUNTED: jointtorso -> jointeye -> siteeyepoint in every twist-capable .SKL; the camera + canopy ride the same hinge subtree through HingeRenderable's live matrix-stack compose -- ALREADY WORKING in the port. The crosshair stays screen-centered (center IS the boresight); the twist reads on the tape carets/compass/radar. - The real bug was the port's gBTAimX = tan(twist) slew (the falsified "body-mounted view" model): the camera already carried the twist, so the crosshair counter-slid to hull-forward and the fire ray with it. Deleted; the pick ray inherits the twist from the yawing eye basis. - Two instrumentation traps documented (chase-eye-as-default-camera, BT_FORCE_TORSO clobbering real joints -> the hook now only fills unresolved ones); an over-correcting explicit eye compose was added on those false readings and retired the same day. CONTROLS + REPLICATION: - Q/E spring-center on release (the axis is a twist-RATE demand; the old hold-deflection model drifted forever); X also zeroes the axis and pulses the authentic torso Recenter (@004b6918). M cycles control mode via the real CycleControlMode body. - Torso update-record DIRECTION fixed: engine truth is Write=serialize / Read=apply; @004b6a78 is the READ (was mislabeled Write) and the missing WRITE @004b6a1c recovered from raw disasm (recordLength 0x1C, twist/vel/rate at +0x10/14/18) -- kills the replicant's 0xCDCDCDCD -140-degree ghost twist. - Marching-ghost desync: 4 Standing-case guards zero stale reverse cycleSpeed (negative cadence passed the <= ZeroSpeed stop gate). - Kill credit rerouted to the OBSERVED killer (lastInflictingID -> killer's player link) -- kills count, target K/D populates. KB: subsystems.md (watcher chain), multiplayer.md (record direction), combat-damage.md + gauges-hud.md + cockpit-view.md (torso-mounted view re-correction), decomp-reference.md (new addresses + tag fix), open-questions.md (dead capability-roster loops 2-4, snapshot CD read). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1001 lines
40 KiB
C++
1001 lines
40 KiB
C++
//===========================================================================//
|
|
// 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 <bt.hpp>
|
|
#pragma hdrstop
|
|
|
|
#if !defined(TORSO_HPP)
|
|
# include <torso.hpp>
|
|
#endif
|
|
#if !defined(MECH_HPP)
|
|
# include <mech.hpp> // complete Mech -- owner->ResolveJoint
|
|
#endif
|
|
#include <JOINT.hpp> // Joint, JointSubsystem (fwd shim)
|
|
#include <ROTATION.hpp> // EulerAngles, Radian, Hinge (fwd shim)
|
|
#if !defined(APP_HPP)
|
|
# include <app.hpp>
|
|
#endif
|
|
#if !defined(TESTBT_HPP)
|
|
# include <testbt.hpp>
|
|
#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); }
|
|
inline void
|
|
WriteRecordField(Simulation::UpdateRecord *record, int byte_offset, Scalar value)
|
|
{ *(Scalar*)((char*)record + byte_offset) = value; }
|
|
}
|
|
|
|
//###########################################################################
|
|
// 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"))
|
|
{
|
|
// ⚠ HARNESS TRAP (task #58 post-mortem): this hook used to override the
|
|
// joint nodes UNCONDITIONALLY with the BLH demo names -- on a mech whose
|
|
// resource already resolved REAL torso joints (the MadCat's jointtorso)
|
|
// that silently redirected the whole sweep into the SHADOW hinge, and a
|
|
// probe run "proved" the cockpit camera ignored the twist (it doesn't;
|
|
// the joint chain delivers it). Now: only fill joints that did NOT
|
|
// resolve from the resource; a twist-capable mech sweeps its real ones.
|
|
horizontalEnabled = True; // @0x250
|
|
if (horizontalJointNode == 0)
|
|
{
|
|
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 node=" << (void*)horizontalJointNode;
|
|
if (horizontalJointNode) DEBUG_STREAM << " type=" << (int)horizontalJointNode->GetJointType();
|
|
DEBUG_STREAM << " ; shadow node=" << (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] -- the REPLICANT-side apply (engine semantics
|
|
// [T0]: ReadUpdateRecord = write the object FROM the record). Samples the
|
|
// clock (FUN_00414b60) into lastUpdateTime, biasing it forward by one interval
|
|
// when the elapsed window is below MinSlewMs, chains to the base apply
|
|
// (FUN_0041bd34 = ReadUpdateRecord, 2 args), then applies twistAtUpdate /
|
|
// twistVelocity / twistRate from the record extras (+0x10 / +0x14 / +0x18).
|
|
// (Task #57: previously mislabeled as WriteUpdateRecord -- the direction flip
|
|
// made the MASTER consume its own blank record.)
|
|
//
|
|
void
|
|
Torso::ReadUpdateRecord(UpdateRecord *message)
|
|
{
|
|
lastUpdateTime = GetCurrentTime(); // @0x254
|
|
if ((Scalar)(lastUpdateTime - GetCreationTime()) / MsPerSecond < MinSlewMs)
|
|
{
|
|
lastUpdateTime += (lastUpdateTime - GetCreationTime()); // stretch tiny windows
|
|
}
|
|
|
|
Subsystem::ReadUpdateRecord(message); // FUN_0041bd34
|
|
|
|
twistAtUpdate = RecordField(message, 0x10); // @0x21C
|
|
twistVelocity = RecordField(message, 0x14); // @0x1E8
|
|
twistRate = RecordField(message, 0x18); // @0x238
|
|
}
|
|
|
|
//
|
|
// @004b6a1c (slot 7) [CONFIDENT, raw-disasm recovery -- Ghidra missed the
|
|
// function start] -- the MASTER-side serialize (engine semantics [T0]:
|
|
// WriteUpdateRecord = fill the record FROM the object). Chains the base
|
|
// producer (FUN_0041c500(this, record, model), 3 args), extends the record to
|
|
// 0x1C bytes and appends currentTwist / twistVelocity / twistRate at
|
|
// +0x10/+0x14/+0x18 (exactly what the replicant's @004b6a78 applies), then
|
|
// snapshots twistAtUpdate = currentTwist:
|
|
//
|
|
// 004b6a2e call 0041c500 ; base WriteUpdateRecord
|
|
// 004b6a38 mov [record], 0x1C ; recordLength = 0x1C
|
|
// 004b6a44+ record+0x10 = this+0x1D8 ; currentTwist
|
|
// 004b6a51 record+0x14 = this+0x1E8 ; twistVelocity
|
|
// 004b6a5a record+0x18 = this+0x238 ; twistRate
|
|
// 004b6a63+ this+0x21C = this+0x1D8 ; twistAtUpdate = currentTwist
|
|
//
|
|
void
|
|
Torso::WriteUpdateRecord(UpdateRecord *message, int update_model)
|
|
{
|
|
Subsystem::WriteUpdateRecord(message, update_model); // FUN_0041c500
|
|
|
|
message->recordLength = 0x1C; // base header 0x10 + 3 Scalars
|
|
WriteRecordField(message, 0x10, currentTwist); // @0x1D8
|
|
WriteRecordField(message, 0x14, twistVelocity); // @0x1E8
|
|
WriteRecordField(message, 0x18, twistRate); // @0x238
|
|
twistAtUpdate = currentTwist; // @0x21C snapshot at send
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// 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
|
|
}
|
|
|
|
// TORSO GATE PROBE (BT_TORSO_LOG): why is the twist rate zero?
|
|
if (getenv("BT_TORSO_LOG"))
|
|
{
|
|
static int s_tl = 0;
|
|
if ((s_tl++ % 120) == 0)
|
|
{
|
|
PoweredSubsystem *w = (PoweredSubsystem *)watchedLink.Resolve();
|
|
DEBUG_STREAM << "[torso] hmOff=" << (int)HeatModelOff()
|
|
<< " elec=" << (int)ElectricalStateLevel()
|
|
<< " heatState=" << (int)HeatStateLevel()
|
|
<< " rate=" << effectiveTwistRate
|
|
<< " base=" << baseTwistRate
|
|
<< " hEn=" << (int)horizontalEnabled
|
|
<< " limits=(" << horizontalLimitRight << ".." << horizontalLimitLeft << ")"
|
|
<< " axis=" << analogTwistAxis
|
|
<< " twist=" << currentTwist
|
|
<< " wIdx=" << watchedSubsystem
|
|
<< " w=" << (void*)w
|
|
<< " wElec=" << (w ? w->electricalStateAlarm.GetLevel() : -1)
|
|
<< " wSrc=" << (void*)(w ? w->ResolveVoltageSource() : 0);
|
|
Generator *g = (w ? (Generator *)w->ResolveVoltageSource() : 0);
|
|
if (g)
|
|
DEBUG_STREAM << " gOut=" << g->MeasuredVoltage()
|
|
<< " gRated=" << g->RatedVoltageOf()
|
|
<< " minV=" << minVoltage;
|
|
DEBUG_STREAM << std::endl;
|
|
}
|
|
}
|
|
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
|
|
|
|
// COPY-SIDE PROBE (BT_TORSO_LOG): where does the replicant's twist come from?
|
|
if (getenv("BT_TORSO_LOG"))
|
|
{
|
|
static int s_cl = 0;
|
|
if ((s_cl++ % 120) == 0)
|
|
DEBUG_STREAM << "[torso-copy] cur=" << currentTwist
|
|
<< " target=" << targetTwist
|
|
<< " atUpd=" << twistAtUpdate
|
|
<< " rate=" << twistRate
|
|
<< " vel=" << twistVelocity
|
|
<< " lastUpd=" << lastUpdateTime
|
|
<< " now=" << GetCurrentTime()
|
|
<< " copy=" << (int)isDamagedCopy << std::endl;
|
|
}
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// 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;
|
|
}
|
|
|
|
//
|
|
// Task #56 bridge -- ADDRESS of the live torso twist for the gyro's external-
|
|
// pitch pointer (binary bt_mech stream tail: gyro+0x258 = torso+0x1D8; the gyro
|
|
// damage-response @004b2980 reads it per hit). Same complete-type-TU pattern
|
|
// as BTGetTorsoTwist above.
|
|
//
|
|
Scalar *BTGetTorsoTwistAddr(Subsystem *torso)
|
|
{
|
|
return torso ? ((Torso *)torso)->CurrentTwistAddr() : 0;
|
|
}
|