The Gyroscope subsystem is LIVE again (was a stub since the NaN revert).
Reconstructed byte-exact from @004b3778 / @004b2ec0 / @004b30ec / @004b33e0 /
@004b34ec + the byte-verified dispatch in the unexported Mech performance
FUN_004a9b5c (calls @0x4aaf74/0x4aaf83):
- ctor field map corrected: springConstant@0x1E8 / dampingConstant@0x1F4 were
mislabelled (the old 'eyeOrientation = r->springConstant' poison line), the
0x254-0x2B3 block (externalPitchPtr, work matrix, placement) was missing so
everything after was mis-offset, and clamps/accumulators were 0xCD fill --
every field now initialised per the binary; 33 offsets static_assert-locked,
sizeof == 0x3D0 exact
- integrators: state-minus-target displacements, componentwise damping
OVERWRITE (FUN_004086d0 is a component multiply, not a cross), position step
without dt, clamp to [posSpring, negSpring]; IntegrateBody's X/Z-crossed
force + integration terms
- writers: WriteMechJoint drives ONE node ('jointeye', type 5): TRANSLATION =
eyePosition spring, ROTATION = bodyOrientation spring; WriteEyeJoint is a
multiplicative sway attenuator on 'jointlocal' (post-anim only)
- dispatch moved out of GyroscopeSimulation into the Mech performance tail
(GyroFrameJointWrite bridge, mech4.cpp) with the binary gates; gyro<->torso
pitch link wired (gyro+0x258 = &torso currentTwist) via complete-type-TU
bridges, retiring the SubProxy::linkTarget landmine
- Mech ctor: deathAnimationLatched/legResetLatch were never initialised --
0xCDCDCDCD gated the writers off silently
Runtime-verified: joints resolve (both BallTranslation), Performance installs,
WriteMechJoint finite from frame 0 (no NaN), Madcat cockpit un-regressed,
combat targeting healthy. Empirical: spring targets are SYMMETRIC -> eye
equilibrium (0,0,0), clamps +/-0.1-0.15u -- the gyro is the hit-BOUNCE
mechanism, not a steady eye offset (hypothesis disproven, recorded in KB).
Pending (task #56 tail): the damage->gyro fan-out FUN_004b2980 (unexported
gap) so hits actually kick the springs; the mech+0x3F0 overspeed sway model;
the torso-pitch EyepointRotation writer (FUN_004b66b4) + glance-look states.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
908 lines
42 KiB
C++
908 lines
42 KiB
C++
//===========================================================================//
|
|
// File: gyro.cpp //
|
|
// Project: BattleTech Brick: Entity Manager //
|
|
// Contents: Gyroscope subsystem -- balance / orientation / tip-over model //
|
|
//---------------------------------------------------------------------------//
|
|
// Date Who Modification //
|
|
// -------- --- ---------------------------------------------------------- //
|
|
// --/--/95 ?? Initial coding. //
|
|
//---------------------------------------------------------------------------//
|
|
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
|
|
// PROPRIETARY AND CONFIDENTIAL //
|
|
//===========================================================================//
|
|
//
|
|
// RECONSTRUCTED from the shipped binary (Ghidra pseudo-C, recovered shard
|
|
// part_013.c). No header survived; see gyro.hpp for the class story. Each
|
|
// non-trivial method cites the originating @ADDR. Confidence is flagged per
|
|
// method: [CONFIDENT] body matches the decomp closely; [BEST-EFFORT] the
|
|
// shape is recovered but names/details are inferred; [EXCLUDED] not emitted.
|
|
//
|
|
// Hex constants converted to decimal:
|
|
// 0x3f800000 = 1.0f 0x3f000000 = 0.5f 0x40000000 = 2.0f
|
|
// _DAT_004b5b20 = -1.0f (resource "unset" sentinel)
|
|
// _DAT_004b5b24 = 0.0174532925f (PI/180, deg->rad)
|
|
// _DAT_004b297c = _DAT_004b3774 = 0.0f
|
|
// _DAT_004b34e8 = 0.0001f (quantise epsilon)
|
|
//
|
|
// Helper-function name mapping (engine internals referenced by the decomp):
|
|
// FUN_004b18a4 PowerWatcher base constructor (powersub.cpp)
|
|
// FUN_004b198c PowerWatcher::CreateStreamedSubsystem (powersub.cpp)
|
|
// FUN_004b1804 PowerWatcher::ResetToInitialState (slot10) (powersub.cpp)
|
|
// FUN_004b179c PowerWatcher slot-9 death/voltage handler (powersub.cpp)
|
|
// FUN_004b181c PowerWatcher per-frame watch update (powersub.cpp)
|
|
// FUN_00404118 NotationFile::ReadScalar(model,name,&dst)
|
|
// FUN_00404088 NotationFile::ReadString(model,name,&dst)
|
|
// FUN_004dbb24 DebugStream insert (error reporting)
|
|
// FUN_00408440 Vector3D::operator= / copy(3)
|
|
// FUN_00408e90 Point4D::operator= / copy(4)
|
|
// FUN_0040aadc Matrix/quat identity-init
|
|
// FUN_004085ec Vector3D += ; FUN_004086d0 Vector3D cross/mul
|
|
// FUN_004086ac Vector3D *= scalar ; FUN_00408644 Vector3D -
|
|
// FUN_00408744 Vector3D clamp-to-box ; FUN_004092fc Vector3D *= scalar
|
|
// FUN_0041cfa0/0041d020/0041d0a8/0041d11c skeleton-node get/set transform
|
|
// FUN_004dcd00 fabsf()
|
|
//
|
|
#include <bt.hpp>
|
|
#pragma hdrstop
|
|
|
|
#if !defined(GYRO_HPP)
|
|
# include <gyro.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 (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_004b297c / _DAT_004b3774
|
|
static const Scalar QuantiseEps = 0.0001f; // _DAT_004b34e8
|
|
static const Scalar Unset = -1.0f; // _DAT_004b5b20
|
|
static const Scalar DegToRad = 0.0174532925f; // _DAT_004b5b24
|
|
|
|
|
|
//#############################################################################
|
|
// Reconstruction shims (file-local).
|
|
//
|
|
// The DebugStream trace sink (FUN_004dbb24) is unavailable in this build (the
|
|
// heat.hpp/mechrecon.hpp diagnostic globals collide), so the "missing <field>!"
|
|
// resource diagnostics route through a local no-op sink here.
|
|
//
|
|
static struct GyroDebugSink
|
|
{
|
|
template<class T> GyroDebugSink &operator<<(const T &) { return *this; }
|
|
} DebugSink;
|
|
|
|
//
|
|
// Skeleton-node accessors -- now backed by the REAL engine Joint API (JOINT.h),
|
|
// no longer stubs. The gyro writes the integrated sway/orientation into two
|
|
// resolved skeleton joints (eyeJointNode / mechJointNode, Joint*). Mapping to
|
|
// the recovered helper addresses: FUN_0041cfa0 = GetEulerAngles, FUN_0041d0a8 =
|
|
// SetRotation(Radian) [hinge], FUN_0041d020 = SetRotation(EulerAngles) [ball],
|
|
// FUN_0041d11c = SetTranslation [BallTranslation]. All callers guard the joint
|
|
// TYPE before use (hinge<3 vs ball 4/5); NULL-guarded for bring-up (an unresolved
|
|
// joint simply skips the write instead of dereferencing).
|
|
//
|
|
static int NodeType(Joint *node) { return node ? (int)node->GetJointType() : (int)Joint::NULLJointType; }
|
|
static Scalar NodeScalar(Joint *node) { return node ? (Scalar)node->GetRadians() : 0.0f; } // FUN_0041cf.. (hinge)
|
|
static void SetNodeScalar(Joint *node, Scalar value) { if (node) node->SetRotation(Radian(value)); } // FUN_0041d0a8
|
|
static Vector3D NodeVector(Joint *node)
|
|
{
|
|
if (node == 0) return Vector3D(0.0f, 0.0f, 0.0f);
|
|
const EulerAngles &e = node->GetEulerAngles(); // FUN_0041cfa0
|
|
return Vector3D((Scalar)e.pitch, (Scalar)e.yaw, (Scalar)e.roll);
|
|
}
|
|
static void SetNodeVector(Joint *node, const Vector3D &v)
|
|
{ if (node) node->SetRotation(EulerAngles(v.x, v.y, v.z)); } // FUN_0041d020
|
|
static Logical NodeVectorEquals(Joint *node, const Vector3D &v, Scalar eps)
|
|
{
|
|
if (node == 0) return True; // no node -> skip the set
|
|
const EulerAngles &e = node->GetEulerAngles();
|
|
return fabsf((Scalar)e.pitch - v.x) <= eps
|
|
&& fabsf((Scalar)e.yaw - v.y) <= eps
|
|
&& fabsf((Scalar)e.roll - v.z) <= eps;
|
|
}
|
|
static void SetNodeRotation(Joint *node, const Vector3D &v)
|
|
{ if (node) node->SetTranslation(Point3D(v.x, v.y, v.z)); } // FUN_0041d11c (BallTranslation)
|
|
static Logical NodeRotationEquals(Joint *node, const Vector3D &v, Scalar eps)
|
|
{
|
|
if (node == 0) return True;
|
|
const Point3D &t = node->GetTranslation();
|
|
return fabsf(t.x - v.x) <= eps
|
|
&& fabsf(t.y - v.y) <= eps
|
|
&& fabsf(t.z - v.z) <= eps;
|
|
}
|
|
|
|
//
|
|
// Joint resolution -- forward to the owning Mech's shared resolver (Mech::
|
|
// ResolveJoint == binary FUN_00424b60, the engine GetSegment->GetJoint path).
|
|
//
|
|
static Joint * ResolveJoint(Mech *owner, const char *node_name)
|
|
{ return owner ? owner->ResolveJoint(node_name) : 0; }
|
|
|
|
//
|
|
// CROSS-FAMILY skeleton load/lookup used only by CreateStreamedSubsystem to
|
|
// verify the EyeJoint / MechJoint names exist in the model's skeleton file
|
|
// (FUN_004064fc / FUN_00403e84 / FUN_00403f84). Stubbed.
|
|
//
|
|
struct GyroSkeleton
|
|
{
|
|
Logical FindNode(const char * /*node_name*/) const { return True; }
|
|
};
|
|
static GyroSkeleton g_gyroSkeleton;
|
|
static GyroSkeleton *LoadSkeleton(const ResourceDirectories * /*dirs*/, const char * /*name*/)
|
|
{
|
|
return &g_gyroSkeleton;
|
|
}
|
|
|
|
//###########################################################################
|
|
// BASE-CHAIN RE-BASE -- compile-time layout lock (friend of Gyroscope so it can
|
|
// offsetof the protected own-block). After the Watcher-branch re-base + the shim
|
|
// delete, the gyro's first own field (exageration) must land at 0x1D8 (right after
|
|
// the shared PowerWatcher base), and the object must fit the 0x3D0 factory alloc.
|
|
// (The interior own-field layout is not yet fully binary-exact -- see CLAUDE.md --
|
|
// but it is self-consistent and fits, which is all the scoped un-stub needs.)
|
|
//###########################################################################
|
|
struct GyroLayoutCheck
|
|
{
|
|
// BINARY-EXACT own-block lock (ctor @004b3778 field map, task #56).
|
|
static_assert(offsetof(Gyroscope, exageration) == 0x1D8, "exageration @0x1D8");
|
|
static_assert(offsetof(Gyroscope, eyePosition) == 0x1DC, "eyePosition @0x1DC");
|
|
static_assert(offsetof(Gyroscope, springConstant) == 0x1E8, "springConstant @0x1E8");
|
|
static_assert(offsetof(Gyroscope, dampingConstant) == 0x1F4, "dampingConstant @0x1F4");
|
|
static_assert(offsetof(Gyroscope, eyeClampUpper) == 0x200, "eyeClampUpper @0x200");
|
|
static_assert(offsetof(Gyroscope, eyeClampLower) == 0x20C, "eyeClampLower @0x20C");
|
|
static_assert(offsetof(Gyroscope, posSpring) == 0x218, "posSpring @0x218");
|
|
static_assert(offsetof(Gyroscope, negSpring) == 0x224, "negSpring @0x224");
|
|
static_assert(offsetof(Gyroscope, eyeForce) == 0x230, "eyeForce @0x230");
|
|
static_assert(offsetof(Gyroscope, eyeVelocity) == 0x23C, "eyeVelocity @0x23C");
|
|
static_assert(offsetof(Gyroscope, eyeWork) == 0x248, "eyeWork @0x248");
|
|
static_assert(offsetof(Gyroscope, spare0) == 0x254, "spare0 @0x254");
|
|
static_assert(offsetof(Gyroscope, externalPitchPtr) == 0x258, "externalPitchPtr @0x258");
|
|
static_assert(offsetof(Gyroscope, workMatrix) == 0x25C, "workMatrix @0x25C");
|
|
static_assert(offsetof(Gyroscope, bodyOrientation) == 0x2B4, "bodyOrientation @0x2B4");
|
|
static_assert(offsetof(Gyroscope, rotationSpringConstant) == 0x2C0, "rotationSpringConstant @0x2C0");
|
|
static_assert(offsetof(Gyroscope, rotationDampingConstant) == 0x2CC, "rotationDampingConstant @0x2CC");
|
|
static_assert(offsetof(Gyroscope, bodyClampUpper) == 0x2D8, "bodyClampUpper @0x2D8");
|
|
static_assert(offsetof(Gyroscope, bodyClampLower) == 0x2E4, "bodyClampLower @0x2E4");
|
|
static_assert(offsetof(Gyroscope, rotationPosSpring) == 0x2F0, "rotationPosSpring @0x2F0");
|
|
static_assert(offsetof(Gyroscope, rotationNegSpring) == 0x2FC, "rotationNegSpring @0x2FC");
|
|
static_assert(offsetof(Gyroscope, bodyForce) == 0x308, "bodyForce @0x308");
|
|
static_assert(offsetof(Gyroscope, bodyVelocity) == 0x314, "bodyVelocity @0x314");
|
|
static_assert(offsetof(Gyroscope, bodyWork) == 0x320, "bodyWork @0x320");
|
|
static_assert(offsetof(Gyroscope, damageMultiplier) == 0x32C, "damageMultiplier @0x32C");
|
|
static_assert(offsetof(Gyroscope, damageResponse) == 0x340, "damageResponse @0x340");
|
|
static_assert(offsetof(Gyroscope, swayBias) == 0x3A8, "swayBias @0x3A8");
|
|
static_assert(offsetof(Gyroscope, swayAngle) == 0x3BC, "swayAngle @0x3BC");
|
|
static_assert(offsetof(Gyroscope, eyeJointNode) == 0x3C8, "eyeJointNode @0x3C8");
|
|
static_assert(offsetof(Gyroscope, mechJointNode) == 0x3CC, "mechJointNode @0x3CC");
|
|
static_assert(sizeof(Gyroscope) == 0x3D0, "Gyroscope must be exactly the 0x3D0 factory alloc");
|
|
};
|
|
|
|
|
|
//###########################################################################
|
|
//###########################################################################
|
|
// Gyroscope
|
|
//###########################################################################
|
|
//###########################################################################
|
|
|
|
//#############################################################################
|
|
// Shared Data Support (DefaultData @0050fdb0)
|
|
//
|
|
Derivation
|
|
Gyroscope::ClassDerivations(
|
|
PowerWatcher::GetClassDerivations(), // returns Derivation* (no &)
|
|
"Gyroscope"
|
|
);
|
|
|
|
Receiver::MessageHandlerSet
|
|
Gyroscope::MessageHandlers;
|
|
|
|
Gyroscope::AttributeIndexSet
|
|
Gyroscope::AttributeIndex;
|
|
|
|
Gyroscope::SharedData
|
|
Gyroscope::DefaultData(
|
|
&Gyroscope::ClassDerivations,
|
|
Gyroscope::MessageHandlers,
|
|
Gyroscope::AttributeIndex,
|
|
Gyroscope::StateCount
|
|
);
|
|
|
|
|
|
//#############################################################################
|
|
// Construction / Destruction
|
|
//
|
|
|
|
//
|
|
// @004b3778 [CONFIDENT] -- chains to the PowerWatcher base ctor (FUN_004b18a4)
|
|
// with &Gyroscope::DefaultData, installs the Gyroscope vtable (PTR @00510abc),
|
|
// registers GyroscopeSimulation as the Performance for a live master segment
|
|
// (flags & 0xC == 0 && flags & 1), copies the parsed scalars/vectors out of the
|
|
// resource, identity-inits the work matrices, resolves the two skeleton joints
|
|
// named by the resource (EyeJoint @+0x178, MechJoint @+0x198) into eyeJointNode
|
|
// /mechJointNode, and primes the animation-noise state.
|
|
//
|
|
Gyroscope::Gyroscope(
|
|
Mech *owner,
|
|
int subsystem_ID,
|
|
SubsystemResource *r,
|
|
SharedData &shared_data
|
|
):
|
|
PowerWatcher(owner, subsystem_ID, r, shared_data)
|
|
{
|
|
Check(owner);
|
|
Check_Pointer(r);
|
|
|
|
// BASE-CHAIN RE-BASE: the 4 CROSS-FAMILY shim backing fields were deleted; their
|
|
// accessors now read the real inherited base state, so nothing to prime here.
|
|
// The master/copy gate below reads the authoritative owner->simulationFlags.
|
|
|
|
// INTEGRATION (gate reconcile): read OWNER simulationFlags (param_2+0x28),
|
|
// the oracle-verified authoritative source, instead of the local segment-flag
|
|
// shim. GetSegmentFlags() backs a per-subsystem shim; the binary gate at
|
|
// @004b3778 reads owner+0x28.
|
|
if ((owner->simulationFlags & SegmentCopyMask) == 0 // (owner flags & 0xC)==0
|
|
&& (owner->simulationFlags & MasterHeatSinkFlag) != 0) // owner flags & 0x100
|
|
{
|
|
SetPerformance(&Gyroscope::GyroscopeSimulation); // this[7..9] = PTR @0050fe08
|
|
}
|
|
|
|
exageration = r->exageration; // @0x1D8 <- +0xF8
|
|
minAnimationNoise = r->minAnimationNoise; // @0x3A0 <- +0x100
|
|
maxAnimationNoise = r->maxAnimationNoise; // @0x39C <- +0xFC
|
|
rotationPerSecond = r->rotationPerSecond; // @0x3A4 <- +0x104
|
|
percentageOnNormal = r->percentageOnNormal; // @0x3AC <- +0x108
|
|
percentageOnDestruction = r->percentageOnDestruction; // @0x3B0 <- +0x10C
|
|
percentageOnDegradation = r->percentageOnDegradation; // @0x3B4 <- +0x110
|
|
percentageOnFailure = r->percentageOnFailure; // @0x3B8 <- +0x114
|
|
|
|
// BINARY-EXACT copies + inits (ctor @004b3778 lines 2812-2942 -- the OLD recon's
|
|
// single line `eyeOrientation = r->springConstant` was the NaN poison: it left
|
|
// springConstant/dampingConstant uninitialised AND seeded the eye state with a
|
|
// spring constant. Every accumulator below is zeroed in the binary; nothing is
|
|
// left as MSVC 0xCD fill.)
|
|
springConstant = r->springConstant; // @0x1E8 <- +0x118 (:2821)
|
|
dampingConstant = r->dampingConstant; // @0x1F4 <- +0x124 (:2823)
|
|
posSpring = r->posSpring; // @0x218 <- +0x130 (:2825)
|
|
negSpring = r->negSpring; // @0x224 <- +0x13C (:2827)
|
|
eyeClampUpper = posSpring; // @0x200 identity transform = copy (:2876-2879)
|
|
eyeClampLower = negSpring; // @0x20C (:2880-2883)
|
|
|
|
rotationSpringConstant = r->rotationSpringConstant; // @0x2C0 <- +0x148 (:2830)
|
|
rotationDampingConstant = r->rotationDampingConstant; // @0x2CC <- +0x154 (:2833)
|
|
rotationPosSpring = r->rotationPosSpring; // @0x2F0 <- +0x160 (:2834, already deg->rad)
|
|
rotationNegSpring = r->rotationNegSpring; // @0x2FC <- +0x16C (:2835)
|
|
bodyClampUpper.Multiply(rotationPosSpring, 2.0f); // @0x2D8 = 2.0 * rotPosSpring (:2884)
|
|
bodyClampLower.Multiply(rotationNegSpring, 2.0f); // @0x2E4 = 2.0 * rotNegSpring (:2885)
|
|
|
|
// per-damage-type response multipliers + {Trans,PitchRoll,Yaw,Vibration} quads (:2836-2875)
|
|
damageMultiplier[0] = r->collisionDamageMultiplier; // @0x32C
|
|
damageMultiplier[1] = r->ballisticDamageMultiplier;
|
|
damageMultiplier[2] = r->explosiveDamageMultiplier;
|
|
damageMultiplier[3] = r->laserDamageMultiplier;
|
|
damageMultiplier[4] = r->energyDamageMultiplier;
|
|
damageResponse[0] = r->collisionDamageResponse; // @0x340..0x38F
|
|
damageResponse[1] = r->ballisticDamageResponse;
|
|
damageResponse[2] = r->explosiveDamageResponse;
|
|
damageResponse[3] = r->laserDamageResponse;
|
|
damageResponse[4] = r->energyDamageResponse;
|
|
|
|
// integrator state + placement scratch: ALL zeroed by the binary (:2921-2942)
|
|
{
|
|
const Vector3D vzero(0.0f, 0.0f, 0.0f);
|
|
eyePosition = eyeForce = eyeVelocity = eyeWork = vzero; // @0x1DC/0x230/0x23C/0x248
|
|
bodyOrientation = bodyForce = bodyVelocity = bodyWork = vzero; // @0x2B4/0x308/0x314/0x320
|
|
placePos = placeRot = vzero; // @0x28C/0x2A8
|
|
placeQuat[0] = placeQuat[1] = placeQuat[2] = 0.0f; placeQuat[3] = 1.0f; // @0x298 identity quat (:2928)
|
|
for (int i = 0; i < 12; ++i) workMatrix[i] = 0.0f; // @0x25C identity 3x4 (:2793/:2934)
|
|
workMatrix[0] = workMatrix[5] = workMatrix[10] = 1.0f;
|
|
spare0 = 0.0f; // @0x254 (:2931)
|
|
externalPitchPtr = &spare0; // @0x258 self-pointer (:2933);
|
|
// bt_mech tail re-points to &torso pitch
|
|
}
|
|
animationOffset = 0.0f; // @0x390
|
|
animationScale = 1.0f; // @0x394
|
|
animationPhase = 0.0f; // @0x398
|
|
swayAngle = 0.0f; // @0x3BC
|
|
swayVelocity = 0.0f; // @0x3C0
|
|
swayActive = 0; // @0x3C4
|
|
swayBias = 0.0f; // @0x3A8
|
|
|
|
// EyeJoint / MechJoint were resolved against the skeleton during streaming;
|
|
// the ctor binds the two SkeletonConnection handles to live joint nodes:
|
|
eyeJointNode = ResolveJoint(owner, r->eyeJoint); // @0x3C8 this[0xF2]
|
|
mechJointNode = ResolveJoint(owner, r->mechJoint); // @0x3CC this[0xF3]
|
|
|
|
// bring-up verification (env BT_GYRO_LOG; default OFF): layout + joint resolve.
|
|
if (getenv("BT_GYRO_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[gyro] ctor this=" << (void*)this
|
|
<< " sizeof=" << (unsigned)sizeof(Gyroscope)
|
|
<< " exageration@" << (unsigned)((char*)&exageration - (char*)this)
|
|
<< " (want 0x1D8=" << (unsigned)0x1D8 << ")\n";
|
|
DEBUG_STREAM << "[gyro] eye '" << r->eyeJoint << "' -> " << (void*)eyeJointNode;
|
|
if (eyeJointNode) DEBUG_STREAM << " type=" << (int)eyeJointNode->GetJointType();
|
|
DEBUG_STREAM << " ; mech '" << r->mechJoint << "' -> " << (void*)mechJointNode;
|
|
if (mechJointNode) DEBUG_STREAM << " type=" << (int)mechJointNode->GetJointType();
|
|
DEBUG_STREAM << "\n" << std::flush;
|
|
// spring data (captures the eye equilibrium = (posSpring+negSpring)/2 -- the
|
|
// numeric values live only in the archived .MDL configs, not the EXE):
|
|
DEBUG_STREAM << "[gyro] springK=(" << (float)springConstant.x << "," << (float)springConstant.y
|
|
<< "," << (float)springConstant.z << ") dampK=(" << (float)dampingConstant.x << ","
|
|
<< (float)dampingConstant.y << "," << (float)dampingConstant.z << ")\n";
|
|
DEBUG_STREAM << "[gyro] posSpring=(" << (float)posSpring.x << "," << (float)posSpring.y
|
|
<< "," << (float)posSpring.z << ") negSpring=(" << (float)negSpring.x << ","
|
|
<< (float)negSpring.y << "," << (float)negSpring.z << ") -> eye equilibrium=("
|
|
<< (float)((posSpring.x+negSpring.x)*0.5f) << "," << (float)((posSpring.y+negSpring.y)*0.5f)
|
|
<< "," << (float)((posSpring.z+negSpring.z)*0.5f) << ")\n" << std::flush;
|
|
DEBUG_STREAM << "[gyro] simulationFlags=0x" << std::hex << (unsigned)owner->simulationFlags
|
|
<< std::dec << " performance "
|
|
<< ((((owner->simulationFlags & SegmentCopyMask) == 0)
|
|
&& ((owner->simulationFlags & MasterHeatSinkFlag) != 0)) ? "INSTALLED" : "NOT installed")
|
|
<< "\n" << std::flush;
|
|
}
|
|
|
|
Check_Fpu();
|
|
}
|
|
|
|
//
|
|
// @004b3e88 [CONFIDENT] -- reinstalls the vtable, runs the PowerWatcher
|
|
// teardown (FUN_004b1930 path) and frees the block when the deleting bit is set.
|
|
//
|
|
Gyroscope::~Gyroscope()
|
|
{
|
|
Check(this);
|
|
Check_Fpu();
|
|
}
|
|
|
|
//###########################################################################
|
|
// TestClass / TestInstance -- Gyroscope
|
|
//
|
|
// Standard subsystem convention (cf. HeatSink/Sensor). BEST-EFFORT: no
|
|
// distinct Gyroscope bodies were captured.
|
|
//
|
|
Logical Gyroscope::TestClass(Mech &) { return True; }
|
|
Logical Gyroscope::TestInstance() const { return IsDerivedFrom(ClassDerivations); }
|
|
|
|
|
|
//#############################################################################
|
|
// Subsystem virtual overrides
|
|
//
|
|
|
|
//
|
|
// @004b2678 (slot 10) [CONFIDENT] -- ResetToInitialState. Zeroes the sway and
|
|
// orientation accumulators, re-zeroes the eye/body work vectors (copies the
|
|
// engine zero-vector &DAT_004e0f74 / zero-quat &DAT_004e0f8c) and chains to
|
|
// PowerWatcher::ResetToInitialState (FUN_004b1804).
|
|
//
|
|
void
|
|
Gyroscope::ResetToInitialState()
|
|
{
|
|
swayAngle = 0.0f; // this[0xEF]
|
|
swayVelocity = 0.0f; // this[0xF0]
|
|
swayBias = 0.0f; // this[0xEA]
|
|
swayActive = 0; // this[0xF1]
|
|
|
|
const Vector3D zero(0.0f, 0.0f, 0.0f);
|
|
|
|
// @004b2678 zeroes the EIGHT integrator vectors (:2190-2197): eye pos/force/
|
|
// velocity/work + body orientation/force/velocity/work. (The old recon zeroed
|
|
// the SPRING TARGET bodyPosSpring instead of bodyOrientation -- wrong member.)
|
|
eyePosition = zero; // @0x1DC FUN_00408440(this+0x77, &DAT_004e0f74)
|
|
eyeForce = zero; // @0x230
|
|
eyeVelocity = zero; // @0x23C
|
|
eyeWork = zero; // @0x248
|
|
bodyOrientation = zero; // @0x2B4 FUN_00408e90(this+0xAD, &DAT_004e0f8c)
|
|
bodyForce = zero; // @0x308
|
|
bodyVelocity = zero; // @0x314
|
|
bodyWork = zero; // @0x320
|
|
|
|
// CROSS-FAMILY: PowerWatcher (PowerWatcher : Subsystem in these headers) has
|
|
// no slot-10 ResetToInitialState() to chain to; the base reset (FUN_004b1804)
|
|
// belongs on the PowerWatcher/Subsystem family. See "CROSS-FAMILY NEEDS".
|
|
}
|
|
|
|
//
|
|
// @004b2660 (slot 9) [CONFIDENT] -- forwards to the PowerWatcher slot-9 handler
|
|
// (FUN_004b179c). That handler, on a "destroyed" message (msg->type == 4),
|
|
// resolves the watched power source and clears its voltage alarm; otherwise it
|
|
// chains to the base. Exact message semantics are inherited from PowerWatcher.
|
|
//
|
|
Logical
|
|
Gyroscope::HandleDeathMessage(Message &/*message*/)
|
|
{
|
|
// CROSS-FAMILY: forwards to the PowerWatcher slot-9 death/voltage handler
|
|
// (FUN_004b179c). PowerWatcher in these headers exposes HandleMessage(int),
|
|
// not a HandleDeathMessage(Message&), so there is no compatible base method
|
|
// to chain to here; pass the message through as handled. See "CROSS-FAMILY
|
|
// NEEDS".
|
|
return True;
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// Per-frame simulation
|
|
//
|
|
|
|
//
|
|
// @004b275c [CONFIDENT] -- the registered Performance (PTR @0050fe08).
|
|
//
|
|
// Runs the PowerWatcher watch update first (FUN_004b181c), then walks the idle
|
|
// "sway" toward a damage-state-dependent target at rotationPerSecond * dt:
|
|
// * if the watched power source is dead (this[0x10]==1), the subsystem is not
|
|
// electrically Ready (this @0x198 != 4), OR it is in the Failure heat state
|
|
// (this @0x140 == 2): aim at (percentageOnDestruction + swayBias);
|
|
// * otherwise aim at (percentageOnNormal + swayBias).
|
|
// The step is sign-corrected so it never overshoots the target, then swayAngle
|
|
// is clamped into [minAnimationNoise, maxAnimationNoise]. Finally the two
|
|
// spring-damper integrators run.
|
|
//
|
|
void
|
|
Gyroscope::GyroscopeSimulation(Scalar time_slice)
|
|
{
|
|
Check(this);
|
|
|
|
PowerWatcher::Simulation(time_slice); // FUN_004b181c -- base per-frame watch update
|
|
|
|
const Logical impaired =
|
|
HeatModelOff() // this[0x10] == 1
|
|
|| ElectricalStateLevel() != PoweredSubsystem::Ready // this @0x198 != 4
|
|
|| HeatStateLevel() == HeatSink::FailureHeat; // this @0x140 == 2
|
|
|
|
Scalar target = (impaired ? percentageOnDestruction
|
|
: percentageOnNormal) + swayBias;
|
|
|
|
Scalar step = rotationPerSecond * time_slice;
|
|
if (target < swayAngle) // moving down toward target
|
|
{
|
|
step = -step;
|
|
}
|
|
swayAngle += step;
|
|
|
|
// clamp so we do not pass the target this frame
|
|
if (step <= Zero)
|
|
{
|
|
if (step < Zero && swayAngle < target) swayAngle = target;
|
|
}
|
|
else
|
|
{
|
|
if (swayAngle > target) swayAngle = target;
|
|
}
|
|
|
|
// clamp into the [min, max] animation-noise band
|
|
if (swayAngle > maxAnimationNoise) swayAngle = maxAnimationNoise;
|
|
if (swayAngle < minAnimationNoise) swayAngle = minAnimationNoise;
|
|
|
|
IntegrateEyeJoint(time_slice); // FUN_004b2ec0
|
|
IntegrateBody(time_slice); // FUN_004b30ec
|
|
|
|
// NOTE (task #56, byte-verified): the binary Performance @004b275c ENDS here.
|
|
// WriteEyeJoint/WriteMechJoint are NOT called from the gyro -- they are called
|
|
// from the MECH master performance tail (FUN_004a9b5c, calls @0x4aaf74/0x4aaf83)
|
|
// after the animation pass, with the death/leg-anim gates. See
|
|
// GyroFrameJointWrite (bridge below) + the mech4.cpp dispatch.
|
|
Check_Fpu();
|
|
}
|
|
|
|
//
|
|
// @004b2ec0 [T1 byte-exact, task #56] -- the eye TRANSLATION spring integrator.
|
|
// Displacements are STATE minus TARGET (the config spring constants are negative
|
|
// = restoring), both terms use springConstant, the damping step OVERWRITES the
|
|
// force accumulator with dampingConstant (.) velocity (FUN_004086d0 is a
|
|
// COMPONENT-WISE multiply, not a cross), and the position step has NO dt (the
|
|
// binary passes velocity through an identity matrix). eyeForce is NOT cleared
|
|
// per frame -- it carries last frame's damping term + damage impulses by design.
|
|
// Equilibrium: eyePosition -> (posSpring+negSpring)/2 per axis = the authentic
|
|
// STEADY EYE OFFSET (written to 'jointeye' translation by WriteMechJoint).
|
|
//
|
|
void
|
|
Gyroscope::IntegrateEyeJoint(Scalar time_slice)
|
|
{
|
|
Vector3D d1; d1.Subtract(eyePosition, negSpring); // :2376 state - target
|
|
Vector3D d2; d2.Subtract(eyePosition, posSpring); // :2377
|
|
|
|
Vector3D f;
|
|
f.Multiply(springConstant, d1); eyeForce += f; // :2378-2381 componentwise
|
|
f.Multiply(springConstant, d2); eyeForce += f;
|
|
|
|
eyeWork = eyeForce; // :2383
|
|
f.Multiply(eyeWork, time_slice); eyeVelocity += f; // :2384-2385
|
|
|
|
eyeForce.Multiply(dampingConstant, eyeVelocity); // :2387 OVERWRITE (componentwise)
|
|
eyeWork = eyeForce;
|
|
f.Multiply(eyeWork, time_slice); eyeVelocity += f; // :2388-2390
|
|
|
|
eyePosition += eyeVelocity; // :2391-2393 identity pass-through, NO dt
|
|
|
|
// per-axis clamp: min against eyeClampUpper(0x200 = posSpring), then
|
|
// max against eyeClampLower(0x20C = negSpring)
|
|
if (eyePosition.x > eyeClampUpper.x) eyePosition.x = eyeClampUpper.x;
|
|
if (eyePosition.y > eyeClampUpper.y) eyePosition.y = eyeClampUpper.y;
|
|
if (eyePosition.z > eyeClampUpper.z) eyePosition.z = eyeClampUpper.z;
|
|
if (eyePosition.x < eyeClampLower.x) eyePosition.x = eyeClampLower.x;
|
|
if (eyePosition.y < eyeClampLower.y) eyePosition.y = eyeClampLower.y;
|
|
if (eyePosition.z < eyeClampLower.z) eyePosition.z = eyeClampLower.z;
|
|
}
|
|
|
|
//
|
|
// @004b30ec [T1 byte-exact, task #56] -- the body ROTATION spring integrator.
|
|
// Same shape as the eye integrator with three verified quirks: (1) BOTH spring
|
|
// terms use rotationSpringConstant (never damping); (2) the spring force
|
|
// components are X/Z-CROSSED: force = (k.x*d.z, k.y*d.y, k.z*d.x); (3) the
|
|
// integration back into bodyOrientation is crossed again (ori.x += vel.z,
|
|
// ori.z += vel.x). Damping is componentwise and UNcrossed. Clamps go min
|
|
// against bodyClampUpper (2*rotPosSpring) then max against bodyClampLower
|
|
// (2*rotNegSpring), in y,x,z order.
|
|
//
|
|
void
|
|
Gyroscope::IntegrateBody(Scalar time_slice)
|
|
{
|
|
Vector3D dN; dN.Subtract(bodyOrientation, rotationNegSpring); // state - target
|
|
Vector3D dP; dP.Subtract(bodyOrientation, rotationPosSpring);
|
|
|
|
const Vector3D &k = rotationSpringConstant;
|
|
Vector3D f;
|
|
f.x = k.x * dN.z; f.y = k.y * dN.y; f.z = k.z * dN.x; // :2479-2481 X/Z crossed
|
|
bodyForce += f;
|
|
f.x = k.x * dP.z; f.y = k.y * dP.y; f.z = k.z * dP.x; // :2482-2484
|
|
bodyForce += f;
|
|
|
|
bodyWork = bodyForce;
|
|
f.Multiply(bodyWork, time_slice); bodyVelocity += f;
|
|
|
|
bodyForce.Multiply(rotationDampingConstant, bodyVelocity); // :2498 componentwise, uncrossed, OVERWRITE
|
|
bodyWork = bodyForce;
|
|
f.Multiply(bodyWork, time_slice); bodyVelocity += f;
|
|
|
|
bodyOrientation.x += bodyVelocity.z; // :2504-2506 crossed again
|
|
bodyOrientation.y += bodyVelocity.y;
|
|
bodyOrientation.z += bodyVelocity.x;
|
|
|
|
if (bodyOrientation.y > bodyClampUpper.y) bodyOrientation.y = bodyClampUpper.y; // @0x2DC
|
|
if (bodyOrientation.x > bodyClampUpper.x) bodyOrientation.x = bodyClampUpper.x; // @0x2D8
|
|
if (bodyOrientation.z > bodyClampUpper.z) bodyOrientation.z = bodyClampUpper.z; // @0x2E0
|
|
if (bodyOrientation.y < bodyClampLower.y) bodyOrientation.y = bodyClampLower.y; // @0x2E8
|
|
if (bodyOrientation.x < bodyClampLower.x) bodyOrientation.x = bodyClampLower.x; // @0x2E4
|
|
if (bodyOrientation.z < bodyClampLower.z) bodyOrientation.z = bodyClampLower.z; // @0x2EC
|
|
}
|
|
|
|
//
|
|
// @004b33e0 [CONFIDENT] -- push swayAngle into the EyeJoint skeleton node.
|
|
// Depending on the node's parameterisation (type field @+0x10) it either scales
|
|
// the node's scalar channel by swayAngle (types 0..2) or interpolates the node's
|
|
// vector channel toward the swayed value (types 4..5), writing back only when
|
|
// the change exceeds QuantiseEps. (FUN_004b2eac @004b2eac thunks to here.)
|
|
//
|
|
void
|
|
Gyroscope::WriteEyeJoint()
|
|
{
|
|
Scalar value = swayAngle;
|
|
int type = NodeType(eyeJointNode); // *(node + 0x10)
|
|
|
|
if (type < 3)
|
|
{
|
|
Scalar base = NodeScalar(eyeJointNode); // *(*(node+0xC)+4)
|
|
Scalar out = value * base;
|
|
if (fabsf(base - out) > QuantiseEps)
|
|
{
|
|
SetNodeScalar(eyeJointNode, out); // FUN_0041d0a8
|
|
}
|
|
}
|
|
else if (type == 4 || type == 5)
|
|
{
|
|
Vector3D out = NodeVector(eyeJointNode); out *= value; // FUN_004092fc (scale)
|
|
if (!NodeVectorEquals(eyeJointNode, out, QuantiseEps)) // FUN_004091f4
|
|
{
|
|
SetNodeVector(eyeJointNode, out); // FUN_0041d020
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// @004b34ec [T1 byte-exact, task #56] -- push the integrated state into the
|
|
// MechJoint node ('jointeye', type 5 BallTranslation ONLY): TRANSLATION <-
|
|
// eyePosition (the steady eye offset + hit bounce; siteeyepoint rides this
|
|
// joint), ROTATION <- bodyOrientation (the body tip), each written only when
|
|
// it differs by more than QuantiseEps. ONE node, both channels.
|
|
//
|
|
void
|
|
Gyroscope::WriteMechJoint()
|
|
{
|
|
if (NodeType(mechJointNode) != 5)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// bring-up verification (env BT_GYRO_LOG): confirm the per-frame write fires
|
|
// with the integrated eye/body orientation (sampled periodically).
|
|
static const int s_glog = getenv("BT_GYRO_LOG") ? 1 : 0;
|
|
static int s_gc = 0;
|
|
if (s_glog && (s_gc % 60) == 0 && s_gc < 1200)
|
|
{
|
|
DEBUG_STREAM << "[gyro] WriteMechJoint eyePos=(" << (float)eyePosition.x << "," << (float)eyePosition.y
|
|
<< "," << (float)eyePosition.z << ") body=(" << (float)bodyOrientation.x << ","
|
|
<< (float)bodyOrientation.y << "," << (float)bodyOrientation.z << ")\n" << std::flush;
|
|
if (!(eyePosition.x == eyePosition.x) || !(bodyOrientation.x == bodyOrientation.x))
|
|
DEBUG_STREAM << "[gyro] *** NaN DETECTED in integrator state ***\n" << std::flush;
|
|
}
|
|
++s_gc;
|
|
if (!NodeRotationEquals(mechJointNode, eyePosition, QuantiseEps)) // FUN_004084fc vs value+0
|
|
{
|
|
SetNodeRotation(mechJointNode, eyePosition); // FUN_0041d11c = SetTRANSLATION
|
|
}
|
|
if (!NodeVectorEquals(mechJointNode, bodyOrientation, QuantiseEps)) // FUN_004091f4
|
|
{
|
|
SetNodeVector(mechJointNode, bodyOrientation); // FUN_0041d020
|
|
}
|
|
}
|
|
|
|
//
|
|
// @004b357c [BEST-EFFORT] -- procedural animation-noise generator. Lazily
|
|
// (re)arms a 2.0s noise window (swayVelocity timer @0x3C0, swayActive @0x3C4),
|
|
// interpolates the EyeJoint node toward a randomised pose proportional to the
|
|
// remaining window, and returns True while the window is still open. The switch
|
|
// over the node type mirrors WriteEyeJoint. Returns whether the timer expired.
|
|
//
|
|
Logical
|
|
Gyroscope::UpdateAnimationNoise(Scalar time_slice)
|
|
{
|
|
if (swayActive == 0)
|
|
{
|
|
swayActive = 1;
|
|
swayVelocity = 2.0f; // arm a 2.0s window (0x40000000)
|
|
}
|
|
|
|
Scalar phase = time_slice / swayVelocity;
|
|
|
|
switch (NodeType(eyeJointNode))
|
|
{
|
|
case 0: case 1: case 2:
|
|
// scalar-channel noise injection (FUN_00408dd4 + FUN_0041d0a8)
|
|
break;
|
|
case 4: case 5:
|
|
// vector-channel slerp toward random target (FUN_00409390 + FUN_0041d020)
|
|
break;
|
|
}
|
|
|
|
swayVelocity -= time_slice;
|
|
if (swayVelocity <= Zero)
|
|
{
|
|
swayActive = 0;
|
|
}
|
|
return (swayVelocity <= Zero);
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// Damage / impulse hooks
|
|
//
|
|
|
|
//
|
|
// @004b2d8c [CONFIDENT] -- negate the (x,y,z) hit direction, scale the supplied
|
|
// magnitude by `exageration`, and add the resulting impulse into eyeAccel.
|
|
//
|
|
void
|
|
Gyroscope::ApplyDamageImpulse(Scalar x, Scalar y, Scalar z, Scalar magnitude)
|
|
{
|
|
Vector3D dir(-x, -y, -z);
|
|
magnitude *= exageration; // *(this+0x1D8)
|
|
dir *= magnitude; // FUN_004086ac
|
|
eyeForce += dir; // FUN_004085ec (this+0x230)
|
|
}
|
|
|
|
//
|
|
// @004b2de4 [CONFIDENT] -- as above but adds into the body force accumulator
|
|
// (bodyForce @0x308), then zeroes its .y (this+0x30C) and flips the sign of
|
|
// the whole vector -- a one-shot torque kick. (The old recon wrote a member
|
|
// at the WRONG offset here; 0x30C is bodyForce.y in the binary layout.)
|
|
//
|
|
void
|
|
Gyroscope::ApplyDamageTorque(Scalar x, Scalar y, Scalar z, Scalar magnitude)
|
|
{
|
|
Vector3D dir(-x, -y, -z);
|
|
magnitude *= exageration;
|
|
dir *= magnitude;
|
|
bodyForce += dir; // FUN_004085ec (this+0x308)
|
|
bodyForce.y = 0.0f; // *(this+0x30C) = 0
|
|
bodyForce.Negate(bodyForce); // *(this+0x308) = -*(this+0x308)
|
|
}
|
|
|
|
//
|
|
// @004b2e50 [CONFIDENT] -- a purely-vertical (pitch) kick: only the first
|
|
// component survives (y,z forced to 0), scaled by exageration, added to bodyForce
|
|
// and mirrored into its .y (this+0x30C = this+0x308).
|
|
//
|
|
void
|
|
Gyroscope::ApplyVerticalImpulse(Scalar pitch, Scalar, Scalar, Scalar magnitude)
|
|
{
|
|
Vector3D dir(pitch, 0.0f, 0.0f);
|
|
magnitude *= exageration;
|
|
dir *= magnitude;
|
|
bodyForce += dir; // FUN_004085ec (this+0x308)
|
|
bodyForce.y = bodyForce.x; // *(this+0x30C) = *(this+0x308)
|
|
}
|
|
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// CreateStreamedSubsystem -- Gyroscope
|
|
//
|
|
// @004b3eb4 [CONFIDENT for the field list / classID / size; the body below is
|
|
// the heat.cpp-style linearisation of the deeply-nested original]. Chains to
|
|
// PowerWatcher::CreateStreamedSubsystem (FUN_004b198c), stamps the resource, and
|
|
// reads every mandatory field in the exact order the binary does. On pass 1
|
|
// each Scalar is primed to -1.0f (Unset); a field is "missing" if the read fails
|
|
// AND the slot still equals -1.0f. The angular Rotation*Spring* fields are
|
|
// multiplied by PI/180 after reading. The EyeJoint / MechJoint names are
|
|
// resolved against the model's "skeleton" file.
|
|
//
|
|
int
|
|
Gyroscope::CreateStreamedSubsystem(
|
|
NotationFile *model_file,
|
|
const char *model_name,
|
|
const char *subsystem_name,
|
|
SubsystemResource *r,
|
|
NotationFile *subsystem_file,
|
|
const ResourceDirectories *directories,
|
|
int passes
|
|
)
|
|
{
|
|
if (
|
|
!PowerWatcher::CreateStreamedSubsystem( // FUN_004b198c
|
|
model_file, model_name, subsystem_name,
|
|
r, subsystem_file, directories, passes
|
|
)
|
|
)
|
|
{
|
|
return False;
|
|
}
|
|
|
|
r->subsystemModelSize = sizeof(*r); // resource +0x24 = 0x21C
|
|
r->classID = RegisteredClass::GyroscopeClassID; // resource +0x20 = 0x0BC4
|
|
|
|
#define REQ_SCALAR(NAME, FIELD) \
|
|
if (!model_file->GetEntry(subsystem_name, NAME, &r->FIELD) \
|
|
&& r->FIELD == Unset) \
|
|
{ DebugSink << subsystem_name << " missing " << NAME << "!"; return False; }
|
|
|
|
REQ_SCALAR("PercentageOnNormal", percentageOnNormal) // +0x108
|
|
REQ_SCALAR("PercentageOnDestruction", percentageOnDestruction) // +0x10C
|
|
REQ_SCALAR("PercentageOnDegradation", percentageOnDegradation) // +0x110
|
|
REQ_SCALAR("PercentageOnFailure", percentageOnFailure) // +0x114
|
|
REQ_SCALAR("RotationPerSecond", rotationPerSecond) // +0x104
|
|
REQ_SCALAR("Exageration", exageration) // +0xF8
|
|
REQ_SCALAR("MaxAnimationNoise", maxAnimationNoise) // +0xFC
|
|
REQ_SCALAR("MinAnimationNoise", minAnimationNoise) // +0x100
|
|
|
|
REQ_SCALAR("SpringConstantX", springConstant.x) REQ_SCALAR("SpringConstantY", springConstant.y) REQ_SCALAR("SpringConstantZ", springConstant.z)
|
|
REQ_SCALAR("DampingConstantX", dampingConstant.x) REQ_SCALAR("DampingConstantY", dampingConstant.y) REQ_SCALAR("DampingConstantZ", dampingConstant.z)
|
|
REQ_SCALAR("NegSpringX", negSpring.x) REQ_SCALAR("NegSpringY", negSpring.y) REQ_SCALAR("NegSpringZ", negSpring.z)
|
|
REQ_SCALAR("PosSpringX", posSpring.x) REQ_SCALAR("PosSpringY", posSpring.y) REQ_SCALAR("PosSpringZ", posSpring.z)
|
|
|
|
// rotationSpringConstant / rotationDampingConstant are Vector3D ordered
|
|
// Roll(.x,+0x148) / Yaw(.y,+0x14C) / Pitch(.z,+0x150):
|
|
REQ_SCALAR("RotationSpringConstantPitch", rotationSpringConstant.z) // +0x150
|
|
REQ_SCALAR("RotationSpringConstantYaw", rotationSpringConstant.y) // +0x14C
|
|
REQ_SCALAR("RotationSpringConstantRoll", rotationSpringConstant.x) // +0x148
|
|
REQ_SCALAR("RotationDampingConstantPitch", rotationDampingConstant.z) // +0x15C
|
|
REQ_SCALAR("RotationDampingConstantYaw", rotationDampingConstant.y) // +0x158
|
|
REQ_SCALAR("RotationDampingConstantRoll", rotationDampingConstant.x) // +0x154
|
|
|
|
// the eight Rotation{Neg,Pos}Spring{Pitch,Yaw,Roll} fields read into a temp
|
|
// (default -1.0f) and, when present, are stored * DegToRad:
|
|
#define REQ_ANGLE(NAME, FIELD) \
|
|
{ Scalar a = Unset; \
|
|
if (!model_file->GetEntry(subsystem_name, NAME, &a) && r->FIELD == Unset) \
|
|
{ DebugSink << subsystem_name << " missing " << NAME << "!"; return False; } \
|
|
if (a != Unset) r->FIELD = a * DegToRad; }
|
|
|
|
// rotationPos/NegSpring are Vector3D ordered Pitch(.x) / Yaw(.y) / Roll(.z):
|
|
REQ_ANGLE("RotationNegSpringPitch", rotationNegSpring.x) // +0x16C
|
|
REQ_ANGLE("RotationNegSpringYaw", rotationNegSpring.y) // +0x170
|
|
REQ_ANGLE("RotationNegSpringRoll", rotationNegSpring.z) // +0x174
|
|
REQ_ANGLE("RotationPosSpringPitch", rotationPosSpring.x) // +0x160
|
|
REQ_ANGLE("RotationPosSpringYaw", rotationPosSpring.y) // +0x164
|
|
REQ_ANGLE("RotationPosSpringRoll", rotationPosSpring.z) // +0x168
|
|
|
|
REQ_SCALAR("CollisionDamageMultiplier", collisionDamageMultiplier) // +0x1B8
|
|
REQ_SCALAR("BallisticDamageMultiplier", ballisticDamageMultiplier) // +0x1BC
|
|
REQ_SCALAR("ExplosiveDamageMultiplier", explosiveDamageMultiplier) // +0x1C0
|
|
REQ_SCALAR("LaserDamageMultiplier", laserDamageMultiplier) // +0x1C4
|
|
REQ_SCALAR("EnergyDamageMultiplier", energyDamageMultiplier) // +0x1C8
|
|
|
|
// each damage type then reads Trans / PitchRoll / Yaw / Vibration:
|
|
REQ_SCALAR("CollisionDamageTrans", collisionDamageResponse.trans) REQ_SCALAR("CollisionDamagePitchRoll", collisionDamageResponse.pitchRoll) REQ_SCALAR("CollisionDamageYaw", collisionDamageResponse.yaw) REQ_SCALAR("CollisionDamageVibration", collisionDamageResponse.vibration)
|
|
REQ_SCALAR("BallisticDamageTrans", ballisticDamageResponse.trans) REQ_SCALAR("BallisticDamagePitchRoll", ballisticDamageResponse.pitchRoll) REQ_SCALAR("BallisticDamageYaw", ballisticDamageResponse.yaw) REQ_SCALAR("BallisticDamageVibration", ballisticDamageResponse.vibration)
|
|
REQ_SCALAR("ExplosiveDamageTrans", explosiveDamageResponse.trans) REQ_SCALAR("ExplosiveDamagePitchRoll", explosiveDamageResponse.pitchRoll) REQ_SCALAR("ExplosiveDamageYaw", explosiveDamageResponse.yaw) REQ_SCALAR("ExplosiveDamageVibration", explosiveDamageResponse.vibration)
|
|
REQ_SCALAR("LaserDamageTrans", laserDamageResponse.trans) REQ_SCALAR("LaserDamagePitchRoll", laserDamageResponse.pitchRoll) REQ_SCALAR("LaserDamageYaw", laserDamageResponse.yaw) REQ_SCALAR("LaserDamageVibration", laserDamageResponse.vibration)
|
|
REQ_SCALAR("EnergyDamageTrans", energyDamageResponse.trans) REQ_SCALAR("EnergyDamagePitchRoll", energyDamageResponse.pitchRoll) REQ_SCALAR("EnergyDamageYaw", energyDamageResponse.yaw) REQ_SCALAR("EnergyDamageVibration", energyDamageResponse.vibration)
|
|
|
|
// optional joint names default to "Unspecified"; if specified they must
|
|
// resolve in the skeleton file (else "missing <EyeJoint|MechJoint>!").
|
|
const char *eye = "Unspecified";
|
|
model_file->GetEntry(subsystem_name, "EyeJoint", &eye);
|
|
if (strcmp(eye, "Unspecified") != 0) strcpy(r->eyeJoint, eye); // -> +0x178
|
|
|
|
const char *mech = "Unspecified";
|
|
model_file->GetEntry(subsystem_name, "MechJoint", &mech);
|
|
if (strcmp(mech, "Unspecified") != 0) strcpy(r->mechJoint, mech); // -> +0x198
|
|
|
|
// load the model's "skeleton" file and verify both joints exist in it.
|
|
// CROSS-FAMILY: LoadSkeleton/Skeleton::FindNode are stubbed here (see top of
|
|
// file); the real skeleton lookup belongs to the Skeleton/Mech family.
|
|
const char *skeleton = 0;
|
|
if (!model_file->GetEntry("video", "skeleton", &skeleton))
|
|
{
|
|
DebugSink << model_name << " is missing skeleton file!";
|
|
return -1;
|
|
}
|
|
GyroSkeleton *skl = LoadSkeleton(directories, skeleton); // FUN_004064fc / FUN_00403e84
|
|
if (!skl->FindNode(r->eyeJoint)) // FUN_00403f84
|
|
{
|
|
DebugSink << r->eyeJoint << " not found in " << skeleton; return -1;
|
|
}
|
|
if (!skl->FindNode(r->mechJoint))
|
|
{
|
|
DebugSink << r->mechJoint << " not found in " << skeleton; return -1;
|
|
}
|
|
|
|
#undef REQ_SCALAR
|
|
#undef REQ_ANGLE
|
|
Check_Fpu();
|
|
return True;
|
|
}
|
|
|
|
|
|
//===========================================================================//
|
|
// WAVE 5 factory bridge -- Gyroscope (factory case 0xBC4). Constructs the real
|
|
// Gyroscope (ctor @004b3778) in the binary's 0x3D0 alloc. The Watcher base is
|
|
// re-based (exageration@0x1D8, locked above) so it shares only MechSubsystem with
|
|
// the heat leaves -- no heat-roster interaction (Gyroscope is not a HeatSink).
|
|
//===========================================================================//
|
|
Subsystem *CreateGyroSubsystem(Mech *owner, int id, void *seg)
|
|
{
|
|
return (Subsystem *) new (Memory::Allocate(0x3D0))
|
|
Gyroscope(owner, id, (Gyroscope::SubsystemResource *)seg, Gyroscope::DefaultData);
|
|
}
|
|
|
|
//===========================================================================//
|
|
// Per-frame joint-write dispatch bridge (task #56, byte-verified): the binary
|
|
// Mech master performance FUN_004a9b5c calls WriteEyeJoint @0x4aaf74 /
|
|
// WriteMechJoint @0x4aaf83 each frame AFTER the animation pass, preceded by
|
|
// gyro->swayBias = mech sway accumulator, gated !deathAnimationLatched (both)
|
|
// and legAnimationState != 0 (eye writer only; WriteEyeJoint is MULTIPLICATIVE
|
|
// on the joint's animated rotation, so it must never run on a joint the
|
|
// animation pass didn't just re-write). mech4.cpp calls this bridge from the
|
|
// same spot in the port's master performance (complete-type TU rule).
|
|
//===========================================================================//
|
|
void GyroFrameJointWrite(Subsystem *gyro, Scalar sway_bias,
|
|
int leg_animation_state, int death_animation_latched)
|
|
{
|
|
if (gyro == 0)
|
|
return;
|
|
Gyroscope *g = static_cast<Gyroscope *>(gyro);
|
|
g->SetSwayBias(sway_bias); // gyro+0x3A8 = mech+0x3F0 (bytes 0x4aaf18-3c)
|
|
if (death_animation_latched != 0) // mech+0x650 gate
|
|
return;
|
|
if (leg_animation_state != 0) // mech+0x3B0 gate (+ mech+0x57C, always 0)
|
|
g->WriteEyeJoint(); // @0x4aaf74
|
|
g->WriteMechJoint(); // @0x4aaf83
|
|
}
|
|
|
|
//===========================================================================//
|
|
// Post-stream gyro<->torso link (task #56, binary bt_mech tail): the binary
|
|
// re-points gyro+0x258 to &torso pitch (torso+0x1D8) after subsystem streaming;
|
|
// the damage-response path reads it. (Replaces the old SubProxy::linkTarget
|
|
// landmine, which wrote through an engine base field at gyro+4.)
|
|
//===========================================================================//
|
|
void GyroBindExternalPitch(Subsystem *gyro, Scalar *pitch)
|
|
{
|
|
if (gyro != 0)
|
|
static_cast<Gyroscope *>(gyro)->BindExternalPitch(pitch);
|
|
}
|