//===========================================================================//
// 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, vibrationDirection)      == 0x390, "vibrationDirection @0x390");
	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
	}
	vibrationDirection		= Vector3D(0.0f, 1.0f, 0.0f);			// @0x390  the up vibration axis (:2921-2923)
	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;
	// bounce visibility probe: log the first N frames where the eye is actually
	// displaced (the periodic sampler above misses short oscillations).
	static int s_bounceSeen = 0;
	if (s_glog && s_bounceSeen < 40
		&& (eyePosition.x > 1e-3f || eyePosition.x < -1e-3f
		 || eyePosition.y > 1e-3f || eyePosition.y < -1e-3f
		 || eyePosition.z > 1e-3f || eyePosition.z < -1e-3f))
	{
		++s_bounceSeen;
		DEBUG_STREAM << "[gyro-bounce] eyePos=(" << (float)eyePosition.x << ","
			<< (float)eyePosition.y << "," << (float)eyePosition.z << ")\n" << std::flush;
	}
	// BOUNCE TRACE (task #56 verification): BT_GYRO_TRACE=1 logs this gyro's
	// integrated state EVERY frame while displaced, uncapped, tagged with the
	// instance + a running frame index -- enough to PLOT the oscillation and
	// check the damped-spring behaviour against the authored constants.
	static const int s_gtrace = getenv("BT_GYRO_TRACE") ? 1 : 0;
	if (s_gtrace)
	{
		static int s_traceFrame = 0;
		++s_traceFrame;
		const float m2 = eyePosition.x*eyePosition.x + eyePosition.y*eyePosition.y
					   + eyePosition.z*eyePosition.z
					   + bodyOrientation.x*bodyOrientation.x
					   + bodyOrientation.y*bodyOrientation.y
					   + bodyOrientation.z*bodyOrientation.z;
		if (m2 > 1e-9f)
			DEBUG_STREAM << "[gtrace] g=" << (void *)this << " f=" << s_traceFrame
				<< " eye=" << (float)eyePosition.x << " " << (float)eyePosition.y
				<< " " << (float)eyePosition.z
				<< " body=" << (float)bodyOrientation.x << " " << (float)bodyOrientation.y
				<< " " << (float)bodyOrientation.z << "\n" << std::flush;
	}
	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)
}

//
// @004b2980 [T1 -- re-disassembled byte-exact, task #56] -- the damage->gyro
// fan-out (the cockpit hit-BOUNCE).  Constants from the dump: eps 1e-4
// (@0x38d1b717), sign threshold 0.5f (@0x4b2d84), clamp 1.3f (@0x4b2d88).
// Collision (type 0) and zero damage no-op.  Direction: the Damage record's
// damageForce, or a RANDOM horizontal direction when ~zero (per component a
// sign roll then a value roll); rotated by the yaw-only torso-twist frame
// (placeRot=(0, *externalPitchPtr, 0) -> matrix @workMatrix, the engine
// euler->matrix conversion == the binary's euler->quat->matrix FUN_00409a00 +
// FUN_0040ab44) then re-normalized.  Per-type scaling: amount / multiplier *
// response{trans,pitchRoll,yaw,vibration}; the Explosive case alone multiplies
// by burstCount.  Four kicks: directional impulse (trans), torque (pitchRoll),
// a vibration shake along the FIXED member axis vibrationDirection@0x390 (up),
// and the vertical impulse fed by the row's third field ("yaw").
//
void
	Gyroscope::ApplyDamageResponse(const Damage &damage)
{
	if (damage.damageAmount == 0.0f)						// @4b298c (0.0f @0x4b2d80)
		return;
	if (damage.damageType == Damage::CollisionDamageType)	// @4b299e -- collisions never bounce here
		return;

	Scalar trans     = 0.0f;								// ebp-0x10
	Scalar pitchRoll = 0.0f;								// ebp-0x0C
	Scalar yaw       = 0.0f;								// ebp-0x08
	Scalar vibration = 0.0f;								// ebp-0x04

	Vector3D dir;											// ebp-0x1C
	if (Close_Enough(damage.damageForce, Vector3D(0.0f, 0.0f, 0.0f), 1e-4f))	// @4b29bd FUN_004084fc
	{
		// random horizontal: sign roll (>= 0.5 keeps +) then value roll, y=0
		dir.x = (RandomUnit() >= 0.5f) ? RandomUnit() : -RandomUnit();	// @4b29d7
		dir.z = (RandomUnit() >= 0.5f) ? RandomUnit() : -RandomUnit();	// @4b2a0d
		dir.y = 0.0f;													// @4b2a43
	}
	else
	{
		dir = damage.damageForce;							// @4b2a4a FUN_00408440
	}
	dir.Normalize(dir);										// @4b2a62 FUN_004087f4 (unguarded in the binary too)

	// @4b2a67-4b2af5: yaw-only body frame from the torso twist; rotate the
	// WORLD hit direction into it (FUN_00408744 raw row-dot on entries[0..2]/
	// [4..6]/[8..10] -- reproduced verbatim below) and re-normalize.  The
	// placement state @0x28C/0x298/0x2A8 is written for member fidelity.
	placeRot = Vector3D(0.0f, *externalPitchPtr, 0.0f);		// gyro+0x2A8..0x2B0
	placePos = Vector3D(0.0f, 0.0f, 0.0f);					// @4b2aa8
	{
		AffineMatrix m;
		m = EulerAngles(Radian(placeRot.x), Radian(placeRot.y), Radian(placeRot.z));
		for (int i = 0; i < 12; ++i) workMatrix[i] = m.entries[i];	// @4b2ac2 FUN_0040ab44
		Vector3D tmp = dir;									// ebp-0x28 @4b2ac7
		dir.x = tmp.x*workMatrix[0] + tmp.y*workMatrix[1] + tmp.z*workMatrix[2];	// FUN_00408744
		dir.y = tmp.x*workMatrix[4] + tmp.y*workMatrix[5] + tmp.z*workMatrix[6];
		dir.z = tmp.x*workMatrix[8] + tmp.y*workMatrix[9] + tmp.z*workMatrix[10];
	}
	dir.Normalize(dir);										// @4b2aed

	// @4b2afd: per-type scaling (jump table @0x4b2b10; type > 4 leaves all
	// four at zero).  Each term computed independently, matching the binary's
	// per-term fld/fdiv/fmul.
	switch (damage.damageType)
	{
	case Damage::BallisticDamageType:						// @4b2b3d  mult@0x330 resp@0x350
		trans     = damage.damageAmount / damageMultiplier[1] * damageResponse[1].trans;
		pitchRoll = damage.damageAmount / damageMultiplier[1] * damageResponse[1].pitchRoll;
		yaw       = damage.damageAmount / damageMultiplier[1] * damageResponse[1].yaw;
		vibration = damage.damageAmount / damageMultiplier[1] * damageResponse[1].vibration;
		break;
	case Damage::ExplosiveDamageType:						// @4b2b8a  the ONLY case reading burstCount
		trans     = (Scalar)damage.burstCount * damage.damageAmount / damageMultiplier[2] * damageResponse[2].trans;
		pitchRoll = (Scalar)damage.burstCount * damage.damageAmount / damageMultiplier[2] * damageResponse[2].pitchRoll;
		yaw       = (Scalar)damage.burstCount * damage.damageAmount / damageMultiplier[2] * damageResponse[2].yaw;
		vibration = (Scalar)damage.burstCount * damage.damageAmount / damageMultiplier[2] * damageResponse[2].vibration;
		break;
	case Damage::LaserDamageType:							// @4b2be3  mult@0x338 resp@0x370
		trans     = damage.damageAmount / damageMultiplier[3] * damageResponse[3].trans;
		pitchRoll = damage.damageAmount / damageMultiplier[3] * damageResponse[3].pitchRoll;
		yaw       = damage.damageAmount / damageMultiplier[3] * damageResponse[3].yaw;
		vibration = damage.damageAmount / damageMultiplier[3] * damageResponse[3].vibration;
		break;
	case Damage::EnergyDamageType:							// @4b2c2d  mult@0x33C resp@0x380
		trans     = damage.damageAmount / damageMultiplier[4] * damageResponse[4].trans;
		pitchRoll = damage.damageAmount / damageMultiplier[4] * damageResponse[4].pitchRoll;
		yaw       = damage.damageAmount / damageMultiplier[4] * damageResponse[4].yaw;
		vibration = damage.damageAmount / damageMultiplier[4] * damageResponse[4].vibration;
		break;
	default:
		break;
	}

	// @4b2c75-4b2ce2: upper-clamp each at 1.3f; NO lower clamp.
	if (trans     > 1.3f)	trans     = 1.3f;
	if (pitchRoll > 1.3f)	pitchRoll = 1.3f;
	if (yaw       > 1.3f)	yaw       = 1.3f;
	if (vibration > 1.3f)	vibration = 1.3f;

	if (getenv("BT_GYRO_LOG"))
		DEBUG_STREAM << "[gyro-dmg] type=" << (int)damage.damageType
			<< " amt=" << (float)damage.damageAmount << " burst=" << (int)damage.burstCount
			<< " dir=(" << (float)dir.x << "," << (float)dir.y << "," << (float)dir.z
			<< ") t/p/y/v=" << (float)trans << "/" << (float)pitchRoll << "/"
			<< (float)yaw << "/" << (float)vibration << "\n" << std::flush;

	if (getenv("BT_GYRO_TRACE"))
		DEBUG_STREAM << "[gtrace] g=" << (void *)this << " HIT t=" << (float)trans
			<< " p=" << (float)pitchRoll << " y=" << (float)yaw << " v=" << (float)vibration
			<< " dir=" << (float)dir.x << " " << (float)dir.y << " " << (float)dir.z
			<< "\n" << std::flush;

	ApplyDamageImpulse  (dir.x, dir.y, dir.z, trans);		// @4b2d00 the directional knock
	ApplyDamageTorque   (dir.x, dir.y, dir.z, pitchRoll);	// @4b2d23
	ApplyDamageImpulse  (vibrationDirection.x, vibrationDirection.y,
						 vibrationDirection.z, vibration);	// @4b2d4b the up-shake
	ApplyVerticalImpulse(dir.x, dir.y, dir.z, yaw);			// @4b2d6e

	if (getenv("BT_GYRO_LOG"))
		DEBUG_STREAM << "[gyro-dmg]   post-kick exag=" << (float)exageration
			<< " eyeForce=(" << (float)eyeForce.x << "," << (float)eyeForce.y << "," << (float)eyeForce.z
			<< ") bodyForce=(" << (float)bodyForce.x << "," << (float)bodyForce.y << ","
			<< (float)bodyForce.z << ")\n" << std::flush;
}


//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 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);
}

//===========================================================================//
// Damage->gyro bridges (task #56 fan-out; binary call sites: the take-damage
// hub @0x4a02fb, the performance crunch/jolt/rumble kicks @0x4aa254/0x4aa288/
// 0x4aa342/0x4aa81e/0x4aa86c, and the firing-recoil kick @0x4bc194).
// Complete-type-TU pattern: callers hold only Subsystem*.
//===========================================================================//
void GyroApplyDamage(Subsystem *gyro, const Damage &damage)
{
	if (gyro != 0)
		static_cast<Gyroscope *>(gyro)->ApplyDamageResponse(damage);
}
void GyroApplyDamageImpulse(Subsystem *gyro, Scalar x, Scalar y, Scalar z, Scalar magnitude)
{
	if (gyro != 0)
		static_cast<Gyroscope *>(gyro)->ApplyDamageImpulse(x, y, z, magnitude);
}
void GyroApplyDamageTorque(Subsystem *gyro, Scalar x, Scalar y, Scalar z, Scalar magnitude)
{
	if (gyro != 0)
		static_cast<Gyroscope *>(gyro)->ApplyDamageTorque(x, y, z, magnitude);
}
