//===========================================================================//
// File:	torso.cpp                                                           //
// Project: BattleTech           Brick: Entity Manager                        //
// Contents: Torso subsystem -- torso twist (yaw) and elevation (pitch) aim    //
//---------------------------------------------------------------------------//
//   Date    Who  Modification                                               //
// --------  ---  ---------------------------------------------------------- //
//  --/--/95  ??   Initial coding.                                            //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
//                      PROPRIETARY AND CONFIDENTIAL                         //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary (Ghidra pseudo-C, recovered shard
// part_013.c).  No header survived; see torso.hpp.  Each non-trivial method
// cites the originating @ADDR.  Confidence flags:  [CONFIDENT] / [BEST-EFFORT]
// / [EXCLUDED] as in gyro.cpp.
//
// Hex constants converted to decimal (from section_dump.txt):
//      _DAT_004b6500 = 0.0f        _DAT_004b6504 = 0.0001f (zero-vel epsilon)
//      _DAT_004b6508 = 0.02f       _DAT_004b650c = 0.05f   (settle tolerances)
//      _DAT_004b64f8 = 0.5f        _DAT_004b64fc = 1.0f    (ramp cap)
//      _DAT_004b6a14 = 0.0f        _DAT_004b6a18 = 0.0001f (recenter epsilon)
//      _DAT_004b6b08 = 10.0f       (min slew-window, ms->s)
//      _DAT_004b6fb8 = 0.0174532925f (PI/180, deg->rad)   _DAT_004b6fbc = 0.5f
//      _DAT_004b76c4 = -1.0f       (resource "unset" sentinel)
//      DAT_0052140c  = milliseconds-per-second tick scale (runtime-initialised;
//                      reads 0 in the static image -- used as (now-t0)/scale).
//
// Helper-function name mapping:
//      FUN_004b18a4  PowerWatcher base constructor
//      FUN_004b198c  PowerWatcher::CreateStreamedSubsystem
//      FUN_004b1804  PowerWatcher::ResetToInitialState (slot 10)
//      FUN_004b179c  PowerWatcher slot-9 death/voltage handler
//      FUN_004b181c  PowerWatcher per-frame watch update
//      FUN_0041bd34  Subsystem::WriteUpdateRecord (base)
//      FUN_00414b60  Clock::Now()  (ms)
//      FUN_004081e0  Scalar lerp(dst,a,b,t)
//      FUN_0041cfa0/0041d020/0041d0a8  skeleton-node get/set transform
//      FUN_00404118  NotationFile::ReadScalar    FUN_00404088 ReadString
//      FUN_004d4b58  stricmp        FUN_004dcd00 fabsf
//
#include <bt.hpp>
#pragma hdrstop

#if !defined(TORSO_HPP)
#	include <torso.hpp>
#endif
#if !defined(MECH_HPP)
#	include <mech.hpp>				// complete Mech -- owner->ResolveJoint
#endif
#include <JOINT.hpp>				// Joint, JointSubsystem (fwd shim)
#include <ROTATION.hpp>			// EulerAngles, Radian, Hinge (fwd shim)
#if !defined(APP_HPP)
#	include <app.hpp>
#endif
#if !defined(TESTBT_HPP)
#	include <testbt.hpp>
#endif

static const Scalar Zero        = 0.0f;			// _DAT_004b6500 / _DAT_004b6a14
static const Scalar VelEps      = 0.0001f;		// _DAT_004b6504 / _DAT_004b6a18
static const Scalar TwistEps    = 0.02f;		// _DAT_004b6508
static const Scalar SettleEps   = 0.05f;		// _DAT_004b650c
static const Scalar RampCap     = 1.0f;			// _DAT_004b64fc
static const Scalar MinSlewMs   = 10.0f;		// _DAT_004b6b08
static const Scalar DegToRad    = 0.0174532925f;	// _DAT_004b6fb8
static const Scalar Unset       = -1.0f;		// _DAT_004b76c4

// DAT_0052140c -- the runtime "ticks-per-second" scale (reads 0 in the static
// image; the engine initialises it from the system clock).  TODO: confirm the
// real value once the clock-init is recovered; 1000.0f (ms->s) is consistent
// with the MinSlewMs window above.
static const Scalar MsPerSecond = 1000.0f;		// DAT_0052140c

//
// CROSS-FAMILY compile shims for the streamed-subsystem joint validation: the
// engine has no `Skeleton` C++ type with FindNode (only SkeletonClassID streams
// in VDATA.h), so the real joint-existence check could not be recovered.  These
// let CreateStreamedSubsystem compile; they conservatively accept any joint.
//
namespace {
	struct ReconSkeleton
	{
		Logical	FindNode(const char * /*node_name*/) const	{ return True; }
	};
	static ReconSkeleton	g_reconSkeleton;
	inline ReconSkeleton*
		LoadSkeleton(const ResourceDirectories * /*dirs*/, const char * /*name*/)
		{ return &g_reconSkeleton; }

	// The Torso update record appends three trailing Scalars after the base
	// Simulation::UpdateRecord header (recovered offsets +0x10/+0x14/+0x18).
	inline Scalar
		RecordField(Simulation::UpdateRecord *record, int byte_offset)
		{ return *(const Scalar*)((const char*)record + byte_offset); }
}

//###########################################################################
// BASE-CHAIN RE-BASE -- compile-time layout locks (STEP 3).
//
// The Torso is read at RAW absolute offsets externally (the gyro cross-link
// stores (char*)sinkSourceSubsystem+0x1D8 == currentTwist, mech.cpp:740; the
// damage-slice, HUD ctor and radar read the same +0x1D8; MechControlsMapper
// reads +0x1F0/+0x1F4/+0x250/+0x274).  These asserts fail the BUILD if the
// re-based layout ever drifts, long before any runtime mis-read.
//###########################################################################
// A friend of Torso so it can offsetof() the protected own-block fields.
struct TorsoLayoutCheck
{
	static_assert(sizeof(Torso)                              == 0x280, "sizeof(Torso) must be 0x280 (factory alloc + gyro cross-link)");
	static_assert(offsetof(Torso, currentTwist)              == 0x1D8, "Torso currentTwist must be at 0x1D8 (gyro linkTarget +0x1D8)");
	static_assert(offsetof(Torso, currentElevation)          == 0x1E4, "Torso currentElevation must be at 0x1E4");
	static_assert(offsetof(Torso, analogTwistAxis)           == 0x1F0, "Torso analogTwistAxis must be at 0x1F0 (mapper +0x1F0)");
	static_assert(offsetof(Torso, analogElevationAxis)       == 0x1F4, "Torso analogElevationAxis must be at 0x1F4 (mapper +0x1F4)");
	static_assert(offsetof(Torso, horizontalEnabled)         == 0x250, "Torso horizontalEnabled must be at 0x250 (mapper +0x250)");
	static_assert(offsetof(Torso, recenterActive)            == 0x274, "Torso recenterActive must be at 0x274 (proves the 0x270 pad)");
	static_assert(offsetof(Torso, horizontalShadowJointNode) == 0x27C, "Torso last own field at 0x27C (+4 => sizeof 0x280)");
};


//###########################################################################
//###########################################################################
//					Torso
//###########################################################################
//###########################################################################

//#############################################################################
// Shared Data Support  (DefaultData @00510af8)
//
Derivation
	Torso::ClassDerivations(
		PowerWatcher::GetClassDerivations(),	// returns Derivation* (no &)
		"Torso"
	);

Receiver::MessageHandlerSet
	Torso::MessageHandlers;

Torso::AttributeIndexSet
	Torso::AttributeIndex;

Torso::SharedData
	Torso::DefaultData(
		&Torso::ClassDerivations,
		Torso::MessageHandlers,
		Torso::AttributeIndex,
		Torso::StateCount
	);


//#############################################################################
// Construction / Destruction
//

//
// @004b6b0c [CONFIDENT] -- chains to the PowerWatcher base ctor (FUN_004b18a4)
// with &Torso::DefaultData, installs the Torso vtable (PTR @0051103c).  A live
// master segment (flags & 0xC == 0 && flags & 1) gets TorsoSimulation as its
// Performance and isDamagedCopy=0; any other segment gets TorsoCopySimulation
// and isDamagedCopy=1.  Angular resource values are converted deg->rad here.
//
Torso::Torso(
	Mech				*owner,
	int					subsystem_ID,
	SubsystemResource	*r,
	SharedData			&shared_data
):
	PowerWatcher(owner, subsystem_ID, r, shared_data)
{
	Check(owner);
	Check_Pointer(r);

	// BASE-CHAIN RE-BASE: the 7 CROSS-FAMILY shim backing fields were deleted
	// (they over-sized the object); their accessors now read the real inherited
	// base state, so there is nothing to prime here.  The master/copy selection
	// below already reads the authoritative owner->simulationFlags.

	// INTEGRATION (gate reconcile): read OWNER simulationFlags (param_2+0x28) —
	// the oracle-verified authoritative source — not the local segment shim.
	if ((owner->simulationFlags & SegmentCopyMask)    == 0
		&& (owner->simulationFlags & MasterHeatSinkFlag) != 0)	// owner flags & 0x100 (binary @004b6b0c)
	{
		isDamagedCopy = 0;								// @0x24C
		SetPerformance(&Torso::TorsoSimulation);		// PTR @00510c10 (-> @004b5cf0)
	}
	else
	{
		isDamagedCopy = 1;
		SetPerformance(&Torso::TorsoCopySimulation);	// PTR @00510c1c (-> @004b65f8)
	}

	statusFlags                 = 0;								// @0x20C
	buttonAccelerationPerSecond = r->buttonAccelerationPerSecond;	// @0x210 <- +0x150
	buttonAccelerationStart     = r->buttonAccelerationStartValue;	// @0x214 <- +0x154

	baseTwistRate         = r->horizontalRotationPerSecond * DegToRad;	// @0x23C <- +0xF4
	baseElevationRate     = r->verticalRotationPerSecond   * DegToRad;	// @0x240 <- +0xF8
	horizontalLimitRight  = r->horizontalLimitRight * DegToRad;			// @0x1DC <- +0xFC
	horizontalLimitLeft   = r->horizontalLimitLeft  * DegToRad;			// @0x1E0 <- +0x100
	verticalLimitTop      = r->verticalLimitTop     * DegToRad;			// @0x220 <- +0x104
	verticalLimitBottom   = r->verticalLimitBottom  * DegToRad;			// @0x224 <- +0x108

	// derived limit copies + half-bottom (used as soft centre / settle band)
	twistCenterHigh     = verticalLimitTop;			// @0x230 = @0x220
	twistCenterLow      = verticalLimitBottom;		// @0x234 = @0x224
	elevationCenter     = verticalLimitTop;			// @0x228 = @0x220
	elevationHalfBottom = verticalLimitBottom * 0.5f;	// @0x22C (_DAT_004b6fbc)

	buttonRampActive = 0;	// @0x268
	buttonRamp       = 0.0f;// @0x26C

	horizontalEnabled = r->torsoHorizontalEnabled;	// @0x250 <- +0x14C
	if (horizontalEnabled)
	{
		// resolve the two skeleton joints named in the resource:
		horizontalJointNode       = ResolveJoint(r->torsoHorizontalJoint);		// @0x278
		horizontalShadowJointNode = ResolveJoint(r->torsoHorizontalShadowJoint);	// @0x27C

		// bring-up verification (env BT_TORSO_LOG; default OFF): confirm the
		// named joints resolved to live nodes and report their joint types.
		if (getenv("BT_TORSO_LOG"))
		{
			DEBUG_STREAM << "[torso] resolve '" << r->torsoHorizontalJoint
				<< "' -> " << (void*)horizontalJointNode;
			if (horizontalJointNode)
				DEBUG_STREAM << " type=" << (int)horizontalJointNode->GetJointType();
			DEBUG_STREAM << " ; shadow '" << r->torsoHorizontalShadowJoint
				<< "' -> " << (void*)horizontalShadowJointNode;
			if (horizontalShadowJointNode)
				DEBUG_STREAM << " type=" << (int)horizontalShadowJointNode->GetJointType();
			DEBUG_STREAM << "\n" << std::flush;
		}
	}

	// ---- BRING-UP DEMO (env BT_FORCE_TORSO; default OFF; NOT faithful) --------
	// The Blackhawk 0xBC5 record has TorsoHorizontalEnabled=0 + empty joint names
	// (the binary skips torso joints for this mech, verified).  To exercise the
	// reconstructed twist path end-to-end (ResolveJoint -> TorsoSimulation ->
	// UpdateJoints -> PushTwist -> Joint::SetRotation), force-enable and resolve
	// the REAL BLH.SKL torso joints ('jointshakey2' torso body / 'jointtshadow'
	// = the "apply torso twist to yaw" hinge), widen the limits, and give a slew
	// rate.  TorsoSimulation drives the sweep (below).  Remove after verification.
	if (isDamagedCopy == 0 && getenv("BT_FORCE_TORSO"))
	{
		horizontalEnabled = True;						// @0x250
		const char *mj = getenv("BT_FORCE_TORSO_JOINT");
		if (mj == 0 || *mj == '\0') mj = "jointshakey2";
		horizontalJointNode       = ResolveJoint(mj);				// torso body (ball)
		horizontalShadowJointNode = ResolveJoint("jointtshadow");	// shadow twist (hingey)
		horizontalLimitLeft  =  0.7f;					// @0x1E0  ~40 deg
		horizontalLimitRight = -0.7f;					// @0x1DC
		baseTwistRate        =  1.0f;					// @0x23C  rad/s slew
		if (getenv("BT_TORSO_LOG"))
		{
			DEBUG_STREAM << "[torso] FORCE-ENABLE '" << mj << "' -> " << (void*)horizontalJointNode;
			if (horizontalJointNode)       DEBUG_STREAM << " type=" << (int)horizontalJointNode->GetJointType();
			DEBUG_STREAM << " ; shadow 'jointtshadow' -> " << (void*)horizontalShadowJointNode;
			if (horizontalShadowJointNode) DEBUG_STREAM << " type=" << (int)horizontalShadowJointNode->GetJointType();
			DEBUG_STREAM << "\n" << std::flush;
		}
	}
	// --------------------------------------------------------------------------

	effectiveTwistRate     = baseTwistRate;		// @0x244 = @0x23C
	effectiveElevationRate = baseElevationRate;	// @0x248 = @0x240

	currentTwist     = 0.0f;	// @0x1D8
	currentElevation = 0.0f;	// @0x1E4
	twistVelocity    = 0.0f;	// @0x1E8
	twistRate        = 0.0f;	// @0x238
	analogTwistAxis    = analogElevationAxis = 0.0f;	// @0x1F0 / @0x1F4
	elevateUpCommand   = elevateDownCommand  = 0;		// @0x1F8 / @0x1FC
	twistLeftCommand   = twistRightCommand   = 0;		// @0x200 / @0x204
	centerCommand      = 0;								// @0x208
	hitElevTop   = hitElevBottom  = 0;					// @0x258 / @0x25C
	hitTwistLeft = hitTwistRight  = 0;					// @0x260 / @0x264
	recenterActive = 0;									// @0x274
	lastUpdateTime = GetCreationTime();					// @0x254 = this[5]

	// bring-up verification (env BT_TORSO_LOG): the gyro cross-links to
	// (Torso*)+0x1D8 (== currentTwist) at a RAW offset (mech.cpp:740), so the
	// compiled layout MUST place currentTwist at 0x1D8 and the object must fit the
	// 0x280 factory alloc.  Log both so a mismatch is caught immediately.
	if (getenv("BT_TORSO_LOG"))
	{
		DEBUG_STREAM << "[torso] ctor this=" << (void*)this
			<< " sizeof(Torso)=" << (unsigned)sizeof(Torso)
			<< " (0x280=" << (unsigned)0x280 << ")"
			<< " currentTwist@" << (unsigned)((char*)&currentTwist - (char*)this)
			<< " (want 0x1D8=" << (unsigned)0x1D8 << ")"
			<< " damagedCopy=" << isDamagedCopy
			<< " horizEnabled=" << (int)horizontalEnabled << "\n" << std::flush;

		// RESOURCE DIAGNOSTIC: is horizEnabled=0 a genuine content value or a
		// mis-read?  Dump the resource enable flag + joint names (valid strings =>
		// seg is the right record) + the raw int at record+0x14C + the two limit
		// scalars (to confirm the resource is a plausible Torso record at all).
		DEBUG_STREAM << "[torso] res enabled=" << (int)r->torsoHorizontalEnabled
			<< " raw@0x14C=" << *(const int*)((const char*)r + 0x14C)
			<< " hJoint='" << r->torsoHorizontalJoint << "'"
			<< " sJoint='" << r->torsoHorizontalShadowJoint << "'"
			<< " hRotPerSec=" << r->horizontalRotationPerSecond
			<< " hLimL=" << r->horizontalLimitLeft
			<< "\n" << std::flush;
		// Is the record correctly positioned? classID@+0x20 should be 0xBC5 and
		// modelSize@+0x24 should be 0x158 for a real Torso record; garbage => the
		// seg pointer / stream position is wrong (not a content issue).
		DEBUG_STREAM << "[torso] res classID=0x" << std::hex
			<< *(const int*)((const char*)r + 0x20)
			<< " modelSize=0x" << *(const int*)((const char*)r + 0x24) << std::dec
			<< " (want classID=0xBC5 size=0x158)\n" << std::flush;
	}

	Check_Fpu();
}

//
// @004b6fc0 [BEST-EFFORT, prologue not captured] -- reinstalls the vtable, runs
// the PowerWatcher teardown and frees on the deleting bit.  (Mirrors @004b3e88.)
//
Torso::~Torso()
{
	Check(this);
	Check_Fpu();
}

Logical	Torso::TestClass(Mech &)		{ return True; }
Logical	Torso::TestInstance() const		{ return IsDerivedFrom(ClassDerivations); }


//#############################################################################
// Subsystem virtual overrides
//

//
// @004b5bf8 (slot 10) [CONFIDENT] -- ResetToInitialState.  Chains to
// PowerWatcher::ResetToInitialState (FUN_004b1804) first; when (re)powering,
// snaps the effective rates back to the base rates and clears the button ramp.
// Always clears the command inputs / velocity / latches, then re-pushes the
// (centred) twist into the joints via WriteJoints.
//
void
	Torso::ResetToInitialState()
{
	WatcherResetToInitialState();	// CROSS-FAMILY: PowerWatcher::ResetToInitialState (FUN_004b1804)

	// @004b5bf8: on a (re)power (param_2 != 0) restore the effective rates and
	// clear all FOUR limit latches (0x258/0x25c/0x260/0x264); always clear the
	// command + analog inputs, the aim state and velocities, then re-push the
	// centred twist into the joints via WriteJoints.
	const Logical powered = True;					// bring-up: reset always re-powers
	if (powered)
	{
		effectiveTwistRate     = baseTwistRate;		// @0x244 = @0x23C
		effectiveElevationRate = baseElevationRate;	// @0x248 = @0x240
		hitElevTop   = hitElevBottom  = 0;			// @0x258 / @0x25C
		hitTwistLeft = hitTwistRight  = 0;			// @0x260 / @0x264
	}

	elevateUpCommand   = elevateDownCommand = 0;	// @0x1F8 / @0x1FC
	twistLeftCommand   = twistRightCommand  = 0;	// @0x200 / @0x204
	centerCommand      = 0;							// @0x208
	analogTwistAxis    = analogElevationAxis = 0.0f;// @0x1F0 / @0x1F4
	currentTwist       = 0.0f;						// @0x1D8
	currentElevation   = 0.0f;						// @0x1E4
	twistVelocity      = 0.0f;						// @0x1E8
	elevationVelocity  = 0.0f;						// @0x1EC
	twistAtUpdate      = 0.0f;						// @0x21C
	targetTwist        = 0.0f;						// @0x218
	twistRate          = 0.0f;						// @0x238
	recenterActive     = 0;							// @0x274

	Scalar verticalOut;
	WriteJoints(verticalOut);						// FUN_004b66b4
}

//
// @004b5be0 (slot 9) [BEST-EFFORT, prologue not captured] -- forwards to the
// PowerWatcher slot-9 handler (FUN_004b179c): on a "destroyed" message it clears
// the watched power source's voltage alarm, otherwise chains to the base.
//
Logical
	Torso::HandleDeathMessage(Message &message)
{
	return WatcherHandleDeathMessage(message);	// CROSS-FAMILY: PowerWatcher::HandleDeathMessage (FUN_004b179c)
}

//
// @004b6a78 (slot 6) [CONFIDENT] -- network/replay update record.  Samples the
// clock (FUN_00414b60) into lastUpdateTime, biasing it forward by one interval
// when the elapsed window is below MinSlewMs, chains to Subsystem::WriteUpdateRecord
// (FUN_0041bd34), then writes twistAtUpdate / twistVelocity / twistRate from the
// record fields (record +0x10 / +0x14 / +0x18).
//
void
	Torso::WriteUpdateRecord(UpdateRecord *message, int update_model)
{
	lastUpdateTime = GetCurrentTime();				// @0x254
	if ((Scalar)(lastUpdateTime - GetCreationTime()) / MsPerSecond < MinSlewMs)
	{
		lastUpdateTime += (lastUpdateTime - GetCreationTime());	// stretch tiny windows
	}

	Subsystem::WriteUpdateRecord(message, update_model);	// FUN_0041bd34

	twistAtUpdate = RecordField(message, 0x10);		// @0x21C
	twistVelocity = RecordField(message, 0x14);		// @0x1E8
	twistRate     = RecordField(message, 0x18);		// @0x238
}


//#############################################################################
// Per-frame simulation
//

//
// @004b5cf0 [CONFIDENT] -- the live-master Performance (PTR @00510c10).
//
// 1. PowerWatcher watch update (FUN_004b181c).
// 2. Latch the effective rates from the base rates, then ZERO them if the
//    watched power source is dead (this[0x10]==1), not Ready (this @0x198 != 4),
//    or in the Failure heat state (this @0x140 == 2); the Degradation heat state
//    (==1) instead halves the twist rate.
// 3. Apply the button-acceleration ramp (start value -> cap) while a command is
//    held, integrate the per-axis commands into currentTwist / currentElevation,
//    clamp to the software limits, set the limit latches, and -- if releasing --
//    run Recenter.  The "moved" dirty bit (this @0x18 |= 1) is raised when the
//    aim changed beyond TwistEps/SettleEps, and statusFlags reflects which limit
//    (if any) is being held.
//
void
	Torso::TorsoSimulation(Scalar time_slice)
{
	Check(this);

	WatcherUpdateWatch();					// CROSS-FAMILY: PowerWatcher per-frame watch (FUN_004b181c)

	Scalar twist0 = currentTwist;					// snapshot for velocity calc
	Scalar elev0  = currentElevation;

	effectiveTwistRate     = baseTwistRate;			// @0x244 = @0x23C
	effectiveElevationRate = baseElevationRate;		// @0x248 = @0x240

	if (HeatModelOff())											effectiveTwistRate = 0.0f;	// this[0x10]==1
	if (ElectricalStateLevel() != PoweredSubsystem::Ready)		effectiveTwistRate = 0.0f;	// @0x198 != 4

	switch (HeatStateLevel())						// @0x140
	{
	case HeatSink::DegradationHeat:	effectiveTwistRate = baseTwistRate * 0.5f; break;	// _DAT_004b64f8
	case HeatSink::FailureHeat:		effectiveTwistRate = 0.0f;                 break;
	default:						break;
	}

	// BRING-UP DEMO (env BT_FORCE_TORSO): un-gate the slew rate (the forced demo
	// torso isn't wired to a live power source, so ElectricalStateLevel would zero
	// it) and drive a left/right analog sweep so the reconstructed twist path is
	// exercised visibly.  Faithful behavior is untouched when the env is unset.
	static const int s_forceTorso = getenv("BT_FORCE_TORSO") ? 1 : 0;
	if (s_forceTorso)
	{
		effectiveTwistRate = baseTwistRate;						// un-gate (ctor set 1.0 rad/s)
		static int s_sweep = 0;
		analogTwistAxis = ((++s_sweep / 90) & 1) ? -1.0f : 1.0f;	// +/- every ~90 frames
	}

	Scalar twistStep = effectiveTwistRate     * time_slice;
	Scalar elevStep  = effectiveElevationRate * time_slice;

	// button-acceleration ramp (forced to RampCap in the shipped build -- see note)
	if (buttonRampActive == 0)
	{
		buttonRamp = buttonAccelerationStart;		// @0x26C <- @0x214
	}
	else
	{
		buttonRamp += buttonAccelerationPerSecond * time_slice;	// ramp up
		buttonRamp  = Min(buttonRamp, RampCap);
		buttonRampActive = 0;
	}
	buttonRamp = 1.0f;	// NB: @004b5cf0 unconditionally overwrites @0x26C with 1.0f

	// ---- digital elevation (pitch) commands @0x1F8 / @0x1FC ----
	if (elevateUpCommand > 0)						// @0x1F8
	{
		currentElevation += elevStep * buttonRamp;
		currentElevation  = Min(currentElevation, verticalLimitTop);	// @0x220
		buttonRampActive  = 1;
	}
	if (elevateDownCommand > 0)						// @0x1FC
	{
		currentElevation -= elevStep * buttonRamp;
		currentElevation  = Max(currentElevation, verticalLimitBottom);	// @0x224
		buttonRampActive  = 1;
	}

	// ---- digital twist (yaw) commands @0x200 / @0x204 ----
	if (twistLeftCommand > 0)						// @0x200
	{
		currentTwist += twistStep * buttonRamp;
		currentTwist  = Min(currentTwist, horizontalLimitLeft);		// @0x1E0
		recenterActive   = 0;
		buttonRampActive = 1;
	}
	if (twistRightCommand > 0)						// @0x204
	{
		currentTwist -= twistStep * buttonRamp;
		currentTwist  = Max(currentTwist, horizontalLimitRight);	// @0x1DC
		recenterActive   = 0;
		buttonRampActive = 1;
	}
	if (centerCommand > 0)							// @0x208
	{
		recenterActive   = 1;
		buttonRampActive = 0;
	}

	// ---- analog (RIO/stick) axes @0x1F0 / @0x1F4: proportional, no button ramp ----
	if (analogTwistAxis != Zero)					// @0x1F0
	{
		currentTwist += analogTwistAxis * twistStep;
		currentTwist  = Min(currentTwist, horizontalLimitLeft);		// @0x1E0
		currentTwist  = Max(currentTwist, horizontalLimitRight);	// @0x1DC
		recenterActive = 0;
	}
	if (analogElevationAxis != Zero)				// @0x1F4
	{
		currentElevation += analogElevationAxis * elevStep;
		currentElevation  = Min(currentElevation, verticalLimitTop);	// @0x220
		currentElevation  = Max(currentElevation, verticalLimitBottom);	// @0x224
	}

	if (recenterActive != 0)
	{
		recenterActive = Recenter(time_slice);		// FUN_004b6918
	}

	// ---- derive angular velocities + settle detection ----
	Scalar oldTwistRate = twistRate;				// local_90: OLD rate, snapshot before recompute
	if (time_slice > Zero)
	{
		twistVelocity     = (currentTwist     - twist0) / time_slice;	// @0x1E8
		elevationVelocity = (currentElevation - elev0)  / time_slice;	// @0x1EC
		twistRate         = twistVelocity;								// @0x238 (signed)
		twistVelocity     = fabsf(twistVelocity);
		elevationVelocity = fabsf(elevationVelocity);
	}
	if (fabsf(twistRate)    <= VelEps)	twistRate    = 0.0f;
	if (fabsf(oldTwistRate) <= VelEps)	oldTwistRate = 0.0f;

	ComputeTargetTwist();							// FUN_004b6510 -> targetTwist (@0x218)

	// raise the "moved" dirty bit unless we have settled within tolerance.  The
	// aim error is measured against targetTwist (0x218) and the settle test uses
	// the OLD rate snapshot (not the previous twist angle).
	Scalar  aimError    = currentTwist - targetTwist;					// 0x1D8 - 0x218
	Logical justStopped = (twistRate == Zero) && (oldTwistRate != Zero);
	Scalar  rateDelta   = twistRate - oldTwistRate;
	if (!(fabsf(aimError)  <= TwistEps									// _DAT_004b6508
	   && fabsf(rateDelta) <= SettleEps									// _DAT_004b650c
	   && !justStopped))
	{
		SetMovedFlag();								// this @0x18 |= 1
	}

	// ---- limit latches (edge-triggered) + statusFlags ----
	// Cleared when off the limit; set (with statusFlags=2) only on the FRAME the
	// limit is newly reached.  @0x260/@0x264 latch the twist limits, @0x258/@0x25C
	// the elevation limits (verified against part_013.c 4658-4692).
	statusFlags = 0;
	if (fabsf(currentTwist - horizontalLimitLeft)  > VelEps) hitTwistLeft  = 0;		// @0x260
	else if (hitTwistLeft  == 0) { hitTwistLeft  = 1; statusFlags = 2; }
	if (fabsf(currentTwist - horizontalLimitRight) > VelEps) hitTwistRight = 0;		// @0x264
	else if (hitTwistRight == 0) { hitTwistRight = 1; statusFlags = 2; }
	if (fabsf(currentElevation - verticalLimitTop)    > VelEps) hitElevTop    = 0;	// @0x258
	else if (hitElevTop    == 0) { hitElevTop    = 1; statusFlags = 2; }
	if (fabsf(currentElevation - verticalLimitBottom) > VelEps) hitElevBottom = 0;	// @0x25C
	else if (hitElevBottom == 0) { hitElevBottom = 1; statusFlags = 2; }
	if (recenterActive != 0) statusFlags = 1;

	// Per-frame skeleton write: push currentTwist into the resolved horizontal
	// joint(s).  In the shipped binary this is UpdateJoints (@004b67ec) -- the
	// out-param-free twin of WriteJoints -- with no DIRECT caller in the recovered
	// decomp: it was dispatched by the engine's generic per-frame joint pass (an
	// indirect/virtual call).  This port ticks the torso's Performance here, so we
	// resolve that dispatch to a direct call at the same per-frame cadence, which
	// is what makes the torso visibly track the aim.
	UpdateJoints();									// FUN_004b67ec

	Check_Fpu();
}

//
// @004b65f8 [CONFIDENT] -- the damaged-copy Performance (PTR @00510c1c).  A copy
// segment does not take commands; it just eases its twist toward the master's
// target.  ComputeTargetTwist() reports whether the master is still slewing: if
// not, snap currentTwist to the target; otherwise lerp by
// dt / ((now - lastUpdateTime)/scale + dt).  Result is clamped to the limits.
//
void
	Torso::TorsoCopySimulation(Scalar time_slice)
{
	if (!ComputeTargetTwist())						// FUN_004b6510 -> 0 = settled
	{
		currentTwist = targetTwist;					// @0x1D8 = @0x218
	}
	else
	{
		Scalar age = (Scalar)(lastUpdateTime - GetCreationTime()) / MsPerSecond;
		currentTwist = Lerp(currentTwist, targetTwist, time_slice / (age + time_slice));	// FUN_004081e0
	}
	targetTwist = Min(targetTwist, horizontalLimitLeft);	// @0x1E0
	targetTwist = Max(targetTwist, horizontalLimitRight);	// @0x1DC

	// Per-frame skeleton write for the replicated (damaged-copy) torso -- same
	// external joint pass as the master path (see TorsoSimulation for the @004b67ec
	// note).  Harmless in single-player bring-up (no copies); correct for MP.
	UpdateJoints();											// FUN_004b67ec
}


//#############################################################################
// Internal model helpers
//

//
// @004b6510 [CONFIDENT] -- compute the extrapolated twist target.  Picks the
// time base (clamped command timestamp vs. clock), forms an elapsed time in
// seconds, predicts targetTwist = twistAtUpdate + twistRate * elapsed, clamps to
// the limits, and returns 1 while still extrapolating (live), 0 once settled.
//
Logical
	Torso::ComputeTargetTwist()
{
	int   base;
	Logical slewing;

	if (isDamagedCopy == 0 || lastUpdateTime <= GetCurrentTime())
	{
		base    = GetCurrentTime() - GetCreationTime();	// this[0x10] - this[0x14]
		slewing = False;
	}
	else
	{
		base    = lastUpdateTime - GetCreationTime();	// @0x254 - this[0x14]
		slewing = True;
	}

	Scalar elapsed = (Scalar)base / MsPerSecond;
	targetTwist = twistAtUpdate + twistRate * elapsed;	// @0x218 = @0x21C + @0x238*t

	targetTwist = Min(targetTwist, horizontalLimitLeft);	// @0x1E0
	targetTwist = Max(targetTwist, horizontalLimitRight);	// @0x1DC
	return slewing;
}

//
// ResolveJoint -- forward to the owning Mech's shared resolver (FUN_00424b60,
// inlined by the Torso ctor @004b6b0c).  Out-of-line so the complete Mech type
// (mech.hpp) is visible; returns NULL for an absent node (ctor guards on it).
//
Joint*
	Torso::ResolveJoint(const char *joint_name)
{
	Check(owner);							// inherited MechSubsystem::owner (Mech*)
	return owner->ResolveJoint(joint_name);
}

//
// @004b66b4 (inner block) [CONFIDENT] -- write one twist (yaw) scalar into a
// resolved skeleton node, preserving the node's other DOF.  Dispatched on the
// joint type exactly like AnimationInstance::Animate (JMOVER.cpp:1518-1567):
// hinge nodes (types 0..2) take a scalar Radian (FUN_0041d0a8 ==
// Joint::SetRotation(Radian)); ball nodes (types 4..5) take an EulerAngles whose
// YAW carries the twist while pitch/roll are read back and kept (FUN_0041cfa0 ==
// GetEulerAngles, FUN_0041d020 == SetRotation(EulerAngles)).  SetRotation sets
// jointModified + ModifyJoints() internally, so we write unconditionally.
//
void
	Torso::PushTwist(Joint *node, Scalar twist)
{
	if (node == NULL)						// ctor may have failed to resolve the node;
	{										// the binary trusts it -- guard for bring-up
		return;
	}

	Joint::JointType jt = node->GetJointType();		// node+0x10

	// bring-up verification (env BT_TORSO_LOG; default OFF): show the first few
	// joint writes so the per-frame path can be confirmed in a headless run.
	static const int s_log   = getenv("BT_TORSO_LOG") ? 1 : 0;
	static int       s_count = 0;
	if (s_log && (s_count % 30) == 0 && s_count < 1800)		// sample periodically to show the sweep
	{
		DEBUG_STREAM << "[torso] PushTwist node=" << (void*)node << " type=" << (int)jt
			<< " twist=" << (float)twist << "\n" << std::flush;
	}
	++s_count;

	switch (jt)								// node+0x10
	{
	case Joint::HingeXJointType:			// types 0..2
	case Joint::HingeYJointType:
	case Joint::HingeZJointType:
		node->SetRotation(Radian(twist));	// FUN_0041d0a8
		break;

	case Joint::BallJointType:				// types 4..5
	case Joint::BallTranslationJointType:
	{
		EulerAngles angles = node->GetEulerAngles();			// FUN_0041cfa0 (keep pitch/roll)
		EulerAngles twisted(angles.pitch, twist, angles.roll);	// yaw <- twist (index 1)
		node->SetRotation(twisted);								// FUN_0041d020
		break;
	}

	default:								// StaticJointType(3) / NULLJointType(-1)
		break;
	}
}

//
// @004b66b4 [CONFIDENT] -- write currentTwist into the two horizontal joints
// (main @0x278 + shadow @0x27C) when horizontalEnabled, and report the current
// elevation through `verticalOut`.  Each joint is updated as a scalar channel
// (node type < 3) or a vector channel (types 4..5), matching the gyro pattern.
//
void
	Torso::WriteJoints(Scalar &verticalOut)
{
	verticalOut = currentElevation;					// *param_2 = @0x1E4
	if (!horizontalEnabled)							// @0x250
	{
		return;
	}
	PushTwist(horizontalJointNode,       currentTwist);	// @0x278
	PushTwist(horizontalShadowJointNode, currentTwist);	// @0x27C
}

//
// @004b67ec [CONFIDENT] -- identical to WriteJoints minus the elevation output;
// used on the paths that only need to refresh the skeleton.
//
void
	Torso::UpdateJoints()
{
	if (!horizontalEnabled)
	{
		return;
	}
	PushTwist(horizontalJointNode,       currentTwist);
	PushTwist(horizontalShadowJointNode, currentTwist);
}

//
// @004b6918 [CONFIDENT] -- ease currentTwist back toward 0 by effectiveTwistRate
// * dt, clamping so it does not cross zero, and return True while still off
// centre (|currentTwist| > VelEps).
//
Logical
	Torso::Recenter(Scalar time_slice)
{
	Scalar step = effectiveTwistRate * time_slice;	// @0x244

	if (currentTwist > Zero)
	{
		currentTwist -= step;
		if (currentTwist < 0.0f) currentTwist = 0.0f;
	}
	if (currentTwist < Zero)
	{
		currentTwist += step;
		if (currentTwist > 0.0f) currentTwist = 0.0f;
	}
	return fabsf(currentTwist - Zero) > VelEps;		// _DAT_004b6a18
}


//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CreateStreamedSubsystem -- Torso
//
// @004b6fec [CONFIDENT for the field list / classID / size; body linearised].
// Chains to PowerWatcher::CreateStreamedSubsystem (FUN_004b198c), stamps the
// resource (classID 0x0BC5 @+0x20, model size 0x158 @+0x24), reads the optional
// "TorsoHorizontalEnabled" flag (default True; the string "false" -> False), the
// six mandatory angular/limit scalars and the two acceleration scalars, then --
// only when enabled -- the two joint names, which must resolve in the model's
// "skeleton" file.
//
int
	Torso::CreateStreamedSubsystem(
		NotationFile				*model_file,
		const char					*model_name,
		const char					*subsystem_name,
		SubsystemResource			*r,
		NotationFile				*subsystem_file,
		const ResourceDirectories	*directories,
		int							passes
	)
{
	if (
		!PowerWatcher::CreateStreamedSubsystem(			// FUN_004b198c
			model_file, model_name, subsystem_name,
			r, subsystem_file, directories, passes
		)
	)
	{
		return False;
	}

	r->subsystemModelSize = sizeof(*r);					// +0x24 = 0x158
	r->classID            = RegisteredClass::TorsoClassID;	// +0x20 = 0x0BC5

	if (passes == 1)
	{
		// "TorsoHorizontalEnabled": absent => True; the literal "false" => False.
		const char *flag = 0;
		if (!model_file->GetEntry(subsystem_name, "TorsoHorizontalEnabled", &flag))
		{
			r->torsoHorizontalEnabled = True;
		}
		else
		{
			r->torsoHorizontalEnabled =
				(stricmp(flag, "false") == 0) ? False : True;	// FUN_004d4b58
		}
	}

	#define REQ_SCALAR(NAME, FIELD)												\
		if (!model_file->GetEntry(subsystem_name, NAME, &r->FIELD)			\
			&& r->FIELD == Unset)											\
		{ DebugStream << subsystem_name << " missing " << NAME << "!"; return False; }

	REQ_SCALAR("ButtonAccelerationPerSecond",  buttonAccelerationPerSecond)	// +0x150
	REQ_SCALAR("ButtonAccelerationStartValue", buttonAccelerationStartValue)// +0x154
	REQ_SCALAR("HorizontalRotationPerSecond",  horizontalRotationPerSecond)	// +0xF4
	REQ_SCALAR("VerticalRotationPerSecond",    verticalRotationPerSecond)	// +0xF8
	REQ_SCALAR("HorizontalLimitRight",         horizontalLimitRight)		// +0xFC
	REQ_SCALAR("HorizontalLimitLeft",          horizontalLimitLeft)			// +0x100
	REQ_SCALAR("VerticalLimitTop",             verticalLimitTop)			// +0x104
	REQ_SCALAR("VerticalLimitBottom",          verticalLimitBottom)			// +0x108

	if (r->torsoHorizontalEnabled)
	{
		const char *hj = "Unspecified";
		if (!model_file->GetEntry(subsystem_name, "TorsoHorizontalJoint", &hj)
			&& strcmp(hj, "Unspecified") == 0)
		{ DebugStream << subsystem_name << " missing TorsoHorizontalJoint!"; return False; }
		if (strcmp(hj, "Unspecified") != 0) strcpy(r->torsoHorizontalJoint, hj);	// +0x10C

		const char *sj = "Unspecified";
		if (!model_file->GetEntry(subsystem_name, "TorsoHorizontalShadowJoint", &sj)
			&& strcmp(sj, "Unspecified") == 0)
		{ DebugStream << subsystem_name << " missing TorsoHorizontalShadowJoint!"; return False; }
		if (strcmp(sj, "Unspecified") != 0) strcpy(r->torsoHorizontalShadowJoint, sj);	// +0x12C

		const char *skeleton = 0;
		if (!model_file->GetEntry("video", "skeleton", &skeleton))
		{
			DebugStream << model_name << " is missing skeleton file!";
			return -1;
		}
		ReconSkeleton *skl = LoadSkeleton(directories, skeleton);
		if (!skl->FindNode(r->torsoHorizontalJoint))
		{
			DebugStream << r->torsoHorizontalJoint << " not found in " << skeleton;
			return -1;
		}
		// (shadow-joint lookup follows the same pattern)
	}

	#undef REQ_SCALAR
	Check_Fpu();
	return True;
}


//===========================================================================//
// WAVE 4 factory bridge -- Torso (factory case 0xBC5, "SinkSource" label).
//   The real class at 0xBC5 (ctor @004b6b0c) is Torso; the factory built a
//   HeatSinkSource RECON_SUBSYS stub in its place.  Constructing the real Torso
//   here is what lets the reconstructed twist -> skeleton path run.  The object
//   is read at RAW offsets externally (the gyro cross-link -> currentTwist@0x1D8,
//   mech.cpp:740), so the compiled layout MUST match the binary 0x280 -- now
//   compile-time proven by TorsoLayoutCheck (sizeof(Torso)==0x280) above.
//===========================================================================//
Subsystem *CreateTorsoSubsystem(Mech *owner, int id, void *seg)
{
	return (Subsystem *) new (Memory::Allocate(0x280))
		Torso(owner, id, (Torso::SubsystemResource *)seg, Torso::DefaultData);
}

//
// STEP 6 bridge -- expose the live torso twist (Torso::currentTwist == the binary
// torso+0x1d8) to mech.cpp / the cylinder damage table without pulling the Torso
// header (which collides with mech.cpp's local subsystem stubs) into mech.cpp.
// The roster torso @mech+0x438 is a Torso (ClassID 0xBC5); 0 when absent.
//
Scalar BTGetTorsoTwist(Subsystem *torso)
{
	return torso ? ((Torso *)torso)->CurrentTwist() : 0.0f;
}
