//===========================================================================//
// File:	btl4mppr.cpp                                                        //
// Project: BattleTech           Brick: BattleTech LBE Application            //
// Contents: The L4-application-side Mech controls mappers.  L4MechControlsMapper//
//           is the platform layer over the in-Mech MechControlsMapper; the    //
//           two concrete mappers (MechThrustmasterMapper / MechRIOMapper)      //
//           bind the physical cockpit hardware (throttle/pedal axes, aux       //
//           quadrant buttons, target hotbox, keyboard) to Mech demands, the    //
//           exponential target-range "zoom", and the control/display modes.    //
//---------------------------------------------------------------------------//
//   Date    Who  Modification                                               //
// --------  ---  ---------------------------------------------------------- //
//                                                                           //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
//                      PROPRIETARY AND CONFIDENTIAL                         //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary BTL4OPT.EXE (no header survived).  The
// class shape follows the Red Planet analog RP_L4\RPL4MPPR.cpp
// (L4VTVControlsMapper / VTVThrustmasterMapper / VTVRIOMapper); method bodies
// are recovered from the Ghidra pseudo-C for the 0x4d11e8-0x4d27f8 cluster
// (all/part_014.c).  Names come from the .rdata string pool
// ("L4MechControlsMapper" @0051e1eb, "MechThrustmasterMapper" @0051e2b3,
// "MechRIOMapper" @0051e2e3) and from the base MechControlsMapper attribute
// map already recovered in mechmppr.cpp (NOT re-emitted here).
//
// Function -> method map:
//   L4MechControlsMapper ----------------------------- vtable @0051e440
//     @004d17ac  ctor            @004d1814  dtor
//     @004d196c  InterpretControls (target-range ramp + review-mode watch)
//     @004d1b64  ZoomTargetRangeIn   @004d1b9c  ZoomTargetRangeOut
//     @004d1ae4  SetControlMode (mode-mask switch; calls base NotifyOfDisplayMode)
//     @004d1b24  SetPresetMode (group/item -> preset mode-mask)
//     @004d1840  CreateTemporaryEventMappings   @004d18dc  RemoveTemporaryEventMappings
//     @004d195c  NotifyOfControlModeChange (no-op)   @004d1964  NotifyOfConfigurationModeChange (no-op)
//     @004d1bf0  KeypressMessageHandler (the shared L4 keyboard dispatcher)
//   MechThrustmasterMapper --------------------------- vtable @0051e3f0
//     @004d21d0  ctor   @004d22b4  dtor   @004d22e0  TestInstance
//     @004d2150  InterpretControls
//   MechRIOMapper ------------------------------------ vtable @0051e3a0
//     @004d266c  ctor   @004d27cc  dtor   @004d27f8  TestInstance
//     @004d25c0  InterpretControls
//     @004d22fc..@004d24bc  Aux{1,2,3}{Quad,Eng1..4} message handlers
//     @004d24dc  ZoomIn   @004d24f8  ZoomOut   @004d2574  Hotbox (pilot select)
//     @004d25e8/@004d262c  AddOrErase(Receiver*)/AddOrErase(ControlsButton*)
//   File-scope helpers:
//     @004d13f0  SendFakeButtonEvent(name, value)     (find local-mech subsystem)
//     @004d133c  SendFakeButtonEventPair(mech, name, msgID)  (press + release)
//     @004d141c  DispatchDeveloperFakeEvent(this, message)   (debug subsystem keys)
//     @004d168c  (best-effort) developer designate message    [see note]
//     @004d11e8  module static cleanup (frees DAT_004f1aac list)   [best-effort]
//
// Engine-helper name mapping:
//     FUN_004b02f0  MechControlsMapper::MechControlsMapper  (base ctor, mechmppr.cpp)
//     FUN_004b044c  MechControlsMapper::~MechControlsMapper
//     FUN_004b0494  MechControlsMapper::NotifyOfDisplayModeChange
//     FUN_004b049c  MechControlsMapper::UpdateCurrentPilot(page)
//     FUN_004b04d8  MechControlsMapper::ChooseNearestPilot(self_id)
//     FUN_004b0600  MechControlsMapper::BuildPilotArray
//     FUN_004afbe0  MechControlsMapper::CycleControlModeMessageHandler
//     FUN_004afcac  MechControlsMapper::CycleDisplayModeMessageHandler
//     FUN_004afd10  MechControlsMapper::InterpretControls  (the in-Mech master perf)
//     FUN_0041f98c  Entity::FindSubsystem(name) / Mech::FindSubsystemByName
//     FUN_0041a1a4  IsDerivedFrom(classDerivations)
//     FUN_004024d8  SharedData/Resource::Release(refcount)
//     FUN_004022d0  operator delete
//     FUN_00476fc0  ControlsManager::MakeLinkedLamp(button, modeMask)
//     FUN_00444e64  L4Lamp::SetAutomaticOperation / SetState
//     FUN_00476e34/00476e54/00476e8c  ControlsManager Begin/End/Center Joystick
//     FUN_0044e6c0  Application::ProfileReport
//     FUN_00448654  GaugeRenderer::ProfileReport
//     FUN_004dcd00  fabsf()        FUN_004dce94  powf(2.0, x)
//     FUN_004dbb24  DEBUG_STREAM << (stream insert)
//     DAT_004efc94  the global Application*
//       (app+0x3c controlsManager, app+0x50 modeManager, app+0x4c gaugeRenderer)
//
// Read-only constants resolved from section_dump.txt:
//     _DAT_004d1ac0 = 0x3f800000 = 1.0f   (full-throttle detent center)
//     _DAT_004d1ac4 = 0x3d4ccccd = 0.05f  (detent snap tolerance)
//     _DAT_004d1ac8 = 0x437a0000 = 250.0f (target-range scale: range = 250*2^x)
//     _DAT_004d1b94 = 1.0f  zoom-in step   _DAT_004d1b98 = 0.0f  min exponent
//     _DAT_004d1bcc = 1.0f  zoom-out step  _DAT_004d1bd0 = 5.0f  max exponent
//     DAT_0051dbe4[3] = {0x40000,0x80000,0x100000}  control-mode mode-masks
//     DAT_0051dbf0[15] = {clear,set} mode-mask pairs (group*5+item), preset select
//     DAT_0051dcd0[8]  = {0x37,0x36,0x35,0x34,0x33,0x32,0x31,0x30}  hotbox buttons
//
// ModeManager (app+0x50): +0x4 currentMode mask, +0x8 savedMode mask.
// Mech (mapper owner @this+0xd0): +0x404 targetRange, +0x414 missionReviewMode.
//

#include <bt.hpp>
#pragma hdrstop

#if !defined(BTL4MPPR_HPP)
#	include <btl4mppr.hpp>
#endif
#if !defined(BTL4APP_HPP)
#	include <btl4app.hpp>
#endif
#if !defined(BTL4MODE_HPP)
#	include <btl4mode.hpp>
#endif
#if !defined(L4CTRL_HPP)
#	include <l4ctrl.hpp>
#endif
#if !defined(MECH_HPP)
#	include <mech.hpp>
#endif
#if !defined(PLAYER_HPP)
#	include <player.hpp>
#endif

//
// The keyboard message the L4 dispatcher receives (recovered name; it is just
// the engine's ReceiverDataMessageOf<ControlsKey>).
//
typedef ReceiverDataMessageOf<ControlsKey> ControlsKeyMessage;

//#############################################################################
// File-scope helpers
//#############################################################################

//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// @004d13f0 -- look the named subsystem up on the LOCAL player's Mech and
// poke its config slot (vtbl+0x20) with the supplied value.  Used by the
// developer "fake button" keys below.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
static void
	SendFakeButtonEvent(const char *subsystem_name, int value)
{
	// Engine drift: the original read the local Mech via Application::localStation
	// (DAT_004efc94+0x6c).  WinTesla exposes it through the mission player's
	// vehicle instead.
	Player *player = application->GetMissionPlayer();
	if (player == NULL)
	{
		return;
	}
	Mech *mech = (Mech*)player->GetPlayerVehicle();
	if (mech == NULL)
	{
		return;
	}
	Subsystem *subsystem =
		mech->FindSubsystem(subsystem_name);		// FUN_0041f98c
	if (subsystem != NULL)
	{
		// Engine drift: Subsystem::SetConfigurationValue (vtbl+0x20) is gone in
		// WinTesla.  Translate the developer "poke" into a configuration-style
		// button dispatch carrying the value.
		ReceiverDataMessageOf<ControlsButton>
			poke(0, sizeof(ReceiverDataMessageOf<ControlsButton>),
				(ControlsButton)value);
		subsystem->Dispatch(&poke);
	}
}

//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// @004d133c -- find a subsystem by name on the given Mech and dispatch a
// momentary press (1) immediately followed by a release (-1) so the target
// behaves as if a real cockpit button were tapped.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
static void
	SendFakeButtonEventPair(
		Mech *mech,
		const char *subsystem_name,
		Receiver::MessageID message_id
	)
{
	Subsystem *subsystem = mech->FindSubsystem(subsystem_name);	// FUN_0041f98c
	if (subsystem == NULL)
	{
		DEBUG_STREAM << "SendFakeButtonEvents: subsystem '"		// @0051e1b6
			<< subsystem_name << "' not found\n";				// @0051e1d8
	}
	else
	{
		ReceiverDataMessageOf<ControlsButton>
			press(message_id, sizeof(ReceiverDataMessageOf<ControlsButton>), 1);
		subsystem->Dispatch(&press);							// vtbl+0xc

		ReceiverDataMessageOf<ControlsButton>
			release(message_id, sizeof(ReceiverDataMessageOf<ControlsButton>), -1);
		subsystem->Dispatch(&release);
	}
}

//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// @004d141c -- the "default" keyboard case: a developer/test dispatcher that
// maps single key codes to fake button events on individual named subsystems
// (GeneratorA..D, Condenser1, the LRM15 launcher and its ammo bin, Avionics,
// Hud) plus two special keys.  Faithful to the shipped switch.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
static void
	DispatchDeveloperFakeEvent(L4MechControlsMapper *mapper, ControlsKeyMessage *message)
{
	switch (message->dataContents)
	{
	case 0x5c:		// '\' -- momentary press straight onto the owner Mech
		{
			ReceiverDataMessageOf<ControlsButton>
				press(0x19, sizeof(ReceiverDataMessageOf<ControlsButton>), 1);
			mapper->GetMech()->Dispatch(&press);	// (this+0xd0)->vtbl+0xc
		}
		break;
	case 0x15e: SendFakeButtonEvent("GeneratorA", 0);    break;
	case 0x15f: SendFakeButtonEvent("GeneratorB", 0);    break;
	case 0x160: SendFakeButtonEvent("GeneratorC", 0);    break;
	case 0x161: SendFakeButtonEvent("GeneratorD", 0);    break;
	case 0x162: SendFakeButtonEvent("Condenser1", 0);    break;
	case 0x163: SendFakeButtonEvent("LRM15", 2);         break;
	case 0x164: SendFakeButtonEvent("AmmoBinLRM15", 1);  break;
	case 0x165: SendFakeButtonEvent("LRM15", 0);         break;
	case 0x166: SendFakeButtonEvent("Avionics", 0);      break;
	case 0x167: SendFakeButtonEvent("Hud", 0);           break;
	case 0x168: SendFakeButtonEvent("GeneratorA", 1);    break;
	case 0x169: SendFakeButtonEvent("GeneratorB", 1);    break;
	case 0x16a: SendFakeButtonEvent("GeneratorC", 1);    break;
	case 0x16b: SendFakeButtonEvent("GeneratorD", 1);    break;
	case 0x16c: SendFakeButtonEvent("Condenser1", 1);    break;
	case 0x185:		// dump the gauge renderer's profile, if one exists
		if (application->GetGaugeRenderer() != NULL)		// DAT_004efc94+0x4c
		{
			application->GetGaugeRenderer()->ProfileReport();	// FUN_00448654
		}
		break;
	}
}


//#############################################################################
//#############################################################################
//						L4MechControlsMapper
//#############################################################################
//#############################################################################

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

Derivation*
	L4MechControlsMapper::GetClassDerivations()
{
	static Derivation classDerivations(
		MechControlsMapper::GetClassDerivations(),
		"L4MechControlsMapper"						// @0051e1eb
	);
	return &classDerivations;
}

//#############################################################################
// Messaging Support
//
const L4MechControlsMapper::HandlerEntry
	L4MechControlsMapper::MessageHandlerEntries[]=
{
	MESSAGE_ENTRY(L4MechControlsMapper, Keypress)	// id 0x17/0x19, handler @004d1bf0
};

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

//
//#############################################################################
// L4MechControlsMapper                                             @004d17ac
//#############################################################################
//
// Chains to MechControlsMapper (FUN_004b02f0); stamps vtable &0051e440 and
// initialises the target-range zoom (exponent 2.0 == 250*2^2 == 1000m) and the
// review-mode watcher.
//
	L4MechControlsMapper::L4MechControlsMapper(
		Mech				*owner,
		int					subsystem_ID,
		SubsystemResource	*subsystem_resource,
		ClassID				class_ID,
		SharedData			&shared_data
	):
		// MechControlsMapper (mechmppr.hpp) takes 4 args -- the ClassID is
		// stamped by the streamed-resource factory, not the mapper ctor, so it
		// is consumed here and not forwarded.
		MechControlsMapper(
			owner,
			subsystem_ID,
			subsystem_resource,
			shared_data
		)
	{
		(void)class_ID;
		previousMissionReviewMode	= 0;			// this[0x6a] @0x1a8
		targetRangeExponentDemand	= 2.0f;			// this[0x68] @0x1a0
		targetRangeExponent			= 2.0f;			// this[0x69] @0x1a4
		Check_Fpu();
	}

//
//#############################################################################
// ~L4MechControlsMapper                                            @004d1814
//#############################################################################
//
	L4MechControlsMapper::~L4MechControlsMapper()
	{
		// scalar-deleting dtor; reinstalls vtable and chains to ~MechControlsMapper
		Check_Fpu();
	}

//
//#############################################################################
// InterpretControls                                                @004d196c
//#############################################################################
//
// The L4-layer performance, run every frame ahead of the in-Mech mapper:
//   1. snap the throttle to a full-throttle detent,
//   2. (re)build the pilot roster,
//   3. watch the Mech's mission-review flag and toggle the corresponding mode
//      bit (0x200000) on the application mode manager,
//   4. slew the smoothed target-range exponent toward the panel demand and
//      push the resulting range (250 * 2^exponent) onto the Mech,
//   5. delegate to MechControlsMapper::InterpretControls for the actual
//      locomotion / torso / eyepoint work.
//
	void
		L4MechControlsMapper::InterpretControls(Scalar time_slice)
	{
		Check(this);

		//
		// (1) Full-throttle detent.
		//
		if (Abs(throttlePosition - 1.0f) <= 0.05f)		// _DAT_004d1ac0 / _DAT_004d1ac4
		{
			throttlePosition = 1.0f;					// @0x11c
		}

		//
		// (2) Roster.
		//
		BuildPilotArray();								// FUN_004b0600

		//
		// (3) Mission-review mode change watcher.
		//
		Mech *mech = GetMech();
		int review_mode = mech->GetMissionReviewMode();	// (mech)+0x414
		if (review_mode != previousMissionReviewMode)	// @0x1a8
		{
			previousMissionReviewMode = review_mode;

			BTL4ModeManager *mode_manager =
				(BTL4ModeManager*)application->GetModeManager();	// DAT_004efc94+0x50
			// ModeManager API: Add/RemoveModeMask save the old mask into
			// previousModeMask (the recovered "savedMode = currentMode" step).
			if (review_mode == 0)
			{
				mode_manager->RemoveModeMask(0x200000);
			}
			else
			{
				mode_manager->AddModeMask(0x200000);
			}
		}

		//
		// (4) Target-range zoom: slew current exponent toward demand, then set
		// the Mech's target range = 250 * 2^exponent.
		//
		if (targetRangeExponentDemand != targetRangeExponent)	// @0x1a0 != @0x1a4
		{
			if (targetRangeExponent <= targetRangeExponentDemand)
			{
				targetRangeExponent -= time_slice;
				if (targetRangeExponent < targetRangeExponentDemand)
				{
					targetRangeExponent = targetRangeExponentDemand;
				}
			}
			else
			{
				targetRangeExponent += time_slice;
				if (targetRangeExponentDemand < targetRangeExponent)
				{
					targetRangeExponent = targetRangeExponentDemand;
				}
			}
			mech->SetTargetRange(						// (mech)+0x404
				powf(2.0f, targetRangeExponent) * 250.0f);	// FUN_004dce94, _DAT_004d1ac8
		}

		//
		// (5) Hand off to the in-Mech master performance.
		//
		MechControlsMapper::InterpretControls(time_slice);	// FUN_004afd10
		Check_Fpu();
	}

//
//#############################################################################
// ZoomTargetRangeIn / ZoomTargetRangeOut             @004d1b64 / @004d1b9c
//#############################################################################
//
	void
		L4MechControlsMapper::ZoomTargetRangeIn()
	{
		targetRangeExponentDemand -= 1.0f;				// _DAT_004d1b94
		if (targetRangeExponentDemand < 0.0f)			// _DAT_004d1b98
		{
			targetRangeExponentDemand = 0.0f;
		}
	}

	void
		L4MechControlsMapper::ZoomTargetRangeOut()
	{
		targetRangeExponentDemand += 1.0f;				// _DAT_004d1bcc
		if (targetRangeExponentDemand > 5.0f)			// _DAT_004d1bd0
		{
			targetRangeExponentDemand = 5.0f;
		}
	}

//
//#############################################################################
// SetControlMode                                                   @004d1ae4
//#############################################################################
//
// Swap the application mode-manager's control-mode bits (18..20) for the bit
// selected by new_mode, then notify the base of the implied display change.
//
	void
		L4MechControlsMapper::SetControlMode(int new_mode)
	{
		static const ModeMask control_mode_mask[] =		// @0051dbe4
			{ 0x40000, 0x80000, 0x100000 };

		BTL4ModeManager *mode_manager =
			(BTL4ModeManager*)application->GetModeManager();	// DAT_004efc94+0x50

		mode_manager->RemoveModeMask(0x001c0000);		// clear bits 18..20 (~0xffe3ffff)
		mode_manager->AddModeMask(control_mode_mask[new_mode]);

		NotifyOfDisplayModeChange(displayMode);			// FUN_004b0494
	}

//
//#############################################################################
// SetPresetMode                                                    @004d1b24
//#############################################################################
//
// Each (group,item) selects a preset; the table @0051dbf0 holds {clear,set}
// mode-mask pairs that remove the previous preset of that group and enable the
// chosen one.
//
	void
		L4MechControlsMapper::SetPresetMode(int group, int item)
	{
		struct PresetMaskPair { ModeMask clear, set; };
		static const PresetMaskPair preset_mask[15] =	// @0051dbf0 (group*5+item)
		{
			{0x1e,  0x01000000}, {0x1f,  0x02000000}, {0x1f,  0x04000000},
			{0x1f,  0x08000000}, {0x1f,  0x10000000},
			{0x3c0, 0x20000000}, {0x3e0, 0x40000000}, {0x3e0, 0x80000000},
			{0x3e0, 0x00010000}, {0x3e0, 0x00020000},
			{0x7800,0x00040000}, {0x7c00,0x00080000}, {0x7c00,0x00100000},
			{0x7c00,0x00200000}, {0x7c00,0x00400000}
		};

		int index = group * 5 + item;

		BTL4ModeManager *mode_manager =
			(BTL4ModeManager*)application->GetModeManager();

		mode_manager->RemoveModeMask(preset_mask[index].clear);
		mode_manager->AddModeMask(preset_mask[index].set);
	}

//
//#############################################################################
// CreateTemporaryEventMappings                                     @004d1840
//#############################################################################
//
// During configuration, route every mappable button (the 0x40..0x47 block,
// skipping the joystick hat 0x41..0x44) at the configuration receiver so the
// pilot can press a button to bind it.  Mode mask 0x8000 == ModeConfigReady.
//
	void
		L4MechControlsMapper::CreateTemporaryEventMappings(
			Receiver *receiver,
			Receiver::MessageID config_message_id
		)
	{
		LBE4ControlsManager *controls =
			(LBE4ControlsManager*)application->GetControlsManager();	// DAT_004efc94+0x3c

		// Route every mappable button (0x40..0x47, skipping the joystick hat
		// 0x41..0x44) at the configuration receiver; mode 0x8000 == ModeConfigReady.
		// (Loop bounds mirror the RP twin L4VTVControlsMapper.)
		for (int i = LBE4ControlsManager::FirstMappableButton;
				i <= LBE4ControlsManager::LastMappableButton; ++i)
		{
			if (i == LBE4ControlsManager::ButtonJoystickHatDown)	// 0x41 -> skip hat
			{
				i = LBE4ControlsManager::ButtonJoystickPinky;	// 0x45
			}
			controls->buttonGroup[i].Add(
				0x8000, receiver, config_message_id, receiver);
		}
		Check_Fpu();
	}

//
//#############################################################################
// RemoveTemporaryEventMappings                                     @004d18dc
//#############################################################################
//
	void
		L4MechControlsMapper::RemoveTemporaryEventMappings()
	{
		LBE4ControlsManager *controls =
			(LBE4ControlsManager*)application->GetControlsManager();

		for (int i = LBE4ControlsManager::FirstMappableButton;
				i <= LBE4ControlsManager::LastMappableButton; ++i)
		{
			if (i == LBE4ControlsManager::ButtonJoystickHatDown)
			{
				i = LBE4ControlsManager::ButtonJoystickPinky;
			}
			controls->buttonGroup[i].Remove(0x8000);
		}
		Check_Fpu();
	}

//
//#############################################################################
// NotifyOfControlModeChange / NotifyOfConfigurationModeChange
//                                                    @004d195c / @004d1964
//#############################################################################
//
// No-ops at the L4 layer (the gauges are driven directly off the mode masks),
// exactly like RP's L4VTVControlsMapper::NotifyOf* pair.
//
	void
		L4MechControlsMapper::NotifyOfControlModeChange(int /*new_mode*/)
	{
	}

	void
		L4MechControlsMapper::NotifyOfConfigurationModeChange(Logical /*new_state*/)
	{
	}

//
//#############################################################################
// KeypressMessageHandler                                           @004d1bf0
//#############################################################################
//
// The shared L4 keyboard dispatcher used by BOTH cockpits.  It handles the
// target-range zoom, the preset / control-group selection (number and letter
// keys, via SetPresetMode), pilot-target cycling (UpdateCurrentPilot /
// ChooseNearestPilot), control / display-mode cycling, eyepoint look keys, the
// joystick service keys, and a couple of fake-button developer shortcuts;
// anything else falls through to DispatchDeveloperFakeEvent.
//
	void
		L4MechControlsMapper::KeypressMessageHandler(
			ReceiverDataMessageOf<ControlsKey> *message
		)
	{
		switch (message->dataContents)
		{
		// ---- target range zoom ----
		case '+':	ZoomTargetRangeIn();   return;		// 0x2b
		case '-':	ZoomTargetRangeOut();  return;		// 0x2d

		// ---- group 0 presets (number row) ----
		case '1':	SetPresetMode(0, 1);   return;
		case '2':	SetPresetMode(0, 2);   return;
		case '3':	SetPresetMode(0, 3);   return;
		case '4':	SetPresetMode(0, 4);   return;
		case '5':	SetPresetMode(0, 0);   return;

		// ---- group 1 / group 2 presets (letter keys) ----
		case 'a':	SetPresetMode(1, 1);   return;
		case 'b':	SetPresetMode(2, 0);   return;
		case 'c':	SetPresetMode(2, 3);   return;
		case 'd':	SetPresetMode(1, 3);   return;
		case 'f':	SetPresetMode(1, 4);   return;
		case 'g':	SetPresetMode(1, 0);   return;
		case 's':	SetPresetMode(1, 2);   return;
		case 'v':	SetPresetMode(2, 4);   return;
		case 'x':	SetPresetMode(2, 2);   return;
		case 'z':	SetPresetMode(2, 1);   return;

		// ---- pilot target selection ----
		case 'w':	UpdateCurrentPilot(0); return;		// FUN_004b049c
		case 'e':	UpdateCurrentPilot(1); return;
		case 'r':	UpdateCurrentPilot(2); return;
		case 't':	UpdateCurrentPilot(3); return;
		case 'y':	UpdateCurrentPilot(4); return;
		case 'u':	UpdateCurrentPilot(5); return;
		case 'i':	UpdateCurrentPilot(6); return;
		case 'o':	UpdateCurrentPilot(7); return;
		// NOTE (engine drift): the recovered 'p' -> ChooseNearestPilot(0) shortcut
		// is dropped here because in the WinTesla keymap VK_F1 (PCK_F1) == 0x70 ==
		// 'p', which would collide with the joystick-align case below.  The F1/F2
		// joystick-service pair is kept; ChooseNearestPilot remains reachable via
		// the RIO Hotbox (button 8, see HotboxMessageHandler).

		// ---- fake-button developer shortcuts ----
		case 'j':	SendFakeButtonEventPair(GetMech(), "LRM10",       0xb); return;	// @0051e2e3
		case 'l':	SendFakeButtonEventPair(GetMech(), "Searchlight", 3);   return;	// @0051e2d7

		// ---- joystick service / mode cycling ----
		case PCK_F1:	((LBE4ControlsManager*)application->GetControlsManager())
							->BeginAlignJoystick();		// FUN_00476e34
						return;
		case PCK_F2:	((LBE4ControlsManager*)application->GetControlsManager())
							->EndAlignJoystick();		// FUN_00476e54
						return;
		case 0x13d:		// cycle control mode
			{
				ReceiverDataMessageOf<ControlsButton>
					m(0, sizeof(ReceiverDataMessageOf<ControlsButton>), 1);
				CycleControlModeMessageHandler(&m);		// FUN_004afbe0
			}
			return;
		case 0x13e:		// cycle display mode
			{
				ReceiverDataMessageOf<ControlsButton>
					m(0, sizeof(ReceiverDataMessageOf<ControlsButton>), 1);
				CycleDisplayModeMessageHandler(&m);		// FUN_004afcac
			}
			return;

		// ---- eyepoint look keys ----
		case PCK_UP:		lookForward = 1; lookLeft = -1; lookRight = -1; lookBehind = -1; lookDown = -1; return;	// 0x148
		case PCK_LEFT:		lookForward =-1; lookLeft =  1; lookRight = -1; lookBehind = -1; lookDown = -1; return;	// 0x14b
		case PCK_RIGHT:		lookForward =-1; lookLeft = -1; lookRight =  1; lookBehind = -1; lookDown = -1; return;	// 0x14d
		case PCK_DOWN:		lookForward =-1; lookLeft = -1; lookRight = -1; lookBehind =  1; lookDown = -1; return;	// 0x150
		case PCK_CTL_UP:	lookForward =-1; lookLeft = -1; lookRight = -1; lookBehind = -1; lookDown =  1; return;	// 0x18d
		}

		//
		// Everything else -> developer fake-event dispatcher.
		//
		DispatchDeveloperFakeEvent(this, (ControlsKeyMessage*)message);	// FUN_004d141c
	}


//#############################################################################
//#############################################################################
//						MechThrustmasterMapper
//#############################################################################
//#############################################################################

//#############################################################################
// Shared Data Support
//
MechThrustmasterMapper::SharedData
	MechThrustmasterMapper::DefaultData(
		MechThrustmasterMapper::GetClassDerivations(),
		MechThrustmasterMapper::MessageHandlers,
		MechThrustmasterMapper::GetAttributeIndex(),
		MechThrustmasterMapper::StateCount
	);

Derivation*
	MechThrustmasterMapper::GetClassDerivations()		// @0051dc78
{
	static Derivation classDerivations(
		L4MechControlsMapper::GetClassDerivations(),
		"MechThrustmasterMapper"						// @0051e2b3
	);
	return &classDerivations;
}

//#############################################################################
// Messaging Support
//
const MechThrustmasterMapper::HandlerEntry
	MechThrustmasterMapper::MessageHandlerEntries[]=
{
	MESSAGE_ENTRY(MechThrustmasterMapper, Keypress)		// id 0x17 -> L4 @004d1bf0
};

MechThrustmasterMapper::MessageHandlerSet
	MechThrustmasterMapper::MessageHandlers(
		ELEMENTS(MechThrustmasterMapper::MessageHandlerEntries),
		MechThrustmasterMapper::MessageHandlerEntries,
		L4MechControlsMapper::GetMessageHandlers()
	);

//
//#############################################################################
// MechThrustmasterMapper                                           @004d21d0
//#############################################################################
//
	MechThrustmasterMapper::MechThrustmasterMapper(
		Mech				*owner,
		int					subsystem_ID,
		SubsystemResource	*subsystem_resource,
		ClassID				class_ID,
		SharedData			&shared_data
	):
		L4MechControlsMapper(
			owner,
			subsystem_ID,
			subsystem_resource,
			class_ID,
			shared_data
		)
	{
		SetPerformance(&MechThrustmasterMapper::InterpretControls);	// this[7..9] = @004d2150

		throttleForward	= 0;		// this[0x6b] @0x1ac
		throttleReverse	= 0;		// this[0x6c] @0x1b0
		leftPedal		= 0;		// this[0x6d] @0x1b4
		rightPedal		= 0;		// this[0x6e] @0x1b8

		LBE4ControlsManager *controls =
			(LBE4ControlsManager*)application->GetControlsManager();	// DAT_004efc94+0x3c

		//
		// Analog throttle axis, reverse-throttle button, and the PC keyboard.
		// (ModeManager::ModeAlwaysActive == -1.)  Engine drift: the recovered
		// "ScalarThrottleReverse" scalar group does not exist in WinTesla; the
		// reverse throttle is the ButtonThrottle1 button (as in the era's
		// L4MPPR.CPP), so throttleReverse is staged from that button.
		//
		controls->scalarGroup[LBE4ControlsManager::ScalarThrottle]
			.Add(ModeManager::ModeAlwaysActive, &throttleForward, this);
		controls->buttonGroup[LBE4ControlsManager::ButtonThrottle1]
			.Add(ModeManager::ModeAlwaysActive, &throttleReverse, this);
		controls->keyboardGroup[LBE4ControlsManager::KeyboardPC]
			.Add(ModeManager::ModeAlwaysActive, this, KeypressMessageID, this);	// msg 0x17

		Check_Fpu();
	}

//
//#############################################################################
// ~MechThrustmasterMapper                                          @004d22b4
//#############################################################################
//
	MechThrustmasterMapper::~MechThrustmasterMapper()
	{
	}

//
//#############################################################################
// TestInstance                                                     @004d22e0
//#############################################################################
//
	Logical
		MechThrustmasterMapper::TestInstance() const
	{
		return IsDerivedFrom(*GetClassDerivations());	// FUN_0041a1a4(.,0x51dc78)
	}

//
//#############################################################################
// InterpretControls                                                @004d2150
//#############################################################################
//
// Reduce the throttle/reverse axes to the Mech's throttlePosition (@0x11c) and
// reverseThrust (@0x124), sum the pedals into pedalsPosition (@0x120), then run
// the L4 performance.
//
	void
		MechThrustmasterMapper::InterpretControls(Scalar time_slice)
	{
		if (throttleForward < 1)
		{
			if (throttleReverse < 1)
			{
				reverseThrust	= 0;
				throttlePosition	= 0.0f;
			}
			else
			{
				throttlePosition	= 1.0f;
				reverseThrust	= 1;
			}
		}
		else
		{
			throttlePosition	= 1.0f;
			reverseThrust	= 0;
		}

		pedalsPosition = (Scalar)(leftPedal - rightPedal);	// @0x120

		L4MechControlsMapper::InterpretControls(time_slice);	// FUN_004d196c
		Check_Fpu();
	}


//#############################################################################
//#############################################################################
//						MechRIOMapper
//#############################################################################
//#############################################################################

//#############################################################################
// Shared Data Support
//
MechRIOMapper::SharedData
	MechRIOMapper::DefaultData(
		MechRIOMapper::GetClassDerivations(),
		MechRIOMapper::MessageHandlers,
		MechRIOMapper::GetAttributeIndex(),
		MechRIOMapper::StateCount
	);

Derivation*
	MechRIOMapper::GetClassDerivations()				// @0051dd00
{
	static Derivation classDerivations(
		L4MechControlsMapper::GetClassDerivations(),
		"MechRIOMapper"									// @0051e2e3
	);
	return &classDerivations;
}

//#############################################################################
// Messaging Support
//
// The aux quadrant buttons each select a control-group preset via SetPresetMode
// (group = quadrant 0..2, item = engine 0..4).  ZoomIn / ZoomOut drive the
// target-range exponent; Hotbox selects a pilot/target.  (Table @0051dd30.)
//
const MechRIOMapper::HandlerEntry
	MechRIOMapper::MessageHandlerEntries[]=
{
	MESSAGE_ENTRY(MechRIOMapper, Aux1Quad),  MESSAGE_ENTRY(MechRIOMapper, Aux1Eng1),
	MESSAGE_ENTRY(MechRIOMapper, Aux1Eng2),  MESSAGE_ENTRY(MechRIOMapper, Aux1Eng3),
	MESSAGE_ENTRY(MechRIOMapper, Aux1Eng4),
	MESSAGE_ENTRY(MechRIOMapper, Aux2Quad),  MESSAGE_ENTRY(MechRIOMapper, Aux2Eng1),
	MESSAGE_ENTRY(MechRIOMapper, Aux2Eng2),  MESSAGE_ENTRY(MechRIOMapper, Aux2Eng3),
	MESSAGE_ENTRY(MechRIOMapper, Aux2Eng4),
	MESSAGE_ENTRY(MechRIOMapper, Aux3Quad),  MESSAGE_ENTRY(MechRIOMapper, Aux3Eng1),
	MESSAGE_ENTRY(MechRIOMapper, Aux3Eng2),  MESSAGE_ENTRY(MechRIOMapper, Aux3Eng3),
	MESSAGE_ENTRY(MechRIOMapper, Aux3Eng4),
	MESSAGE_ENTRY(MechRIOMapper, ZoomIn),    MESSAGE_ENTRY(MechRIOMapper, ZoomOut),
	MESSAGE_ENTRY(MechRIOMapper, Hotbox)
};

MechRIOMapper::MessageHandlerSet
	MechRIOMapper::MessageHandlers(
		ELEMENTS(MechRIOMapper::MessageHandlerEntries),
		MechRIOMapper::MessageHandlerEntries,
		L4MechControlsMapper::GetMessageHandlers()
	);

//
//#############################################################################
// Aux quadrant handlers                              @004d22fc .. @004d24bc
//#############################################################################
//
// Fifteen near-identical "press a quadrant button -> select a preset" handlers.
// Each fires on press only (dataContents > 0) and calls SetPresetMode(group,item).
//
#define RIO_AUX_HANDLER(method, grp, itm)								\
	void																\
		MechRIOMapper::method##MessageHandler(							\
			ReceiverDataMessageOf<ControlsButton> *message)			\
	{																	\
		if (message->dataContents > 0) SetPresetMode((grp), (itm));	\
	}

RIO_AUX_HANDLER(Aux1Quad, 0, 0)	// @004d22fc
RIO_AUX_HANDLER(Aux1Eng1, 0, 1)	// @004d231c
RIO_AUX_HANDLER(Aux1Eng2, 0, 2)	// @004d233c
RIO_AUX_HANDLER(Aux1Eng3, 0, 3)	// @004d235c
RIO_AUX_HANDLER(Aux1Eng4, 0, 4)	// @004d237c
RIO_AUX_HANDLER(Aux2Quad, 1, 0)	// @004d239c
RIO_AUX_HANDLER(Aux2Eng1, 1, 1)	// @004d23bc
RIO_AUX_HANDLER(Aux2Eng2, 1, 2)	// @004d23dc
RIO_AUX_HANDLER(Aux2Eng3, 1, 3)	// @004d23fc
RIO_AUX_HANDLER(Aux2Eng4, 1, 4)	// @004d241c
RIO_AUX_HANDLER(Aux3Quad, 2, 0)	// @004d243c
RIO_AUX_HANDLER(Aux3Eng1, 2, 1)	// @004d245c
RIO_AUX_HANDLER(Aux3Eng2, 2, 2)	// @004d247c
RIO_AUX_HANDLER(Aux3Eng3, 2, 3)	// @004d249c
RIO_AUX_HANDLER(Aux3Eng4, 2, 4)	// @004d24bc

#undef RIO_AUX_HANDLER

//
//#############################################################################
// ZoomIn / ZoomOut handlers                          @004d24dc / @004d24f8
//#############################################################################
//
	void
		MechRIOMapper::ZoomInMessageHandler(
			ReceiverDataMessageOf<ControlsButton> *message)
	{
		if (message->dataContents > 0) ZoomTargetRangeIn();		// FUN_004d1b64
	}

	void
		MechRIOMapper::ZoomOutMessageHandler(
			ReceiverDataMessageOf<ControlsButton> *message)
	{
		if (message->dataContents > 0) ZoomTargetRangeOut();	// FUN_004d1b9c
	}

//
//#############################################################################
// HotboxMessageHandler                                             @004d2574
//#############################################################################
//
// The eight "hotbox" panel buttons select a pilot/target.  The pressed button
// is matched against the hotbox button table @0051dcd0; its ordinal selects a
// roster page (UpdateCurrentPilot), or -- for the last button -- the nearest
// living pilot (ChooseNearestPilot).
//
	void
		MechRIOMapper::HotboxMessageHandler(
			ReceiverDataMessageOf<ControlsButton> *message)
	{
		static const int hotbox_button[8] =			// @0051dcd0
			{ 0x37, 0x36, 0x35, 0x34, 0x33, 0x32, 0x31, 0x30 };

		if (message->dataContents > 0)
		{
			for (int i = 0; i < 8; ++i)
			{
				if (message->dataContents - 1 == hotbox_button[i])
				{
					if (i < 7)
					{
						UpdateCurrentPilot(i + 1);		// FUN_004b049c
					}
					else
					{
						ChooseNearestPilot(0);			// FUN_004b04d8
					}
					return;
				}
			}
		}
	}

//
//#############################################################################
// AddOrErase (event / direct)                        @004d25e8 / @004d262c
//#############################################################################
//
// During configuration these bind the just-pressed button (1-based ID) either
// to a receiver+message or to a direct ControlsButton sink, under the base
// (ModeBasic, 0x10000) mode mask.  Mirrors RP VTVRIOMapper::AddOrErase.
//
	void
		MechRIOMapper::AddOrErase(
			unsigned int button_ID,
			Receiver *target,
			Receiver::MessageID message_ID
		)
	{
		LBE4ControlsManager *controls =
			(LBE4ControlsManager*)application->GetControlsManager();
		controls->buttonGroup[button_ID - 1]
			.AddOrErase(0x10000, target, message_ID, this);		// vtbl+0x1c
		Check_Fpu();
	}

	void
		MechRIOMapper::AddOrErase(
			unsigned int button_ID,
			ControlsButton *destination
		)
	{
		LBE4ControlsManager *controls =
			(LBE4ControlsManager*)application->GetControlsManager();
		controls->buttonGroup[button_ID - 1]
			.AddOrErase(0x10000, destination, this);			// vtbl+0x20
		Check_Fpu();
	}

//
//#############################################################################
// MechRIOMapper                                                    @004d266c
//#############################################################################
//
	MechRIOMapper::MechRIOMapper(
		Mech				*owner,
		int					subsystem_ID,
		SubsystemResource	*subsystem_resource,
		ClassID				class_ID,
		SharedData			&shared_data
	):
		L4MechControlsMapper(
			owner,
			subsystem_ID,
			subsystem_resource,
			class_ID,
			shared_data
		)
	{
		SetPerformance(&MechRIOMapper::InterpretControls);	// this[7..9] = @004d25c0

		throttleForward	= 0;		// this[0x6b] @0x1ac
		throttleReverse	= 0;		// this[0x6c] @0x1b0
		leftPedal		= 0;		// this[0x6d] @0x1b4
		rightPedal		= 0;		// this[0x6e] @0x1b8

		LBE4ControlsManager *controls =
			(LBE4ControlsManager*)application->GetControlsManager();	// DAT_004efc94+0x3c

		//
		// Keyboard, pedal axes, and the panel-wide preset-mode keyboard hook.
		// Engine drift: the recovered buttonGroup[ButtonLeftPedal/RightPedal]
		// slots do not exist; the pedals are scalar axes (ScalarLeftPedal /
		// ScalarRightPedal), as in the era's L4MPPR.CPP.  KeyboardPanel maps to
		// the pilot's internal keypad (KeyboardPilot); 0x200000 is the panel
		// preset mode mask (the recovered "ModePreset1").
		//
		controls->keyboardGroup[LBE4ControlsManager::KeyboardPC]
			.Add(ModeManager::ModeAlwaysActive, this, KeypressMessageID, this);	// msg 0x19
		controls->scalarGroup[LBE4ControlsManager::ScalarLeftPedal]
			.Add(ModeManager::ModeAlwaysActive, &leftPedal, this);
		controls->scalarGroup[LBE4ControlsManager::ScalarRightPedal]
			.Add(ModeManager::ModeAlwaysActive, &rightPedal, this);
		controls->keyboardGroup[LBE4ControlsManager::KeyboardPilot]
			.Add(0x200000, owner, KeypressMessageID, this);			// mode 0x200000

		// configuration-control lamp (button 0x3d == ButtonPanic in this engine)
		Lamp *config_lamp =
			controls->MakeLinkedLamp(LBE4ControlsManager::ButtonPanic, 0x200000);	// FUN_00476fc0(.,0x3d,0x200000)
		if (config_lamp != NULL)
		{
			config_lamp->SetState(Lamp::LampStateOn);				// FUN_00444e64
		}

		//
		// The eight hotbox / preset buttons -> Hotbox handler, each with a lamp.
		//
		static const int hotbox_button[8] =				// @0051dcd0
			{ 0x37, 0x36, 0x35, 0x34, 0x33, 0x32, 0x31, 0x30 };

		for (int i = 0; i < 8; ++i)
		{
			controls->buttonGroup[hotbox_button[i]]
				.Add(ModeManager::ModeAlwaysActive, this, HotboxMessageID, this);	// msg 0x1a
			controls->MakeLinkedLamp(hotbox_button[i], ModeManager::ModeAlwaysActive);	// FUN_00476fc0(.,btn,-1)
		}

		Check_Fpu();
	}

//
//#############################################################################
// ~MechRIOMapper                                                   @004d27cc
//#############################################################################
//
	MechRIOMapper::~MechRIOMapper()
	{
	}

//
//#############################################################################
// TestInstance                                                     @004d27f8
//#############################################################################
//
	Logical
		MechRIOMapper::TestInstance() const
	{
		return IsDerivedFrom(*GetClassDerivations());	// FUN_0041a1a4(.,0x51dd00)
	}

//
//#############################################################################
// InterpretControls                                                @004d25c0
//#############################################################################
//
// The RIO simply sums the pedal buttons into pedalsPosition (@0x120); throttle
// is handled elsewhere.  Then it runs the L4 performance.
//
	void
		MechRIOMapper::InterpretControls(Scalar time_slice)
	{
		pedalsPosition = (Scalar)(leftPedal - rightPedal);	// @0x120

		L4MechControlsMapper::InterpretControls(time_slice);	// FUN_004d196c
		Check_Fpu();
	}
