Files
arcattackandClaude Fable 5 092408041c Cockpit: the CONFIG-MODE weapon-regrouping session + the live-play fire fixes (tasks #6+#11)
TASK #6 -- the pod's in-cockpit weapon regrouping, fully reconstructed and
live-verified (hold-configure -> tap fire button -> toggle membership ->
release-commit):
- MechWeapon handlers id 9 ConfigureMappables @004b9550 / id 10 ChooseButton
  @004b95b8 (binary table @0x511860; the old "Myomers fns" mislabel swept)
  + the GetMessageHandlers() accessor chain through Emitter/ProjectileWeapon/
  MissileLauncher/GAUSS/PPC (empty per-class sets swallowed dispatch).
- Mapper vtable truth: +0x38 EnterConfiguration / +0x3C ExitConfiguration /
  +0x40,+0x44 AddOrErase evt/dir.  No "secondary vtable @0050f498", no
  "CreateTemporaryEventMappings" virtual (RP-name drift; swept incl CLASSMAP).
  L4 Enter/Exit rebuilt complete: StartMappableButtonsConfigure (the
  NonMapping 0x10000 <-> Mapping 0x8000 mode flip + the gauge's active-weapon
  latch) + the held-button re-arm + the 4 fire-button temp maps; only the RIO
  mapper implements the toggle (Thrustmaster no-ops = can't regroup).
- MechSubsystem +0xE8/+0xEC corrected to controlDestination/controlMessageID
  (ex hostEntity/subsystemId2 mislabel); MechWeapon ctor defaults the
  destination to &fireImpulse (@004b99a8).
- ConfigMapGauge state loop reconstructed (PE-recovered DAT_00518eb4 table;
  buttonGroup GetMapState sampler -- the "needs ModeManager" guard rationale
  was wrong).  Finding [T1]: the shipped binary NEVER enables this gauge (no
  SetColor caller) -- authentically dormant; BT_CONFIGMAP=1 is the dev enable.
- Dev harness: HOLD 'G' opens the session (BT_CONFIG_SLOT picks the weapon);
  BT_CONFIG_TEST scripts a headless verify.  @004afbc4 corrected to its real
  Fail-trap body (the guessed AddOrErase(NULL) body would corrupt groups).
- Bonus [T1]: the binary MechWeapon ATTRIBUTE table has ELEVEN entries
  (PercentDone..WeaponState) -- "TriggerState is the only one" was wrong.

TASK #11 -- the user-reported live-play regressions (both real bugs):
- PHANTOM FIRE: the mech4 bring-up shot block painted an explosion at the
  victim on its OWN 0.3s cadence whenever fire was held -- desynced once the
  authentic recharges landed.  Retired (the real impact visual flows from
  each discharge via the messmgr SubmitExplosion).
- WEAPON BRICKING (the "cuts out" bug): the Loading->Loaded snap window
  (+-0.01 around seekV) assumes the pod's LOCKED 60 fps; one port dt-spike
  jumps it, the byte-verified >1.0 clamp (_DAT_004ba830=0) zeroes readiness,
  and the weapon sticks in Loading at level ~10000 forever (observed live).
  Fixed with pod-frame (1/60s) sub-stepping of the binary's own Loading tick
  -- also fixes the I^2R integral over-heating generators on big steps.
  NEW GOTCHA CLASS recorded: reconstruction-gotchas #12 (frame-pacing trap).
- Duty-cycle measured (max-rate autofire): generators equilibrate in the
  degradation band (recharge stretch ~3.5-4.4x), the FailureHeat breaker
  never trips solo, PPCs sustain ~11.8-damage full-charge fire.  The slow
  kill pacing and recharge gaps are the AUTHENTIC heat economy; the pilot's
  counters (this regrouping UI, generator reassignment, coolant valves) are
  the reconstruction queue.

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

1165 lines
43 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
// @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);
}
//
//#############################################################################
// 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
)
{
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();
}