Files
BT411/game/reconstructed/btl4mppr.cpp
T
arcattackandClaude Fable 5 6783619069 Gitea #9: the upper-MFD PRESET pages (3 MFDs x 5) live -- SetPresetMode
table re-decoded to the ModeMFD bits + desktop J/K/L page cycle

The preset system was unwired by ONE defect in the message layer: the
SetPresetMode @004d1b24 table @0051dbf0 had been transcribed from the
section dump as BIG-endian dwords ({0x1e,0x01000000} instead of
{0x1e,0x01}), so a preset press set a garbage high bit -- and for group 1
items 3-4 / group 2 items 0-2 stomped the LIVE NonMapping / Intercom /
ModeSecondary* bits -- while the real page bits never moved.  Ground
truth (section_dump.txt:72901-72908, little-endian + BTL4MODE.HPP [T0]):
set = ModeMFD{1,2,3}{Quad,Eng1-4} = 1<<(group*5+item), bits 0-14, fully
disjoint from the #6 secondary trio (bits 18-20); group 2 is MFD3, NOT a
duplicate of the secondary views.

What the 15 presets show (l4gauge.cfg): group = the MFD (Mfd1 lower left
/ Mfd2 upper center / Mfd3 lower right), item 0 = the btquad.pcx Quad
overview (up to 4 vehicleSubSystems cluster panels), items 1-4 = the
full-screen engineering-detail pages (bteng.pcx + prepEngr screens
group*4+1..4 + the cluster eng children: GENERATOR SELECT A-D, POWER
graph, COOLING loop, DAMAGE, ammo).  Empty screens are authored per mech
(Blackhawk: 3/11/12 empty).

Authentic dispatch (streamed "L4" .CTL EventMappings, BT_CTRLMAP_LOG
dump): each MFD owns the 8-button RIO bank around it, MODE-MASK-gated --
Mfd1 = 0x08-0x0F, Mfd2 = 0x20-0x27, Mfd3 = 0x00-0x07.  Quad page ->
direct-select buttons for the POPULATED eng pages (mapper msgs
Aux1Eng1-4 0x4-0x7 / Aux2* 0x9-0xC / Aux3* 0xE-0x11); eng page -> one
back-to-Quad button (0x3/0x8/0xD) + per-subsystem controls.  These
records already install and fire on desktop (btinput passes the live
manager mask), so the NUMPAD profile's 0x20-0x27 keys page MFD2
authentically.

Port wiring (the #6 pattern): keys J/K/L -> actions Mfd1/2/3Cycle ->
gBTPresetCycle -> L4MechControlsMapper::CyclePresetModeNow(group) -- a
documented desktop shim (24 mode-dependent pod buttons don't fit a
keyboard) that cycles Quad -> populated Eng pages -> Quad, visiting
exactly the pod-reachable set; the body is the authentic SetPresetMode.

Dev-composite: BTDrawGaugeSurfaces now draws the Eng1-3 planes at their
sibling cells and skips any mono plane whose channel is BlankColor,
honoring the mode-driven reconfigure (RemapGraphicsPort) -- each dev
cell shows the ACTIVE page like the pod monitor.  This supersedes and
removes the 2026-07-12 GAUGREND "frozen-dial" scaffold (forced all 15
page bits active under BT_DEV_GAUGES; it pinned the shared Eng plane on
the highest screen and ate the page flips).

Pixel-verified (BT_PRESET_TEST + BT_DEV_GAUGES_DOCK + BT_SHOT,
Blackhawk): all three MFDs page Quad -> SYSTEM NN eng details -> back to
Quad in lockstep with the [mode] preset mask log; group 0 skips the
authored-empty screen 3, group 2 skips 11/12.  Un-regressed: N display
cycle (0x450421->0x490421->0x510421, page bits intact), M control mode,
CONTROLS.MAP 52 bindings parse clean.

Diags: BT_MODE_LOG ([mode] preset), BT_PRESET_TEST=<frame>.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 17:30:35 -05:00

1404 lines
53 KiB
C++

//===========================================================================//
// 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
// @004d1acc NotifyOfControlModeChange (+0x48; forwards to base no-op @004b048c)
// @004d1ae4 NotifyOfDisplayModeChange (+0x4C; the secondary-view mask swap
// -- Gitea #6 relabel; ex-"SetControlMode")
// @004d1b24 SetPresetMode (group/item -> preset mode-mask)
// @004d1840 EnterConfiguration @004d18dc ExitConfiguration
// @004d195c/@004d1964 AddOrErase evt/dir (+0x40/+0x44, RET no-ops)
// @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} secondary-VIEW mode-masks
// (ModeSecondaryDamage/Critical/Heat, indexed by displayMode)
// DAT_0051dbf0[15] = {clear,set} mode-mask pairs (group*5+item), preset select
// (little-endian: set = ModeMFD{1,2,3}{Quad,Eng1-4} bits 0-14
// -- Gitea #9; see SetPresetMode below)
// 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
#include <new> // placement-new (the '\' fake-event padded message)
#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
{
// STACK-OVERWRITE FIX (tester "crash" hunt, 2026-07-17):
// Entity::Dispatch STAMPS entityID/interestZoneID into the message
// at Entity::Message offsets (ENTITY.cpp:236) -- dispatching a bare
// ReceiverDataMessageOf<ControlsButton> at an ENTITY writes past
// the stack object and corrupts the frame (the '\' key was an
// instant wild-jump crash; the 1995 build survived the identical
// overwrite on stack-layout luck). Give the message
// Entity::Message-sized backing so the stamp lands in owned
// memory; messageID/messageLength semantics are unchanged.
double storage[
(sizeof(Entity::Message) + sizeof(ReceiverDataMessageOf<ControlsButton>))
/ sizeof(double) + 1];
ReceiverDataMessageOf<ControlsButton> *press =
new ((void *)storage) ReceiverDataMessageOf<ControlsButton>(
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
//
// TargetRangeExponent -> the live @0x1a4 zoom member (AUDIO_FIDELITY.md F20):
// the authored AudioScalarDeltaTrigger blips per zoom step; it bound NULL until
// this publication. Chained to MechControlsMapper's dense index.
const L4MechControlsMapper::IndexEntry
L4MechControlsMapper::AttributePointers[]=
{
ATTRIBUTE_ENTRY(L4MechControlsMapper, TargetRangeExponent, targetRangeExponent) // @0x1a4
};
L4MechControlsMapper::AttributeIndexSet&
L4MechControlsMapper::GetAttributeIndex()
{
static L4MechControlsMapper::AttributeIndexSet attributeIndex(
ELEMENTS(L4MechControlsMapper::AttributePointers),
L4MechControlsMapper::AttributePointers,
MechControlsMapper::GetAttributeIndex()
);
return attributeIndex;
}
L4MechControlsMapper::SharedData
L4MechControlsMapper::DefaultData(
L4MechControlsMapper::GetClassDerivations(),
L4MechControlsMapper::GetMessageHandlers(),
L4MechControlsMapper::GetAttributeIndex(), // own table -> chained to 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.
//
// NB: the Abs() macro (STYLE.H:118) is unparenthesized -- Abs(throttlePosition
// - 1.0f) mis-expands to -(throttlePosition + 1.0f) on the false branch (always,
// since throttle <= 1), which is <= 0.05 UNCONDITIONALLY -> the detent used to
// snap throttle to full every frame. Diff into a temp so the macro sees one token.
const Scalar _thrOff = throttlePosition - 1.0f;
if (((_thrOff < 0.0f) ? -_thrOff : _thrOff) <= 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);
}
}
//
// (3b) PORT (Gitea #9): the desktop J/K/L per-MFD preset-page cycle
// edges (mech4 poll) -- the sender for the pod's per-MFD page-button
// banks; the body is the authentic SetPresetMode.
//
{
extern int gBTPresetCycle[3];
for (int mfd_group = 0; mfd_group < 3; ++mfd_group)
{
if (gBTPresetCycle[mfd_group])
{
gBTPresetCycle[mfd_group] = 0;
CyclePresetModeNow(mfd_group);
}
}
}
//
// (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;
}
}
//
//#############################################################################
// NotifyOfControlModeChange @004d1acc (vtbl+0x48)
// NotifyOfDisplayModeChange @004d1ae4 (vtbl+0x4C)
//#############################################################################
//
// Gitea #6 RELABEL (2026-07-19). The old reconstruction carried @004d1ae4 as
// a non-virtual "SetControlMode" that nothing ever called, so the desktop's
// secondary MFD stayed pinned on the Damage view. The vtable @0051e440
// (section_dump.txt:73033) pins the truth:
//
// +0x48 = @004d1acc -- dispatched by CycleControlModeMessageHandler
// (FUN_004afbe0) with the new controlMode. Capstone disasm: pushes
// its two args and forwards to the BASE no-op @004b048c. A
// control-mode (BAS/MID/ADV) change never touches the secondary
// view -- in the shipped binary OR here.
// +0x4C = @004d1ae4 -- dispatched by CycleDisplayModeMessageHandler
// (FUN_004afcac) with the new displayMode. THE secondary-view
// selector: clears mode bits 18..20 (& 0xffe3ffff =
// ~ModeSecondaryAll), sets the mask picked from the table @0051dbe4
// = {ModeSecondaryDamage 0x40000, ModeSecondaryCritical 0x80000,
// ModeSecondaryHeat 0x100000}, then chains the base no-op
// @004b0494. displayMode 0/1/2 = Damage/Critical/Heat -- the
// manual-p13 "'Mech status Info center" schematic cycle (bottom
// left of the secondary screen).
//
void
L4MechControlsMapper::NotifyOfControlModeChange(int new_mode)
{
MechControlsMapper::NotifyOfControlModeChange(new_mode); // @004b048c (no-op)
}
void
L4MechControlsMapper::NotifyOfDisplayModeChange(int new_mode)
{
static const ModeMask display_mode_mask[] = // @0051dbe4
{
BTL4ModeManager::ModeSecondaryDamage, // 0 -> 0x040000
BTL4ModeManager::ModeSecondaryCritical, // 1 -> 0x080000
BTL4ModeManager::ModeSecondaryHeat // 2 -> 0x100000
};
BTL4ModeManager *mode_manager =
(BTL4ModeManager*)application->GetModeManager(); // DAT_004efc94+0x50
mode_manager->RemoveModeMask( // clear bits 18..20
BTL4ModeManager::ModeSecondaryAll); // (& 0xffe3ffff)
mode_manager->AddModeMask(display_mode_mask[new_mode]);
// DIAG (BT_MODE_LOG): the resulting manager mask -- proves the
// secondary-view bit actually flipped for the gauge layer gating.
if (getenv("BT_MODE_LOG"))
DEBUG_STREAM << "[mode] display notify " << new_mode
<< " -> manager mask 0x" << std::hex
<< mode_manager->GetModeMask() << std::dec << "\n" << std::flush;
MechControlsMapper::NotifyOfDisplayModeChange(new_mode); // @004b0494 (no-op)
}
//
//#############################################################################
// SetPresetMode @004d1b24
//#############################################################################
//
// Each (group,item) selects an upper-MFD PRESET page; the table @0051dbf0
// holds {clear,set} mode-mask pairs that remove the previous preset of that
// group and enable the chosen one. Group = the MFD (0/1/2 = Mfd1 lower-left /
// Mfd2 upper-center / Mfd3 lower-right); item 0 = the Quad overview page
// (four subsystem cluster panels), items 1-4 = the four full-screen
// engineering-detail pages (aux screens group*4+1..group*4+4, prepEngr).
//
// Gitea #9 TABLE FIX (2026-07-19): the set column had been transcribed from
// the section dump as BIG-endian dwords ({0x1e,0x01000000} instead of
// {0x1e,0x01}, etc.) -- so a preset press set a garbage high bit (and, for
// group 1 items 3-4 / group 2 items 0-2, stomped the LIVE ModeNonMapping /
// ModeIntercom / ModeSecondary* bits) while the ModeMFD* page bits never
// moved: the panels stayed pinned on their Quad pages. Ground truth
// (section_dump.txt:72901-72908, little-endian) decodes the table to exactly
// the BTL4MODE.HPP [T0] ModeMFD{1,2,3}{Quad,Eng1-4} allocation, bits 0-14:
// set = 1 << (group*5 + item); clear = the group's other four page bits
// (item 0) or all five (items 1-4). The presets are fully disjoint from the
// Mapping/NonMapping (15/16), Intercom (17) and Secondary* (18-20) bits.
//
void
L4MechControlsMapper::SetPresetMode(int group, int item)
{
struct PresetMaskPair { ModeMask clear, set; };
static const PresetMaskPair preset_mask[15] = // @0051dbf0 (group*5+item)
{
// group 0 = Mfd1 (lower left)
{0x001e, BTL4ModeManager::ModeMFD1Quad}, // 0x0001
{0x001f, BTL4ModeManager::ModeMFD1Eng1}, // 0x0002
{0x001f, BTL4ModeManager::ModeMFD1Eng2}, // 0x0004
{0x001f, BTL4ModeManager::ModeMFD1Eng3}, // 0x0008
{0x001f, BTL4ModeManager::ModeMFD1Eng4}, // 0x0010
// group 1 = Mfd2 (upper center)
{0x03c0, BTL4ModeManager::ModeMFD2Quad}, // 0x0020
{0x03e0, BTL4ModeManager::ModeMFD2Eng1}, // 0x0040
{0x03e0, BTL4ModeManager::ModeMFD2Eng2}, // 0x0080
{0x03e0, BTL4ModeManager::ModeMFD2Eng3}, // 0x0100
{0x03e0, BTL4ModeManager::ModeMFD2Eng4}, // 0x0200
// group 2 = Mfd3 (lower right)
{0x7800, BTL4ModeManager::ModeMFD3Quad}, // 0x0400
{0x7c00, BTL4ModeManager::ModeMFD3Eng1}, // 0x0800
{0x7c00, BTL4ModeManager::ModeMFD3Eng2}, // 0x1000
{0x7c00, BTL4ModeManager::ModeMFD3Eng3}, // 0x2000
{0x7c00, BTL4ModeManager::ModeMFD3Eng4} // 0x4000
};
if (group < 0 || group > 2 || item < 0 || item > 4)
{
return;
}
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);
// DIAG (BT_MODE_LOG): prove the ModeMFD* page bit actually flipped for
// the gauge reconfigure/prepEngr layer gating.
if (getenv("BT_MODE_LOG"))
DEBUG_STREAM << "[mode] preset (" << group << "," << item
<< ") -> manager mask 0x" << std::hex
<< mode_manager->GetModeMask() << std::dec << "\n" << std::flush;
}
//
//#############################################################################
// CyclePresetModeNow (PORT desktop sender -- Gitea #9, no binary analog)
//#############################################################################
//
// On the pod every MFD has its own 8-button bank (Mfd1 lower-left = RIO
// buttons 0x08-0x0F, Mfd2 upper-center = 0x20-0x27, Mfd3 lower-right =
// 0x00-0x07) whose meanings are MODE-MASK-gated by the streamed "L4" .CTL
// EventMappings: on a Quad page the bank's outer buttons DIRECT-SELECT the
// populated Eng pages (mapper messages Aux1Eng1..Aux3Eng4 = 0x4-0x7 / 0x9-0xC
// / 0xE-0x11 -> SetPresetMode), on an Eng page one button returns to Quad
// (Aux1Quad/Aux2Quad/Aux3Quad = 0x3/0x8/0xD) and the rest drive the shown
// subsystem. A desktop keyboard cannot host 24 mode-dependent buttons, so
// the port cycles each MFD with one key (J/K/L -> mech4 edge poll ->
// gBTPresetCycle -> here); the message BODY is the authentic SetPresetMode.
// The cycle visits exactly the pages the pod's streamed buttons could reach:
// Quad always, an Eng page only if a roster subsystem is assigned to that
// aux screen (screens group*4+1 .. group*4+4; the shipped .CTL carries NO
// select button for an unpopulated screen -- e.g. the Blackhawk's screens
// 3, 11 and 12 are authored empty).
//
// The vehicleSubSystems/prepEngr aux-screen bridge (powersub.cpp).
extern bool BTGetSubsystemAuxScreen(
Subsystem *sub, int *screen, int *placement, char *label64);
void
L4MechControlsMapper::CyclePresetModeNow(int group)
{
if (group < 0 || group > 2)
{
return;
}
BTL4ModeManager *mode_manager =
(BTL4ModeManager*)application->GetModeManager();
//
// Current page = the set bit among the group's five ModeMFD* bits.
//
ModeMask mask = mode_manager->GetModeMask();
int current = 0; // default: Quad
for (int i = 0; i < 5; ++i)
{
if (mask & (1 << (group * 5 + i)))
{
current = i;
break;
}
}
//
// Which aux screens are populated on this mech (streamed sub+0x1dc)?
//
Mech *mech = GetMech();
bool populated[5] = { true, false, false, false, false }; // Quad always
if (mech != NULL)
{
int count = mech->GetSubsystemCount();
for (int i = 1; i < count; ++i)
{
Subsystem *sub = mech->GetSubsystem(i);
if (sub == NULL)
{
continue;
}
int screen = 0, placement = -1;
if (!BTGetSubsystemAuxScreen(sub, &screen, &placement, NULL))
{
continue;
}
int item = screen - group * 4; // Eng item 1..4 of THIS group?
if (item >= 1 && item <= 4)
{
populated[item] = true;
}
}
}
//
// Advance to the next reachable page (wraps through Quad).
//
int next = current;
for (int step = 1; step <= 5; ++step)
{
int candidate = (current + step) % 5;
if (populated[candidate])
{
next = candidate;
break;
}
}
if (next != current)
{
SetPresetMode(group, next);
}
}
//
//#############################################################################
// EnterConfiguration @004d1840
//#############################################################################
//
// THE CONFIG-MODE SESSION OPEN (task #6; the old reconstruction named this
// "CreateTemporaryEventMappings" and carried only the fire-button loop --
// missing the mode flip, so config mode never actually ENGAGED). Called by
// MechWeapon handler id 9 (ConfigureMappables, @004b9550) when the pilot
// presses-and-HOLDS a weapon's configure button:
// 1. StartMappableButtonsConfigure (engine T0, ControlsManager vtbl+0x20):
// latches the active weapon (destination/receiver) for the ConfigMap
// gauge and flips the GLOBAL mode NonMapping(0x10000) -> Mapping(0x8000)
// -- every permanent fire mapping goes dormant for the session.
// 2. Re-arm the HELD configure button under 0x8000 so its RELEASE still
// reaches the weapon (-> ExitConfiguration).
// 3. Temp-map the four mappable fire buttons (0x40 Trigger, 0x45 Pinky,
// 0x46 ThumbLow, 0x47 ThumbHigh -- the 0x41..0x44 hat is skipped) to
// send ChooseButton (id 10) to the configuring weapon.
//
void
L4MechControlsMapper::EnterConfiguration(
int held_element,
ControlsButton *destination,
Receiver *receiver,
Receiver::MessageID choose_message_id,
Receiver::MessageID configure_message_id,
Receiver::MessageID active_message_id
)
{
LBE4ControlsManager *controls =
(LBE4ControlsManager*)application->GetControlsManager(); // DAT_004efc94+0x3c
controls->StartMappableButtonsConfigure( // vtbl+0x20 (CONTROLS.cpp:291)
destination, receiver, receiver, active_message_id,
/*remove*/ 0x10000 /*ModeNonMapping*/,
/*add*/ 0x8000 /*ModeMapping*/);
controls->buttonGroup[held_element].Add( // keep the held button live
0x8000, receiver, configure_message_id, receiver);
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, choose_message_id, receiver);
}
Check_Fpu();
}
//
//#############################################################################
// ExitConfiguration @004d18dc
//#############################################################################
//
// THE SESSION CLOSE (the configure button's RELEASE): restore the mode masks
// (Mapping -> NonMapping; the engine also clears the gauge's active* latch),
// then strip the 0x8000 temp layer off the held button + the four mappables.
// The regrouping itself needs no commit -- the RIO AddOrErase toggled the
// permanent 0x10000 mappings directly.
//
void
L4MechControlsMapper::ExitConfiguration(int held_element)
{
LBE4ControlsManager *controls =
(LBE4ControlsManager*)application->GetControlsManager();
controls->StopMappableButtonsConfigure( // vtbl+0x24 (CONTROLS.cpp:319)
/*remove*/ 0x8000, /*add*/ 0x10000);
controls->buttonGroup[held_element].Remove(0x8000);
for (int i = LBE4ControlsManager::FirstMappableButton;
i <= LBE4ControlsManager::LastMappableButton; ++i)
{
if (i == LBE4ControlsManager::ButtonJoystickHatDown)
{
i = LBE4ControlsManager::ButtonJoystickPinky;
}
controls->buttonGroup[i].Remove(0x8000);
}
Check_Fpu();
}
//
//#############################################################################
// AddOrErase (event / direct) -- the L4/Thrustmaster no-ops
// @004d195c / @004d1964
//#############################################################################
//
// task #6 CORRECTION: these two RET stubs occupy the +0x40/+0x44 AddOrErase
// vtable slots (section_dump @0051e440/@0051e3f0) -- the old reconstruction
// mislabeled them NotifyOfControlModeChange/NotifyOfConfigurationModeChange.
// A joystick (Thrustmaster) cockpit inherits them: pressing a fire button
// during a config session does NOTHING there. Only the pod RIO variant
// (@004d25e8/@004d262c) implements the group toggle.
//
void
L4MechControlsMapper::AddOrErase(
unsigned int /*button_ID*/,
Receiver * /*receiver*/,
Receiver::MessageID /*message_ID*/
)
{
}
void
L4MechControlsMapper::AddOrErase(
unsigned int /*button_ID*/,
ControlsButton * /*destination*/
)
{
}
//
//#############################################################################
// 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
)
{
// DIAG (BT_KEY_LOG): trace every key that reaches the cockpit keyboard
// dispatcher -- proves delivery + names the key in a crash repro.
if (getenv("BT_KEY_LOG"))
DEBUG_STREAM << "[keych] 0x" << std::hex << (int)message->dataContents
<< std::dec << "\n" << std::flush;
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
//#############################################################################
//
// THE REGROUP EDIT (task #6): during a config session these TOGGLE the
// just-pressed fire button's (1-based ID) permanent mapping under the
// ModeNonMapping (0x10000) mask -- the engine ControlsUpdateManager AddOrErase
// (CONTROLS.h:431-597) deletes a matching instance or creates one, so the
// weapon JOINS the button's fire group if absent and LEAVES it if present.
// The toggled 0x10000 instances ARE the commit; there is no save step.
// 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
if (getenv("BT_FIRE_LOG"))
{
int state = controls->buttonGroup[button_ID - 1].GetMapState(
destination, 0, 0, 0x10000);
DEBUG_STREAM << "[rio-map] toggle btn=0x" << std::hex << (button_ID - 1)
<< std::dec << " dest=" << (void *)destination
<< " -> " << (((state & 2) != 0) ? "JOINED" : "LEFT")
<< " (state=" << state << ")" << std::endl;
}
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();
}