//===========================================================================//
// File:	mechmppr.cpp                                                        //
// Project: BattleTech           Brick: Entity Manager                        //
// Contents: Mech controls mapper -- maps pilot control inputs and view       //
//           selection onto the Mech's motion / torso / eyepoint demands      //
//---------------------------------------------------------------------------//
//   Date    Who  Modification                                               //
// --------  ---  ---------------------------------------------------------- //
//  --/--/95  ??   Initial coding.                                            //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
//                      PROPRIETARY AND CONFIDENTIAL                         //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary.  Behaviour follows the Ghidra
// pseudo-C for the mechmppr cluster (@004afbe0-@004b08c0); class / member /
// method names follow the Red Planet sibling RP\VTVMPPR.cpp (VTVControlsMapper)
// and the strings recovered from the .rdata class string pool.  Each non-
// trivial method cites the originating @ADDR.
//
// Read-only globals / constants resolved from section_dump.txt:
//      _DAT_004b0274 = 0x00000000 = 0.0f
//      _DAT_004b0278 = 0x3f800000 = 1.0f
//      _DAT_004b027c = 0x38d1b717 = 1.0e-4f   (JM_CLOSE_ENOUGH)
//      0x40490fdb    = PI (3.14159274f)        (eyepoint yaw, "look behind")
//      DAT_004e0f8c  = EulerAngles::Identity   (all zero)
//
// Helper-function name mapping (engine internals referenced by the decomp):
//      FUN_004ac530  Subsystem base constructor (8-arg)
//      FUN_004ac868  Subsystem destructor
//      FUN_0040385c  Verify()/Fail(msg,file,line)
//      FUN_0041a1a4  IsDerivedFrom(classDerivations)
//      FUN_00417ab4  SharedData::Resolve()
//      FUN_00408644  Vector3D::Subtract(result, a, b)
//      FUN_00408e90  EulerAngles::operator=(dst, src)
//      FUN_0049fb54  Entity::IsDestroyed() -> Logical
//      FUN_004022b0  Allocate(bytes)        FUN_004022e8  Free(ptr)
//      FUN_004022d0  operator delete         FUN_004024d8  Resource::Release
//      FUN_00403ad0  Group::FindGroup(root, name)
//      FUN_00421414  CollectionIterator::CollectionIterator(it, collection)
//      FUN_00421452  CollectionIterator::~CollectionIterator(it)
//      FUN_004afacf  ChildIterator::ChildIterator(it, list)   (eyepoint list)
//      FUN_004afb0d  ChildIterator::~ChildIterator(it)
//      FUN_004dcd00  fabsf()
//      FUN_004bff74  VoiceAssist::Toggle()   (lives in another module)
//      DAT_004efc94  the global Application object
//

#include <bt.hpp>
#pragma hdrstop

#if !defined(MECHMPPR_HPP)
#	include <mechmppr.hpp>
#endif
#if !defined(APP_HPP)
#	include <app.hpp>
#endif
#if !defined(NTTMGR_HPP)
#	include <nttmgr.hpp>		// EntityManager / EntityGroup / FindGroup
#endif
#if !defined(MECH_HPP)
#	include <mech.hpp>
#endif
#if !defined(PILOT_HPP)
#	include <pilot.hpp>
#endif
#include <torso.hpp>			// real Torso: analog aim axes @0x1F0/0x1F4, horizontalEnabled @0x250
#include <hud.hpp>				// real HUD: freeAimSlew @0x28C

#define JM_CLOSE_ENOUGH		1.0e-4f		// _DAT_004b027c

//
// Reconstruction helpers -- engine internals referenced by the decomp that
// have no direct analog in the surviving WinTesla MUNGA/MUNGA_L4 headers.
// (CROSS-FAMILY / engine gap -- see report.)  Declared here so the recovered
// bodies compile; the linker will bind them to the real engine symbols.
//
//   FUN_004bff74  toggles the voice-assist flag on the pilot subsystem.
//   FUN_0049fb54  Entity::IsDestroyed() reached through an entity handle.
//
extern void		ToggleVoiceAssist(int voice_assist_subsystem);
extern Logical	Is_Destroyed(int entity_handle);

//
// Mech::GetHorizontalFiringReach -- the reachable horizontal firing half-arc
// (radians) the mech's torso can bring its guns to bear off dead-ahead.  Weapons
// carry no arc field (the .SUB resources have none); the TORSO mount is what lets
// a mech point its guns to the side, so the authentic per-mech weapon traverse IS
// the torso's horizontal twist range (0 for a fixed torso like the Blackhawk).
// Defined here (not mech.cpp) because the cast to the full Torso type needs its
// header, which mech.cpp deliberately does not include (subsystem-stub collision).
//
Scalar
	Mech::GetHorizontalFiringReach()
{
	Torso *torso = (Torso *)GetTorsoSubsystem();		// @0x438 cache (real Torso or 0)
	return (torso != 0) ? torso->GetHorizontalReach() : 0.0f;
}

//
// ChildIterator -- walks the eyepoint / camera child list hung off the torso
// articulation block (engine FUN_004afacf/FUN_004afb0d).  The real type is a
// scene-graph DCS child iterator; the recovered InterpretControls() touches
// the list head as a raw address and the entries as raw int* records, so the
// iterator is reconstructed here as a minimal intrusive-list walk.  BEST-EFFORT.
//
namespace {
	struct ChildIterator {
		int *node;
		explicit ChildIterator(int list_head)
			: node(*(int **)list_head)
			{}
		int *Next() {
			int *current = node;
			if (current != 0) {
				node = *(int **)current;	// next-link assumed at offset 0
			}
			return current;
		}
	};
}

//
// Owner (Mech) sub-object offsets touched by this mapper.  The exact Mech
// layout is not recoverable from the pseudo-C; these accessors document the
// observed byte offsets and are flagged best-effort.  A human should fold them
// back onto the real Mech / TorsoArticulation / Cockpit accessors.
//
//   mech + 0x438  ->  torso articulation block
//        +0x220/+0x224   current torso yaw / pitch demand
//        +0x228/+0x22c   neutral (centered) yaw / pitch
//        +0x230/+0x234   torso yaw / pitch travel limits
//        +0x1f0          torso free-aim demand
//        +0x274          torso auto-center flag
//        +0x1f4 (500)    torso pitch demand
//        +0x250          free-aim-enabled flag
//        +0x34c          maximum yaw rate            (also reverse scale)
//        +0x534          minimum (high-speed) yaw rate
//        +0x5c0          forward throttle scale
//        +0x360          eyepoint rotation (EulerAngles)
//        +0x378          eyepoint slave flag
//        +0x410          eyepoint slave amount
//        +0x564/+0x568/+0x56c/+0x570  look L/R/down/behind eyepoint pitches
//        +0x7bc          eyepoint / camera child list
//   mech + 0x5b4  ->  cockpit block (+0x28c torso free-aim demand, +0x2a0 flag)
//

//
// Pilot record offsets (entries of pilotArray):
//   pilot + 0x100   world position (Vector3D)
//   pilot + 0x1e0   pilot id (network ordinal)
//   pilot + 0x1fc   linked entity handle
//   pilot + 0x284   current target handle      (written on the LOCAL pilot)
//


// Player drive input, owned by the launcher (btbuild/btl4main.cpp).  Consumed by
// the dev-box key bridge at the top of MechControlsMapper::InterpretControls.
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;

//###########################################################################
//###########################################################################
//					MechControlsMapper
//###########################################################################
//###########################################################################

//#############################################################################
// Shared Data Support
//
MechControlsMapper::SharedData
	MechControlsMapper::DefaultData(
		MechControlsMapper::GetClassDerivations(),
		MechControlsMapper::GetMessageHandlers(),
		MechControlsMapper::GetAttributeIndex(),
		MechControlsMapper::StateCount
	);

Derivation*
	MechControlsMapper::GetClassDerivations()		// @0050ee10
{
	static Derivation classDerivations(
		Subsystem::GetClassDerivations(),
		"MechControlsMapper"						// @0050f173
	);
	return &classDerivations;
}


//#############################################################################
// Messaging Support
//
// Message table @0050ee40.  IDs 3..0x13 share the single ConfigureMappable
// handler; the last three drive the dedicated cycle / toggle handlers.  The
// engine MESSAGE_ENTRY macro forces handler-name == message-name, so the rows
// that fan into the shared handler are written out explicitly.
//
#define MAPPABLE_ENTRY(message)									\
	{															\
		MechControlsMapper::message##MessageID,					\
		#message,												\
		(Receiver::Handler)										\
			&MechControlsMapper::ConfigureMappableMessageHandler	\
	}

const MechControlsMapper::HandlerEntry
	MechControlsMapper::MessageHandlerEntries[]=
{
	MAPPABLE_ENTRY(Aux1Quad),
	MAPPABLE_ENTRY(Aux1Eng1),
	MAPPABLE_ENTRY(Aux1Eng2),
	MAPPABLE_ENTRY(Aux1Eng3),
	MAPPABLE_ENTRY(Aux1Eng4),
	MAPPABLE_ENTRY(Aux2Quad),
	MAPPABLE_ENTRY(Aux2Eng1),
	MAPPABLE_ENTRY(Aux2Eng2),
	MAPPABLE_ENTRY(Aux2Eng3),
	MAPPABLE_ENTRY(Aux2Eng4),
	MAPPABLE_ENTRY(Aux3Quad),
	MAPPABLE_ENTRY(Aux3Eng1),
	MAPPABLE_ENTRY(Aux3Eng2),
	MAPPABLE_ENTRY(Aux3Eng3),
	MAPPABLE_ENTRY(Aux3Eng4),
	MAPPABLE_ENTRY(ZoomIn),
	MAPPABLE_ENTRY(ZoomOut),
	MESSAGE_ENTRY(MechControlsMapper, CycleControlMode),
	MESSAGE_ENTRY(MechControlsMapper, CycleDisplayMode),
	MESSAGE_ENTRY(MechControlsMapper, ToggleVoiceAssist)
};

#undef MAPPABLE_ENTRY

MechControlsMapper::MessageHandlerSet&
	MechControlsMapper::GetMessageHandlers()
{
	static MessageHandlerSet messageHandlers(
		ELEMENTS(MechControlsMapper::MessageHandlerEntries),
		MechControlsMapper::MessageHandlerEntries,
		Subsystem::GetMessageHandlers()
	);
	return messageHandlers;
}


//#############################################################################
// Attribute Support
//
// Attribute table @0050efd0.  (Recorded offsets carry the engine scalar tag,
// e.g. 0x115 -> stickPosition @0x114.)
//
const MechControlsMapper::IndexEntry
	MechControlsMapper::AttributePointers[]=
{
	ATTRIBUTE_ENTRY(MechControlsMapper, StickPosition,	stickPosition),		// 0x114
	ATTRIBUTE_ENTRY(MechControlsMapper, ThrottlePosition,	throttlePosition),	// 0x11C
	ATTRIBUTE_ENTRY(MechControlsMapper, PedalsPosition,	pedalsPosition),	// 0x120
	ATTRIBUTE_ENTRY(MechControlsMapper, ReverseThrust,	reverseThrust),		// 0x124
	ATTRIBUTE_ENTRY(MechControlsMapper, SpeedDemand,		speedDemand),		// 0x128
	ATTRIBUTE_ENTRY(MechControlsMapper, TurnDemand,		turnDemand),		// 0x12C
	ATTRIBUTE_ENTRY(MechControlsMapper, LookForward,		lookForward),		// 0x130
	ATTRIBUTE_ENTRY(MechControlsMapper, LookLeft,			lookLeft),			// 0x134
	ATTRIBUTE_ENTRY(MechControlsMapper, LookRight,		lookRight),			// 0x138
	ATTRIBUTE_ENTRY(MechControlsMapper, LookBehind,		lookBehind),		// 0x13C
	ATTRIBUTE_ENTRY(MechControlsMapper, LookDown,			lookDown),			// 0x140
	ATTRIBUTE_ENTRY(MechControlsMapper, TorsoUp,			torsoUp),			// 0x144
	ATTRIBUTE_ENTRY(MechControlsMapper, TorsoDown,		torsoDown),			// 0x148
	ATTRIBUTE_ENTRY(MechControlsMapper, TorsoLeft,		torsoLeft),			// 0x14C
	ATTRIBUTE_ENTRY(MechControlsMapper, TorsoRight,		torsoRight),		// 0x150
	ATTRIBUTE_ENTRY(MechControlsMapper, TorsoCenter,		torsoCenter),		// 0x154
	ATTRIBUTE_ENTRY(MechControlsMapper, ControlMode,		controlMode),		// 0x190
	ATTRIBUTE_ENTRY(MechControlsMapper, DisplayMode,		displayMode),		// 0x194
	ATTRIBUTE_ENTRY(MechControlsMapper, PilotArrayPage,	pilotArrayPage),	// 0x158
	ATTRIBUTE_ENTRY(MechControlsMapper, PilotArray,		pilotArray)			// 0x15C
};

MechControlsMapper::AttributeIndexSet&
	MechControlsMapper::GetAttributeIndex()
{
	static AttributeIndexSet attributeIndex(
		ELEMENTS(MechControlsMapper::AttributePointers),
		MechControlsMapper::AttributePointers,
		Subsystem::GetAttributeIndex()
	);
	return attributeIndex;
}


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

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b02f0
//
MechControlsMapper::MechControlsMapper(
	Mech				*owner,
	int					subsystem_ID,
	SubsystemResource	*subsystem_resource,
	SharedData			&shared_data
):
	Subsystem(										// FUN_004ac530(...,0,0)
		owner,
		subsystem_ID,
		subsystem_resource,
		shared_data
	)
{
	Check(owner);

	//
	// This mapper is always the active "Performance" of its Mech; the engine
	// installs the default InterpretControls member-pointer (vtable @0050f120)
	// into the simulation slot (this[7..9]).
	//
	SetPerformance(&MechControlsMapper::InterpretControls);

	//
	// Clear all published control inputs / demands.
	//
	stickPosition.x	= 0.0f;
	stickPosition.y	= 0.0f;
	throttlePosition	= 0.0f;
	pedalsPosition		= 0.0f;
	speedDemand		= 0.0f;
	turnDemand			= 0.0f;
	reverseThrust		= 0;
	lookForward		= 0;
	lookLeft			= 0;
	lookRight			= 0;
	lookDown			= 0;
	lookBehind			= 0;
	lookState			= LookNone;

	torsoUp			= 0;
	torsoDown			= 0;
	torsoLeft			= 0;
	torsoRight			= 0;
	torsoCenter		= 0;

	controlMode		= BasicMode;
	displayMode		= 0;
	pilotArrayPage		= 0;

	//
	// Recenter the torso articulation: current yaw/pitch <- neutral yaw/pitch.
	//
	// TODO(bring-up): the shipped code reads a torso-source pointer at the binary
	// byte offset owner+0x438 (Mech::sinkSourceSubsystem) and copies that object's
	// +0x228/+0x22c into +0x220/+0x224.  Our RECONSTRUCTED Mech has a different
	// object layout (sizeof != the binary's 0x854), so this raw offset yields a
	// garbage pointer and faults.  Skipped: the Mech ctor already identity-inits
	// torsoAimCurrent/torsoAimTarget, so the torso starts centered.  Re-enable
	// via a named Mech accessor once the cross-module field layout is mapped.
	{
		int torso = *(int *)((int)owner + 0x438);		// torso articulation block
		if (torso != 0 && torso != (int)0xcdcdcdcd)
		{
			// (intentionally inert until the layout is mapped -- see note above)
		}
		(void)torso;
	}

	pilotArrayBuilt	= False;
	pilotIDs			= 0;
	pilotArray[0]		= 0;

	Check_Fpu();
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b044c -- frees the pilot id table, then chains to ~Subsystem.
//
MechControlsMapper::~MechControlsMapper()
{
	Check(this);

	if (pilotIDs != 0)
	{
		delete [] pilotIDs;				// FUN_004022e8 (Free of the id table)
	}

	Check_Fpu();
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b08c0
//
Logical
	MechControlsMapper::TestInstance() const
{
	return IsDerivedFrom(*GetClassDerivations());	// FUN_0041a1a4(**this[3], 0x50ee10)
}


//#############################################################################
// Message Handlers
//

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004afbc4 -- shared handler for the auxiliary-equipment and zoom buttons.
// The decomp body for this thunk was not captured; per the VTVControlsMapper
// pattern it routes the press through the configuration / mapping interface.
// BEST-EFFORT.
//
void
	MechControlsMapper::ConfigureMappableMessageHandler(
		ReceiverDataMessageOf<ControlsButton> *message
	)
{
	Check(this);
	Check(message);

	if (message->dataContents > 0)
	{
		// TODO: verify against @004afbc4 -- expected to add or erase a
		// temporary mapping for the pressed mappable button.
		AddOrErase(message->dataContents, (ControlsButton *)0);
	}
	Check_Fpu();
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004afbe0 -- cycle BasicMode -> StandardMode -> VeteranMode -> BasicMode.
// Each mode reconfigures the torso articulation: basic mode recenters and
// auto-centers the torso, the assisted modes free the torso to its limits.
//
void
	MechControlsMapper::CycleControlModeMessageHandler(
		ReceiverDataMessageOf<ControlsButton> *message
	)
{
	Check(this);
	Check(message);

	if (message->dataContents > 0)
	{
		controlMode = (ControlMode)(controlMode + 1);
		if (controlMode > VeteranMode)
		{
			controlMode = BasicMode;
		}
		NotifyOfControlModeChange(controlMode);		// vtable+0x48

		Mech *mech	= GetMech();
		int torso	= *(int *)((int)mech + 0x438);
		int cockpit	= *(int *)((int)mech + 0x5b4);

		if (controlMode == BasicMode)
		{
			*(LWord *)(torso + 0x1f0) = 0;					// no free aim
			*(LWord *)(torso + 0x274) = 1;					// auto-center on
			*(LWord *)(torso + 0x220) = *(LWord *)(torso + 0x228);	// recenter yaw
			*(LWord *)(torso + 0x224) = *(LWord *)(torso + 0x22c);	// recenter pitch
			*(LWord *)(cockpit + 0x2a0) = 1;
		}
		else if ((unsigned)(controlMode - 1) < 2)			// Standard / Veteran
		{
			*(LWord *)(torso + 0x220) = *(LWord *)(torso + 0x230);	// yaw -> limit
			*(LWord *)(torso + 0x224) = *(LWord *)(torso + 0x234);	// pitch -> limit
		}
	}
	Check_Fpu();
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004afcac -- cycle the HUD display mode (0 -> 1 -> 2 -> 0).
//
void
	MechControlsMapper::CycleDisplayModeMessageHandler(
		ReceiverDataMessageOf<ControlsButton> *message
	)
{
	Check(this);
	Check(message);

	if (message->dataContents > 0)
	{
		displayMode = displayMode + 1;
		if (displayMode > 2)
		{
			displayMode = 0;
		}
		NotifyOfDisplayModeChange(displayMode);		// vtable+0x4c
	}
	Check_Fpu();
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004afce8 -- toggle voice assist on the Mech's pilot subsystem.
//
void
	MechControlsMapper::ToggleVoiceAssistMessageHandler(
		ReceiverDataMessageOf<ControlsButton> *message
	)
{
	Check(this);
	Check(message);

	if (message->dataContents > 0)
	{
		// FUN_004bff74(mech->subsystem(0x190)) -- toggles the voice-assist flag.
		ToggleVoiceAssist(*(int *)((int)GetMech() + 0x190));
	}
	Check_Fpu();
}


//#############################################################################
// Model Support -- InterpretControls
//

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// Owner accessor.  The mapper is always owned by a Mech; the decomp used the
// stored owner pointer directly (this[?]).  Routed here through the engine's
// Subsystem::GetEntity().
//
Mech*
	MechControlsMapper::GetMech()
{
	Check(this);
	return (Mech *)GetEntity();
}


//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004afd10 -- the active master-Mech performance.  Reads the raw stick,
// throttle, pedals and look buttons and writes the locomotion demands
// (speedDemand / turnDemand), the torso aim and the eyepoint pose.  The
// per-control-mode steering/torso math is faithful to the decomp; the Mech
// sub-object offsets are documented at the top of this file.  BEST-EFFORT on
// the precise Mech-field semantics.
//
void
	MechControlsMapper::InterpretControls(Scalar time_slice)
{
	Check(this);

	//
	// OFFSET RECONCILIATION (the revival fix): the earlier draft read the owner
	// at RAW binary offsets, including TYPED-POINTER arithmetic on Mech*
	// ("*(Scalar *)(mech + 0x34c)" == mech + 0x34c*sizeof(Mech) -- a wild pointer,
	// the AV that kept this tick bypassed).  Every owner access now goes through
	// the reconciled members: mech+0x34c -> reverseStrideLength (naming caveat:
	// LoadLocomotionClips measures it from the rr* clips -- it is the TOP/run
	// cycle speed the throttle scales), mech+0x534 -> walkStrideLength,
	// mech+0x5c0 -> forwardThrottleScale, mech+0x438 -> sinkSourceSubsystem (the
	// re-based binary-exact Torso), mech+0x5b4 -> hudSubsystem (the HUD -- raw
	// factory part_012.c:10164; MechTech's 0x104 alloc cannot hold +0x28c).
	// Torso/HUD writes go through their real members (analog axes @0x1F0/0x1F4,
	// horizontalEnabled @0x250, freeAimSlew @0x28C).  Null-guards are bring-up
	// safety for mechs without those subsystems (the binary trusts the data).
	//
	Mech	*mech	= GetMech();
	Torso	*torso	= (Torso *)mech->GetTorsoSubsystem();	// @0x438 (raw iVar5)
	HUD		*cockpit= (HUD *)mech->GetHudSubsystem();		// @0x5b4 (raw iVar3)

	//
	// BRING-UP INPUT BRIDGE (dev box; env BT_KEY_BRIDGE=0 to disable on pods).
	// The engine controls push refreshes the registered input attributes from
	// DEVICE elements every frame BEFORE this Performance runs; on a keyboard
	// dev box the RIO scalar throttle channel doesn't exist and its element
	// reads a constant (observed: throttlePosition forced back to 1.0 each
	// frame -> the mech could never stop).  So key state is written HERE --
	// after the push, immediately before interpretation -- making the keyboard
	// authoritative on the dev box.  Interpretation below stays 100% authentic.
	//
	{
		static const int s_keyBridge =
			(getenv("BT_KEY_BRIDGE") == 0 || *getenv("BT_KEY_BRIDGE") != '0');
		if (s_keyBridge && mech != 0 && application != 0
			&& (Entity *)mech == application->GetViewpointEntity())
		{
			float key_throttle = gBTDrive.forced
				? gBTDrive.forcedThrottle : gBTDrive.throttle;
			float key_turn = gBTDrive.forced ? 0.0f : gBTDrive.turn;
			// Headless harness (forced mode only): BT_FORCE_TURN holds a steering
			// demand; BT_FORCE_SECONDS releases the forced throttle after n
			// sim-seconds while KEEPING the turn (the turn-in-place repro).
			if (gBTDrive.forced)
			{
				static float s_hClock = 0.0f, s_hLimit = -1.0f, s_hTurn = -999.0f;
				if (s_hLimit < 0.0f)
				{
					const char *fs = getenv("BT_FORCE_SECONDS");
					s_hLimit = fs ? (float)atof(fs) : 0.0f;
				}
				if (s_hTurn < -900.0f)
				{
					const char *ft = getenv("BT_FORCE_TURN");
					s_hTurn = ft ? (float)atof(ft) : 0.0f;
				}
				key_turn = s_hTurn;
				// BT_GOTO beeline (DEBUG harness): the turn demand is computed in
				// mech4.cpp's drive block (where position/heading are in scope)
				// and published through gBTGotoTurn/gBTGotoActive.
				{
					extern int   gBTGotoActive;
					extern float gBTGotoTurn;
					extern float gBTGotoThrottle;
					if (gBTGotoActive)
					{
						key_turn = gBTGotoTurn;
						key_throttle = key_throttle * gBTGotoThrottle;
					}
				}
				// BT_FORCE_FLIP=<t>: invert the forced throttle after t sim-seconds
				// (the mid-stride direction-change repro).
				static float s_hFlip = -1.0f;
				if (s_hFlip < 0.0f)
				{
					const char *ff = getenv("BT_FORCE_FLIP");
					s_hFlip = ff ? (float)atof(ff) : 0.0f;
				}
				if (s_hLimit > 0.0f || s_hFlip > 0.0f)
				{
					s_hClock += time_slice;
					if (s_hFlip > 0.0f && s_hClock > s_hFlip)
					{
						key_throttle = -key_throttle;
					}
					else if (s_hLimit > 0.0f && s_hClock > s_hLimit)
					{
						key_throttle = 0.0f;
					}
				}
			}
			throttlePosition = (key_throttle >= 0.0f) ? key_throttle : -key_throttle;
			reverseThrust    = (key_throttle < 0.0f) ? 1 : 0;
			stickPosition.x  = key_turn;
			stickPosition.y  = 0.0f;
		}
	}

	//
	//------------------------------------------------------------------
	// Throttle -> forward speed demand (reverse thrust inverts & rescales)
	//------------------------------------------------------------------
	//
	// CANARY HEAL: something stomps mech->forwardThrottleScale at runtime
	// nondeterministically (observed 0.14 and -1.0 across runs; ctor sets 1.0
	// guarded) -- a wild raw-offset write, hunt via ba w4 on mech+0x5c0 (on the
	// ledger).  Until the writer is caught, restore a sane value and log.
	// (band tightened: the stomp value 0.14 -- observed repeatedly, ~8 degrees
	// in radians, likely an angle write landing on the wrong member -- passed
	// the old 0.01..100 sanity band and the user's forward speed silently
	// capped at 8.6 u/s while reverse ran full.  The ctor guard currently
	// always yields exactly 1.0, so heal anything else.)
	if (mech->forwardThrottleScale != 1.0f)
	{
		DEBUG_STREAM << "[mppr] forwardThrottleScale STOMPED to "
			<< mech->forwardThrottleScale << " -- healed to 1.0" << "\n" << std::flush;
		mech->forwardThrottleScale = 1.0f;
	}
	if (reverseThrust < 1)
	{
		speedDemand =
			mech->reverseStrideLength * throttlePosition * mech->forwardThrottleScale;
	}
	else
	{
		speedDemand = -mech->reverseStrideLength * throttlePosition;
	}
	{
		static int s_cTrace = -1;
		if (s_cTrace < 0) { const char *e = getenv("BT_MPPR_TRACE"); s_cTrace = (e && *e != '0') ? 1 : 0; }
		if (s_cTrace)
		{
			static float s_cAcc = 0.0f; s_cAcc += time_slice;
			if (s_cAcc >= 0.5f) { s_cAcc = 0.0f;
				DEBUG_STREAM << "[mppr-c] thr=" << throttlePosition << " rev=" << reverseThrust
					<< " topSpd=" << mech->reverseStrideLength
					<< " fScale=" << mech->forwardThrottleScale
					<< " -> dmd=" << speedDemand << "\n" << std::flush; }
		}
	}

	//
	//------------------------------------------------------------------
	// Square the stick for a soft response; cube the pedals.  Preserve sign.
	//------------------------------------------------------------------
	//
	Scalar stick_x = stickPosition.x * stickPosition.x;
	Scalar stick_y = stickPosition.y * stickPosition.y;
	Scalar pedal_3 = pedalsPosition * pedalsPosition * pedalsPosition;
	if (stickPosition.x < 0.0f)	stick_x = -stick_x;		// _DAT_004b0274 == 0.0f
	if (stickPosition.y < 0.0f)	stick_y = -stick_y;

	turnDemand	= 0.0f;

	if (controlMode == BasicMode)
	{
		//
		// Basic: stick yaw drives the turn directly; stick pitch drives the
		// torso pitch.  The achievable turn rate is clamped down with speed.
		//
		turnDemand = stick_x;
		if (torso)
		{
			torso->SetAnalogElevationAxis(stick_y);	// raw torso+0x1f4 (500)
			torso->SetAnalogTwistAxis(0.0f);		// raw torso+0x1f0
		}
		if (cockpit)
		{
			cockpit->SetFreeAimSlew(0.0f);			// raw cockpit+0x28c
		}

		Scalar max_turn =
			(mech->reverseStrideLength - mech->walkStrideLength)
				* (1.0f - Abs(turnDemand))			// _DAT_004b0278 == 1.0f
			+ mech->walkStrideLength;

		if (fabsf(turnDemand) > JM_CLOSE_ENOUGH)	// _DAT_004b027c
		{
			Clamp(speedDemand, -max_turn, max_turn);
		}
	}
	else if (controlMode == StandardMode)
	{
		//
		// Standard: torso free-aim follows the stick yaw; pedals provide the
		// turn.  Turn rate clamped as in basic mode.
		//
		if (torso == 0 || !torso->GetHorizontalEnabled())	// raw torso+0x250 == 0
		{
			if (cockpit)	cockpit->SetFreeAimSlew(stick_x);	// raw cockpit+0x28c
		}
		else
		{
			torso->SetAnalogTwistAxis(stick_x);				// raw torso+0x1f0
		}
		if (torso)	torso->SetAnalogElevationAxis(stick_y);	// raw torso+0x1f4

		turnDemand = (pedal_3 == 0.0f) ? 0.0f : pedal_3;

		Scalar max_turn =
			(mech->reverseStrideLength - mech->walkStrideLength)
				* (1.0f - Abs(turnDemand))
			+ mech->walkStrideLength;

		if (fabsf(turnDemand) > JM_CLOSE_ENOUGH)
		{
			Clamp(speedDemand, -max_turn, max_turn);
		}
	}
	else if (controlMode == VeteranMode)
	{
		//
		// Veteran: as standard, but no speed-dependent turn clamp.
		//
		if (torso == 0 || !torso->GetHorizontalEnabled())
		{
			if (cockpit)	cockpit->SetFreeAimSlew(stick_x);
		}
		else
		{
			torso->SetAnalogTwistAxis(stick_x);
		}
		if (torso)	torso->SetAnalogElevationAxis(stick_y);

		turnDemand = (pedal_3 == 0.0f) ? 0.0f : pedal_3;
	}

	//
	//------------------------------------------------------------------
	// Look / eyepoint selection.  Choose a look direction from the buttons;
	// when it changes, re-aim the eyepoint and re-slave the camera children.
	//------------------------------------------------------------------
	//
	previousLookState = lookState;
	if		(lookLeft	> 0)	lookState = LookLeftState;
	else if (lookRight	> 0)	lookState = LookRightState;
	else if (lookBehind	> 0)	lookState = LookBehindState;
	else if (lookDown	> 0)	lookState = LookDownState;
	else						lookState = LookNone;

	if (lookState != previousLookState)
	{
		// TODO(look-eyepoint): the commit block below writes the eyepoint pose at
		// binary offsets whose Mech-member mapping is CONFLICTED (mech+0x360 is
		// claimed by mechName, +0x410 by stateFlags, +0x7bc by poweredSubsystems --
		// and the old draft even routed them through the TORSO pointer).  Arbitrate
		// those labels against the raw before enabling; the port has no eyepoint
		// consumer yet.  Look-state SELECTION above stays live (this-only).
		DEBUG_STREAM << "[mppr] look state -> " << lookState
			<< " (eyepoint commit deferred)\n" << std::flush;
	}
	else if (0)		// DEFERRED body (kept for the reconstruction record)
	{
		const int		torso = 0;		// shadow: the raw-offset text below predates
										// the reconciliation (see TODO above)
		EulerAngles		eyepoint;
		ChildIterator	cameras(torso + 0x7bc);		// FUN_004afacf

		switch (lookState)
		{
		case LookNone:
			eyepoint = EulerAngles::Identity;		// DAT_004e0f8c
			*(LWord *)(torso + 0x378) = 1;			// re-slave eyepoint
			for (int *camera; (camera = cameras.Next()) != 0; )
			{
				*(LWord *)(camera + 0x3e0) = (*(int *)(camera + 0x334) == 0);
			}
			break;

		case LookLeftState:
			eyepoint		= EulerAngles::Identity;
			eyepoint.pitch	= *(Scalar *)(torso + 0x564);
			*(LWord *)(torso + 0x378) = 0;
			for (int *camera; (camera = cameras.Next()) != 0; )
			{
				*(LWord *)(camera + 0x3e0) = 0;
			}
			break;

		case LookRightState:
			eyepoint		= EulerAngles::Identity;
			eyepoint.pitch	= *(Scalar *)(torso + 0x568);
			*(LWord *)(torso + 0x378) = 0;
			for (int *camera; (camera = cameras.Next()) != 0; )
			{
				*(LWord *)(camera + 0x3e0) = 0;
			}
			break;

		case LookBehindState:
			eyepoint		= EulerAngles::Identity;
			eyepoint.yaw	= PI;						// 0x40490fdb
			eyepoint.pitch	= *(Scalar *)(torso + 0x570);
			*(LWord *)(torso + 0x378) = *(LWord *)(torso + 0x410);
			for (int *camera; (camera = cameras.Next()) != 0; )
			{
				*(LWord *)(camera + 0x3e0) = *(int *)(camera + 0x334);
			}
			break;

		case LookDownState:
			eyepoint		= EulerAngles::Identity;
			eyepoint.pitch	= *(Scalar *)(torso + 0x56c);
			*(LWord *)(torso + 0x378) = 0;
			for (int *camera; (camera = cameras.Next()) != 0; )
			{
				*(LWord *)(camera + 0x3e0) = 0;
			}
			break;
		}

		*(EulerAngles *)(torso + 0x360) = eyepoint;	// commit eyepoint rotation
	}

	Check_Fpu();
}


//#############################################################################
// Pilot-array (other-player roster) management
//

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b0600 -- build the pilot roster once, lazily, from the application's
// "Players" group (skipping spectators flagged 0x40).
//
void
	MechControlsMapper::BuildPilotArray()
{
	if (pilotArrayBuilt)
	{
		return;
	}
	pilotArrayBuilt = True;

	pilotCount = 0;
	EntityGroup *players =
		application->GetEntityManager()->FindGroup("Players");	// @0050f44b

	if (players != 0)
	{
		ChainIteratorOf<Node*> pilots(players->groupMembers);	// FUN_00421414
		for (Node *entry; (entry = pilots.ReadAndNext()) != 0; )
		{
			if ((*(byte *)((int)entry + 0x29) & 0x40) == 0)		// not a spectator
			{
				++pilotCount;
			}
		}
	}

	pilotIDs = new int[pilotCount];								// FUN_004022b0

	for (int i = 0; i <= pilotCount; ++i)
	{
		pilotArray[i] = 0;
	}

	FillPilotArray();			// FUN_004b06cc
	ChooseDefaultPilot();		// FUN_004b07f0
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b06cc -- slot 0 holds the local pilot; the remaining slots are filled
// from the "Players" group (paged by pilotArrayPage).
//
void
	MechControlsMapper::FillPilotArray()
{
	for (int i = 0; i < pilotCount; ++i)
	{
		pilotIDs[i] = -1;
	}

	//
	// The local "station" (the local network player record) lives at
	// application+0x6c in the binary; its pilot roster entry at +0x190.
	// ENGINE DRIFT FIX (the AV that kept the mapper tick bypassed): the WinTesla
	// Application exposes the local player through GetMissionPlayer() (APP.h:169)
	// -- the same reconciliation SendFakeButtonEvent uses (this file's sibling in
	// btl4mppr.cpp).  The old draft read the 1995 byte offsets through the 2007
	// object -> wild "pilot" pointer -> access violation.
	//
	Player *local_pilot =
		(application != 0) ? application->GetMissionPlayer() : 0;
	if (local_pilot == 0)
	{
		return;
	}

	pilotArray[0]	= (Pilot *)local_pilot;
	pilotIDs[0]		= local_pilot->GetEntityID();		// raw read pilot+0x1e0 (the entity id)

	EntityGroup *players =
		application->GetEntityManager()->FindGroup("Players");
	if (players != 0)
	{
		ChainIteratorOf<Node*> pilots(players->groupMembers);

		// skip earlier pages
		int skip = pilotArrayPage * pilotCount;
		while (skip > 0 && pilots.ReadAndNext() != 0)
		{
			--skip;
		}

		int slot = 1;
		for (Node *entry; (entry = pilots.ReadAndNext()) != 0; )
		{
			// TODO(mp-roster): the binary skips spectators via a flag byte at
			// entry+0x29 (bit 0x40) -- its WinTesla home is unmapped; solo has no
			// spectators, so the check is omitted until the flag is reconciled.
			if (entry == (Node *)local_pilot)					// already slot 0
			{
				continue;
			}
			pilotArray[slot]	= (Pilot *)entry;
			pilotIDs[slot]		= ((Entity *)entry)->GetEntityID();	// raw entry+0x1e0
			if (++slot >= pilotCount)
			{
				break;
			}
		}
	}
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b07f0 -- pick the initial target for the local pilot: the next pilot id
// after the local one (or the first non-self pilot).
//
void
	MechControlsMapper::ChooseDefaultPilot()
{
	int target = 0;

	if (pilotCount != 1)
	{
		int index = 0;

		if (pilotCount == pilotIDs[0])
		{
			for (int i = 1; i < pilotCount; ++i)
			{
				if (pilotIDs[i] != 1)
				{
					index = i;
					break;
				}
			}
		}
		else
		{
			for (int i = 1; i < pilotCount; ++i)
			{
				if (pilotIDs[i] == pilotIDs[0] + 1)
				{
					index = i;
					break;
				}
			}
		}

		Pilot *chosen = pilotArray[index];
		if (chosen != 0)
		{
			target = *(int *)((int)chosen + 0x1fc);
		}
		*(int *)((int)pilotArray[0] + 0x284) = target;
	}
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b049c -- set the local pilot's current target to the pilot on the given
// roster page.
//
void
	MechControlsMapper::UpdateCurrentPilot(int page)
{
	if (pilotArrayBuilt && pilotArray[0] != 0)
	{
		int target = 0;
		Pilot *chosen = pilotArray[page];
		if (chosen != 0)
		{
			target = *(int *)((int)chosen + 0x1fc);
		}
		*(int *)((int)pilotArray[0] + 0x284) = target;
	}
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b04d8 -- choose the nearest living pilot (other than self) as the local
// pilot's current target.
//
void
	MechControlsMapper::ChooseNearestPilot(int self_id)
{
	if (!pilotArrayBuilt || pilotArray[0] == 0)
	{
		return;
	}

	int		target			= 0;
	Scalar	nearest_distance	= 0.0f;
	int		nearest_index		= 0;
	Logical	none_yet			= True;

	for (int i = 1; i < pilotCount; ++i)
	{
		int		entity = *(int *)((int)pilotArray[i] + 0x1fc);
		Vector3D	delta;
		delta.Subtract(									// FUN_00408644
			*(Vector3D *)((int)pilotArray[i] + 0x100),
			*(Vector3D *)((int)pilotArray[0] + 0x100)
		);
		Scalar distance = delta.x * delta.x + delta.y * delta.y + delta.z * delta.z;

		if (none_yet)
		{
			if (!Is_Destroyed(entity) && entity != self_id)
			{
				none_yet			= False;
				nearest_distance	= distance;
				nearest_index		= i;
			}
		}
		else if (!Is_Destroyed(entity) && entity != self_id
				&& distance < nearest_distance)
		{
			nearest_distance	= distance;
			nearest_index		= i;
		}
	}

	if (!none_yet)
	{
		Pilot *chosen = pilotArray[nearest_index];
		if (chosen != 0)
		{
			target = *(int *)((int)chosen + 0x1fc);
		}
		*(int *)((int)pilotArray[0] + 0x284) = target;
	}
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b0898 -- bounds-checked roster accessor.
//
Pilot *
	MechControlsMapper::GetPilot(int index)
{
	if (index >= pilotCount || index < 0)
	{
		return 0;
	}
	return pilotArray[index];
}


//#############################################################################
// Configuration / event-mapping interface (secondary vtable @0050f498)
//
// These are "not overridden" defaults: a platform-specific derived mapper is
// expected to supply real implementations.  The base class traps the call.
//

//
// @004b029c
//
void
	MechControlsMapper::ExitConfiguration()
{
	Fail("ExitConfiguration not overridden!\n");	// MECHMPPR.CPP:0x245
}

//
// (companion to the above; "EnterConfiguration not overridden!" @0050f370)
//
void
	MechControlsMapper::EnterConfiguration(
		ControlsButton		* /*destination*/,
		Receiver			* /*receiver*/,
		Receiver::MessageID	  /*activation_message_id*/,
		Receiver::MessageID	  /*configuration_message_id*/
	)
{
	Fail("EnterConfiguration not overridden!\n");
}

//
// @004b02b8
//
void
	MechControlsMapper::CreateTemporaryEventMappings(
		Receiver			* /*receiver*/,
		Receiver::MessageID	  /*config_message_id*/
	)
{
	Fail("Unhandled mapping!\n");					// MECHMPPR.CPP:0x24f
}

//
// @004b02d4
//
void
	MechControlsMapper::AddOrErase(
		unsigned int	 /*button_ID*/,
		ControlsButton	* /*destination*/
	)
{
	Fail("Unhandled mapping!\n");					// MECHMPPR.CPP:0x25a
}

//
// @004b048c -- vtable+0x48; defaults to a no-op (override for HUD feedback).
//
void
	MechControlsMapper::NotifyOfControlModeChange(int /*new_mode*/)
{
}

//
// @004b0494 -- vtable+0x4c; defaults to a no-op.
//
void
	MechControlsMapper::NotifyOfDisplayModeChange(int /*new_mode*/)
{
}
