//===========================================================================//
// File:	mech4.cpp                                                           //
// Project: BattleTech           Brick: Entity Manager                        //
// Contents: Mech per-frame simulation: motion integration, move-and-collide, //
//           weapon-impact damage routing, cockpit-gauge feed                 //
//           -- fourth implementation slice                                   //
//---------------------------------------------------------------------------//
//   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 in
// all/part_012.c, cluster 0x004ab188-0x004ac064) cross-referenced with the
// mech.hpp/mech2.cpp member maps and the surviving damage-state keyword table
// at .rdata:0050de74.
//
// The decompiler tagged every function in this window as file=? .  Attribution
// to mech4.cpp is by RANGE (this is the upper half of the 0x4a8054-0x4ac868
// gap, just below where heat.cpp's HeatableSubsystem family begins at
// 0x4ac530 / 0x4ac644) and by COHESION: these are the Mech's PER-FRAME runtime
// path, the consumers of the gait clips that mech2.cpp/mech3.cpp produced.
//
//      @004ab188  Mech::DeadReckonPose       (63 bytes; helper)
//      @004ab1c8  Mech::IntegrateMotion      (615 bytes; mentioned by mech2.cpp)
//      @004ab430  Mech::ReplicantPerformance (1432 bytes; REPLICANT-only interior --
//                         dead-reckon + ground snap, no collisions; wrapped by the
//                         perf @004ab9d8 = PTR_LAB_0050c0e8.  CORRECTED by the
//                         ground-model-decode workflow: NOT "Simulate"; the MASTER
//                         per-frame performance is FUN_004a9b5c = PTR_LAB_0050c0f4.)
//      @004abb40  Mech::ProcessCollision     (1284 bytes; vtable slot +0x3c -- the
//                         override of the engine protected virtual
//                         Mover::ProcessCollision.  CORRECTED: the earlier draft
//                         misread it as a weapon sweep "ResolveWeaponImpact".)
//      @004ac04c  Mech::SetDuckedCollisionTemplate  (23 bytes; template maxY <- 0x51c)
//      @004ac064  Mech::SetStandingCollisionTemplate(23 bytes; template maxY <- 0x518;
//                         earlier "heat gauge feeder" labels were wrong -- this+0x2ec
//                         is the engine Mover collisionTemplate)
//      @004ac194  Mech::LookupDamageState    (61 bytes; static keyword parse)
//
// These are Mech METHODS.  The Mech class declaration is owned by
// mech.cpp / mech.hpp; this file declares no header of its own.  Member
// offsets it touches are documented in the "Mech runtime member map" block
// below for the mech.hpp owner to fold in (see report).  The decomp addresses
// the object as int words, so this+0xNN there == byte offset 0xNN here.
//
//---------------------------------------------------------------------------//
// NOT recovered here (sit in this address window but are NOT Mech methods):
//   @004a9b5a-@004ab188   the ~5.6 KB "undefined" gap IS the MASTER per-frame
//                         performance FUN_004a9b5c (PTR_LAB_0050c0f4, installed
//                         as activePerformance for non-replicant mechs, ctor
//                         part_012.c:9947-9956) -- decoded from the raw asm by
//                         the ground-model-decode workflow; its ground/collision
//                         half is reconstructed as AuthenticGroundAndCollide +
//                         the real Mech::ProcessCollision below (task #15).
//   @004ac07c,@004ac0bc,@004ac144,@004ac1d4,@004ac22c,@004ac274  -- methods of
//                         a HeatableSubsystem-family class (owner-Mech ptr at
//                         this[0x34]/+0xD0, inner heat object at +0xE0; vtable
//                         0050e210).  These belong to heat.cpp, NOT Mech.
//   @004ac4fc            free lerp helper.   @004ac530/@004ac644  HeatableSubsystem
//                         ctors -- heat.cpp.
//
//---------------------------------------------------------------------------//
// Helper / engine routine name mapping used below:
//      FUN_0049fb54  Mech::IsDisabled() -> Logical
//      FUN_004a5678  Mech::AdvanceBodyAnimation(dt, loop)          [mech2.cpp]
//      FUN_004a5bf8  Mech::AdvanceBodyAnimationAirborne(dt, loop)  [mech2.cpp]
//      FUN_004ab188  Mech::DeadReckonPose(dt)
//      FUN_004ab1c8  Mech::IntegrateMotion(dt, loop) -> Logical (disabled?)
//      FUN_004086ac  Vector::Scale(out, in, scalar)
//      FUN_00409f58  ReconQuatIntegrate(out, base, omega)
//      FUN_00408644  Vector::Subtract(out, a, b)
//      FUN_00408614  Vector::AddScaled(out, a, b, t)
//      FUN_004085ec  Vector::Add(out, a, b)
//      FUN_00408848  Vector::Lerp(out, a, w0, b, w1)
//      FUN_00408440  Assign/Copy(dst, src)              (struct or string copy)
//      FUN_00408644/00408614  see above
//      FUN_0040a7f4  ReconQuatIdentity(q, &kIdentity)
//      FUN_0040aadc  ReconMatrix::Identity(m)
//      FUN_0040ab44  Matrix34::FromQuaternion(m, q)
//      FUN_0040a938  Matrix34::SetRotation(m, q)
//      FUN_0040a4d8  ReconQuatSlerp(out, a, b, t)
//      FUN_0040e36c  BoundingBoxTreeNode::FindSmallestNodeContainingColumn
//                    (BOXTREE.CPP:503; CORRECTED -- earlier "Terrain::CellAt")
//      FUN_0040e5f0  BoundingBoxTreeNode::FindBoundingBoxUnder(point, &height)
//                    (BOXTREE.CPP:867; the ground-height query; h=-1 = miss;
//                    CORRECTED -- earlier "Terrain::HeightAt" / "heightfield")
//      FUN_00433ed4  InterestManager::GetInterestZone (zone -> collision tree root)
//      FUN_00421b6c / 00421b2c  Mover::UpdateLocalMotion / UpdateWorldMotion
//                    (CORRECTED -- earlier "BeginPose/EndPose")
//      FUN_0043ade4  FilteredScalar::Push(filter, sample)
//      FUN_0043ae47  FilteredScalar::Sum(filter)
//      FUN_0043ae0b  FilteredScalar::Average(filter)
//      FUN_00422ff8  Mover::StaticBounce (MOVER.CPP:1421; CORRECTED -- earlier
//                    "Mech::ComputeImpactDamage")
//      FUN_0041a1a4  ReconIsDerived(classID)
//      FUN_0041db7c  DamageMessage::DamageMessage(&msg)  (ctor/clear)
//      FUN_00420ea4  CString::CString(dst, src)
//      FUN_0041bbd8  AlarmIndicator::SetLevel(alarm, n)
//      FUN_004d4b58  Strcmp(a, b) -> 0 if equal
//
// Read-only constants resolved from CODE literal pools (see section_dump):
//      DAT_0052140c        ticks-per-second timing scale (runtime global)
//      _DAT_004ab9c8 = 1.0f     _DAT_004ab9cc = 0.2f
//      _DAT_004ab9d0 = -1.0f    _DAT_004ab9d4 = 0.0f
//      _DAT_004ac044 ~= -1.0e-4f (back-face/behind cull threshold)
//      _DAT_004ac048 = 0.0f
//      &DAT_004e0f74 = "" / zero vector ;  &DAT_004e0fd4 = quaternion identity
//

#include <bt.hpp>
#pragma hdrstop

#if !defined(MECH_HPP)
#	include <mech.hpp>		// Mech class -- owned by mech.cpp slice
#endif
#if !defined(MECHMPPR_HPP)
#	include <mechmppr.hpp>	// MechControlsMapper -- the real-controls bridge/consumption
#endif
#if !defined(APP_HPP)
#	include <app.hpp>
#endif
#if !defined(SUBSYSTM_HPP)
#	include <subsystm.hpp>	// Subsystem -- the per-frame roster entry (PerformAndWatch)
#endif
#if !defined(EXPLODE_HPP)
#	include <EXPLODE.hpp>	// Explosion::Make / MakeMessage -- bring-up fire effect
#endif
#if !defined(DAMAGE_HPP)
#	include <DAMAGE.hpp>	// Damage -- the per-hit damage payload
#endif
#if !defined(MECHDMG_HPP)
#	include <mechdmg.hpp>	// Mech__DamageZone (complete type -- Zone()->structureLevel)
#endif
// AUTHENTIC GROUND MODEL (task #15, ground-model-decode): complete engine types
// for the probe/snap/response block + the real ProcessCollision override.
#include <BOXTREE.hpp>		// BoundingBoxTreeNode::FindBoundingBoxUnder / ...ContainingColumn
#include <BOXSOLID.hpp>		// BoxedSolid / BoxedSolidCollision / BoxedSolidCollisionList
#include <cultural.hpp>		// CulturalIcon::IsStoppingCollisionVolume / GetClassDerivations

static const Scalar kBehindCull = -1.0e-4f;		// _DAT_004ac044


//###########################################################################
// BASE-REGION LAYOUT LOCK (P3 STEP-6 audit -- the shared P3/P5/gyro de-risk).
//
// Compile-time proof of the ground-truth layout (cdb `dt btl4!Mover/JointedMover/
// Mech`).  The 1995 raw offsets IntegrateMotion/Simulate still use land ON these
// engine-base fields; these asserts pin exactly where they are, so the base-region
// reconciliation (see btbuild/P3_LOCOMOTION.md "BASE-REGION RECONCILIATION") is
// verifiable and any raw-offset stomp becomes provable at compile time.  Friend of
// Mech (mech.hpp) for inherited-member offsetof access.
//###########################################################################
struct MechBaseLayoutCheck
{
	// engine Mover/JointedMover base -- the fields the stale raw offsets corrupt:
	static_assert(offsetof(Mech, localToWorld)         == 0x0C8, "localToWorld@0xC8");
	static_assert(offsetof(Mech, localOrigin)          == 0x0F8, "localOrigin@0xF8 (raw this+0x100 stomps here)");
	static_assert(offsetof(Mech, projectedOrigin)      == 0x250, "projectedOrigin@0x250 (raw this+0x260 stomps here)");
	static_assert(offsetof(Mech, previousOrigin)       == 0x26C, "previousOrigin@0x26C (raw this+0x26c stomps here)");
	static_assert(offsetof(Mech, projectedVelocity)    == 0x288, "projectedVelocity@0x288 (raw this+0x298 stomps here)");
	static_assert(offsetof(Mech, collisionVolumeCount) == 0x2D4, "collision cluster @0x2D4 (raw this+0x2d4 stomps here)");
	static_assert(offsetof(Mech, collisionLists)       == 0x2E4, "collisionLists@0x2E4 (the P5 teardown victim)");
	static_assert(offsetof(Mech, segmentTable)         == 0x2F0, "segmentTable@0x2F0");
	// reconstructed Mech relocated members -- the CORRECT reconciliation targets.
	// (These sit BEFORE legAnimation/bodyAnimation in declaration order, so they do
	// NOT shift when those controllers grow to the real SequenceController size.)
	static_assert(offsetof(Mech, torsoAimTarget)       == 0x3E0, "torsoAimTarget@0x3E0 (raw this+0x2a4 should be this)");
	static_assert(offsetof(Mech, netOrientation)       == 0x3EC, "netOrientation@0x3EC (raw this+0x2d4 should be this)");
	// NOTE: arrivalTime/simTime/spinRate/the gait-accumulator members are declared
	// AFTER legAnimation/bodyAnimation, so they shift with the SequenceController
	// growth.  They are accessed BY NAME only (never at a raw external offset), so
	// their absolute offset is not load-bearing and is intentionally NOT locked.
};


//###########################################################################
//##################   Mech runtime member map (offsets)   ##################
//###########################################################################
//
// For the mech.hpp owner.  Continues the mech.hpp / mech2 maps; "?" flags an
// uncertain semantic.  Scalar unless noted.
//
//   @0x010 simTime              (Time)   current frame time (Mover base)
//   @0x028 movementFlags        (Word)   base motion flags; &0xC==4 => "arrived"
//   @0x040 movementMode         (int)    gait/jump selector (mech2)
//   @0x100 maxSpeed             (Scalar) (mech.hpp; here read as a vector w)
//   @0x260 motionDelta          (Vector) accumulated per-frame world translation
//   @0x26c worldPose            (Quaternion) integrated body orientation (this[0x9b])
//   @0x298 torsoAimCurrent      (mech.hpp) reused as angular-impulse accumulator
//   @0x2a0 spinRate                       = -bodyCycleDistance/dt
//   @0x2a4 torsoAimTarget       (mech.hpp) snapshot of netOrientation @0x2d4
//   @0x2d4 netOrientation       (mech.hpp)
//   @0x2e0 arrivalTime          (Time)    dead-reckon target timestamp
//   @0x2e8 collisionVolume      (BoxedSolid*)  ENGINE Mover member (1995 MOVER.HPP:
//                               370).  CORRECTED by the ground decode -- earlier
//                               "physicsBody/weapon sweep" reading was wrong;
//                               vtable+0x1c on it is BoxedSolid::ProcessCollision.
//   @0x2ec collisionTemplate    (BoxedSolid*)  ENGINE Mover member.  The "heat
//                               gauge / groundRef" conflict is resolved: the duck
//                               swappers write template->maxY(+0xC).
//   @0x2f0 containedByNode      (BoundingBoxTreeNode*)  ENGINE Mover member --
//                               the cached collision-tree node (GetMoverCollisionRoot).
//   @0x2f8 lastCollisionList    (BoxedSolidCollisionList*)  ENGINE Mover member.
//   @0x344 forwardCycleRate     (mech2)   set each frame from 0x5b8/0x5bc
//   @0x3f4 airborneSelect       (int)     0 => use groundCycleRate, else airborne
//   @0x44c collisionTemporaryState (int)  CORRECTED (was "ammoState"): zeroed
//                               pre-collision-list each frame (@4aa741); the
//                               ProcessCollision state tail writes 1/2.  DEFERRED
//                               together (see BINARY-TAIL-DEFERRED markers).
//   @0x4b8 templateBottomLift   (mech.hpp)  ctor: 0.05 x volume X width
//   @0x518 standingTemplateMaxY (mech.hpp)  CORRECTED (was "heatLevel")
//   @0x51c duckedTemplateMaxY   (mech.hpp)  CORRECTED (was "heatCapacity"); 0.6 x standing
//   @0x580 jumpCapable          (mech3)
//   @0x598 motionEventName      (mech2)
//   @0x5a4 motionEventArmed     (mech2)
//   @0x5b8 groundCycleRate      (mech3)
//   @0x5bc airborneCycleRate    (mech3)
//   @0x778 creationTime         (mech.hpp)
//   @0x77c motionEventPending   (int)     queued footstep/turn event flag
//   @0x7e0 telemetryFilter[5]   (mech.hpp this[0x1f8..0x204]) 15-sample filters;
//                               fed headPitch/torsoTwist/turretBase/legAngle + dt
//   @0x81c prevTele[4]          this[0x207..0x20a] last filtered angle samples
//   @0x1dc..0x1ec aimRate[4]    this[0x77..0x7b] telemetry-derived angular rates
//                               (cockpit/aim feed: d(angle)/dt across the filter)
//


//###########################################################################
//###########################################################################
//					DeadReckonPose
//
// @004ab188
//
// Blend the net (server) orientation @0x2d4 toward the live body pose by a
// fraction of dt and fold the result onto the world position quaternion at
// @0x26c.  Tiny leaf called once per frame from IntegrateMotion.
//###########################################################################
//###########################################################################
void
	Mech::DeadReckonPose(Scalar fraction)
{
	Vector3D scaled;
	Vector::Scale(&scaled, &netOrientation, fraction);		// FUN_004086ac(.,this+0x2d4,frac)
	ReconQuatIntegrate(						// FUN_00409f58
		&worldPose,									// was raw 0x26c (stomped previousOrigin)
		&worldPoseBase,								// was raw 0x138 (stomped updateOrigin)
		&scaled);
}


//###########################################################################
//###########################################################################
//					IntegrateMotion
//
// @004ab1c8   (named by mech2.cpp's banner as Mech::IntegrateMotion)
//
// Advance the *displayed-motion* (channel-B) body gait one frame and integrate
// the resulting cycle distance into the world transform.  Picks the airborne
// body updater when (movementMode==3||4) && jumpCapable, else the ground one.
// Also fires the queued footstep / motion event when the mech "arrives".
// Returns the IsDisabled() result captured at entry (the caller branches on it).
//###########################################################################
//###########################################################################
Logical
	Mech::IntegrateMotion(Scalar time_slice, int loop)
{
	// Choose this frame's forward-cycle slew rate.
	forwardCycleRate = airborneSelect ? airborneCycleRate : groundCycleRate;	// 0x344<-0x5bc/0x5b8

	if (IsDisabled())								// FUN_0049fb54
	{
		ReconQuatIdentity(&angularAccum, &kIdentityQuat);		// was raw this+0x298 (stomped projectedVelocity)
		return True;
	}

	// Time remaining to the dead-reckon arrival target (or elapsed since
	// creation if we have already arrived).
	Scalar	dr;
	Logical	arrived;
	if ((movementFlags & 0xC) == 4 && simTime < arrivalTime)	// 0x28, 0x10, 0x2e0
	{
		dr      = (Scalar)(arrivalTime - *(Scalar *)&creationTime) / DAT_0052140c;
		arrived = True;
	}
	else
	{
		dr      = (Scalar)(simTime - *(Scalar *)&creationTime) / DAT_0052140c;
		arrived = False;
	}

	// Fire any queued motion event (footstep / turn) on arrival.
	if (motionEventPending)							// 0x77c
	{
		if ((movementFlags & 0xC) == 4)
		{
			FUN_00408644((Scalar *)&motionEventVector,	// was raw 0x598/0x12c/0x100
				(Scalar *)&motionSourceB, (Scalar *)&motionSourceA);
			motionEventArmed   = 1;					// 0x5a4
			motionEventPending = 0;
		}
		else
		{
			Assign((void *)&motionDelta, (void *)&motionSourceB);	// was raw 0x260 <- 0x12c
			motionEventPending = 0;
		}
	}

	// Advance the body gait (airborne flavour while jumping).
	Scalar cycleDistance;
	if ((movementMode == 3 || movementMode == 4) && jumpCapable)	// 0x40, 0x580
		cycleDistance = AdvanceBodyAnimationAirborne(time_slice, loop);	// FUN_004a5bf8
	else
		cycleDistance = AdvanceBodyAnimation(time_slice, loop);			// FUN_004a5678

	// The raw velocity vector (0x298) = {0, 0, -cycleDistance/dt}; the world-step
	// FUN_00408744 reads all three components, so 0x2a0 (== angularAccum[2]) MUST be
	// set here -- the earlier draft wrote only the separate `spinRate`@0x508 and left
	// angularAccum[2] stale (a latent bug, never exercised while this fn was dead).
	spinRate                     = -cycleDistance / time_slice;	// 0x2a0 mirror (telemetry)
	((Scalar *)&angularAccum)[2] = -cycleDistance / time_slice;	// 0x2a0 velocity.z (forward)
	((Scalar *)&angularAccum)[1] = 0.0f;						// 0x29c velocity.y
	((Scalar *)&angularAccum)[0] = 0.0f;						// 0x298 velocity.x

	DeadReckonPose(dr);									// FUN_004ab188
	Assign((void *)&torsoAimTarget, (void *)&netOrientation);	// was raw 0x2a4 <- 0x2d4

	// Build the per-frame world-translation increment and fold it onto
	// motionDelta and worldPose (declared members; were raw 0x260 / 0x26c).
	Matrix34	bodyFrame;
	ReconMatrix::Identity(&bodyFrame);						// FUN_0040aadc
	Matrix34::FromQuaternion(&bodyFrame, &motionDelta);		// FUN_0040ab44 (was raw 0x260)

	Vector3D	worldStep;
	FUN_00408744(&worldStep, (Scalar *)&angularAccum, &bodyFrame);	// was raw 0x298

	Vector3D	tmp;
	Assign(&tmp, &kZeroVector);							// &DAT_004e0f74
	Vector::AddScaled(&tmp, &tmp, &worldStep, time_slice);
	Vector::Add((Scalar *)&motionDelta, (Scalar *)&motionDelta, &tmp);	// was raw 0x260

	Assign(&tmp, &kZeroVector);
	Vector::AddScaled(&tmp, &tmp, (Scalar *)&torsoAimTarget, time_slice);	// was raw 0x2a4

	Quaternion	prevPose = worldPose;					// was raw 0x26c
	ReconQuatIntegrate(&worldPose, &prevPose, &tmp);	// was raw 0x26c

	return arrived;
}


//###########################################################################
//###########################################################################
//					Simulate     (per-frame move-and-collide + telemetry)
//
// @004ab430
//
// The Mech's main per-frame tick.  Integrate body motion, slerp the live
// orientation / torso toward the dead-reckon target, drop the body onto the
// terrain (query terrain height at the new cell and correct the vertical),
// notify the renderer, then push the head/torso/turret/leg angles through the
// 15-sample telemetry FilteredScalars and emit the per-axis angular RATES used
// by the cockpit and the aiming reticle.
//###########################################################################
//###########################################################################
void
	Mech::Simulate(Scalar time_slice)
{
	ReconQuatIdentity(&aimRate, &kIdentityQuat);		// clear aim/torso (was raw this+0x1dc -> localAcceleration)

	Logical arrived = IntegrateMotion(time_slice, 1);		// FUN_004ab1c8

	if (!arrived)
	{
		// Live integration branch.
		if (*(int *)(this + 0x5a4) == 1)				// motionEventArmed
		{
			Vector3D ev = *(Vector3D *)(this + 0x598);
			Scalar   span = (_DAT_004ab9cc /*0.2*/ -
				(Scalar)(*(int *)(this + 0x10) - *(int *)(this + 0x14)) / DAT_0052140c)
				+ time_slice;
			if (span <= time_slice)
				*(int *)(this + 0x5a4) = 0;
			else
				Vector::Scale(&ev, &ev, time_slice / span);
			Vector::Add((Scalar *)(this + 0x260), (Scalar *)(this + 0x260), &ev);
			*(int *)(this + 0x264) = *(int *)(this + 0x104);
			Matrix34::SetRotation((Matrix34 *)(this + 0x100), (Quaternion *)(this + 0x260));
			FUN_00408644((Scalar *)(this + 0x598), (Scalar *)(this + 0x598), &ev);
		}
		else
		{
			*(int *)(this + 0x264) = *(int *)(this + 0x104);
			Matrix34::SetRotation((Matrix34 *)(this + 0x100), (Quaternion *)(this + 0x260));
		}
		Assign((void *)(this + 0x1f4), (void *)(this + 0x298));		// legAngle  <- accumulator
		Assign((void *)(this + 0x1d0), (void *)(this + 0x2a4));		// headPitch <- aim target
	}
	else
	{
		// Dead-reckon / arrived branch: slerp orientation & torso to target.
		Scalar t = time_slice /
			((Scalar)(*(int *)(this + 0x2e0) - *(int *)(this + 0x10)) / DAT_0052140c + time_slice);
		ReconQuatSlerp((Quaternion *)(this + 0x10c), (Quaternion *)(this + 0x10c),
			(Quaternion *)(this + 0x26c), t);						// FUN_0040a4d8
		FUN_00408848((Scalar *)(this + 0x1d0), (Scalar *)(this + 0x1d0),
			_DAT_004ab9c8 - t, (Scalar *)(this + 0x2a4), t);		// headPitch lerp
		if (*(int *)(this + 0x5a4) == 1)
		{
			Vector3D ev = *(Vector3D *)(this + 0x598);
			Scalar   span = (_DAT_004ab9cc -
				(Scalar)(*(int *)(this + 0x10) - *(int *)(this + 0x14)) / DAT_0052140c)
				+ time_slice;
			if (span <= time_slice)
				*(int *)(this + 0x5a4) = 0;
			else
				Vector::Scale(&ev, &ev, time_slice / span);
			Vector::Add((Scalar *)(this + 0x260), (Scalar *)(this + 0x260), &ev);
			*(int *)(this + 0x264) = *(int *)(this + 0x104);
			Assign((void *)(this + 0x100), (void *)(this + 0x260));
			FUN_00408644((Scalar *)(this + 0x598), (Scalar *)(this + 0x598), &ev);
		}
		else
		{
			*(int *)(this + 0x264) = *(int *)(this + 0x104);
			Assign((void *)(this + 0x100), (void *)(this + 0x260));
		}
		FUN_00408848((Scalar *)(this + 0x1f4), (Scalar *)(this + 0x1f4),
			_DAT_004ab9c8 - t, (Scalar *)(this + 0x298), t);		// legAngle lerp
	}

	// --- Drop onto terrain (skip if the "no-collide" flag 0x40 is set) ------
	if ((*(byte *)(this + 0x29) & 0x40) == 0)
	{
		(*(void (**)(Mech *))(*(int **)this + 13))(this);		// vtable slot 13 (build world xform)
		int  world   = *(int *)(DAT_004efc94 + 0x30);
		int  grid    = FUN_00433ed4(world + 0x14);				// World::Terrain
		int *cellTab = *(int **)(grid + 0x30);
		*(int *)(this + 0x2f0) = FUN_0040e36c(*cellTab, *(int *)(this + 0x2e8));	// Terrain::CellAt
	}
	else
	{
		Matrix34::FromQuaternion((Matrix34 *)(this + 0xd0), (Quaternion *)(this + 0x100));
	}

	{
		// Query terrain height at the current planar cell; correct Z so the
		// feet rest on the ground.
		struct { int x; uint xf; int z; } probe;
		probe.x = *(int *)(this + 0x100);
		probe.z = *(int *)(this + 0x108);
		Scalar  baseH = *(Scalar *)(*(int *)(this + 0x2ec /*cell*/) + 8) + (Scalar)*(int *)(this + 0x104);
		Scalar  height;
		FUN_0040e5f0(*(int *)(this + 0x2f0), (int)&probe, &height);	// Terrain::HeightAt
		if (height != _DAT_004ab9d0 /*-1*/)
		{
			height -= *(Scalar *)(*(int *)(this + 0x2ec) + 8);
			*(Scalar *)(this + 0x104) -= height;
			*(Scalar *)(*(int *)(this + 0x2e8) + 8) -= height;
			*(Scalar *)(*(int *)(this + 0x2e8) + 0xc) -= height;
		}
		(void)baseH;
	}

	ReconBeginPose((int)this);						// FUN_00421b6c

	// --- Telemetry filters: push the four cockpit angles + dt, then emit the
	//     per-axis angular rates (cockpit / reticle feed). -------------------
	FilteredScalar *f0 = (FilteredScalar *)(this + 0x7e0);	// this[0x1f8]
	FilteredScalar::Push(f0,                               *(Scalar *)(this + 0x1cc));	// headPitch [0x73]
	FilteredScalar::Push((FilteredScalar *)(this + 0x7ec),*(Scalar *)(this + 0x1c8));	// aim Y     [0x72]
	FilteredScalar::Push((FilteredScalar *)(this + 0x7f8),*(Scalar *)(this + 0x1c4));	// turretBase[0x71]
	FilteredScalar::Push((FilteredScalar *)(this + 0x804),*(Scalar *)(this + 0x1d4));	// aim/leg   [0x75]
	FilteredScalar::Push((FilteredScalar *)(this + 0x810), time_slice);					// dt        [0x76]

	Scalar sHead = FilteredScalar::Sum(f0);
	Scalar aTurr = FilteredScalar::Average((FilteredScalar *)(this + 0x7f8));
	Scalar aHead = FilteredScalar::Average(f0);
	Scalar aLeg  = FilteredScalar::Average((FilteredScalar *)(this + 0x804));
	Scalar aDt   = FilteredScalar::Average((FilteredScalar *)(this + 0x810));

	ReconQuatIdentity(&aimRate, &kIdentityQuat);		// was raw this+0x1dc (dead Simulate tail)

	if (_DAT_004ab9d4 /*0*/ < aDt)
	{
		// rate = (filtered_now - last_sample) / filtered_dt
		*(Scalar *)(this + 0x1e4) = (sHead - *(Scalar *)(this + 0x81c)) / aDt;	// [0x79]
		*(Scalar *)(this + 0x1e0) = (aHead - *(Scalar *)(this + 0x828)) / aDt;	// [0x78]
		*(Scalar *)(this + 0x1dc) = (aTurr - *(Scalar *)(this + 0x820)) / aDt;	// [0x77]
		*(Scalar *)(this + 0x1ec) = (aLeg  - *(Scalar *)(this + 0x824)) / aDt;	// [0x7b]
	}
	*(Scalar *)(this + 0x81c) = sHead;					// this[0x207]
	*(Scalar *)(this + 0x828) = aHead;					// this[0x20a]
	*(Scalar *)(this + 0x820) = aTurr;					// this[0x208]
	*(Scalar *)(this + 0x824) = aLeg;					// this[0x209]

	ReconEndPose((int)this);							// FUN_00421b2c
}


//###########################################################################
//###########################################################################
//					PerformAndWatch   (BRING-UP drivable locomotion)
//
//  NOT in the shipped binary at this vtable slot -- this is a reconstruction
//  override for Tier-2 bring-up.  The engine simulation director calls the
//  virtual Simulation::PerformAndWatch(till, stream) on every entity every
//  frame (the stasis / sub-system / activePerformance machinery lives INSIDE
//  the base Mover::PerformAndWatch, which we deliberately bypass here).  We
//  read the player drive input, integrate the engine-base localOrigin (the
//  REAL Mover position/orientation), and rebuild localToWorld -- the matrix
//  the BTL4VideoRenderer root renderable AND the chase camera are bound to, so
//  the body walks across the terrain and the camera tracks it by construction.
//
//  This intentionally does NOT run the (reconstructed, still-unsafe) Simulate /
//  subsystem chain; wiring real animation-driven gait + collision is a later
//  step.  Single local player assumption (bring-up): heading is a file static.
//###########################################################################
//###########################################################################

// Player drive input, owned by the launcher (btbuild/btl4main.cpp).
// throttle/turn = the virtual-control OUTPUTS integrated below from the raw key
// state (keyFwd/keyBack/keyLeft/keyRight) -- see the launcher header comment.
struct BTDriveInput { float throttle; float turn; int forced; int fire; int fireForced; float forcedThrottle;
                      int keyFwd; int keyBack; int keyLeft; int keyRight; int allStop; };
extern BTDriveInput gBTDrive;

// Locomotion tuning (bring-up; hand-picked, not yet derived from the .ani
// [RootTranslation] clip speed -- that animation-driven path comes with the
// real gait wiring).  Units are world units/sec and radians/sec.
static const Scalar kDriveMaxSpeed = 30.0f;		// full-throttle forward speed
static const Scalar kDriveTurnRate = 1.2f;		// full-deflection yaw rate
static const Scalar kWeaponRange   = 100.0f;	// bring-up "in range" threshold for the
												// targeting log (real value = Emitter
												// effectiveRange @0x328, per-weapon)

// Single local-player drive state (bring-up).
static Scalar gDriveHeading   = 0.0f;			// yaw about world up (Y)
// BT_GOTO beeline harness outputs (consumed by the mapper bridge, mechmppr.cpp)
int   gBTGotoActive   = 0;
float gBTGotoTurn     = 0.0f;
float gBTGotoThrottle = 1.0f;
static int    gDriveSeeded    = 0;
static Scalar gDriveLogAccum  = 0.0f;			// 1 Hz position log throttle
static Scalar gTickLogAccum   = 0.0f;			// 1 Hz subsystem-tick log throttle
static int    gTickFirstLogged = 0;				// one-shot first-frame dispatch report
static Scalar gTargetLogAccum = 0.0f;			// 1 Hz targeting log throttle

// Firing (bring-up): the weapon-effect renderables (real emitter beams / projectile
// tracers) are not built in the port yet, but the engine DOES render Explosion
// entities -- so a shot is shown as an explosion spawned at the target.  We resolve
// the "explode" effect resource once and rate-limit shots with a cooldown.
// Fire cadence: grounded on the recovered laser data -- DischargeTime=0.2s
// (beam-on) + a short recharge.  The EXACT recharge is not cleanly recoverable
// yet (RechargeRate feeds VoltageCurve/energyCoefficient, best-effort constants,
// and the voltage source isn't linked -> the port force-charges), so this is
// DischargeTime + ~0.1s, faster/snappier than the old 0.8s bring-up guess.
static const Scalar kFireCooldown  = 0.3f;		// damage-block cadence (bring-up stand-in)
static const Scalar kBeamOnTime    = 0.2f;		// laser beam-on = ERx laser DischargeTime
// DECODED per-laser cycle: DischargeTime(0.2) + RechargeRate(2.0, ER-medium) = 2.2s.
// (Recovered: RechargeRate is literally the recharge in seconds -- charge-curve
// constants C1=1.0/C2=1e-4, generator RatedVoltage=1e4, seek 0.8 -> curve arg 0.2,
// -ln(0.2)=ln5 cancels the exponential-charge-to-0.8 ln5 factor exactly.)
static const Scalar kPortRecharge  = 2.2f;		// one gun port's full fire+recharge cycle
static Scalar       gFireCooldown = 0.0f;		// counts down
static Scalar       gBeamCooldown = 0.0f;		// independent cooldown for the VISUAL beam
static const Scalar kMuzzleHeight  = 7.0f;		// gun height above the mech origin (torso)
static const Scalar kMuzzleForward = 3.0f;		// muzzle offset ahead of the mech centre
static int          gExplodeReady = 0;			// 0=untried 1=ok -1=failed
static ResourceDescription::ResourceID gExplodeRes = ResourceDescription::NullResourceID;
static int          gShotCount    = 0;

// --- CRASH/STAGGER STATE (task #15 knockdown; see AuthenticGroundAndCollide) ----
// The binary suppresses the mech's motion while it is CRASHED (spec: localVelocity
// = {0,0,-adv/dt} is ZEROED while crashed) so a knocked-down mech cannot creep back
// into the obstacle and re-trigger every frame.  We reproduce that: while the crash
// clip (legState 0x20) plays, hold the velocity at zero.
// The knockdown must also only fire on a FRESH impact -- the frame the mech first
// strikes something -- NOT every frame it grinds against an obstacle it is already
// pressed into.  Otherwise holding throttle into a wall/mech re-knocks-down every
// recovery (the stuttery leg flip-flop the user sees).  gWasBlocked tracks last
// frame's contact so a continuous press just BLOCKS (stand/walk-in-place) and only a
// separate-then-re-approach produces a new knockdown.
// Block hysteresis: a knockdown fires only when the mech has been OUT of contact
// for kBlockHysteresis seconds (a genuine fresh approach).  A 1-frame edge is too
// brittle -- a glancing / sliding contact alternates blocked/not-blocked (the frame
// rejection pushes the mech out each blocked frame), which would flicker the edge
// back to "fresh" and re-knockdown.  The window bridges those gaps so any sustained
// OR intermittent press just BLOCKS; only separating for real re-arms the knockdown.
static Scalar       gBlockCooldown  = 0.0f;		// seconds since the last blocking contact (counts down)
static const Scalar kBlockHysteresis = 0.4f;

// E8 weapon trigger (bring-up): pulsed per player frame; read by EmitterSimulation
// (emitter.cpp) so the real weapon's CheckFireEdge sees rising edges.  Non-static
// (the emitter externs it).
int                 gBTWeaponTrigger = 0;

// Damage: each shot dispatches a REAL Entity::TakeDamageMessage to the target.  Now
// that the damage zones are constructed (mech.cpp Pass-3 zone build), the engine base
// Entity::TakeDamageMessageHandler routes damageZones[zone]->TakeDamage -> the
// reconstructed Mech__DamageZone::TakeDamage (the real armor/structure model).  We aim
// a VALID zone index (the Mech cylinder-lookup resolver for an unaimed/-1 hit is the
// STEP-6 reconstruction).  Death is driven by the REAL model (Mech::IsDisabled, the
// vital-zone/leg/torso destroyed query FUN_0049fb54), NOT a hit counter.
static const Scalar kShotDamage   = 12.0f;	// per-burst damage amount (units = armor points)
static int          gEnemyDestroyed = 0;

// BRING-UP: the spawned target/enemy mech (defined in btplayer.cpp).  The player
// mech locks onto it as its current target each frame (see targeting step below).
extern Entity *gEnemyMech;

// Mech target slots (verified vs the binary's weapon/fire path, part_013.c):
//   mech+0x37c  Point3D  current target world position  (range/aim source)
//   mech+0x388  Entity*  current target entity          (HasActiveTarget gate)
//   mech+0x38c  int      targeted subsystem index (-1 = whole mech / none)
// These are undocumented in the reconstructed mech.hpp member map; the fire path
// (Emitter/MechWeapon) reads exactly these, so we write them directly.
#define MECH_TARGET_POS(m)		(*(Point3D *)((char *)(m) + 0x37c))
#define MECH_TARGET_ENTITY(m)	(*(Entity **)((char *)(m) + 0x388))
#define MECH_TARGET_SUBIDX(m)	(*(int     *)((char *)(m) + 0x38c))

// Bring-up body-animation player (file scope so OnBodyAnimFinished can re-arm it).
// The engine AnimationInstance walks the mech's joint subsystem from a baked .ani
// clip in btl4.res; we advance it each moving frame so the legs CYCLE.
static AnimationInstance *gBodyAnim     = 0;
static int               gBodyAnimReady = 0;	// 0=untried 1=ok -1=failed
static Scalar            gBodyAnimLog   = 0.0f;
static int               gBodyAnimLoops = 0;	// count completed cycles (sanity)
static int               gCurrentGait   = -1;	// P3 STEP 2: -1=none 0=walk 1=run (bound clip)
static const Scalar      kRunThrottle   = 0.5f;	// |throttle| >= this -> run clip, else walk clip

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Mech::OnBodyAnimFinished -- AnimationInstance finished-callback.
//   AnimationInstance::Animate invokes (moverToAnimate->*finishedCallback) when a
//   clip runs off its last keyframe (JMOVER.cpp ~1592).  The real game uses this to
//   chain/loop gaits; for bring-up we LOOP the same clip by re-arming it at frame 0.
//   Dropping the sub-frame carryover is imperceptible.  Returns extra root movement
//   contributed this call (0 -- the next frame's Animate carries the gait forward).
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Scalar
	Mech::OnBodyAnimFinished(
		ResourceDescription::ResourceID animation_number,
		Scalar /*carryover*/, Logical /*animate_joints*/)
{
	++gBodyAnimLoops;
	if (gBodyAnim)
	{
		gBodyAnim->SetAnimation(
			animation_number,
			reinterpret_cast<JointedMover::AnimationCallback>(&Mech::OnBodyAnimFinished));
	}
	return 0.0f;
}

void
	Mech::PerformAndWatch(const Time& till, MemoryStream *update_stream)
{
	// Frame time slice from the simulation clock (same idiom as Mover::Perform).
	Scalar dt = till - lastPerformance;
	lastPerformance = till;

	// The bring-up DRIVE + ANIMATION path is single-player scaffolding: it reads
	// the global gBTDrive and a single shared gBodyAnim, and integrates THIS body's
	// origin.  It must run ONLY for the local player's mech (the viewpoint entity);
	// any other mech in the world (e.g. a spawned target/enemy) must NOT be driven
	// by the player's input or share the player's AnimationInstance.  Non-player
	// mechs still fall through to the subsystem-roster tick below.
	const Logical isPlayerMech =
		(application != 0 && (Entity *)this == application->GetViewpointEntity());

	// 1-Hz non-player mech telemetry (multiplayer diagnosis): every mech that is
	// NOT the local viewpoint -- whatever its instance claims -- with position.
	if (!isPlayerMech && getenv("BT_REPL_LOG") && dt > 0.0001f)
	{
		static float s_otherLog = 0.0f; s_otherLog += dt;
		if (s_otherLog >= 1.0f)
		{
			s_otherLog = 0.0f;
			DEBUG_STREAM << "[mech-exec] entity " << GetEntityID()
				<< " instance=" << (int)GetInstance()
				<< " pos=(" << localOrigin.linearPosition.x << ", "
				<< localOrigin.linearPosition.z << ")\n" << std::flush;
		}
	}

	// --- REPLICANT MOTION (P6 multiplayer) -----------------------------------
	// A remote-mastered mech does not run the local drive: it dead-reckons
	// between network updates via the engine reckoner (Mover::DeadReckon lerps
	// localOrigin toward projectedOrigin, which ReadUpdateRecord snaps from the
	// master's broadcast update records -- MOVER.cpp:493).  Subsystems still
	// tick below (their ctors carry the replicant gates on simulationFlags).
	if (GetInstance() == ReplicantInstance)
	{
		if (dt > 0.0001f && dt < 0.5f)
		{
			DeadReckon(dt);							// engine: reckoner + lerp
			localToWorld = localOrigin;
			if (getenv("BT_REPL_LOG"))
			{
				static float s_replLog = 0.0f; s_replLog += dt;
				if (s_replLog >= 1.0f)
				{
					s_replLog = 0.0f;
					DEBUG_STREAM << "[repl] mech " << (long)GetEntityID()
						<< " pos=(" << localOrigin.linearPosition.x << ", "
						<< localOrigin.linearPosition.y << ", "
						<< localOrigin.linearPosition.z << ")\n" << std::flush;
				}
			}
		}
	}

	// AUTHENTIC GROUND MODEL for NON-PLAYER masters (task #15): the binary
	// installs FUN_004a9b5c as the performance of EVERY MasterInstance mech
	// (ctor part_012.c:9947-9956), so the spawned test dummy grounds/collides
	// too -- not just the player.  The player's call runs inside the drive
	// block below (after its move, with the true start-of-frame position).
	if (!isPlayerMech && GetInstance() == MasterInstance && GroundReal()
		&& dt > 0.0001f && dt < 0.5f)
	{
		// A stationary master: old_position is a COPY of the current position
		// (must not alias localOrigin.linearPosition -- the block writes it).
		Point3D preMovePos = localOrigin.linearPosition;
		AuthenticGroundAndCollide(dt, preMovePos);
	}

	if (isPlayerMech && dt > 0.0001f && dt < 0.5f)		// ignore zero / huge (stall) slices
	{
		// --- VIRTUAL CONTROLS (dev keyboard -> the pod's analog inputs) --------
		// The pod's throttle was an ABSOLUTE analog LEVER and its turn input an
		// analog stick; momentary 0/1 keys can't express either (at 60fps every
		// tap = a frame-perfect FULL demand = the "controls too sensitive after
		// the perf fix" report -- at the old 10fps most taps aliased away, which
		// only LOOKED like fine control).  Integrate dt-scaled (fps-independent):
		//   lever:  W/S sweep a PERSISTENT lever (tap ~= 0.07 step, full sweep
		//           ~1.4s) with a DETENT at zero -- braking from forward stops AT
		//           0; release and press S again to engage reverse.  X = all stop.
		//   stick:  A/D deflect a momentary stick over ~0.4s, auto-centering on
		//           release -- tap = gentle nudge, hold = full rate.
		{
			static float sLever = 0.0f, sStick = 0.0f;
			static int   sDetent = 0;					// latched at the zero crossing
			const float kLeverRate = 0.7f;				// lever sweep per second
			const float kStickRate = 2.5f;				// stick deflect per second
			const float kStickCenterRate = 5.0f;		// stick auto-center per second

			// POLL the real key state.  WndProc key messages are unusable: the
			// engine keyboard reader (L4CTRL.cpp:1506) GetMessage()s every
			// WM_KEYUP/WM_CHAR out of the queue for its key-command channel, so
			// only KEYDOWNs ever reached the WndProc and key state latched on
			// forever.  GetAsyncKeyState is immune; the foreground guard keeps
			// background typing from driving the mech.
			{
				typedef short (__stdcall *AsyncFn)(int);
				typedef void *(__stdcall *FgFn)(void);
				typedef unsigned long (__stdcall *WtpFn)(void *, unsigned long *);
				typedef unsigned long (__stdcall *PidFn)(void);
				static AsyncFn pAsync = 0; static FgFn pFg = 0;
				static WtpFn pWtp = 0; static PidFn pPid = 0;
				if (pAsync == 0)
				{
					HMODULE u = GetModuleHandleA("user32.dll");
					HMODULE k = GetModuleHandleA("kernel32.dll");
					pAsync = (AsyncFn)GetProcAddress(u, "GetAsyncKeyState");
					pFg    = (FgFn)GetProcAddress(u, "GetForegroundWindow");
					pWtp   = (WtpFn)GetProcAddress(u, "GetWindowThreadProcessId");
					pPid   = (PidFn)GetProcAddress(k, "GetCurrentProcessId");
				}
				int focused = 0;
				if (pFg && pWtp && pPid)
				{
					void *fg = pFg();
					unsigned long fgPid = 0;
					if (fg) pWtp(fg, &fgPid);
					focused = (fgPid == pPid());
				}
				// TEST HOOK (BT_KEY_NOFOCUS=1): accept keys without foreground focus --
				// automation harnesses (SendInput from a background shell) can't grant
				// real foreground; interactive play never needs this.
				static int sNoFocus = -1;
				if (sNoFocus < 0) { const char *nf = getenv("BT_KEY_NOFOCUS"); sNoFocus = (nf && *nf == '1') ? 1 : 0; }
				if (sNoFocus) focused = 1;
				if (pAsync)
				{
					const int dn = 0x8000;
					gBTDrive.keyFwd   = focused && ((pAsync('W') | pAsync(0x26 /*VK_UP*/))    & dn) ? 1 : 0;
					gBTDrive.keyBack  = focused && ((pAsync('S') | pAsync(0x28 /*VK_DOWN*/))  & dn) ? 1 : 0;
					gBTDrive.keyLeft  = focused && ((pAsync('A') | pAsync(0x25 /*VK_LEFT*/))  & dn) ? 1 : 0;
					gBTDrive.keyRight = focused && ((pAsync('D') | pAsync(0x27 /*VK_RIGHT*/)) & dn) ? 1 : 0;
					gBTDrive.fire     = focused && ((pAsync(0x20 /*VK_SPACE*/) | pAsync(0x11 /*VK_CONTROL*/)) & dn) ? 1 : 0;
					static int sPrevX = 0;
					const int xNow = focused && (pAsync('X') & dn) ? 1 : 0;
					if (xNow && !sPrevX) gBTDrive.allStop = 1;	// edge -> one all-stop
					sPrevX = xNow;
				}
			}

			if (gBTDrive.allStop) { sLever = 0.0f; sDetent = 0; gBTDrive.allStop = 0; }

			if (getenv("BT_KEY_LOG"))
			{
				static float sKlog = 0.0f; sKlog += dt;
				if (sKlog >= 1.0f) { sKlog = 0.0f;
					DEBUG_STREAM << "[vctl] fwd=" << gBTDrive.keyFwd << " back=" << gBTDrive.keyBack
						<< " L=" << gBTDrive.keyLeft << " R=" << gBTDrive.keyRight
						<< " fire=" << gBTDrive.fire << " lever=" << sLever
						<< " stick=" << sStick << "\n" << std::flush; }
			}

			const int fwd = gBTDrive.keyFwd, back = gBTDrive.keyBack;
			if (!fwd && !back) sDetent = 0;				// keys released -> detent re-arms
			float sweep = ((fwd ? 1.0f : 0.0f) - (back ? 1.0f : 0.0f)) * kLeverRate * dt;
			if (sweep != 0.0f && !sDetent)
			{
				const float prev = sLever;
				sLever += sweep;
				// zero detent: a sweep that CROSSES 0 stops there and latches until
				// the keys are released (clean stop instead of swinging to reverse).
				if ((prev > 0.0f && sLever <= 0.0f && back) ||
					(prev < 0.0f && sLever >= 0.0f && fwd))
				{
					sLever = 0.0f;
					sDetent = 1;
				}
				if (sLever >  1.0f) sLever =  1.0f;
				if (sLever < -1.0f) sLever = -1.0f;
			}

			const float want = (gBTDrive.keyRight ? 1.0f : 0.0f) - (gBTDrive.keyLeft ? 1.0f : 0.0f);
			if (want != 0.0f)
			{
				sStick += want * kStickRate * dt;
				if (sStick >  1.0f) sStick =  1.0f;
				if (sStick < -1.0f) sStick = -1.0f;
			}
			else if (sStick != 0.0f)					// auto-center
			{
				const float step = kStickCenterRate * dt;
				if      (sStick >  step) sStick -= step;
				else if (sStick < -step) sStick += step;
				else                     sStick  = 0.0f;
			}

			gBTDrive.throttle = sLever;
			gBTDrive.turn     = sStick;
		}

		float throttle = gBTDrive.forced ? gBTDrive.forcedThrottle : gBTDrive.throttle;
		float turn     = gBTDrive.forced ? 0.0f : gBTDrive.turn;

		// BT_SPAWN_AT="x z [hdg]" (DEBUG harness): teleport the player to a
		// world position (+ optional heading, radians) on the first driven
		// frame -- exact-position reproduction of a user-reported render
		// glitch for A/B toggle comparison at the same viewpoint.
		{
			static int s_spawnAt = -1;
			static float s_sx = 0.0f, s_sz = 0.0f, s_sh = 0.0f;
			static int s_haveH = 0;
			if (s_spawnAt < 0)
			{
				const char *sv = getenv("BT_SPAWN_AT");
				if (sv)
				{
					const int n = sscanf(sv, "%f %f %f", &s_sx, &s_sz, &s_sh);
					if (n >= 2) { s_spawnAt = 1; s_haveH = (n >= 3); }
					else s_spawnAt = 0;
				}
				else s_spawnAt = 0;
			}
			if (s_spawnAt == 1)
			{
				s_spawnAt = 2;
				localOrigin.linearPosition.x = s_sx;
				localOrigin.linearPosition.z = s_sz;
				if (s_haveH)
				{
					gDriveHeading = s_sh;
					// the mech's REAL orientation (the chase camera derives from
					// it) is the quaternion, not the scalar mirror -- set both,
					// same convention as the bring-up drive path below.
					localOrigin.angularPosition = EulerAngles(0.0f, s_sh, 0.0f);
				}
				localToWorld = localOrigin;
				DEBUG_STREAM << "[spawnat] teleported to (" << s_sx << ", " << s_sz
					<< ") hdg=" << (s_haveH ? s_sh : gDriveHeading) << "\n" << std::flush;
			}
		}

		// BRING-UP repro harness: BT_FORCE_SECONDS=<n> releases the forced throttle
		// after n sim-seconds (headless stand-in for an interactive tap->release,
		// exercising the walk->stop gait transition the constant-throttle soaks
		// never take); BT_FORCE_TURN=<t> holds a steering demand (the turn path).
		if (gBTDrive.forced)
		{
			static float s_forceClock = 0.0f, s_forceLimit = -1.0f, s_forceTurn = -999.0f;
			if (s_forceLimit < 0.0f)
			{
				const char *fs = getenv("BT_FORCE_SECONDS");
				s_forceLimit = fs ? (float)atof(fs) : 0.0f;
			}
			if (s_forceTurn < -900.0f)
			{
				const char *ft = getenv("BT_FORCE_TURN");
				s_forceTurn = ft ? (float)atof(ft) : 0.0f;
			}
			turn = s_forceTurn;
			if (s_forceLimit > 0.0f)
			{
				s_forceClock += dt;
				if (s_forceClock > s_forceLimit)
				{
					throttle = 0.0f;
					turn = 0.0f;
				}
			}
			// BT_GOTO="x z" (DEBUG harness): beeline toward a world point --
			// steer the turn demand from the heading error each frame (headless
			// reproduction of "run toward that feature").  Uses the scalar
			// heading mirror gDriveHeading (forward = -Z at heading 0).
			{
				static float s_gx = 0.0f, s_gz = 0.0f;
				static int   s_goto = -1;
				if (s_goto < 0)
				{
					const char *gv = getenv("BT_GOTO");
					if (gv && sscanf(gv, "%f %f", &s_gx, &s_gz) == 2) s_goto = 1;
					else s_goto = 0;
				}
				if (s_goto == 1)
				{
					float ddx = s_gx - (float)localOrigin.linearPosition.x;
					float ddz = s_gz - (float)localOrigin.linearPosition.z;
					float want = (float)atan2(-ddx, -ddz);		// heading with fwd=-Z
					float err  = want - gDriveHeading;
					while (err >  3.14159265f) err -= 6.2831853f;
					while (err < -3.14159265f) err += 6.2831853f;
					// publish for the mapper bridge (the real-controls path reads
					// gBTDrive in mechmppr.cpp, NOT this local `turn`)
					gBTGotoTurn = (err > 0.3f) ? 1.0f : (err < -0.3f ? -1.0f
						: (err > 0.02f ? 0.3f : (err < -0.02f ? -0.3f : 0.0f)));
					if (ddx * ddx + ddz * ddz < 25.0f) gBTGotoTurn = 0.0f;
					// throttle down while far off-heading (the authentic
					// speed-vs-turn clamp makes run-speed turn circles huge)
					gBTGotoThrottle = (err > 0.5f || err < -0.5f) ? 0.2f : 1.0f;
					gBTGotoActive = 1;
					turn = gBTGotoTurn;		// non-real-controls fallback path
				}
			}
		}

		// ⭐ REAL CONTROLS (env BT_REAL_CONTROLS): route the raw input through the
		// RECONSTRUCTED MechControlsMapper (@004afd10) instead of consuming it raw.
		// INPUT BRIDGE (dev scaffolding, marked): write the mapper's published input
		// attributes exactly as a streamed Direct .CTL mapping would -- the engine's
		// ControlsInstanceDirectOf<T> writes these same public members from the
		// device groups; the dev box has no RIO/Thrustmaster, so WASD stands in for
		// the stick/throttle HARDWARE.  Everything downstream (speed/turn demands,
		// mode clamps, torso axes, HUD free-aim) is the real reconstructed tick,
		// which runs in the subsystem-roster walk below (un-skipped under this env).
		static const int s_realControls = BTEnvOn("BT_REAL_CONTROLS", 1);	// default ON (=0 to disable)
		if (s_realControls && controlsMapper != 0)
		{
			// Diagnostic: what the ENGINE controls push left in the attribute since
			// our last write (a stale device element overwriting the bridge shows
			// here as pre != our previous write).
			float preThrottle = controlsMapper->throttlePosition;
			(void)preThrottle;
			controlsMapper->throttlePosition = (throttle >= 0.0f) ? throttle : -throttle;
			controlsMapper->reverseThrust    = (throttle < 0.0f) ? 1 : 0;	// ControlsButton: >=1 engaged
			controlsMapper->stickPosition.x  = turn;						// Basic mode: turn = stick yaw
			controlsMapper->stickPosition.y  = 0.0f;
			// Consume the PREVIOUS frame's interpreted demands (the mapper ticks in
			// the roster walk after this block -- one frame of input latency).
			// turnDemand is the mode-shaped steering; speedDemand (world u/s, sign =
			// reverse) feeds the gait target below.
			turn = controlsMapper->turnDemand;
			static float s_mpprLog = 0.0f; s_mpprLog += dt;
			if (s_mpprLog >= 1.0f)
			{
				s_mpprLog = 0.0f;
				DEBUG_STREAM << "[mppr] in thr=" << controlsMapper->throttlePosition
					<< " pre=" << preThrottle
					<< " rev=" << controlsMapper->reverseThrust
					<< " stickX=" << controlsMapper->stickPosition.x
					<< " -> speedDemand=" << controlsMapper->speedDemand
					<< " turnDemand=" << controlsMapper->turnDemand
					<< " mode=" << controlsMapper->controlMode << "\n" << std::flush;
			}
		}

		// BRING-UP one-shot: dump the mech's skeleton segment names + joint indices so
		// we can identify the TORSO horizontal joint for torso-twist tracking (P3).
		{
			static int s_segDump = 0;
			if (s_segDump++ == 0)
			{
				EntitySegment::SegmentTableIterator it(segmentTable);
				EntitySegment *seg;
				int i = 0;
				while ((seg = it.ReadAndNext()) != 0)
				{
					DEBUG_STREAM << "[skel] seg[" << i++ << "] name=" << seg->GetName()
						<< " jointIdx=" << seg->GetJointIndex() << "\n";
				}
				DEBUG_STREAM << "[skel] total=" << i << "\n" << std::flush;
			}
		}

		// Snapshot the pre-move position for the collision resolve after the drive.
		Point3D collisionOldPos = localOrigin.linearPosition;

		if (!gDriveSeeded) { gDriveHeading = 0.0f; gDriveSeeded = 1; }

		// --- turn: integrate heading, write it onto the body orientation -----
		gDriveHeading += turn * kDriveTurnRate * dt;	// scalar mirror (drive log + fallback)
		if (s_realControls)
		{
			// AUTHENTIC ORIENTATION INTEGRATION (the IntegrateMotion-tail form):
			// compose the yaw RATE into the pose quaternion via the engine
			// integrate op (Quaternion::Add == FUN_00409f58) -- the same op the
			// binary's motion tail uses -- instead of rebuilding the orientation
			// from a scalar heading.  The rate SCALE (kDriveTurnRate) remains a
			// bring-up constant; the authentic per-mech turn-rate parameter is
			// part of the deferred full-IntegrateMotion work.
			Vector3D	angStep(0.0f, turn * kDriveTurnRate * dt, 0.0f);
			Quaternion	prevPose = localOrigin.angularPosition;
			localOrigin.angularPosition.Add(prevPose, angStep);
		}
		else
		{
			localOrigin.angularPosition = EulerAngles(0.0f, gDriveHeading, 0.0f);
		}
		localToWorld = localOrigin;					// build the world matrix

		// --- forward step: the mech faces local -Z (gun ports / eyepoint are at
		//     -Z; see btl4vid.cpp).  Take the matrix Z basis in world and negate
		//     to get the facing direction; the ACTUAL forward distance this frame
		//     is the animation's root translation (see the Animate block below) --
		//     P3 STEP 1: locomotion is now ANIMATION-DRIVEN, not a procedural slide,
		//     so the body advances exactly as far as the gait clip's feet step
		//     (feet plant, no skating).  The old `kDriveMaxSpeed * throttle * dt`
		//     slide is removed; `adv` from AnimationInstance::Animate replaces it.
		UnitVector zAxis;
		localToWorld.GetFromAxis(Z_Axis, &zAxis);
		const float fx = -zAxis.x;
		const float fz = -zAxis.z;

		// --- P3 STEP 2: GAIT SELECTION by throttle.  Pick the body clip from throttle
		//     magnitude -- |throttle| >= kRunThrottle -> RUN (blhrrl), else WALK (blhwwl);
		//     each moves at ITS clip's authentic root-translation speed (STEP 1).  Throttle
		//     SIGN gives direction: negative -> reverse (back up).  (The blh set has no
		//     dedicated reverse CYCLE clip -- stand/walk/run + transitions only -- so reverse
		//     is a bring-up: it plays the selected forward clip and drives the body backward.)
		//     SetAnimation only on a gait CHANGE (re-binding every frame would reset the phase).
		//
		// ⚠ GAIT MUST TICK EVERY FRAME (task #15 fix): this block was gated on
		// `throttle != 0`, so releasing the key stopped CALLING the state machine --
		// the skeleton froze at the last written pose and the authentic walk->
		// stop->stand transition chain (BodyClipFinished states 8/9, the wsl/wsr
		// clips, which END in the standing pose) never got the chance to play.
		// The SM handles idle natively: bodyTargetSpeed=0 drives walk->stop->stand
		// and the Standing case returns adv=0 (no motion).  Only the LEGACY inline
		// clip-select path (BT_GAIT_SM=0) still needs the throttle gate (it would
		// loop the walk clip forever at idle).
		{
			static const int s_gaitTicksAlways = BTEnvOn("BT_GAIT_SM", 1);
			if (s_gaitTicksAlways || throttle != 0.0f)
			{
			const int   wantGait = ((throttle < 0.0f ? -throttle : throttle) >= kRunThrottle) ? 1 : 0;
			const char *suffix   = wantGait ? "rrl" : "wwl";

			// ⭐ P3 STEP-7 CUTOVER (env BT_GAIT_CUTOVER; default OFF -> STEP 1-2 path).
			// Drive locomotion via the reconstructed two-channel gait (the real
			// SequenceController bodyAnimation), replacing the STEP-1/2 free-standing
			// AnimationInstance stand-in.  bodyAnimation.Advance runs the reconstructed
			// controller: it animates the skeleton joints AND returns the clip's
			// per-frame root-translation forward step (applied to localOrigin).
			static const int s_gaitCutover = BTEnvOn("BT_GAIT_CUTOVER", 1);	// default ON (=0 to disable)
			if (s_gaitCutover)
			{
				static const int s_gaitSM = BTEnvOn("BT_GAIT_SM", 1);		// default ON (=0 to disable)
				Scalar adv;
				Scalar legAdv = 0.0f;		// leg-channel distance (drives the LOCAL mech)
				if (s_gaitSM)
				{
					// STEP 2: drive the REAL gait STATE MACHINE (Mech::AdvanceBodyAnimation,
					// mech2.cpp) instead of the inline clip-select.  It re-syncs bodyAnimation
					// State from bodyStateAlarm, slews bodyCycleSpeed toward bodyTargetSpeed with
					// the LoadLocomotionClips speed caps, and Advances the bound clip.  Feed the
					// inputs the controls mapper normally would (it is bypassed in bring-up) and
					// force the walk-cycle state 6 (case 6/7 -> slew to walkStrideLength + Advance
					// animationClips[6]=wwr).  Requires BT_GAIT_CUTOVER too (loads the clips).
					globalTimeScale  = 1.0f;
					idleStrideScale  = 1.0f;
					// forwardCycleRate: read from the model record (+0x44) in the
					// ctor, but the Blackhawk's value decodes to ~1 u/s^2 -- a
					// 55-SECOND ramp to run speed (the mech "couldn't go forward"
					// and the clip crawled in slow motion).  Either the record
					// field is mis-offset or its units differ (per-frame?).  Until
					// the field decode is verified against the raw parser, FLOOR
					// the rate at a pod-plausible 25 u/s^2 (0 -> run in ~2.5s);
					// values above that (a genuine tuning read) pass through.
					{
						static int s_rateLogged = 0;
						if (!s_rateLogged)
						{
							s_rateLogged = 1;
							DEBUG_STREAM << "[gait] model accel read=" << forwardCycleRate
								<< " (floored to >=25)" << "\n" << std::flush;
						}
					}
					if (!(forwardCycleRate >= 25.0f && forwardCycleRate < 10000.0f))
						forwardCycleRate = 25.0f;
					movementMode     = 1;						// ground, non-death, non-airborne
					// reverseSpeedMax2@0x7a0 is the run-cycle bodyCycleSpeed CLAMP (AdvanceBody
					// Animation case 12/13); LoadLocomotionClips does not set it -> it reads
					// 0xCDCDCDCD (-4.3e8) and the clamp clobbers bodyCycleSpeed -> the run cycle
					// explodes.  Bring-up: clamp the run cadence to the run stride (reverseStride
					// Length, the case-12 divisor) so bodyCycleSpeed/reverseStrideLength ~ 1, like
					// walk.  (The authentic value comes from the model/LoadLocomotionClips.)
					reverseSpeedMax2 = reverseStrideLength;
					// Gait SELECTION by throttle: walk aims for walkStrideLength; run aims for
					// reverseSpeedMax (the run/reverse cap).  Exceeding walkStrideLength makes the
					// walk handler transition "up" to the run cycle (state 11 -> 12/13) via the real
					// BodyClipFinished callback -- i.e. the authentic walk->run transition.
					// REAL CONTROLS: the commanded speed comes from the reconstructed mapper's
					// speedDemand (topSpeed * throttle * scale, mode-clamped vs steering -- the
					// AUTHENTIC gait selection input; |.| because sign encodes reverse).
					// SIGNED demand (task #15): the earlier Abs() stripped the sign, so
					// the body/motion channel could never see reverse -- motion direction
					// then had to come from the raw throttle sign, flipping INSTANTLY
					// while the animation transitioned gradually (the "physics don't
					// line up with the animation" desync).  Signed, the SM decelerates
					// through stop -> reverse-entry exactly like the leg channel.
					if (s_realControls && controlsMapper != 0)
						bodyTargetSpeed = controlsMapper->speedDemand;
					else
						bodyTargetSpeed = ((throttle < 0.0f) ? -1.0f : 1.0f)
							* (wantGait ? reverseSpeedMax : walkStrideLength);
					if (gCurrentGait != 6)
					{
						bodyCycleSpeed = 0.0f;					// slew up from rest
						// Enter at STANDING (task #15): both channels' case-0 handlers
						// self-arm stand->walk when the demand rises past standSpeed
						// (AdvanceBodyAnimation case 0 == raw FUN_004a5678), so the boot
						// state is the authentic idle stand -- no pre-armed walk clip
						// stuck at frame 1 while waiting for the first throttle.
						bodyStateAlarm.SetLevel(0);
						if (s_realControls)
						{
							// AUTHENTIC TWO-CHANNEL SPLIT: the LEG channel enters
							// NATURALLY from Standing (state 0): AdvanceLegAnimation's
							// case-0 sees the live mapper speedDemand > standSpeed and
							// arms stand->walk (5); the real leg finished-callback
							// (LegClipFinished == FUN_004a6928) then chains the
							// transitions.  Only hygiene here: a clean cycle-speed start.
							legCycleSpeed = 0.0f;
						}
						gCurrentGait = 6;
						DEBUG_STREAM << "[gaitSM] enter walk-6 walkStride=" << walkStrideLength
							<< " reverseSpeedMax=" << reverseSpeedMax << " target=" << bodyTargetSpeed
							<< " split=" << s_realControls << "\n" << std::flush;
					}
					if (s_realControls)
					{
						// FOOT-PLANT FIDELITY (raw FUN_004ab430:15076 -> FUN_004ab1c8):
						// the binary advances the BODY controller with move_joints=1 --
						// the DISPLAYED pose and the travel distance come from the SAME
						// Advance (same clip, same phase), so feet plant by construction.
						// The leg channel (local-sim, hardcoded move_joints=1 in the raw,
						// FUN_004a5028:12006) runs FIRST; the body's writes land after and
						// win the frame, exactly the binary's order.  The earlier split
						// (body silenced with move_joints=0, leg pose displayed) showed
						// leg-channel joints against body-channel travel -- two
						// independently-phased state machines -> visible foot glide.
						legAdv = AdvanceLegAnimation(dt);		// channel A: local sim (live demand)
						adv    = AdvanceBodyAnimation(dt, 1);	// channel B: displayed pose + motion
					}
					else
					{
						adv = AdvanceBodyAnimation(dt, 1);		// single-channel: body does both
					}
					// COCKPIT BOB (task #15): the leg channel writes the clip's vertical
					// root motion into jointlocal (balltranslate) every frame, but the
					// camera's DCS chain doesn't consume animated joints -- publish the
					// baseline-relative bob for DPLEyeRenderable (L4VIDRND.cpp) to add
					// to the view.  Baseline = the first sample after the joint resolves
					// (the clip's neutral root height).
					{
						extern float gBTEyeBobY;
						extern float gBTEyeSwayX;
						static Joint *s_rootJt = 0; static int s_rootTried = 0;
						static float s_bobBase = 0.0f, s_swayBase = 0.0f; static int s_bobBased = 0;
						if (!s_rootTried) { s_rootTried = 1; s_rootJt = ResolveJoint("jointlocal"); }
						if (s_rootJt)
						{
							const Point3D rt = s_rootJt->GetTranslation();
							if (!s_bobBased) { s_bobBased = 1; s_bobBase = rt.y; s_swayBase = rt.x; }
							gBTEyeBobY  = rt.y - s_bobBase;
							gBTEyeSwayX = rt.x - s_swayBase;	// lateral weight shift (the swagger)
						}
					}
					static float s_smlog = 0.0f; s_smlog += dt;
					static double s_advSum = 0.0, s_legSum = 0.0;
					s_advSum += adv; s_legSum += legAdv;
					if (s_smlog >= 1.0f) { s_smlog = 0.0f;
						extern float gBTEyeBobY;
						static Joint *s_dbgRoot = 0; static int s_dbgTried = 0;
						if (!s_dbgTried) { s_dbgTried = 1; s_dbgRoot = ResolveJoint("jointlocal"); }
						Point3D dbgRt(0,0,0);
						if (s_dbgRoot) dbgRt = s_dbgRoot->GetTranslation();
						DEBUG_STREAM << "[sync] advSum=" << (float)s_advSum
							<< " legSum=" << (float)s_legSum
							<< " posZ=" << localOrigin.linearPosition.z
							<< " rootZ=" << dbgRt.z << " rootX=" << dbgRt.x << "\n" << std::flush;
						DEBUG_STREAM << "[gaitSM] adv=" << adv << " legAdv=" << legAdv
							<< " cycleSpeed=" << bodyCycleSpeed << " legCycle=" << legCycleSpeed
							<< " state=" << bodyAnimationState << " legState=" << legAnimationState
							<< " kfCur=" << bodyAnimation.currentFrame
							<< " bob=" << gBTEyeBobY << "\n" << std::flush; }
				}
				else
				{
					if (gCurrentGait != wantGait)
					{
						ResourceDescription::ResourceID *clip = ResolveAnimationClip("blh", suffix);
						if (clip)
						{
							// loop callback: SequenceController::Advance calls it at end-of-clip
							// to re-arm the same clip (bring-up loop; see Mech::LoopBodyClip).
							bodyAnimation.SelectSequence(*clip, (void *)&Mech::LoopBodyClip, 0, 0);
							gCurrentGait = wantGait;
							DEBUG_STREAM << "[gait] SequenceController -> blh" << suffix
								<< " id=" << (long)*clip << " (" << (wantGait ? "run" : "walk")
								<< ")\n" << std::flush;
						}
					}
					adv = bodyAnimation.Advance(dt, 1);			// inline clip-select path
				}
				// Motion direction comes FROM the animation: reverse clips carry
				// negative root translation, so adv is already signed.  (dir kept
				// only for the legacy non-SM path, whose forward clips are unsigned.)
				const float dir = (s_gaitCutover && BTEnvOn("BT_GAIT_SM", 1))
					? 1.0f
					: ((throttle < 0.0f) ? -1.0f : 1.0f);

				// --- AUTHENTIC world-step (Mech::IntegrateMotion tail @004ab1c8) ---
				// The 1995 mech does NOT slide the position directly; it integrates a
				// VELOCITY vector rotated by the body orientation.  VERIFIED mapping: the
				// 1995 motion transform { Point3D @0x260; Quaternion @0x26c } IS the engine
				// `localOrigin` (Origin = { linearPosition; angularPosition }, ORIGIN.h:15;
				// FUN_0040ab44 builds the matrix from BOTH halves).  So reproduce the real
				// integrator: localVelocity = {0,0,-adv/dt} (forward = local -Z), rotate it
				// into world space by the orientation via the now-backed world-step transform
				// FUN_00408744, then position += worldVelocity*dt.  Result == facing*adv, but
				// through the authentic velocity->rotate->integrate model (runs FUN_00408744
				// live).  Remaining full-IntegrateMotion work: velocity STORAGE for dead-reckon,
				// orientation integration, and the AdvanceBodyAnimation gait state machine
				// (+ LoadLocomotionClips) in place of the inline clip select above.
				const Scalar invDt = (dt > 0.0f) ? (1.0f / dt) : 0.0f;
				// VELOCITY SMOOTHING (task #15, the binary's IntegrateMotion model):
				// SequenceController::Advance returns distance only when keyframes
				// are CROSSED -- partial frames interpolate the joints but add zero
				// distance (binary-faithful lumping).  Integrating adv/dt directly
				// stutters the ground speed while the legs sweep smoothly (feet
				// appear to glide/skate).  The original smoothed this through its
				// persistent-velocity integration; reproduce it: velocity = the
				// accumulated distance over the elapsed time since the last
				// keyframe contribution, HELD across zero-distance frames, hard
				// zero when Standing (and after 0.3s of silence, e.g. paused clip).
				// FOOT-PLANT BY CONSTRUCTION (v4 -- BINARY-FAITHFUL: travel = the
				// BODY channel, the SAME Advance that writes the displayed joints;
				// raw FUN_004ab430:15076 advances the body with move_joints=1 and
				// takes the travel from it).  v3 sourced travel from the LEG channel
				// while the BODY drew the joints -- TWO state machines that could
				// (and did) drift apart whenever any event touched one channel's
				// state without the other: the knockdown (fixed by staggering both),
				// then AGAIN on the Mad Cat / analog-lever sweeps crossing gait
				// boundaries (each channel's end-of-clip callback samples the demand
				// at a different instant, so near a walk/run threshold they can pick
				// DIFFERENT next states -> permanent phase split = the foot-slip).
				// Sourcing travel from the displayed channel kills the whole desync
				// CLASS: display == travel by construction.  The earlier "tunnels
				// through obstacles" objection to this is OBSOLETE -- the knockdown
				// now staggers the BODY channel too (SetBodyAnimation(0x20)), so a
				// hard impact freezes travel exactly as before (re-verified).  The
				// leg channel keeps running as the local sim it is in the binary
				// (its joint writes land first and are overwritten by the body's).
				const Scalar localAdv = adv * dir;
				linearSpeed = (localAdv < 0.0f ? -localAdv : localAdv) * invDt;	// forward ground speed -> LinearSpeed gauge
				Vector3D localVel(0.0f, 0.0f, -localAdv * invDt);	// exact frame distance as velocity
				Matrix34 orient;									// rotation from the heading (set @ line ~626)
				Matrix34::FromQuaternion(&orient, &localOrigin.angularPosition);
				Vector3D worldVel;
				FUN_00408744(&worldVel, (Scalar *)&localVel, &orient);	// worldVel = orient * localVel
				localOrigin.linearPosition.x += worldVel.x * dt;
				localOrigin.linearPosition.y += worldVel.y * dt;
				localOrigin.linearPosition.z += worldVel.z * dt;
				localToWorld = localOrigin;
				gBodyAnimLog += dt;
				if (gBodyAnimLog >= 1.0f)
				{
					gBodyAnimLog = 0.0f;
					DEBUG_STREAM << "[gait] adv=" << adv << " pos=("
						<< localOrigin.linearPosition.x << ", "
						<< localOrigin.linearPosition.z << ")\n" << std::flush;
				}
			}
			else
			{
			if (gBodyAnim == 0 || gCurrentGait != wantGait)		// first time, or gait changed
			{
				ResourceDescription::ResourceID *clip = ResolveAnimationClip("blh", suffix);
				if (clip)
				{
					if (gBodyAnim == 0) gBodyAnim = new AnimationInstance(this);
					// LOOP callback (not NULL) -- Animate invokes it at clip end
					// (JMOVER ~1592); NULL there crashes on a null member ptr.
					gBodyAnim->SetAnimation(
						*clip,
						reinterpret_cast<JointedMover::AnimationCallback>(&Mech::OnBodyAnimFinished));
					gCurrentGait   = wantGait;
					gBodyAnimReady = 1;
					DEBUG_STREAM << "[anim] gait -> blh" << suffix << " id=" << (long)*clip
						<< " (" << (wantGait ? "run" : "walk") << ")\n" << std::flush;
				}
				else
				{
					gBodyAnimReady = -1;
					DEBUG_STREAM << "[anim] blh" << suffix << " unresolved -- not moving\n" << std::flush;
				}
			}

			if (gBodyAnimReady == 1 && gBodyAnim != 0)
			{
				// Animate advances the clip, writes the joint DCS (legs cycle) AND returns
				// the clip's per-frame root-translation distance (the [RootTranslation]
				// integral -- CLAUDE.md section 7).  Drive the body by exactly that distance
				// so travel == foot stride (feet plant); negative throttle backs it up.
				Scalar adv = gBodyAnim->Animate(dt, True);
				// Motion direction comes FROM the animation: reverse clips carry
				// negative root translation, so adv is already signed.  (dir kept
				// only for the legacy non-SM path, whose forward clips are unsigned.)
				const float dir = (s_gaitCutover && BTEnvOn("BT_GAIT_SM", 1))
					? 1.0f
					: ((throttle < 0.0f) ? -1.0f : 1.0f);
				localOrigin.linearPosition.x += fx * adv * dir;
				localOrigin.linearPosition.z += fz * adv * dir;
				linearSpeed = (dt > 0.0f) ? ((adv < 0.0f ? -adv : adv) / dt) : 0.0f;	// LinearSpeed gauge
				localToWorld = localOrigin;					// rebuild with the new position

				gBodyAnimLog += dt;
				if (gBodyAnimLog >= 1.0f)
				{
					gBodyAnimLog = 0.0f;
					DEBUG_STREAM << "[anim] " << (wantGait ? "run" : "walk")
						<< (dir < 0 ? " (reverse)" : "") << " adv=" << adv
						<< " loops=" << gBodyAnimLoops
						<< "  pos=(" << localOrigin.linearPosition.x << ", "
						<< localOrigin.linearPosition.z << ")\n" << std::flush;
				}
			}
		}		// close the BT_GAIT_CUTOVER else (STEP 1-2 path)
			}	// close the gait-ticks-always gate
		}		// close the every-frame gait scope (task #15 fix)

		// DIAG (turn-hitch hunt): flag any frame that took abnormally long -- the
		// hang shows up as a big dt on the NEXT tick.  Correlate with [loadobj].
		if (dt > 0.2f)
			DEBUG_STREAM << "[spike] dt=" << dt
				<< " turn=" << turn << " thr=" << throttle << "\n" << std::flush;

		// --- 1 Hz world-position log while moving (proves the body walks) -----
		gDriveLogAccum += dt;
		if ((throttle != 0.0f || turn != 0.0f) && gDriveLogAccum >= 1.0f)
		{
			gDriveLogAccum = 0.0f;
			DEBUG_STREAM << "[drive] pos=("
				<< localOrigin.linearPosition.x << ", "
				<< localOrigin.linearPosition.y << ", "
				<< localOrigin.linearPosition.z << ")  hdg=" << gDriveHeading
				<< "  thr=" << throttle << "  turn=" << turn
				<< "  dt=" << dt << "\n" << std::flush;
		}

		// --- COLLISION (engine Mover flow; gated BT_COLLISION) ------------------
		// After the drive moved localOrigin, resolve penetration against the world's
		// collision solids (terrain buttes/hills + buildings) with the engine's OWN
		// pipeline (MOVER.cpp): reposition the collision volume to the new transform,
		// gather overlapping solids, and let ProcessCollisionList push localOrigin back
		// out of anything it entered (it writes localOrigin.linearPosition + reads
		// worldLinearVelocity).  This is what stops the mech phasing through objects and
		// sinking through the ground.  The dead Mech::Simulate used a 1995 heightfield
		// terrain-follow (FUN_0040e5f0); the 2007 engine models terrain AS collision
		// solids, so this single path covers both terrain-follow AND object collision.
		// Guarded on a real collision volume -> no-op for volume-less mechs.
		// --- VELOCITY STORAGE (the MP update writer; ALWAYS maintained) ---------
		// The engine Mover::WriteUpdateRecord (MOVER.cpp:740) publishes
		// localVelocity + worldLinearVelocity in every network update record --
		// the data remote pods dead-reckon from.  Keep them live each frame:
		// world velocity from the frame's position delta; local velocity =
		// forward speed on local -Z (the mech's facing axis) + the yaw rate.
		{
			const Scalar invDtV = (dt > 0.0001f) ? (1.0f / dt) : 0.0f;
			worldLinearVelocity.x = (localOrigin.linearPosition.x - collisionOldPos.x) * invDtV;
			worldLinearVelocity.y = (localOrigin.linearPosition.y - collisionOldPos.y) * invDtV;
			worldLinearVelocity.z = (localOrigin.linearPosition.z - collisionOldPos.z) * invDtV;
			Scalar fwdSpeed = (Scalar)sqrtf(
				worldLinearVelocity.x * worldLinearVelocity.x +
				worldLinearVelocity.z * worldLinearVelocity.z);
			localVelocity.linearMotion  = Vector3D(0.0f, worldLinearVelocity.y, -fwdSpeed);
			localVelocity.angularMotion = Vector3D(0.0f, turn * kDriveTurnRate, 0.0f);

			// CRASH motion suppression (spec: the binary zeroes localVelocity while
			// crashed).  While the stagger clip (legState 0x20) plays, freeze the
			// velocity so a knocked-down mech does not creep back into the obstacle and
			// re-detect an impact every frame (impactVel = localVelocity below).  Then
			// run the post-recovery refractory so it can't instantly re-knockdown.
			if ((int)legStateAlarm.GetLevel() == 0x20)
			{
				worldLinearVelocity        = Vector3D(0.0f, 0.0f, 0.0f);
				localVelocity.linearMotion = Vector3D(0.0f, 0.0f, 0.0f);
			}
		}

		if (GroundReal())
		{
			// AUTHENTIC GROUND MODEL (task #15, ground-model-decode): the binary's
			// probe/snap/response block (FUN_004a9b5c @4aa630-4aab5f) -- see the
			// AuthenticGroundAndCollide banner below.  NO gravity, NO floor clamp,
			// NO step guard in this path; a probe MISS (h==-1) does NOTHING (Y
			// holds -- byte-faithful, also correct over authentic no-solid content
			// like the rav canyon backdrops / arena detail pieces).
			AuthenticGroundAndCollide(dt, collisionOldPos);
		}
		else if (BTEnvOn("BT_COLLISION", 1) && GetCollisionVolumeCount() > 0)
		{
			// GRAVITY / terrain-settle: the collision below only pushes the mech OUT of
			// solids -- there is no downward force, so a floating mech never comes down.
			// Press it down each frame; the collision then holds it at the terrain surface
			// (gravity down + collision up = equilibrium ON the ground).  A FLOOR CLAMP at
			// the spawn/ground Y is the safety net: on a map with NO terrain solid the mech
			// rests at ground level instead of falling forever.  On real terrain (modeled as
			// solids) the collision catches it above the floor -> it follows hills/valleys.
			// kGravityRate world-units/sec (tunable via BT_GRAVITY).
			static Scalar s_groundY = 0.0f; static int s_groundYSet = 0;
			static const Scalar kGravityRate =
				getenv("BT_GRAVITY") ? (Scalar)atof(getenv("BT_GRAVITY")) : 20.0f;
			if (!s_groundYSet) { s_groundY = localOrigin.linearPosition.y; s_groundYSet = 1; }
			localOrigin.linearPosition.y -= kGravityRate * dt;			// fall

			const Scalar invDtC = (dt > 0.0f) ? (1.0f / dt) : 0.0f;
			worldLinearVelocity.x = (localOrigin.linearPosition.x - collisionOldPos.x) * invDtC;
			worldLinearVelocity.y = (localOrigin.linearPosition.y - collisionOldPos.y) * invDtC;
			worldLinearVelocity.z = (localOrigin.linearPosition.z - collisionOldPos.z) * invDtC;
			MoveCollisionVolume();							// reposition the volume to localToWorld
			BoxedSolidCollisionList *cols = GetCurrentCollisions();
			if (cols)
			{
				Point3D before = localOrigin.linearPosition;
				Damage collisionDamage;						// filled by ProcessCollisionList (unused)
				ProcessCollisionList(cols, dt, collisionOldPos, &collisionDamage);	// pushes localOrigin out
				Scalar dx = localOrigin.linearPosition.x - before.x;
				Scalar dz = localOrigin.linearPosition.z - before.z;
				if (dx*dx + dz*dz > 0.01f)					// log only real object/wall pushes (not ground settle)
					DEBUG_STREAM << "[collide] pushed out by (" << dx << ", "
						<< (localOrigin.linearPosition.y - before.y) << ", " << dz
						<< ") pos=(" << localOrigin.linearPosition.x << ", "
						<< localOrigin.linearPosition.z << ")\n" << std::flush;
			}
			// Floor clamp: never sink below the base ground (safety on solid-less maps).
			if (localOrigin.linearPosition.y < s_groundY)
				localOrigin.linearPosition.y = s_groundY;
			localToWorld = localOrigin;						// rebuild after gravity/collision/clamp
		}

		// LOD EYEPOINT (authentic): feed the renderer the MECH's eyepoint as the
		// LOD-distance reference.  The pod's eyepoint sat ON the mech (cockpit),
		// so turning in place never changed an object's LOD distance; our chase
		// camera ORBITS the mech +-40u as it turns, which swept objects near
		// their 0x2046 band edges in and out = "scenery blinks with viewing
		// angle" / floor flicker at the arena fringe.
		{
			extern void BTSetLodEye(float, float, float);
			BTSetLodEye((float)localOrigin.linearPosition.x,
			            (float)localOrigin.linearPosition.y + 7.0f,	// cockpit height
			            (float)localOrigin.linearPosition.z);
		}

		// SHADOW TILT (task #20 + #15): pose 'jointshadow' (the *_tshd proxy's
		// terrain-angle channel; SKL "apply terrain angle to pitch and roll") to the
		// LOCAL ground slope each frame.  The ground decode (#15) is now live, so
		// instead of the old flat-up placeholder we sample the collision surface at
		// the mech's XZ and two world-axis offsets, build the surface normal from the
		// height gradient, and rotate it into the mech's local frame (upright, yaw =
		// gDriveHeading).  On a slope the shadow then LIES ON the ground; the old flat
		// quad was buried in the hillside and Z-culled -> "the shadow disappears on
		// elevation" (and the missing ground-contact cue made the feet read as sunk,
		// though the origin is measurably ON the surface).  A probe miss (mech at the
		// edge of its collision node) falls back to flat.  Gated BT_SHADOW_TILT (=0
		// restores the flat placeholder for A/B).
		{
			Vector3D shadowNormal(0.0f, 1.0f, 0.0f);
			static int s_shTilt = -1;
			if (s_shTilt < 0)
			{
				const char *sv = getenv("BT_SHADOW_TILT");
				s_shTilt = (sv == 0 || sv[0] != '0') ? 1 : 0;
			}
			BoundingBoxTreeNode *gnode = (GetCollisionVolumeCount() > 0 && collisionTemplate != 0)
				? GetMoverCollisionRoot() : 0;
			if (s_shTilt && gnode != 0)
			{
				const Scalar D     = 12.0f;						// probe offset (small -> stays in the node)
				const Scalar baseY = localOrigin.linearPosition.y + collisionTemplate->minY + 50.0f;
				const Scalar cx    = localOrigin.linearPosition.x;
				const Scalar cz    = localOrigin.linearPosition.z;
				Scalar hC = -1.0f, hX = -1.0f, hZ = -1.0f;
				Point3D qC(cx, baseY, cz), qX(cx + D, baseY, cz), qZ(cx, baseY, cz + D);
				BoundingBox *bC = gnode->FindBoundingBoxUnder(qC, &hC);
				BoundingBox *bX = gnode->FindBoundingBoxUnder(qX, &hX);
				BoundingBox *bZ = gnode->FindBoundingBoxUnder(qZ, &hZ);
				if (bC != 0 && bX != 0 && bZ != 0)
				{
					// surfaceY = probeY - downDistance; gradient -> world normal (-dH/dx, 1, -dH/dz)
					const Scalar yC = baseY - hC, yX = baseY - hX, yZ = baseY - hZ;
					Vector3D wn(-(yX - yC) / D, 1.0f, -(yZ - yC) / D);
					Scalar len = (Scalar)sqrtf(wn.x * wn.x + wn.y * wn.y + wn.z * wn.z);
					if (len > 1e-6f) { wn.x /= len; wn.y /= len; wn.z /= len; }
					// world -> mech-local: rotate by -gDriveHeading about Y (mech upright, yaw only)
					const Scalar cth = (Scalar)cosf((float)gDriveHeading);
					const Scalar sth = (Scalar)sinf((float)gDriveHeading);
					shadowNormal.x = wn.x * cth - wn.z * sth;
					shadowNormal.y = wn.y;
					shadowNormal.z = wn.x * sth + wn.z * cth;
				}
			}
			UpdateShadowJoint(shadowNormal);
		}

		// --- VISUAL-GROUND CONFORM (PORT ADDITION, presentation only; btvisgnd.cpp;
		//     gate BT_VISUAL_GROUND, default ON) ---------------------------------
		// The sim rides the coarse 1995 collision solids (authentic snap: origin.y
		// = solid surfaceY); the VISUAL terrain runs 0..~2.1u above them on slopes,
		// clipping the feet.  Invisible in 1995 (cockpit-only view); visible in our
		// external camera.  Lift the RENDER matrix to the visible surface sampled
		// from the actual terrain triangles under the mech.  localOrigin (collision,
		// aim, damage, dead-reckoning) is NEVER touched; next frame's drive rebuilds
		// localToWorld from localOrigin, so the lift is per-frame and render-only.
		{
			extern float BTVisualGroundLift(float x, float y, float z);
			const float vLift = BTVisualGroundLift(
				localOrigin.linearPosition.x,
				localOrigin.linearPosition.y,
				localOrigin.linearPosition.z);
			if (vLift != 0.0f)
			{
				localToWorld(3, 1) = localToWorld(3, 1) + vLift;
				if (GroundLog())
				{
					static float s_vgAccum = 0.0f; s_vgAccum += dt;
					if (s_vgAccum >= 1.0f)
					{
						s_vgAccum = 0.0f;
						DEBUG_STREAM << "[visgnd] lift=" << vLift
							<< " at (" << localOrigin.linearPosition.x << ", "
							<< localOrigin.linearPosition.y << ", "
							<< localOrigin.linearPosition.z << ")\n" << std::flush;
					}
				}
			}
		}

		// --- MASTER UPDATE EMISSION (P6 multiplayer; the VTV pattern, RP/VTV.cpp:
		//     840-861) --------------------------------------------------------
		// Predict where remote pods believe this mech is (run OUR OWN dead
		// reckoner: projectedOrigin advances from the last-broadcast state) and
		// ForceUpdate() when the truth diverges -- position error, orientation
		// deviation, or the 2-second heartbeat.  ForceUpdate marks the update-
		// model bits; the engine update pump then WriteUpdateRecord()s (our
		// localVelocity/worldLinearVelocity are maintained every frame above)
		// and the InterestManager broadcasts to all remote hosts.
		if (GetInstance() == MasterInstance && deadReckoner != 0
			&& application != 0
			&& application->GetApplicationState() == Application::RunningMission)
		{
			// (RunningMission gate: broadcasting motion during mission CREATION kept
			// the PEER's event queue busy, so its CreatingMission->LoadingMission
			// quiet-timeout never fired -- the pods deadlocked pre-launch.  The real
			// system never moves before launch, so the gate is faithful.)
			(this->*deadReckoner)();
			Vector3D error;
			error.Subtract(
				projectedOrigin.linearPosition,
				localOrigin.linearPosition);
			Quaternion angular_deviation;
			angular_deviation.Subtract(
				projectedOrigin.angularPosition,
				localOrigin.angularPosition);
			if (
				error.LengthSquared() > 0.04f
				|| Abs(angular_deviation.w) < 0.997f
				|| lastPerformance - lastUpdate > 2.0f
			)
			{
				ForceUpdate();
			}
			if (getenv("BT_REPL_LOG"))
			{
				static float s_emitLog = 0.0f; s_emitLog += dt;
				if (s_emitLog >= 1.0f)
				{
					s_emitLog = 0.0f;
					DEBUG_STREAM << "[emit] updateModel=" << (int)updateModel
						<< " errSq=" << error.LengthSquared()
						<< " lastPerf=" << (float)lastPerformance
						<< " lastUpd=" << (float)lastUpdate
						<< " interesting=" << (int)IsInteresting() << "\n" << std::flush;
				}
			}
		}

		// --- WEAPON FIRE VISUAL (port addition): on the trigger, draw the muzzle->hit
		//     BEAM + a muzzle flash + (solo) an impact explosion -- ALWAYS, so you can
		//     see your weapons fire even with no locked target (aim = the crosshair,
		//     straight ahead, raycast to the terrain).  The 1995 game rendered
		//     cockpit-only so it never needed a third-person weapon view; our external
		//     camera does, and the dpl_* beam layer was never ported -- this is it.
		//     Damage stays in the target block below; this owns only the visual, on its
		//     own cooldown.  (Real per-weapon fire -- Emitter::FireWeapon heat/charge --
		//     runs in the subsystem tick when a target is locked.)

		// FIRING ARC: weapons fire where the mech can POINT its guns -- a target it
		// cannot bring the guns to bear on is NOT hit (no shooting the enemy out of
		// your back).  The AUTHENTIC, data-driven traverse is the TORSO twist range:
		// weapon .SUB resources carry NO arc field, so what lets a mech aim off
		// dead-ahead is the torso mount (Torso::GetHorizontalReach, the wider software
		// twist limit).  The Blackhawk's torso is fixed-forward (TorsoHorizontalEnabled
		// =0 -> reach 0), so its guns line up with the mech facing; a twist-capable
		// mech widens the arc by its real torso limit.  On top of the torso reach we
		// keep a base aim/convergence tolerance (BT_FIRE_ARC degrees, default 30) --
		// the reticle box, a port presentation parameter for the external camera the
		// 1995 cockpit view never needed.  In-arc: the beam converges on the target
		// and the shot hits; out-of-arc: the guns fire straight ahead, nothing is hit.
		bool targetInArc = false;
		{
			UnitVector zAxisA;
			localToWorld.GetFromAxis(Z_Axis, &zAxisA);
			Vector3D fA(-(Scalar)zAxisA.x, -(Scalar)zAxisA.y, -(Scalar)zAxisA.z);
			Scalar fl = (Scalar)Sqrt(fA.x*fA.x + fA.y*fA.y + fA.z*fA.z);
			if (fl < 1e-4f) fl = 1.0f;
			fA.x /= fl; fA.y /= fl; fA.z /= fl;
			if (gEnemyMech != 0)
			{
				Point3D ep = ((Mech *)gEnemyMech)->localOrigin.linearPosition;
				Vector3D toE(ep.x - localOrigin.linearPosition.x,
				             ep.y - localOrigin.linearPosition.y,
				             ep.z - localOrigin.linearPosition.z);
				Scalar tl = (Scalar)Sqrt(toE.x*toE.x + toE.y*toE.y + toE.z*toE.z);
				if (tl < 1e-4f) tl = 1.0f;
				Scalar d = (fA.x*toE.x + fA.y*toE.y + fA.z*toE.z) / tl;	// cos(angle to target)
				// base aim tolerance (radians) -- read once from BT_FIRE_ARC (degrees).
				static Scalar s_baseRad = -1.0f;
				if (s_baseRad < 0.0f)
				{
					const char *av = getenv("BT_FIRE_ARC");
					Scalar deg = av ? (Scalar)atof(av) : 30.0f;
					s_baseRad = (Scalar)((double)deg * 3.14159265358979 / 180.0);
				}
				// effective half-arc = base tolerance + this mech's real torso reach.
				Scalar half = s_baseRad + GetHorizontalFiringReach();
				if (half > 3.14159265f) half = 3.14159265f;		// clamp: full hemisphere+
				targetInArc = (d >= (Scalar)cos((double)half));
			}
		}

		{
			UnitVector zAxisF;
			localToWorld.GetFromAxis(Z_Axis, &zAxisF);
			Vector3D fwd(-(Scalar)zAxisF.x, -(Scalar)zAxisF.y, -(Scalar)zAxisF.z);	// mech faces -Z
			Scalar flen = (Scalar)Sqrt(fwd.x*fwd.x + fwd.y*fwd.y + fwd.z*fwd.z);
			if (flen < 1e-4f) flen = 1.0f;
			fwd.x /= flen; fwd.y /= flen; fwd.z /= flen;

			// AUTHENTIC MUZZLES: fire from the mech's real gun-port SITE segments
			// (BLH.SKL: siter/lu/dgunport on jointrgun/jointlgun -- the arm guns),
			// resolved to world by (segmentToEntity x localToWorld).  Beams then
			// emit from the actual arm guns and track them as the mech moves/animates,
			// instead of one point above the torso.  Falls back to a centre muzzle for
			// a mech with no resolvable gun ports.
			Point3D muzzles[8];
			int     nMuz = 0;
			static const char *const kGunPorts[] =
				{ "siterugunport", "sitelugunport", "siterdgunport", "siteldgunport",
				  "siterbgunport", "sitelbgunport" };
			for (int gp = 0; gp < 6 && nMuz < 8; ++gp)
			{
				EntitySegment *seg = GetSegment(CString(kGunPorts[gp]));
				if (seg != 0)
				{
					AffineMatrix mw;
					mw.Multiply(seg->GetSegmentToEntity(), localToWorld);
					muzzles[nMuz] = mw;						// Point3D = matrix translation (W_Axis)
					++nMuz;
				}
			}
			if (nMuz == 0)
			{
				Point3D c = localOrigin.linearPosition;
				c.x += fwd.x * kMuzzleForward;
				c.y += kMuzzleHeight + fwd.y * kMuzzleForward;
				c.z += fwd.z * kMuzzleForward;
				muzzles[0] = c; nMuz = 1;
			}
			const Point3D &muzzle = muzzles[0];				// aim-raycast origin

			// converge on the enemy ONLY when it's within the forward firing arc;
			// otherwise the guns fire straight ahead (crosshair raycast), never back.
			const bool haveEnemy = (gEnemyMech != 0 && targetInArc);
			Point3D aim;
			if (haveEnemy)
			{
				aim = ((Mech *)gEnemyMech)->localOrigin.linearPosition;
				aim.y += kMuzzleHeight;						// aim at the torso, not the feet
			}
			else
			{
				// default aim = straight ahead at max range; the terrain raycast
				// (BTGroundRayHit marches SampleBand over every map instance -- a
				// real per-frame cost on dense maps) only runs when a shot actually
				// needs the impact point: trigger down or a discharge in flight.
				aim.x = muzzle.x + fwd.x * kWeaponRange;
				aim.y = muzzle.y + fwd.y * kWeaponRange;
				aim.z = muzzle.z + fwd.z * kWeaponRange;
				if (gBTDrive.fireForced || gBTDrive.fire || gBeamCooldown > 0.0f)
				{
					extern bool BTGroundRayHit(float,float,float, float,float,float,
						float, float*,float*,float*);
					float hx, hy, hz;
					if (BTGroundRayHit(muzzle.x, muzzle.y, muzzle.z, fwd.x, fwd.y, fwd.z,
							kWeaponRange, &hx, &hy, &hz))
					{ aim.x = hx; aim.y = hy; aim.z = hz; }
				}
			}

			// resolve the "explode" effect once (also used by the target block).
			if (gExplodeReady == 0)
			{
				gExplodeReady = -1;
				if (application != 0 && application->GetResourceFile() != 0)
				{
					ResourceDescription *exp =
						application->GetResourceFile()->FindResourceDescription(
							"explode", (ResourceDescription::ResourceType)1, -1);
					if (exp != 0)
					{
						gExplodeRes = exp->resourceID;
						gExplodeReady = 1;
						DEBUG_STREAM << "[fire] explode effect resolved id=" << (long)gExplodeRes << "\n" << std::flush;
					}
					else DEBUG_STREAM << "[fire] 'explode' effect not found\n" << std::flush;
				}
			}

			const int fireWanted = gBTDrive.fireForced || gBTDrive.fire;
			if (gBeamCooldown > 0.0f)
				gBeamCooldown -= dt;

			// Active-discharge tracker.  The real Emitter fires ONE beam on the trigger
			// edge (FireWeapon @004bace8) then keeps it alive for its DischargeTime via
			// ContinueDischarge (@004baa20) called EVERY frame while Firing.  FireWeapon
			// computes the muzzle LIVE (GetMuzzlePoint @004b9948, the gun segment's current
			// world position) and does NOT store it, so the display re-obtains the muzzle
			// each frame -> the beam ORIGIN tracks the gun as the mech moves during the
			// discharge (you can't run out from under your own beam).  We reproduce that:
			// start a discharge on the edge, then REDRAW from the live muzzle every frame
			// until the beam-on timer runs out.  One discharge at a time (beam-on 0.2s <
			// stagger 0.37s), matching the staggered chain cadence.
			static int    s_dischargePort   = -1;
			static Scalar s_dischargeRemain = 0.0f;

			if (fireWanted && gBeamCooldown <= 0.0f)
			{
				// STAGGERED CHAIN FIRE (decoded cadence): each gun port cycles at
				// kPortRecharge (0.2 DischargeTime + 2.0 RechargeRate = 2.2s).  A synced
				// volley would flash every 2.2s (too slow); the BLH's lasers fire STAGGERED,
				// so fire ONE port per stagger tick and rotate -> aggregate = nMuz beams per
				// 2.2s (~0.37s between beams for 6 arm ports), a rapid alternating stream
				// while each port honours its real 2.2s recharge.
				gBeamCooldown     = kPortRecharge / (Scalar)nMuz;	// stagger interval
				static int s_chainIndex = 0;
				s_dischargePort   = s_chainIndex % nMuz;			// begin this port's discharge
				s_dischargeRemain = kBeamOnTime;					// DischargeTime beam-on
				s_chainIndex      = (s_chainIndex + 1) % nMuz;

				// solo impact (with an enemy, the target block spawns the explosion).
				if (!haveEnemy && gExplodeReady == 1)
				{
					Origin exp_o;
					exp_o.linearPosition  = aim;
					exp_o.angularPosition = localOrigin.angularPosition;	// any valid quat
					Explosion::MakeMessage m(
						Explosion::MakeMessageID, sizeof(Explosion::MakeMessage),
						(Entity::ClassID)RegisteredClass::ExplosionClassID, EntityID::Null,
						gExplodeRes, Explosion::DefaultFlags, exp_o,
						EntityID::Null, GetEntityID());
					Explosion *e = Explosion::Make(&m);
					if (e) Register_Object(e);
				}

				static int s_beamLog = 0;
				if ((s_beamLog++ % 8) == 0)
					DEBUG_STREAM << "[fire] BEAM #" << s_beamLog
						<< (haveEnemy ? " -> target" : " -> free")
						<< " muzzle=(" << muzzle.x << "," << muzzle.y << "," << muzzle.z
						<< ") hit=(" << aim.x << "," << aim.y << "," << aim.z << ")\n" << std::flush;
			}

			// KEEPALIVE (== ContinueDischarge): while a discharge is live, redraw the beam
			// from the LIVE muzzle (muzzles[] is recomputed from the gun segments every
			// frame above) to the live aim.  ttl = one frame so exactly one beam is alive
			// per frame (same brightness as a single persistent beam), re-anchored to the
			// moving gun -- the origin stays on the mech for the whole DischargeTime.
			if (s_dischargeRemain > 0.0f && s_dischargePort >= 0 && s_dischargePort < nMuz)
			{
				extern void BTPushBeam(float,float,float, float,float,float, unsigned, float, float);
				const Point3D &mz = muzzles[s_dischargePort];		// LIVE muzzle this frame
				const Scalar ttl1 = (dt > 1e-4f) ? dt : 1e-4f;		// one-frame life (redrawn each frame)
				// Steady beam for the discharge (a laser is on, not fading): a WIDE dim RED
				// glow carries the ER-laser colour + the scrolling bexp grit, a THIN brighter
				// core hot-spots the centre.  Kept dim enough that the additive core does not
				// wash the glow/grit to white (the run-out fix removed the old fade that used
				// to hide the saturation).  Widths are per-beam (tube honours b.width now).
				BTPushBeam(mz.x, mz.y, mz.z, aim.x, aim.y, aim.z,
					0x00C81404u, ttl1, 3.0f);						// wide red glow (grit shows)
				BTPushBeam(mz.x, mz.y, mz.z, aim.x, aim.y, aim.z,
					0x00A07868u, ttl1, 0.9f);						// thin warm core (no white-out)
				s_dischargeRemain -= dt;
			}
		}

		// --- TARGETING (bring-up): lock the player's current target onto the
		//     spawned enemy mech.  This writes the three mech target slots that the
		//     weapon/fire path reads (target position @0x37c, target entity @0x388,
		//     targeted-subsystem index @0x38c) so HasActiveTarget() becomes true and
		//     the beam/aim has a world point.  Real acquisition -- nearest-enemy scan
		//     or the pilot "hotbox" select through the sensor subsystem -- is the next
		//     refinement; here we lock the one known target.  The enemy is stationary
		//     (drive gated to the player), but we read its LIVE origin so this stays
		//     correct once it can move.
		if (gEnemyMech != 0)
		{
			Point3D enemyPos = ((Mech *)gEnemyMech)->localOrigin.linearPosition;

			MECH_TARGET_POS(this)    = enemyPos;		// mech+0x37c
			MECH_TARGET_ENTITY(this) = gEnemyMech;		// mech+0x388  (HasActiveTarget gate)
			MECH_TARGET_SUBIDX(this) = -1;				// mech+0x38c  whole-mech target

			float ddx = enemyPos.x - localOrigin.linearPosition.x;
			float ddy = enemyPos.y - localOrigin.linearPosition.y;
			float ddz = enemyPos.z - localOrigin.linearPosition.z;
			float range = (float)Sqrt((double)(ddx*ddx + ddy*ddy + ddz*ddz));

			gTargetLogAccum += dt;
			if (gTargetLogAccum >= 1.0f)
			{
				gTargetLogAccum = 0.0f;
				DEBUG_STREAM << "[target] locked enemy entity=" << (void *)gEnemyMech
					<< "  range=" << range
					<< (range <= kWeaponRange ? "  IN RANGE (ready to fire)" : "  (closing)")
					<< (targetInArc ? "  [in arc -> converge]" : "  [out of arc -> fire forward, no hit]")
					<< "  torsoReach=" << GetHorizontalFiringReach() << "rad"
					<< "\n" << std::flush;
			}

			// --- FIRING (bring-up): on the trigger, with a target in range and the
			//     weapon off cooldown, spawn an Explosion at the target.  This is the
			//     real engine fire->effect->render chain (Explosion::Make + the
			//     engine's explosion renderable) standing in for the per-weapon beam
			//     (Emitter::FireWeapon) until the emitter subsystem + its beam
			//     renderable are reconstructed.  The shot is gated exactly like the
			//     real weapon: HasActiveTarget (we just set mech+0x388) AND in range.
			if (gFireCooldown > 0.0f)
				gFireCooldown -= dt;

			const int fireWanted = gBTDrive.fireForced || gBTDrive.fire;

				// E8: pulse the weapon trigger per frame (1,0,1,0...) so the real Emitter's
				// CheckFireEdge sees clean rising edges for repeated auto-fire.  Read by
				// EmitterSimulation; only the player's emitter (with a target) actually fires.
				extern int gBTWeaponTrigger;
				// the real Emitter only fires at a target in the forward arc -- no
				// heat/damage out of arc (you can't shoot the enemy behind you).
				gBTWeaponTrigger = (fireWanted && targetInArc) ? (gBTWeaponTrigger ? 0 : 1) : 0;

			// Resolve the "explode" effect resource once.
			if (gExplodeReady == 0)
			{
				gExplodeReady = -1;
				if (application != 0 && application->GetResourceFile() != 0)
				{
					ResourceDescription *exp =
						application->GetResourceFile()->FindResourceDescription(
							"explode", (ResourceDescription::ResourceType)1, -1);
					if (exp != 0)
					{
						gExplodeRes   = exp->resourceID;
						gExplodeReady = 1;
						DEBUG_STREAM << "[fire] explode effect resolved id="
							<< (long)gExplodeRes << "\n" << std::flush;
					}
					else
					{
						DEBUG_STREAM << "[fire] 'explode' effect not found\n" << std::flush;
					}
				}
			}

			if (fireWanted && targetInArc && gFireCooldown <= 0.0f && range <= kWeaponRange
				&& gExplodeReady == 1)
			{
				gFireCooldown = kFireCooldown;
				++gShotCount;

				Origin exp_origin = ((Mech *)gEnemyMech)->localOrigin;	// at the target

				Explosion::MakeMessage exp_message(
					Explosion::MakeMessageID,
					sizeof(Explosion::MakeMessage),
					(Entity::ClassID)RegisteredClass::ExplosionClassID,
					EntityID::Null,
					gExplodeRes,
					Explosion::DefaultFlags,
					exp_origin,
					gEnemyMech->GetEntityID(),		// entity hit
					GetEntityID());					// shooter (this mech)

				Explosion *shot = Explosion::Make(&exp_message);
				if (shot)
				{
					Register_Object(shot);
					DEBUG_STREAM << "[fire] SHOT #" << gShotCount
						<< " -> explosion at target (range=" << range << ")\n" << std::flush;
				}
				else
				{
					DEBUG_STREAM << "[fire] Explosion::Make returned NULL\n" << std::flush;
				}

				// --- DAMAGE (real): dispatch a TakeDamage message to a VALID zone; the
				//     engine base handler routes it to Mech__DamageZone::TakeDamage (the real
				//     armor/structure model).  Aim a rotating zone so the whole mech degrades;
				//     read back structureLevel (now valid -- friend access) to show it climb
				//     toward 1.0 (destroyed).
				if (gEnemyMech->damageZoneCount > 0)
				{
					int zc   = gEnemyMech->damageZoneCount;
					// Aim the first VITAL zone so concentrated fire destroys it (-> mech
					// death).  The faithful per-impact aim (cylinder lookup from the hit
					// point) is STEP 6; until then we target a vital zone directly.
					int zone = 0;
					for (int k = 0; k < zc; ++k)
						if (((Mech *)gEnemyMech)->Zone(k)->vitalDamageZone) { zone = k; break; }

					Damage dmg;							// default-constructed
					// Explosive: the weapon effect is an Explosion (explosive), not an
					// energy beam.  Also the correct type to exercise the zone armour/
					// structure/death model now -- EnergyDamageType(4) shorts attached
					// generators via criticalSubsystems[]->plug, which needs the REAL
					// subsystem roster (still RECON_SUBSYS stubs -> plug resolves null).
					dmg.damageType   = Damage::ExplosiveDamageType;
					dmg.damageAmount = kShotDamage;
					dmg.burstCount   = 1;
					dmg.impactPoint  = enemyPos;			// world impact point

					Entity::TakeDamageMessage take_damage(
						Entity::TakeDamageMessageID,
						sizeof(Entity::TakeDamageMessage),
						GetEntityID(),			// inflicting = this (shooter)
						zone,					// valid zone -> base handler applies
						dmg);
					gEnemyMech->Dispatch(&take_damage);

					Scalar s = ((Mech *)gEnemyMech)->Zone(zone)->damageLevel;	// [0,1], 1.0=destroyed (engine base field)
					DEBUG_STREAM << "[damage] hit zone " << zone << "/" << zc
						<< " structure=" << s << "\n" << std::flush;
				}

				// Death via the REAL damage model: a mech with a destroyed VITAL zone is
				// dead.  We read the reconstructed zone state directly (damageLevel >= 1.0
				// on a vitalDamageZone).  The faithful Mech::IsDisabled() reads movementMode,
				// which the gait/death-transition (bypassed by the bring-up drive override)
				// would set from exactly this condition.  Friend access (mechdmg.hpp).
				Logical dead = False;
				for (int k = 0; k < gEnemyMech->damageZoneCount; ++k)
				{
					Mech__DamageZone *dz = ((Mech *)gEnemyMech)->Zone(k);
					if (dz->vitalDamageZone && dz->damageLevel >= 1.0f) { dead = True; break; }
				}
				if (!gEnemyDestroyed && dead)
				{
					gEnemyDestroyed = 1;

					// Death explosion at the target.
					Origin death_origin = ((Mech *)gEnemyMech)->localOrigin;
					Explosion::MakeMessage death_exp(
						Explosion::MakeMessageID,
						sizeof(Explosion::MakeMessage),
						(Entity::ClassID)RegisteredClass::ExplosionClassID,
						EntityID::Null,
						gExplodeRes,
						Explosion::DefaultFlags,
						death_origin,
						gEnemyMech->GetEntityID(),
						GetEntityID());
					Explosion *boom = Explosion::Make(&death_exp);
					if (boom) Register_Object(boom);

					// P5 ROOT-CAUSE (env-gated, DEFAULT OFF): actually tear the wreck down so
					// the ~Mover collision-dtor crash reproduces under cdb.  The shipped death
					// stand-in below (explosion + stop-targeting) is unchanged when the flag is off.
					if (getenv("BT_ENABLE_TEARDOWN"))
					{
						char *e = (char *)gEnemyMech;
						DEBUG_STREAM << "[teardown] enemy this=" << (void *)e << " base-region dump (engine fields):\n"
							<< "   collisionVolume@0x2d8=" << *(void **)(e + 0x2d8)
							<< " collisionTemplate@0x2dc=" << *(void **)(e + 0x2dc)
							<< " containedByNode@0x2e0=" << *(void **)(e + 0x2e0) << "\n"
							<< "   collisionLists@0x2e4=" << *(void **)(e + 0x2e4)
							<< " lastCollisionList@0x2e8=" << *(void **)(e + 0x2e8)
							<< " collisionAssistant@0x2ec=" << *(void **)(e + 0x2ec) << "\n"
							<< "   segmentTable.array@0x2f0=" << *(void **)(e + 0x2f0)
							<< " segmentCount@0x308=" << *(int *)(e + 0x308)
							<< " jointSubsystem@0x30c=" << *(void **)(e + 0x30c) << "\n"
							<< "   -> dispatching DestroyEntityMessage\n" << std::flush;
						Entity::DestroyEntityMessage destroy_msg(
							Entity::DestroyEntityMessageID,
							sizeof(Entity::DestroyEntityMessage));
						gEnemyMech->Dispatch(&destroy_msg);
					}

					// By default we do NOT tear the wreck down: FryDeathRow -> ~Mech -> ~JointedMover
					// -> ~Mover CRASHES.  ⚠ ROOT-CAUSED (P5, see btbuild/HARD_PROBLEMS.md): NOT
					// "collision solids never built" (that was wrong -- Mover::Mover allocates
					// collisionLists@0x2e4 unconditionally; it reads a valid heap ptr at death).
					// The real cause is the Entity/Mover BASE-REGION LAYOUT divergence: this file's
					// raw binary-offset accesses (this+0x2d4 netOrientation / +0x2e0 arrivalTime /
					// +0x2e8 physicsBody / +0x2ec groundRef / +0x2f0 groundCell) land on the elsewhen
					// engine Mover's collision cluster (collisionVolumeCount@0x2d4 .. collisionAssistant
					// @0x2ec) and CORRUPT it (incl. deref-writes at :1020 through +0x2ec), so teardown
					// deletes garbage -- crashing at varying base-member sites (~Mover collisionLists,
					// ~JointedMover member).  Enable with BT_ENABLE_TEARDOWN to repro under cdb.
					// FIX = reconcile those raw offsets to the relocated declared members (the base-
					// region audit).  Meanwhile the mech "dies" (explosion + no longer targeted).
					DEBUG_STREAM << "[damage] *** TARGET DESTROYED after "
						<< gShotCount << " hits ***\n" << std::flush;

					gEnemyMech = 0;		// stop targeting/firing the dead entity
				}
			}
		}
	}

	// --- Tick the subsystem roster (BT analog of Entity::PerformAndWatch) -----
	//  The shipped engine's Entity::PerformAndWatch walks subsystemArray[] and
	//  calls PerformAndWatch on every non-null, non-replicant-disabled subsystem
	//  (MUNGA/ENTITY.cpp ~698-790; RP's VTV ticks ITS roster the identical way).
	//  Our drivable override bypasses the unsafe Mech::Simulate, but the
	//  subsystem roster must still tick each frame so heat/weapons/sensors/gyro/
	//  power/etc. run their per-frame Performance.  We mirror the engine loop
	//  here instead of calling Simulate: each Subsystem is a Simulation whose
	//  base PerformAndWatch (engine, safe) computes its OWN time slice from its
	//  own lastPerformance and dispatches activePerformance -- the reconstructed
	//  *::*Simulation method the subsystem's ctor installed via SetPerformance.
	//  Sentinels: index 0 is always NULL; index 1 is the resolved voltage bus;
	//  the real streamed subsystems live at id>=2.  Null + executable guards
	//  make all three cases safe.  (Per-subsystem behaviour deepening is a
	//  separate wave; here we only make the TICK PATH live.)
	int subsystemsTicked  = 0;		// performed this frame (executable)
	int subsystemsPresent = 0;		// non-null roster occupancy (excl. mapper)
	if (subsystemArray != 0 && subsystemCount > 0)
	{
		for (int i = 0; i < subsystemCount; ++i)
		{
			Subsystem *subsystem = subsystemArray[i];
			if (subsystem == 0)
				continue;
			if (subsystem != (Subsystem *)controlsMapper)
				++subsystemsPresent;
			if (!subsystem->IsNonReplicantExecutable())
				continue;

			// The controls-mapping subsystem (roster slot 0 via Mech::SetMapping
			// Subsystem, mirrored to controlsMapper) is TICKED under BT_REAL_CONTROLS
			// -- its InterpretControls chain is now reconciled: FillPilotArray reads
			// the local player via application->GetMissionPlayer() (the old wild
			// application+0x6c read was THE bypass-causing AV), and the main tick
			// @004afd10 reads the owner through declared members (reverseStride
			// Length/walkStrideLength/forwardThrottleScale, the real Torso analog
			// axes, the real HUD freeAimSlew).  Without the env the historic skip
			// stands (default behavior unchanged).
			{
				static const int s_realControlsTick = BTEnvOn("BT_REAL_CONTROLS", 1);
				if (!s_realControlsTick && subsystem == (Subsystem *)controlsMapper)
					continue;
			}

			subsystem->PerformAndWatch(till, update_stream);
			++subsystemsTicked;
		}
	}

	// One-shot: report how many subsystems the tick dispatched to on the FIRST
	// frame, before any DoNothingOnce subsystems latch NeverExecute and opt out.
	if (!gTickFirstLogged)
	{
		gTickFirstLogged = 1;
		DEBUG_STREAM << "[tick] first frame: dispatched to " << subsystemsTicked
			<< " executable subsystem(s) of " << subsystemsPresent
			<< " present / roster " << subsystemCount << "\n" << std::flush;
	}

	// 1 Hz confirmation that the tick path is live (N subsystems simulated).
	gTickLogAccum += dt;
	if (gTickLogAccum >= 1.0f)
	{
		gTickLogAccum = 0.0f;
		DEBUG_STREAM << "[tick] subsystems simulated: " << subsystemsTicked
			<< " (executable) of " << subsystemsPresent
			<< " present / roster " << subsystemCount << "\n" << std::flush;
	}

	// Keep the simulation/networking bookkeeping consistent (this is exactly
	// what the base "no time / stasis" early-out does).
	WriteSimulationUpdate(update_stream);
}


//###########################################################################
//###########################################################################
//					Mech::AuthenticGroundAndCollide
//
// THE AUTHENTIC 1995 GROUND MODEL -- the ground/collision half of the MASTER
// per-frame performance FUN_004a9b5c (@4aa630-4aab5f, decoded from raw asm by
// the ground-model-decode workflow; task #15).  Replaces the bring-up
// gravity+push-out+floor-clamp baseline when GroundReal() is on.
//
// The model (no gravity exists anywhere in the mech):
//  1. MoveCollisionVolume() -- rebuilds localToWorld from localOrigin, places
//     the collision cylinder, and (masters) re-finds containedByNode in the
//     zone's BoundingBoxTree (engine Mover::MoveCollisionVolume, MOVER.cpp:782).
//  2. GROUND PROBE: q = origin + (0, collisionTemplate->minY, 0) -- the
//     ctor-LIFTED template bottom (BLH: 2.0 + 0.37969 = 2.37969 above the feet).
//  3. HEIGHT QUERY: FindBoundingBoxUnder(q, &h) -- the box-tree downward query
//     (BOXTREE.cpp:843); h = distance from q down to the highest solid top
//     under the column; h == -1.0f = MISS.
//  4. THE SNAP (master gate h > 1e-4, const @0x4ab16c): origin.y -= (h - lift)
//     => origin.y = surfaceY EXACTLY.  Absolute placement each frame: walks
//     up-slope within the lift window (implicit step allowance), drops
//     instantly on walk-offs.  On MISS: NOTHING -- Y holds (no gravity to
//     accumulate => the old y=13301 runaway is structurally impossible).
//  5. COLLISIONS: GetCurrentCollisions -> ProcessCollisionList (which calls
//     the Mech::ProcessCollision override below per contact).
//  6. RESPONSE POLICY on the accumulated damage:
//     == 0.00123f  crushable-CulturalIcon sentinel: the frame's move STANDS
//                  (restore the POST-SNAP saves; the bounce is undone).
//     >  0         BLOCKING hit (walls/cliffs/buildings): FULL FRAME REJECTION
//                  -- velocity zeroed, origin restored to START-OF-FRAME.
//                  Walls block by rejection, never by slide/climb.
//
// Deliberate deviations from the binary (each flagged by the verify pass):
//  - GetCollisionVolumeCount()>0 gate + containedByNode NULL-guard cover the
//    WHOLE block (the 1995 code was unconditional and would have crashed for
//    volume-less mechs; GetCurrentCollisions derefs the node unchecked).
//  - impactVel is saved with the post-snap saves (binary saves it after the
//    gather @4aa716 -- provably equivalent, the gather doesn't touch it).
//  - localToWorld is NOT re-synced after a plain snap (binary behavior: it
//    refreshes on the next frame's MoveCollisionVolume or a response path).
//
// DEFERRED (bound together; see the ProcessCollision banner): the pre-list
// collisionTemporaryState zero (@4aa741) + state tail; the 0x4a4/0x4a8/0x4b4
// caches; the self TakeDamageMessage; SetLegAnimation(0x20) crash anim on
// impactVel^2 > 40 (crash-clip slot mapping unverified -- logged instead);
// the gyro crunch feed (Gyroscope is a stub).
//###########################################################################
//###########################################################################
void
	Mech::AuthenticGroundAndCollide(Scalar dt, const Point3D &old_position)
{
	if (GetCollisionVolumeCount() <= 0 || collisionVolume == 0 || collisionTemplate == 0)
		return;

	// 1. place the volume + refresh containedByNode (@4aa630)
	MoveCollisionVolume();
	BoundingBoxTreeNode *node = GetMoverCollisionRoot();
	if (node == 0)
	{
		if (GroundLog())
		{
			static int s_once = 0;
			if (!s_once++) DEBUG_STREAM << "[ground] NO collision tree node -- "
				"zone tree unavailable; block skipped\n" << std::flush;
		}
		return;
	}

	// 2-3. ground probe + height query (@4aa633-4aa67d)
	Point3D q = localOrigin.linearPosition;
	q.y += collisionTemplate->minY;						// the LIFTED template bottom
	Scalar h = -1.0f;
	BoundingBox *floorBox = node->FindBoundingBoxUnder(q, &h);
	(void)floorBox;

	// 4. the snap (@4aa685-4aa6cc)
	if (h > 0.0001f)									// master gate, const @0x4ab16c
	{
		Scalar d = h - collisionTemplate->minY;			// signed: down OR bounded up
		localOrigin.linearPosition.y -= d;				// => origin.y = surfaceY exactly
		collisionVolume->minY -= d;						// keep the placed volume in sync
		collisionVolume->maxY -= d;
	}

	// 5. collisions (@4aa6cf-4aa764)
	// ASSISTANT GUARD (defensive; matches the authentic invariant): the engine
	// GetCurrentCollisions iterates collisionAssistant UNCHECKED (MOVER.cpp:894;
	// Check() compiles out).  In the real game exactly ONE master exists per pod
	// -- the viewpoint mech, which MakeViewpointEntity gave an assistant
	// (btl4app.cpp:591); every other mech was a replicant (no collision half).
	// A master WITHOUT an assistant (the BT_SPAWN_ENEMY dummy before its
	// StartCollisionAssistant) is a configuration the original never had --
	// ground it (probe/snap above) but skip the collision half.
	if (collisionAssistant == 0)
		return;
	Vector3D savedWorldVel = worldLinearVelocity;		// POST-SNAP saves
	Point3D  savedPos      = localOrigin.linearPosition;
	BoxedSolidCollisionList *cols = GetCurrentCollisions();
	Vector3D impactVel = localVelocity.linearMotion;
	// CONTACT EDGE (player only): a knockdown fires only on a FRESH strike (not blocked
	// last frame).  While the mech stays pressed against the obstacle it just BLOCKS --
	// no repeated knockdowns.  gWasBlocked is a single global, so gate it to the
	// viewpoint mech; other masters (the stationary dummy, MP replicant-driven masters)
	// keep the original per-fresh-block behavior and never touch the shared state.
	const bool isPlayer   = (application != 0
		&& (Entity *)this == application->GetViewpointEntity());
	const int  freshBlock = isPlayer ? (gBlockCooldown <= 0.0f ? 1 : 0) : 1;
	if (isPlayer && gBlockCooldown > 0.0f)
		gBlockCooldown -= dt;							// decay the out-of-contact window
	// BINARY-TAIL-DEFERRED: collisionTemporaryState = 0 here (@4aa741).
	Damage dmg;											// ctor zeroes damageAmount
	if (cols != 0)
		ProcessCollisionList(cols, dt, old_position, &dmg);

	// 6. response policy (@4aa76c-4aab5f)
	if (dmg.damageAmount == 0.00123f)					// crushable icon: move STANDS
	{
		worldLinearVelocity = savedWorldVel;
		localOrigin.linearPosition = savedPos;
		MoveCollisionVolume();
		dmg.damageAmount = 0.0f;
		// gyro crunch feed (0.4*normal + 0.2*up): DORMANT until the Gyroscope un-stub
		if (GroundLog())
			DEBUG_STREAM << "[ground] CRUNCH (crushable icon) at ("
				<< savedPos.x << ", " << savedPos.z << ")\n" << std::flush;
	}
	else if (dmg.damageAmount > 0.0f)					// blocking hit: FULL FRAME REJECTION
	{
		worldLinearVelocity = Vector3D(0.0f, 0.0f, 0.0f);
		localOrigin.linearPosition = old_position;
		MoveCollisionVolume();
		if (isPlayer)
			gBlockCooldown = kBlockHysteresis;			// (re)arm the in-contact window
		// DEFERRED: self TakeDamageMessage{0x64, zone=-1} (inert today) +
		// material/normal/approach caches + collisionState indicators.
		Scalar iv2 = impactVel.x * impactVel.x + impactVel.y * impactVel.y
				   + impactVel.z * impactVel.z;
		if (iv2 > 40.0f)								// crash-anim threshold @0x4ab184
		{
			// CRASH / KNOCKDOWN (binary @4aaae2-4aab0b): bind the bump clip
			// (animationClips[0x20] = "bmp"; slot verified 0x5cc+0x80 == 0x64c)
			// on the LEG channel -- the mech plays the fall/stagger instead of
			// grinding into the wall; LegClipFinished case 32 drops back to
			// Standing at end-of-clip (slot1 @0x4a6b4d, binary-verified).  The
			// two action-request calls are the binary's FUN_004a4c54(this,1)
			// then (this,0x20) -- TWO calls, not one OR'd (fidelity verdict).
			// Guard 1: only when the bmp clip resolved (no substitute clip).
			// Guard 2 (BRING-UP, marked): don't REBIND while the crash clip is
			// already playing -- the bmp clip carries ~6.5 u/s of root motion
			// that presses back into the wall, and without the actionRequestFlags
			// consumers (bits 1/0x20 -- likely the drive suppressor during the
			// stagger, NOT yet reconstructed) the re-trigger restarts the clip
			// every frame.  Remove this guard when the flag consumers land.
			if (animationClips[0x20] != 0
				&& legStateAlarm.GetLevel() != 0x20
				&& freshBlock)							// only on a FRESH strike, not continuous grinding
			{
				SetLegAnimation(0x20);					// FUN_004a7fc4(this, 0x20)
				// SYNC THE DISPLAY CHANNEL (fix the persistent post-bump foot-slip): the
				// two-channel gait draws the legs from the BODY channel but moves the mech
				// from the LEG channel.  Staggering only the leg channel froze the motion
				// while the body kept animating -> the channels desynced PERMANENTLY (the
				// [sync] log: advSum 229 vs legSum 112 = displayed legs run ~2x the real
				// travel = foot slip that lasts until a full stop resyncs both to stand).
				// Stagger the BODY channel too so both freeze + recover from the SAME bmp
				// clip IN PHASE.  BodyClipFinished case 32 recovers it (mech2.cpp:308).
				SetBodyAnimation(0x20);
				RequestActionFlags(1);					// FUN_004a4c54(this, 1)
				RequestActionFlags(0x20);				// FUN_004a4c54(this, 0x20)
				if (GroundLog())
				{
					static int s_binds = 0;
					DEBUG_STREAM << "[knock] BIND #" << ++s_binds
						<< " iv2=" << iv2 << "\n" << std::flush;
				}
			}
			else if (GroundLog())
			{
				static int s_noclip = 0;
				if (!s_noclip++) DEBUG_STREAM << "[ground] crash-anim trigger but "
					"bmp clip unresolved -- knockdown skipped\n" << std::flush;
			}
			if (GroundLog())
				DEBUG_STREAM << "[ground] BLOCK + CRASH (iv2=" << iv2
					<< " clip=" << animationClips[0x20] << ")\n" << std::flush;
		}
		else if (GroundLog())
			DEBUG_STREAM << "[ground] BLOCK dmg=" << dmg.damageAmount
				<< " at (" << old_position.x << ", " << old_position.z
				<< ")\n" << std::flush;
	}

	// 1-Hz telemetry (verification: h / y / hit-miss); INSIDE-SOLID detector:
	// h clamps to EXACTLY 0 when the probe point is at/inside a solid surface
	// (every FindDistanceBelowBounded is Max(...,0)-clamped in BT content) --
	// a sustained h==0 streak means the mech is buried in a solid.
	if (GroundLog())
	{
		static float s_accum = 0.0f;
		static int   s_zeroStreak = 0;
		if (h == 0.0f)
		{
			if (++s_zeroStreak == 10)
				DEBUG_STREAM << "[ground] INSIDE-SOLID streak at ("
					<< localOrigin.linearPosition.x << ", "
					<< localOrigin.linearPosition.y << ", "
					<< localOrigin.linearPosition.z << ")\n" << std::flush;
		}
		else
			s_zeroStreak = 0;
		s_accum += (float)dt;
		if (s_accum >= 1.0f)
		{
			s_accum = 0.0f;
			DEBUG_STREAM << "[ground] h=" << h
				<< (h == -1.0f ? " (MISS)" : (h == 0.0f ? " (ZERO/inside)" : " (hit)"))
				<< " lift=" << collisionTemplate->minY
				<< " pos=(" << localOrigin.linearPosition.x << ", "
				<< localOrigin.linearPosition.y << ", "
				<< localOrigin.linearPosition.z << ")\n" << std::flush;
		}
	}
}

//###########################################################################
//###########################################################################
//					Mech::ProcessCollision      (vtable slot +0x3c)
//
// @004abb40 -- THE REAL per-contact collision responder (task #15,
// ground-model-decode workflow; raw part_012.c:15206-15416).  The earlier
// draft here ("ResolveWeaponImpact") misread this function as a weapon sweep;
// it is the override of the ENGINE protected virtual Mover::ProcessCollision
// (MOVER.h:359-365), called per contact by Mover::ProcessCollisionList.
//
// Semantics (all raw-verified):
//  - BoxedSolid resolver (collisionVolume->ProcessCollision, BOXDISKS.cpp) --
//    on a miss NOTHING runs (the binary's entire body is inside the hit branch).
//  - StaticBounce computes the positional resolution + collision damage.
//  - Owner classification: a Mover owner -> separating-contact gate + (deferred)
//    mech-vs-mech damage dispatch; a CulturalIcon owner -> separating gate +
//    (deferred) crunch dispatch + the 0.00123f WALK-THROUGH SENTINEL when the
//    icon has no StoppingCollisionVolume flag (crushable trees/props).
//  - Plain UnscalableTerrain owners (ground/hills/cliff tiers/canyon walls)
//    match NEITHER branch: their StaticBounce damage stands and the caller
//    (AuthenticGroundAndCollide) rejects the whole frame -- walls/cliffs BLOCK.
//
// DEFERRED (marked, to add together in one fidelity pass):
//  - the TakeDamageMessage dispatches (self/other-mech/icon-crunch): zone==-1
//    is dropped by the engine base handler today (the Mech cylinder-lookup
//    override is unreconstructed) AND the binary message carries an inflictor
//    GLOBAL (DAT_0050b9ac) + inline name fields not yet mapped -- do NOT fake.
//  - the collisionTemporaryState tail (:15406-15413) -- BINARY-TAIL-DEFERRED,
//    bound with the pre-list zero @4aa741 in AuthenticGroundAndCollide.
//  - the 0x4a4/0x4a8/0x4b4 caches (material / local-frame surface normal /
//    approach speed) -- HUD/telemetry feeds, deferred with the indicators.
//
// Gated: falls through to the engine base when GroundReal() is off, so the
// baseline BT_COLLISION push-out path is byte-identical.
//###########################################################################
//###########################################################################
void
	Mech::ProcessCollision(
		Scalar				time_slice,
		BoxedSolidCollision	&collision,
		const Point3D		&old_position,
		Damage				*damage)
{
	if (!GroundReal())
	{
		Mover::ProcessCollision(time_slice, collision, old_position, damage);
		return;
	}

	// --- the BoxedSolid resolver (:15302-15304); miss => nothing runs -------
	Scalar penetration = 0.0f;
	if (!collisionVolume->ProcessCollision(collision, worldLinearVelocity,
			lastCollisionList, &damage->surfaceNormal, &penetration))
	{
		return;
	}

	if (penetration > time_slice)						// Max_Clamp (:15306-15308)
		penetration = time_slice;
	Scalar r          = penetration / time_slice;
	Scalar elasticity = elasticityCoefficient;			// :15310 (engine Mover member)
	Scalar friction   = frictionCoefficient;			// :15311

	Simulation *ownerSim =
		collision.GetTreeVolume()->GetOwningSimulation();	// tagPointer (:15312)

	damage->damageAmount = StaticBounce(old_position, time_slice, r,
		damage->surfaceNormal, &elasticity, minimumBounceSpeed, &friction);	// :15313-15315

	Entity *owner = (Entity *)ownerSim;

	// CONTACT TELEMETRY (task #15 verification): identify every resolved contact.
	if (GroundLog())
	{
		BoxedSolid *ws = collision.GetTreeVolume();
		DEBUG_STREAM << "[contact] solidType=" << (int)ws->solidType
			<< " mat=" << (int)ws->materialType
			<< " ownerClass=" << (owner ? (int)owner->GetClassID() : -1)
			<< " slice X[" << collision.collisionSlice.minX << "," << collision.collisionSlice.maxX
			<< "] Y[" << collision.collisionSlice.minY << "," << collision.collisionSlice.maxY
			<< "] Z[" << collision.collisionSlice.minZ << "," << collision.collisionSlice.maxZ
			<< "] pen=" << penetration << " dmg=" << damage->damageAmount
			<< "\n" << std::flush;
	}

	// --- Mover owner (another mech / vehicle) (:15316-15358) ----------------
	if (owner != 0 && owner->IsDerivedFrom(*Mover::GetClassDerivations()))
	{
		Vector3D rel;
		Vector3D ownerVel = owner->GetWorldLinearVelocity();	// virtual (+0x20)
		rel.Subtract(worldLinearVelocity, ownerVel);
		// Separating-contact gate.  MEMBER dot (Normal::operator*, NORMAL.h:29)
		// -- NEVER the free Dot() recon stub (a variadic no-op returning 0
		// that would silently defeat this gate).
		if (damage->surfaceNormal * rel < -1.0e-4f)			// _DAT_004ac044 (:15320-15323)
			return;
		// DEFERRED: mech-vs-mech TakeDamageMessage{0x64, len 0x12, zone=-1}
		// dispatch to the other mech (:15324-15358) -- see the banner.
	}

	// --- CulturalIcon owner (buildings/trees/props) (:15361-15404) ----------
	if (owner != 0 && owner->IsDerivedFrom(*CulturalIcon::GetClassDerivations()))
	{
		Logical stopping =
			((CulturalIcon *)owner)->IsStoppingCollisionVolume();	// flags & 0x8000 (:15362)
		// Separating gate vs a STATIC icon (owner velocity == {0,0,0}, inlined
		// by the 1995 compiler) (:15364-15368).
		if (damage->surfaceNormal * worldLinearVelocity < -1.0e-4f)
			return;
		// DEFERRED: crunch TakeDamageMessage to the icon (:15369-15401).
		if (!stopping)
			damage->damageAmount = 0.00123f;	// walk-through sentinel 0x3aa137f4 (:15402-15404)
	}

	// BINARY-TAIL-DEFERRED: collisionTemporaryState tail (:15406-15413) --
	// implement together with the per-frame zero (@4aa741) and the named
	// StateIndicator members.
}


//###########################################################################
//###########################################################################
//					FeedHeatCapacityGauge / FeedHeatLevelGauge   (cockpit)
//
// @004ac04c / @004ac064
//
// Two-line gauge callbacks: push the current heat capacity / heat level into
// the value field (+0xc) of the object pointed to by this+0x2ec.  Registered
// as a per-frame gauge value source.
//
// NOTE: this+0x2ec is read by Simulate as a terrain "groundRef" (its +8 is a
// base height), so labelling it a GraphicGauge here is uncertain -- the two
// uses share a byte offset.  The heatLevel/heatCapacity *sources* (0x518/0x51c)
// are certain Mech members; the *sink* identity is best-effort.
//###########################################################################
//###########################################################################
void
	Mech::FeedHeatCapacityGauge()
{
	*(Scalar *)(*(int *)(this + 0x2ec) + 0xc) = heatCapacity;		// 0x51c -> +0xc
}

void
	Mech::FeedHeatLevelGauge()
{
	*(Scalar *)(*(int *)(this + 0x2ec) + 0xc) = heatLevel;			// 0x518 -> +0xc
}


//###########################################################################
//###########################################################################
//					LookupDamageState        (static; keyword parse)
//
// @004ac194
//
// Map a damage-zone state keyword to its enum value, using the (name,value)
// table at .rdata:0050de74.  Returns 1 and writes *out on a match, else 0.
// Recognized keywords (from the table):
//     Destroyed=0  Damaged=1  CoolantLeaking=2  Overheating=3
//     AmmoBurning=4  Jammed=5  BadPower=6
//###########################################################################
//###########################################################################
//
// Damage-state keyword table @0x50de74 (shared with mechsub's status states).
//
struct DamageStateEntry { const char *name; int value; };
static const DamageStateEntry kDamageStateTable[] =
{
	{ "Destroyed",		0 },
	{ "Damaged",		1 },
	{ "CoolantLeaking",	2 },
	{ "Overheating",	3 },
	{ "AmmoBurning",	4 },
	{ "Jammed",			5 },
	{ "BadPower",		6 },
	{ 0,				0 }
};

/*static*/ Logical
	Mech::LookupDamageState(const char *keyword, int *out)
{
	for (const DamageStateEntry *e = kDamageStateTable;	// &PTR_s_Destroyed_0050de74
	     e->name != 0; ++e)
	{
		if (Strcmp(keyword, e->name) == 0)				// FUN_004d4b58
		{
			*out = e->value;
			return True;
		}
	}
	return False;
}

//===========================================================================//
// End of recovered mech4.cpp slice.
//===========================================================================//
