Files
BT411/game/reconstructed/gyro.cpp
T
arcattackandClaude Opus 4.8 7b7d465e5e Initial commit: bt411 -- standalone Windows BattleTech (Tesla 4.10 port)
Clean, self-contained extraction of the BattleTech-specific work from the
reverse-engineering workspace -- engine + game + content + build, with nothing
from Red Planet or the raw archive dumps. Builds green (Win32) and runs the
single-player drive->animate->target->fire->damage->destroy loop out of the box.

Layout:
  engine/   MUNGA + MUNGA_L4 shared 2007 engine, carrying our BT render/loader
            work (bgfload/L4D3D/L4VIDEO: BSL bit-slice decode, LOD/ground/shadow
            models) + image codec; the minimal rp/ headers the audio HAL needs
  game/     reconstructed BT logic + surviving-original BT source + fwd shims
            + WinMain launcher
  content/  full runtime tree (BTL4.RES, VIDEO/, GAUGE/, AUDIO/, eggs, BTDPL.INI)
  docs/     format specs + reconstruction ledgers
  reference/ raw Ghidra pseudocode (recon source-of-truth) + decomp exporter
  tools/    MP console emulator + map/resource scanners

One top-level CMake builds munga_engine lib + bt410_l4 game lib + btl4.exe.
All paths relativized (186 fwd shims + ~437 CMake abs paths -> repo-relative);
DXSDK is the one external, overridable via -DDXSDK. Verified: builds to a
byte-identical 2.27MB exe and runs combat (TARGET DESTROYED, 0 crashes) against
the bundled content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:03:40 -05:00

790 lines
34 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
{
static_assert(offsetof(Gyroscope, exageration) == 0x1D8, "Gyroscope exageration must be at 0x1D8 after re-base");
static_assert(sizeof(Gyroscope) <= 0x3D0, "Gyroscope must fit 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
eyeOrientation = r->springConstant; // @0x1DC (+0x118 -> spring work)
// the eye spring/damping + pos/neg-spring vectors are copied into the
// integrator's working set (see IntegrateEyeJoint):
eyePosSpring = r->posSpring; // @0x218 <- +0x130
eyeNegSpring = r->negSpring; // @0x224 <- +0x13C
rotationSpringConstant = r->rotationSpringConstant; // @0x2C0 <- +0x148
rotationDampingConstant = r->rotationDampingConstant; // @0x2CC <- +0x154
bodyPosSpring = r->rotationPosSpring; // @0x2F0 <- +0x160
bodyNegSpring = r->rotationNegSpring; // @0x2FC <- +0x16C
// per-damage-type response multipliers and {Trans,PitchRoll,Yaw,Vibration}
// curves are stored at this[0xCB..0xE0] (@0x32C..0x380); copied verbatim.
// identity-init the two orientation work matrices (FUN_0040aadc) and seed
// the eye/body negative-spring axes (FUN_00408744 clamp-box) -- see @004b3778.
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;
}
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);
eyeOrientation = zero; // FUN_00408440(this+0x77, &DAT_004e0f74)
eyeForce = zero;
eyeAccel = zero;
bodyPosSpring = zero; // FUN_00408e90(this+0xAD, &DAT_004e0f8c) -- zero rotation
bodyAccel = zero;
bodyForce = zero;
// 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
// Per-frame skeleton write: push the integrated eye/body orientation into the
// resolved joints. Like the torso's UpdateJoints, the binary's WriteEyeJoint
// (@004b33e0, via thunk @004b2eac) + WriteMechJoint (@004b34ec) have NO direct
// caller in the decomp -- they were dispatched by the engine's generic per-frame
// joint pass. This port ticks the gyro's Performance here, so resolve that
// dispatch to a direct call at the same cadence (== what makes the eye/body move).
WriteEyeJoint(); // FUN_004b33e0
WriteMechJoint(); // FUN_004b34ec
Check_Fpu();
}
//
// @004b2ec0 [BEST-EFFORT] -- integrate the cockpit "eye" orientation with a
// spring toward (posSpring/negSpring) and damping, scaled by time_slice, then
// clamp each axis into [eyeLimitLow, eyeLimitHigh]. Vector ops are the engine
// SIMD-ish helpers; member names are inferred from the access pattern.
//
void
Gyroscope::IntegrateEyeJoint(Scalar time_slice)
{
Vector3D toPos; toPos.Subtract(eyePosSpring, eyeOrientation); // FUN_00408644
Vector3D toNeg; toNeg.Subtract(eyeNegSpring, eyeOrientation); // (using @0x218 / @0x224)
Vector3D springForce; springForce.Multiply(eyeSpringConstant, toPos); // FUN_004086d0 (this+0x1E8)
Vector3D dampingForce; dampingForce.Multiply(eyeSpringConstant, toNeg);
eyeAccel += springForce; // FUN_004085ec (this+0x230)
eyeAccel += dampingForce;
eyeWork = eyeAccel; // this+0x248
Vector3D step; step.Multiply(eyeWork, time_slice); // FUN_004086ac
eyeForce += step; // FUN_004085ec (this+0x23C)
eyeAccel.Cross(eyeSpringConstant, eyeForce); // FUN_004086d0 cross
eyeWork = eyeAccel;
step.Multiply(eyeWork, time_slice);
eyeForce += step;
// the original clamped eyeForce against an identity orientation box
// (FUN_00408744 with a just-built identity matrix) -- a pass-through -- then
// integrated it into eyeOrientation.
Vector3D delta = eyeForce; // FUN_00408744 (identity box)
eyeOrientation += delta; // FUN_004085ec (this+0x1DC)
// per-axis clamp into [eyeLimitLow @0x200, eyeLimitHigh @0x20C]
if (eyeOrientation.x > eyeLimitLow.x) eyeOrientation.x = eyeLimitLow.x;
if (eyeOrientation.y > eyeLimitLow.y) eyeOrientation.y = eyeLimitLow.y;
if (eyeOrientation.z > eyeLimitLow.z) eyeOrientation.z = eyeLimitLow.z;
if (eyeOrientation.x < eyeLimitHigh.x) eyeOrientation.x = eyeLimitHigh.x;
if (eyeOrientation.y < eyeLimitHigh.y) eyeOrientation.y = eyeLimitHigh.y;
if (eyeOrientation.z < eyeLimitHigh.z) eyeOrientation.z = eyeLimitHigh.z;
}
//
// @004b30ec [BEST-EFFORT] -- the body tip integrator. Same structure as
// IntegrateEyeJoint but driven by rotationSpringConstant / rotationDampingConstant
// (this+0x2C0/0x2CC) against the rotationPos/NegSpring limits, accumulating into
// bodyOrientation (this+0x2B4) and clamping into [bodyLimitLow, bodyLimitHigh].
//
void
Gyroscope::IntegrateBody(Scalar time_slice)
{
Vector3D toNeg; toNeg.Subtract(bodyOrientation, bodyNegSpring); // (this+0x2B4 - this+0x2FC)
Vector3D toPos; toPos.Subtract(bodyOrientation, bodyPosSpring); // (this+0x2B4 - this+0x2F0)
Vector3D springForce; springForce.Multiply(rotationSpringConstant, toNeg); // (this+0x2C0)
Vector3D dampingForce; dampingForce.Multiply(rotationDampingConstant, toPos); // (this+0x2CC)
bodyAccel += springForce; // this+0x308
bodyAccel += dampingForce;
bodyWork = bodyAccel; // this+0x320
Vector3D step; step.Multiply(bodyWork, time_slice);
bodyForce += step; // this+0x314
bodyAccel.Cross(rotationDampingConstant, bodyForce); // cross (this+0x2CC)
bodyWork = bodyAccel;
step.Multiply(bodyWork, time_slice);
bodyForce += step;
Vector3D delta = bodyForce; // FUN_00408744 (identity box)
bodyOrientation += delta;
if (bodyOrientation.y > bodyLimitLow.y) bodyOrientation.y = bodyLimitLow.y; // @0x2DC
if (bodyOrientation.x > bodyLimitLow.x) bodyOrientation.x = bodyLimitLow.x; // @0x2D8
if (bodyOrientation.z > bodyLimitLow.z) bodyOrientation.z = bodyLimitLow.z; // @0x2E0
if (bodyOrientation.y < bodyLimitHigh.y) bodyOrientation.y = bodyLimitHigh.y; // @0x2E8
if (bodyOrientation.x < bodyLimitHigh.x) bodyOrientation.x = bodyLimitHigh.x; // @0x2E4
if (bodyOrientation.z < bodyLimitHigh.z) bodyOrientation.z = bodyLimitHigh.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 [CONFIDENT] -- push the integrated orientations into the MechJoint
// node (type 5 only): write eyeOrientation as the node rotation and bodyOrientation
// as the node translation, again only when they differ by more than QuantiseEps.
//
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 eye=(" << (float)eyeOrientation.x << "," << (float)eyeOrientation.y
<< "," << (float)eyeOrientation.z << ") body=(" << (float)bodyOrientation.x << ","
<< (float)bodyOrientation.y << "," << (float)bodyOrientation.z << ")\n" << std::flush;
}
++s_gc;
if (!NodeRotationEquals(mechJointNode, eyeOrientation, QuantiseEps)) // FUN_004084fc
{
SetNodeRotation(mechJointNode, eyeOrientation); // FUN_0041d11c
}
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
eyeAccel += dir; // FUN_004085ec (this+0x230)
}
//
// @004b2de4 [CONFIDENT] -- as above but adds into the body accumulator
// (bodyAccel @0x308), then forces bodyForce.x (this+0x30C) to 0 and flips the
// sign of bodyAccel -- a one-shot torque kick.
//
void
Gyroscope::ApplyDamageTorque(Scalar x, Scalar y, Scalar z, Scalar magnitude)
{
Vector3D dir(-x, -y, -z);
magnitude *= exageration;
dir *= magnitude;
bodyAccel += dir; // FUN_004085ec (this+0x308)
bodyForce.x = 0.0f; // *(this+0x30C) = 0
bodyAccel.Negate(bodyAccel); // *(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 bodyAccel
// and mirrored into bodyForce (this+0x30C = this+0x308).
//
void
Gyroscope::ApplyVerticalImpulse(Scalar pitch, Scalar, Scalar, Scalar magnitude)
{
Vector3D dir(pitch, 0.0f, 0.0f);
magnitude *= exageration;
dir *= magnitude;
bodyAccel += dir; // FUN_004085ec (this+0x308)
bodyForce.x = bodyAccel.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);
}