Files
BT411/game/reconstructed/mechmppr.cpp
T
Joe DiPrimaandClaude Opus 5 9657fbb11e control mode: REPRODUCE the "centering fought my control" fight, and prove the fix in Sauron's config
Follow-up to 4ccc2a7, which fixed the mechanism but could not reproduce the
field symptom.  His fuller wording -- the centering "FOUGHT" his control, not
"the torso died" -- is what cracked it.

WHY "FOUGHT" IS THE PRECISE SYMPTOM.  TorsoSimulation's frame order is
  1. digital twist commands   -> currentTwist += d;  recenterActive = 0
  2. centerCommand > 0        -> recenterActive = 1        (re-armed)
  3. analog twist axis != 0   -> currentTwist += d;  recenterActive = 0
  4. if (recenterActive)      -> Recenter(dt)              (drags toward 0)
Desktop/glass torso input is ANALOG (Q/E -> gBTTwistAxis -> stickPosition.x), so
with centerCommand stuck the torso HOLDS while you are actively pushing (step 3
clears the arm) and snaps back the instant you ease off (step 2's arm survives
into step 4).  You can only hold it off-centre by pushing continuously.  That is
"the centering fought my control", exactly.

WHY IT ONLY BITES THE GLASS/POD BUILD -- and why the first bench came back clean.
The ONLY caller of ClearRecenterCommand() sits INSIDE the desktop key-bridge
block, gated on `gBTDrive.forced || !BTRIODevicePresent()`.  With a RIO present
-- and on glass builds PadRIO IS the rioPointer -- the bridge is OFF and NOTHING
ever clears centerCommand, so one pass through Basic pins it at 1 for good.  A
plain desktop build clears it every frame and self-recovers.
The first modecycle.sh run needed BT_KEY_BRIDGE=1 to make the mode-cycle hook
run at all -- and that same flag switched on the only thing that clears the cell,
masking the bug under test.  The hook is now deliberately OUTSIDE that block so
the bench can run the RIO-present configuration.

MEASURED A/B, bridge OFF (Sauron's config), BT_TWIST_PULSE deflect/release:

                          LEGACY                      FIXED
  ctrCmd=1 samples        310  (latched for good)     0
  twist during RELEASE    decays 0.443->0,            HOLDS 2.44346
                          0.900->0.436  (recen=1)
  recen=1 samples         permanently armed           14 (one-shot per Basic
                                                      entry, then self-clears)

So the authentic one-shot re-centre still happens on entering Basic; it just
settles instead of fighting the pilot forever.

New bench hook BT_TWIST_PULSE=<n>: deflect the analog twist axis for n ticks
then RELEASE for n ticks, repeating.  BT_LOCK_SWEEP never releases, so it cannot
show this symptom at all -- the release window IS the measurement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
2026-08-08 13:59:39 -05:00

1782 lines
64 KiB
C++

//===========================================================================//
// File: mechmppr.cpp //
// Project: BattleTech Brick: Entity Manager //
// Contents: Mech controls mapper -- maps pilot control inputs and view //
// selection onto the Mech's motion / torso / eyepoint demands //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// --/--/95 ?? Initial coding. //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary. Behaviour follows the Ghidra
// pseudo-C for the mechmppr cluster (@004afbe0-@004b08c0); class / member /
// method names follow the Red Planet sibling RP\VTVMPPR.cpp (VTVControlsMapper)
// and the strings recovered from the .rdata class string pool. Each non-
// trivial method cites the originating @ADDR.
//
// Read-only globals / constants resolved from section_dump.txt:
// _DAT_004b0274 = 0x00000000 = 0.0f
// _DAT_004b0278 = 0x3f800000 = 1.0f
// _DAT_004b027c = 0x38d1b717 = 1.0e-4f (JM_CLOSE_ENOUGH)
// 0x40490fdb = PI (3.14159274f) (eyepoint yaw, "look behind")
// DAT_004e0f8c = EulerAngles::Identity (all zero)
//
// Helper-function name mapping (engine internals referenced by the decomp):
// FUN_004ac530 Subsystem base constructor (8-arg)
// FUN_004ac868 Subsystem destructor
// FUN_0040385c Verify()/Fail(msg,file,line)
// FUN_0041a1a4 IsDerivedFrom(classDerivations)
// FUN_00417ab4 SharedData::Resolve()
// FUN_00408644 Vector3D::Subtract(result, a, b)
// FUN_00408e90 EulerAngles::operator=(dst, src)
// FUN_0049fb54 Entity::IsDestroyed() -> Logical
// FUN_004022b0 Allocate(bytes) FUN_004022e8 Free(ptr)
// FUN_004022d0 operator delete FUN_004024d8 Resource::Release
// FUN_00403ad0 Group::FindGroup(root, name)
// FUN_00421414 CollectionIterator::CollectionIterator(it, collection)
// FUN_00421452 CollectionIterator::~CollectionIterator(it)
// FUN_004afacf ChildIterator::ChildIterator(it, list) (eyepoint list)
// FUN_004afb0d ChildIterator::~ChildIterator(it)
// FUN_004dcd00 fabsf()
// FUN_004bff74 VoiceAssist::Toggle() (lives in another module)
// DAT_004efc94 the global Application object
//
#include <bt.hpp>
#pragma hdrstop
#if !defined(MECHMPPR_HPP)
# include <mechmppr.hpp>
#endif
#if !defined(APP_HPP)
# include <app.hpp>
#endif
#if !defined(NTTMGR_HPP)
# include <nttmgr.hpp> // EntityManager / EntityGroup / FindGroup
#endif
#if !defined(MECH_HPP)
# include <mech.hpp>
#endif
#if !defined(PILOT_HPP)
# include <pilot.hpp>
#endif
#include <torso.hpp> // real Torso: analog aim axes @0x1F0/0x1F4, horizontalEnabled @0x250
#include <hud.hpp> // real HUD: freeAimSlew @0x28C
#define JM_CLOSE_ENOUGH 1.0e-4f // _DAT_004b027c
//
// Reconstruction helpers -- engine internals referenced by the decomp that
// have no direct analog in the surviving WinTesla MUNGA/MUNGA_L4 headers.
// (CROSS-FAMILY / engine gap -- see report.) Declared here so the recovered
// bodies compile; the linker will bind them to the real engine symbols.
//
// FUN_004bff74 toggles the voice-assist flag on the pilot subsystem.
// FUN_0049fb54 Entity::IsDestroyed() reached through an entity handle.
//
extern void ToggleVoiceAssist(int voice_assist_subsystem);
extern Logical Is_Destroyed(int entity_handle);
//
// TARGET-DESIGNATION bridges (btplayer.cpp, the complete-BTPlayer TU). The three
// designation sites used to write RAW at `pilotArray[0] + 0x284` -- the BINARY's
// `objectiveMech` offset. Measured on our compiled object, 0x284 is
// **`deathPending`**, the death-cycle latch, so every designation silently
// disabled the pilot's respawn (Gitea #57) while the target never reached
// `objectiveMech` at all (Gitea #48 / input-audit finding #1). Named members
// only, from here on -- see the static_asserts in btplayer.cpp.
//
extern void BTPilotSetObjectiveMech(void *pilot, void *target_vehicle);
extern void *BTPilotObjectiveMech(void *pilot);
extern void *BTPilotVehicle(void *pilot);
extern int BTPilotPosition(void *pilot, float *out_xyz);
extern int BTVehicleDestroyed(void *vehicle);
//
// Mech::GetHorizontalFiringReach -- the reachable horizontal firing half-arc
// (radians) the mech's torso can bring its guns to bear off dead-ahead. Weapons
// carry no arc field (the .SUB resources have none); the TORSO mount is what lets
// a mech point its guns to the side, so the authentic per-mech weapon traverse IS
// the torso's horizontal twist range (0 for a fixed torso like the Blackhawk).
// Defined here (not mech.cpp) because the cast to the full Torso type needs its
// header, which mech.cpp deliberately does not include (subsystem-stub collision).
//
Scalar
Mech::GetHorizontalFiringReach()
{
Torso *torso = (Torso *)GetTorsoSubsystem(); // @0x438 cache (real Torso or 0)
return (torso != 0) ? torso->GetHorizontalReach() : 0.0f;
}
//
// ChildIterator -- walks the eyepoint / camera child list hung off the torso
// articulation block (engine FUN_004afacf/FUN_004afb0d). The real type is a
// scene-graph DCS child iterator; the recovered InterpretControls() touches
// the list head as a raw address and the entries as raw int* records, so the
// iterator is reconstructed here as a minimal intrusive-list walk. BEST-EFFORT.
//
namespace {
struct ChildIterator {
int *node;
explicit ChildIterator(int list_head)
: node(*(int **)list_head)
{}
int *Next() {
int *current = node;
if (current != 0) {
node = *(int **)current; // next-link assumed at offset 0
}
return current;
}
};
}
//
// Owner (Mech) sub-object offsets touched by this mapper. The exact Mech
// layout is not recoverable from the pseudo-C; these accessors document the
// observed byte offsets and are flagged best-effort. A human should fold them
// back onto the real Mech / TorsoArticulation / Cockpit accessors.
//
// mech + 0x438 -> torso articulation block
// +0x220/+0x224 current torso yaw / pitch demand
// +0x228/+0x22c neutral (centered) yaw / pitch
// +0x230/+0x234 torso yaw / pitch travel limits
// +0x1f0 torso free-aim demand
// +0x274 torso auto-center flag
// +0x1f4 (500) torso pitch demand
// +0x250 free-aim-enabled flag
// +0x34c maximum yaw rate (also reverse scale)
// +0x534 minimum (high-speed) yaw rate
// +0x5c0 forward throttle scale
// +0x360 eyepoint rotation (EulerAngles)
// +0x378 eyepoint slave flag
// +0x410 eyepoint slave amount
// +0x564/+0x568/+0x56c/+0x570 look L/R/down/behind eyepoint pitches
// +0x7bc eyepoint / camera child list
// mech + 0x5b4 -> cockpit block (+0x28c torso free-aim demand, +0x2a0 flag)
//
//
// Pilot record offsets (entries of pilotArray):
// pilot + 0x100 world position (Vector3D)
// pilot + 0x1e0 pilot id (network ordinal)
// pilot + 0x1fc linked entity handle
// pilot + 0x284 current target handle (written on the LOCAL pilot)
//
// Player drive input, owned by the launcher (btbuild/btl4main.cpp). Consumed by
// the dev-box key bridge at the top of MechControlsMapper::InterpretControls.
struct BTDriveInput { float throttle; float turn; int forced; int fire; int fireForced; float forcedThrottle;
int keyFwd; int keyBack; int keyLeft; int keyRight; int allStop; };
extern BTDriveInput gBTDrive;
//###########################################################################
//###########################################################################
// MechControlsMapper
//###########################################################################
//###########################################################################
//#############################################################################
// Shared Data Support
//
MechControlsMapper::SharedData
MechControlsMapper::DefaultData(
MechControlsMapper::GetClassDerivations(),
MechControlsMapper::GetMessageHandlers(),
MechControlsMapper::GetAttributeIndex(),
MechControlsMapper::StateCount
);
Derivation*
MechControlsMapper::GetClassDerivations() // @0050ee10
{
static Derivation classDerivations(
Subsystem::GetClassDerivations(),
"MechControlsMapper" // @0050f173
);
return &classDerivations;
}
//#############################################################################
// Messaging Support
//
// Message table @0050ee40. IDs 3..0x13 share the single ConfigureMappable
// handler; the last three drive the dedicated cycle / toggle handlers. The
// engine MESSAGE_ENTRY macro forces handler-name == message-name, so the rows
// that fan into the shared handler are written out explicitly.
//
#define MAPPABLE_ENTRY(message) \
{ \
MechControlsMapper::message##MessageID, \
#message, \
(Receiver::Handler) \
&MechControlsMapper::ConfigureMappableMessageHandler \
}
const MechControlsMapper::HandlerEntry
MechControlsMapper::MessageHandlerEntries[]=
{
MAPPABLE_ENTRY(Aux1Quad),
MAPPABLE_ENTRY(Aux1Eng1),
MAPPABLE_ENTRY(Aux1Eng2),
MAPPABLE_ENTRY(Aux1Eng3),
MAPPABLE_ENTRY(Aux1Eng4),
MAPPABLE_ENTRY(Aux2Quad),
MAPPABLE_ENTRY(Aux2Eng1),
MAPPABLE_ENTRY(Aux2Eng2),
MAPPABLE_ENTRY(Aux2Eng3),
MAPPABLE_ENTRY(Aux2Eng4),
MAPPABLE_ENTRY(Aux3Quad),
MAPPABLE_ENTRY(Aux3Eng1),
MAPPABLE_ENTRY(Aux3Eng2),
MAPPABLE_ENTRY(Aux3Eng3),
MAPPABLE_ENTRY(Aux3Eng4),
MAPPABLE_ENTRY(ZoomIn),
MAPPABLE_ENTRY(ZoomOut),
MESSAGE_ENTRY(MechControlsMapper, CycleControlMode),
MESSAGE_ENTRY(MechControlsMapper, CycleDisplayMode),
MESSAGE_ENTRY(MechControlsMapper, ToggleVoiceAssist)
};
#undef MAPPABLE_ENTRY
MechControlsMapper::MessageHandlerSet&
MechControlsMapper::GetMessageHandlers()
{
static MessageHandlerSet messageHandlers(
ELEMENTS(MechControlsMapper::MessageHandlerEntries),
MechControlsMapper::MessageHandlerEntries,
Subsystem::GetMessageHandlers()
);
return messageHandlers;
}
//#############################################################################
// Attribute Support
//
// Attribute table @0050efd0. (Recorded offsets carry the engine scalar tag,
// e.g. 0x115 -> stickPosition @0x114.)
//
// Step-2a fix (2026-07-17): ids are pinned to the binary numbering (stick=3)
// and the id-2 chain gap is padded -- see the enum note in mechmppr.hpp.
// Locked so a future engine-base attribute publish shows up here instead of
// silently double-assigning ids:
static_assert((int)Subsystem::NextAttributeID
== (int)MechControlsMapper::MechControlsMapperPadFirstAttributeID,
"MechControlsMapper pad base != Subsystem::NextAttributeID -- re-count the pad");
static_assert((int)MechControlsMapper::StickPositionAttributeID == 3
&& (int)MechControlsMapper::PilotArrayAttributeID == 0x16,
"MechControlsMapper ids drifted off the binary .CTL numbering");
const MechControlsMapper::IndexEntry
MechControlsMapper::AttributePointers[]=
{
//
// The PAD fills the chain-vs-binary id gap (id 2) with a valid, named,
// never-bound entry (unique name, harmless target) so Find()'s strcmp
// walk never reads an uninitialized slot.
//
{ (int)MechControlsMapper::MechControlsMapperPadFirstAttributeID,
"MechControlsMapperPad02",
(Simulation::AttributePointer)&MechControlsMapper::throttlePosition },
ATTRIBUTE_ENTRY(MechControlsMapper, StickPosition, stickPosition), // 0x114
ATTRIBUTE_ENTRY(MechControlsMapper, ThrottlePosition, throttlePosition), // 0x11C
ATTRIBUTE_ENTRY(MechControlsMapper, PedalsPosition, pedalsPosition), // 0x120
ATTRIBUTE_ENTRY(MechControlsMapper, ReverseThrust, reverseThrust), // 0x124
ATTRIBUTE_ENTRY(MechControlsMapper, SpeedDemand, speedDemand), // 0x128
ATTRIBUTE_ENTRY(MechControlsMapper, TurnDemand, turnDemand), // 0x12C
ATTRIBUTE_ENTRY(MechControlsMapper, LookForward, lookForward), // 0x130
ATTRIBUTE_ENTRY(MechControlsMapper, LookLeft, lookLeft), // 0x134
ATTRIBUTE_ENTRY(MechControlsMapper, LookRight, lookRight), // 0x138
ATTRIBUTE_ENTRY(MechControlsMapper, LookBehind, lookBehind), // 0x13C
ATTRIBUTE_ENTRY(MechControlsMapper, LookDown, lookDown), // 0x140
ATTRIBUTE_ENTRY(MechControlsMapper, TorsoUp, torsoUp), // 0x144
ATTRIBUTE_ENTRY(MechControlsMapper, TorsoDown, torsoDown), // 0x148
ATTRIBUTE_ENTRY(MechControlsMapper, TorsoLeft, torsoLeft), // 0x14C
ATTRIBUTE_ENTRY(MechControlsMapper, TorsoRight, torsoRight), // 0x150
ATTRIBUTE_ENTRY(MechControlsMapper, TorsoCenter, torsoCenter), // 0x154
ATTRIBUTE_ENTRY(MechControlsMapper, ControlMode, controlMode), // 0x190
ATTRIBUTE_ENTRY(MechControlsMapper, DisplayMode, displayMode), // 0x194
ATTRIBUTE_ENTRY(MechControlsMapper, PilotArrayPage, pilotArrayPage), // 0x158
ATTRIBUTE_ENTRY(MechControlsMapper, PilotArray, pilotArray) // 0x15C
};
MechControlsMapper::AttributeIndexSet&
MechControlsMapper::GetAttributeIndex()
{
static AttributeIndexSet attributeIndex(
ELEMENTS(MechControlsMapper::AttributePointers),
MechControlsMapper::AttributePointers,
Subsystem::GetAttributeIndex()
);
return attributeIndex;
}
//#############################################################################
// Construction and Destruction
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b02f0
//
MechControlsMapper::MechControlsMapper(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
Subsystem( // FUN_004ac530(...,0,0)
owner,
subsystem_ID,
subsystem_resource,
shared_data
)
{
Check(owner);
//
// This mapper is always the active "Performance" of its Mech; the engine
// installs the default InterpretControls member-pointer (vtable @0050f120)
// into the simulation slot (this[7..9]).
//
SetPerformance(&MechControlsMapper::InterpretControls);
//
// Clear all published control inputs / demands.
//
stickPosition.x = 0.0f;
stickPosition.y = 0.0f;
throttlePosition = 0.0f;
pedalsPosition = 0.0f;
speedDemand = 0.0f;
turnDemand = 0.0f;
reverseThrust = 0;
lookForward = 0;
lookLeft = 0;
lookRight = 0;
lookDown = 0;
lookBehind = 0;
lookState = LookNone;
torsoUp = 0;
torsoDown = 0;
torsoLeft = 0;
torsoRight = 0;
torsoCenter = 0;
controlMode = BasicMode;
displayMode = 0;
pilotArrayPage = 0;
if (getenv("BT_MODE_LOG"))
DEBUG_STREAM << "[mode-ctor] mapper=" << (void*)this
<< " controlMode=" << (int)controlMode
<< " &controlMode=" << (void*)&controlMode << "\n" << std::flush;
//
// Recenter the torso articulation: current yaw/pitch <- neutral yaw/pitch.
//
// TODO(bring-up): the shipped code reads a torso-source pointer at the binary
// byte offset owner+0x438 (Mech::sinkSourceSubsystem) and copies that object's
// +0x228/+0x22c into +0x220/+0x224. Our RECONSTRUCTED Mech has a different
// object layout (sizeof != the binary's 0x854), so this raw offset yields a
// garbage pointer and faults. Skipped: the Mech ctor already identity-inits
// torsoAimCurrent/torsoAimTarget, so the torso starts centered. Re-enable
// via a named Mech accessor once the cross-module field layout is mapped.
{
int torso = *(int *)((int)owner + 0x438); // torso articulation block
if (torso != 0 && torso != (int)0xcdcdcdcd)
{
// (intentionally inert until the layout is mapped -- see note above)
}
(void)torso;
}
pilotArrayBuilt = False;
pilotIDs = 0;
for (int pi = 0; pi < PilotArraySlots; ++pi)
pilotArray[pi] = 0;
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b044c -- frees the pilot id table, then chains to ~Subsystem.
//
MechControlsMapper::~MechControlsMapper()
{
Check(this);
if (pilotIDs != 0)
{
delete [] pilotIDs; // FUN_004022e8 (Free of the id table)
}
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b08c0
//
Logical
MechControlsMapper::TestInstance() const
{
return IsDerivedFrom(*GetClassDerivations()); // FUN_0041a1a4(**this[3], 0x50ee10)
}
//#############################################################################
// Message Handlers
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004afbc4 -- shared handler for the auxiliary-equipment and zoom buttons.
// task #6 CORRECTION (disasm-verified): the base body is a pure FAIL TRAP --
// push 0x7a / "Unhandled button mapping!" @0x50f24a / MECHMPPR.CPP @0x50f265 /
// call Fail -- i.e. "not overridden". The real aux/zoom behavior lives in the
// derived L4 layer's own handlers. The old guessed body called
// AddOrErase(value, NULL) -- with the RIO mapper live that would TOGGLE a
// NULL-destination direct mapping into the fire groups.
//
void
MechControlsMapper::ConfigureMappableMessageHandler(
ReceiverDataMessageOf<ControlsButton> * /*message*/
)
{
DEBUG_STREAM << "[FAIL] Unhandled button mapping (base ConfigureMappableMessageHandler)"
<< std::endl;
//
// Tester guard (the e2c21c4 '&'-gate pattern): under DEBUGOFF Fail()
// is abort() -- ANY aux/zoom button whose L4 override is not yet
// reconstructed KILLED the game on press, and the glass panel / PAD
// bindings make every button one click away. Default: the loud log
// line above + ignore. BT_BUTTON_TRAP=1 restores the authentic hard
// trap (the binary's own base body: push "Unhandled button mapping!"
// + call Fail).
//
static const int s_trap =
(getenv("BT_BUTTON_TRAP") != 0 && *getenv("BT_BUTTON_TRAP") != '0');
if (s_trap)
{
Fail("Unhandled button mapping!\n"); // MECHMPPR.CPP:0x7a
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004afbe0 -- cycle BasicMode -> StandardMode -> VeteranMode -> BasicMode.
// Each mode reconfigures the torso articulation: basic mode recenters and
// auto-centers the torso, the assisted modes free the torso to its limits.
//
void
MechControlsMapper::CycleControlModeMessageHandler(
ReceiverDataMessageOf<ControlsButton> *message
)
{
Check(this);
Check(message);
if (message->dataContents > 0)
{
CycleControlModeNow();
}
}
//
// The mode-cycle body, shared by the console-button message handler above and
// the desktop 'M' key (mech4 key poll -> gBTModeCycle).
//
void
MechControlsMapper::CycleControlModeNow()
{
{
controlMode = (ControlMode)(controlMode + 1);
if (controlMode > VeteranMode)
{
controlMode = BasicMode;
}
NotifyOfControlModeChange(controlMode); // vtable+0x48
// TYPED torso reconfiguration. The raw block this replaces wrote the
// BINARY's offsets straight onto OUR compiled Torso (the databinding
// trap); the typed rewrite that followed then got the SEMANTICS wrong in
// three ways. Corrected 2026-08-08 against @004afbe0, which is a
// complete spec:
//
// iVar1 = mech+0x438 (TORSO) iVar2 = mech+0x5b4 (HUD)
// if (mode == 0) { // BASIC
// *(iVar1 + 0x1f0) = 0; // analogTwistAxis
// *(iVar1 + 0x274) = 1; // recenterActive
// *(iVar1 + 0x220) = *(iVar1 + 0x228); // vertLimitTop
// *(iVar1 + 0x224) = *(iVar1 + 0x22c); // vertLimitBottom
// *(iVar2 + 0x2a0) = 1; // HUD flickerActive
// } else if (mode - 1U < 2) { // STANDARD/VETERAN
// *(iVar1 + 0x220) = *(iVar1 + 0x230);
// *(iVar1 + 0x224) = *(iVar1 + 0x234);
// }
//
// (1) THE BUG Sauron hit. Basic set `centerCommand` (@0x208) via
// CommandRecenter(). That is the HELD-BUTTON cell: TorsoSimulation
// re-arms recenterActive from it EVERY frame it is non-zero, and only
// the input path clears it -- and a MODE SWITCH has no button release
// to follow. So one visit to Basic pinned it at 1 forever, the torso
// re-centred every frame, and the digital twist commands (processed
// BEFORE the centerCommand block) were overridden as fast as they were
// applied. Cycling Standard -> Veteran -> (wraps through BASIC) ->
// Standard is enough to trigger it, which is exactly the reported
// "toggled to advanced and back, lost torso control". The binary sets
// recenterActive (@0x274) directly: a ONE-SHOT that self-clears on
// settle (`recenterActive = Recenter(dt)`) and is cancelled by any
// twist input.
// (2) The ELEVATION LIMIT SWAP was missing entirely. Two authored pairs
// exist -- BASIC @0x228/@0x22C (full top, HALF bottom) vs
// STANDARD/VETERAN @0x230/@0x234 (the full pair) -- and all four were
// ctor-written and never read by anything. So Basic never restricted
// downward travel and the assisted modes never restored it.
// (3) Basic also raises the HUD's flickerActive (@0x2A0) so the horizon
// re-settles with the torso. Not ported.
// Also: the binary zeroes ONLY analogTwistAxis (@0x1F0). The extra
// SetAnalogElevationAxis(0) was invented; removed.
Mech *mech = GetMech();
Torso *torso = (mech != 0) ? (Torso *)mech->GetTorsoSubsystem() : 0;
if (torso != 0)
{
if (controlMode == BasicMode)
{
torso->SetAnalogTwistAxis(0.0f); // @0x1F0
// BT_LEGACY_MODE_RECENTER=1 restores the defective pre-2026-08-08
// behaviour (the sticky centerCommand) for A/B measurement.
static const int s_legacyRecenter =
getenv("BT_LEGACY_MODE_RECENTER") ? 1 : 0;
if (s_legacyRecenter)
torso->CommandRecenter(); // @0x208 STICKY -- the bug
else
torso->BeginRecenterOnce(); // @0x274 (NOT centerCommand)
torso->ApplyBasicElevationLimits(); // @0x220/@0x224 <- @0x228/@0x22C
extern void BTSetHudFlickerActive(Subsystem *hud);
BTSetHudFlickerActive(mech->GetHudSubsystem()); // HUD @0x2A0 = 1
}
else // StandardMode / VeteranMode -- `mode - 1U < 2` in the binary
{
torso->ApplyAssistedElevationLimits(); // @0x220/@0x224 <- @0x230/@0x234
}
}
DEBUG_STREAM << "[mode] control mode -> " << (int)controlMode
<< " (0=Basic 1=Standard 2=Veteran)" << std::endl;
}
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004afcac -- cycle the DISPLAY mode (0 -> 1 -> 2 -> 0). Gitea #6: this is
// the secondary screen's Damage/Critical/Heat schematic cycle -- the L4
// NotifyOfDisplayModeChange override (vtbl+0x4C, @004d1ae4) swaps the
// ModeSecondary* mask bits 18..20. The pod input was the "'Mech status Info
// center" button (manual p13, bottom left of the secondary screen); the DOS
// keyboard fallback was extended-F4 (Keypress 0x13e), dead under the WinTesla
// VK map.
//
void
MechControlsMapper::CycleDisplayModeMessageHandler(
ReceiverDataMessageOf<ControlsButton> *message
)
{
Check(this);
Check(message);
if (message->dataContents > 0)
{
CycleDisplayModeNow();
}
Check_Fpu();
}
//
// The display-cycle body, shared by the console-button message handler above
// and the desktop 'N' key (mech4 key poll -> gBTDisplayCycle) -- the same
// split as CycleControlModeNow.
//
void
MechControlsMapper::CycleDisplayModeNow()
{
displayMode = displayMode + 1;
if (displayMode > 2)
{
displayMode = 0;
}
NotifyOfDisplayModeChange(displayMode); // vtable+0x4c
DEBUG_STREAM << "[mode] display mode -> " << displayMode
<< " (0=Damage 1=Critical 2=Heat)" << std::endl;
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004afce8 -- toggle voice assist on the Mech's pilot subsystem.
//
void
MechControlsMapper::ToggleVoiceAssistMessageHandler(
ReceiverDataMessageOf<ControlsButton> *message
)
{
Check(this);
Check(message);
if (message->dataContents > 0)
{
// FUN_004bff74(mech->subsystem(0x190)) -- toggles the voice-assist flag.
ToggleVoiceAssist(*(int *)((int)GetMech() + 0x190));
}
Check_Fpu();
}
//#############################################################################
// Model Support -- InterpretControls
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// Owner accessor. The mapper is always owned by a Mech; the decomp used the
// stored owner pointer directly (this[?]). Routed here through the engine's
// Subsystem::GetEntity().
//
Mech*
MechControlsMapper::GetMech()
{
Check(this);
return (Mech *)GetEntity();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004afd10 -- the active master-Mech performance. Reads the raw stick,
// throttle, pedals and look buttons and writes the locomotion demands
// (speedDemand / turnDemand), the torso aim and the eyepoint pose. The
// per-control-mode steering/torso math is faithful to the decomp; the Mech
// sub-object offsets are documented at the top of this file. BEST-EFFORT on
// the precise Mech-field semantics.
//
void
MechControlsMapper::InterpretControls(Scalar time_slice)
{
Check(this);
//
// OFFSET RECONCILIATION (the revival fix): the earlier draft read the owner
// at RAW binary offsets, including TYPED-POINTER arithmetic on Mech*
// ("*(Scalar *)(mech + 0x34c)" == mech + 0x34c*sizeof(Mech) -- a wild pointer,
// the AV that kept this tick bypassed). Every owner access now goes through
// the reconciled members: mech+0x34c -> reverseStrideLength (naming caveat:
// LoadLocomotionClips measures it from the rr* clips -- it is the TOP/run
// cycle speed the throttle scales), mech+0x534 -> walkStrideLength,
// mech+0x5c0 -> forwardThrottleScale, mech+0x438 -> sinkSourceSubsystem (the
// re-based binary-exact Torso), mech+0x5b4 -> hudSubsystem (the HUD -- raw
// factory part_012.c:10164; MechTech's 0x104 alloc cannot hold +0x28c).
// Torso/HUD writes go through their real members (analog axes @0x1F0/0x1F4,
// horizontalEnabled @0x250, freeAimSlew @0x28C). Null-guards are bring-up
// safety for mechs without those subsystems (the binary trusts the data).
//
Mech *mech = GetMech();
Torso *torso = (Torso *)mech->GetTorsoSubsystem(); // @0x438 (raw iVar5)
HUD *cockpit= (HUD *)mech->GetHudSubsystem(); // @0x5b4 (raw iVar3)
//
// BRING-UP INPUT BRIDGE (dev box; env BT_KEY_BRIDGE=0 to disable on pods).
// The engine controls push refreshes the registered input attributes from
// DEVICE elements every frame BEFORE this Performance runs; on a keyboard
// dev box the RIO scalar throttle channel doesn't exist and its element
// reads a constant (observed: throttlePosition forced back to 1.0 each
// frame -> the mech could never stop). So key state is written HERE --
// after the push, immediately before interpretation -- making the keyboard
// authoritative on the dev box. Interpretation below stays 100% authentic.
//
// BENCH (BT_MODECYCLE_EVERY=<n>): cycle the control mode every n
// InterpretControls calls, driving the SAME body the 'M' key and the pod
// console button (key 0x13d -- not a RIO button, so BT_BTNTEST cannot press
// it) drive. Dev-only; default off.
//
// ⚠ DELIBERATELY OUTSIDE the key-bridge block below. The ONLY caller of
// ClearRecenterCommand() lives inside that block, so forcing BT_KEY_BRIDGE=1
// to make this hook run would ALSO switch on the one thing that clears
// centerCommand -- masking the very bug under test. That is exactly how the
// first run of modecycle.sh came back clean. Keeping the hook out here lets
// the bench reproduce the RIO-present (glass/PadRIO) configuration, where the
// bridge is OFF and nothing clears the cell.
{
static const char *s_mcEvery = getenv("BT_MODECYCLE_EVERY");
if (s_mcEvery != 0)
{
static int s_mcN = 0;
int period = atoi(s_mcEvery);
if (period < 1) period = 300;
if (++s_mcN % period == 0)
CycleControlModeNow();
}
}
{
// STAND-DOWN (glass-cockpit step 2c): BT_KEY_BRIDGE unset = AUTO --
// the bridge runs only when NO live cockpit device (serial RIO /
// PadRIO) owns these attributes through the engine push; "0" =
// force off (the old pod setting, still honored), anything else =
// force on. The headless forced harness (BT_FORCE_THROTTLE)
// always rides the bridge.
extern int BTRIODevicePresent(void);
static const char *s_kbEnv = getenv("BT_KEY_BRIDGE");
int s_keyBridge = s_kbEnv ? (*s_kbEnv != '0')
: (gBTDrive.forced || !BTRIODevicePresent());
if (s_keyBridge && mech != 0 && application != 0
&& (Entity *)mech == application->GetViewpointEntity())
{
float key_throttle = gBTDrive.forced
? gBTDrive.forcedThrottle : gBTDrive.throttle;
// PORT SIGN (user live-verified 2026-07-18: left arrow turned the
// mech RIGHT): the sim's positive turnDemand is CCW (LEFT) -- math
// convention, same as the torso twist. On the pod the RIO Ranger
// owned the hardware sign; the desktop bridge must negate so that
// input-right (positive) = turn RIGHT. Forced/BT_GOTO harness
// demands are sim-frame already and stay un-negated.
float key_turn = gBTDrive.forced ? 0.0f : -gBTDrive.turn;
// Headless harness (forced mode only): BT_FORCE_TURN holds a steering
// demand; BT_FORCE_SECONDS releases the forced throttle after n
// sim-seconds while KEEPING the turn (the turn-in-place repro).
if (gBTDrive.forced)
{
static float s_hClock = 0.0f, s_hLimit = -1.0f, s_hTurn = -999.0f;
if (s_hLimit < 0.0f)
{
const char *fs = getenv("BT_FORCE_SECONDS");
s_hLimit = fs ? (float)atof(fs) : 0.0f;
}
if (s_hTurn < -900.0f)
{
const char *ft = getenv("BT_FORCE_TURN");
s_hTurn = ft ? (float)atof(ft) : 0.0f;
}
key_turn = s_hTurn;
// BT_GOTO beeline (DEBUG harness): the turn demand is computed in
// mech4.cpp's drive block (where position/heading are in scope)
// and published through gBTGotoTurn/gBTGotoActive.
{
extern int gBTGotoActive;
extern float gBTGotoTurn;
extern float gBTGotoThrottle;
if (gBTGotoActive)
{
key_turn = gBTGotoTurn;
key_throttle = key_throttle * gBTGotoThrottle;
}
}
// BT_FORCE_OSC=<period>: square-wave the forced throttle between the
// commanded value and near-idle every <period> sim-seconds -- the
// headless FEATHERING repro (run->walk-downshift->run churn, the
// live-play gait-desync trigger pattern, task #49).
{
static float s_oscP = -1.0f, s_oscClock = 0.0f;
if (s_oscP < 0.0f)
{
const char *fo = getenv("BT_FORCE_OSC");
s_oscP = fo ? (float)atof(fo) : 0.0f;
}
if (s_oscP > 0.0f)
{
s_oscClock += time_slice;
if (fmodf(s_oscClock, 2.0f * s_oscP) >= s_oscP)
key_throttle = 0.08f; // near-idle half-cycle
}
}
// BT_FORCE_SWEEP=<period>: triangle-sweep the forced throttle 0.2..0.9
// continuously -- mimics a keyboard key-hold sweeping the lever every
// frame (a type-2 speed record per frame), the keyboard-skip repro.
{
static float s_swpP = -1.0f, s_swpClock = 0.0f;
if (s_swpP < 0.0f)
{
const char *fw = getenv("BT_FORCE_SWEEP");
s_swpP = fw ? (float)atof(fw) : 0.0f;
}
if (s_swpP > 0.0f)
{
s_swpClock += time_slice;
float ph = fmodf(s_swpClock, 2.0f * s_swpP) / s_swpP; // 0..2
float tri = (ph < 1.0f) ? ph : (2.0f - ph); // 0..1..0
key_throttle = 0.2f + 0.7f * tri;
// BT_FORCE_STEP=1: quantize the swept throttle to the RIO
// Ranger 0.05 grid (the authentic ADC/deadband step) -- the
// stepped-vs-continuous A/B for the speed-change glitch.
static int s_swpStep = -1;
if (s_swpStep < 0) s_swpStep = getenv("BT_FORCE_STEP") ? 1 : 0;
if (s_swpStep)
key_throttle = floorf(key_throttle / 0.05f + 0.5f) * 0.05f;
}
}
// BT_FORCE_FLIP=<t>: invert the forced throttle after t sim-seconds
// (the mid-stride direction-change repro).
static float s_hFlip = -1.0f;
if (s_hFlip < 0.0f)
{
const char *ff = getenv("BT_FORCE_FLIP");
s_hFlip = ff ? (float)atof(ff) : 0.0f;
}
if (s_hLimit > 0.0f || s_hFlip > 0.0f)
{
s_hClock += time_slice;
if (s_hFlip > 0.0f && s_hClock > s_hFlip)
{
key_throttle = -key_throttle;
}
else if (s_hLimit > 0.0f && s_hClock > s_hLimit)
{
key_throttle = 0.0f;
}
}
}
throttlePosition = (key_throttle >= 0.0f) ? key_throttle : -key_throttle;
//
// REVERSE THRUST -- the pod's red throttle-HANDLE button (RIO unit
// 0x3F, LALT on the keyboard), NOT a lever sweep through zero. The
// authentic wiring is BTL4.RES's "L4" streamed record [2]:
// `Button Throttle1 (0x3F) -> attr 6 ReverseThrust@0x124`, and the
// 1995 manual says HOLD it (release = forward) -- see
// context/pod-hardware.md.
//
// This line used to read ONLY the negative-throttle case, which is
// unreachable on the desktop: L4PADRIO clamps the Throttle channel to
// [0,1] (see its "low = 0.0f" clamp), so key_throttle never goes
// negative -- and because the bridge owns reverseThrust EVERY frame,
// it also zeroed the flag right back out even when the streamed
// button mapping had set it. Net effect: Alt did nothing.
// (User-reported 2026-07-24, "i cant seem to reverse".)
//
// The negative-throttle term stays for the headless harnesses, which
// drive forcedThrottle directly and CAN go negative.
//
{
extern int gBTReverseHeld; // L4PADRIO::EmitButton (any 0x3F source)
reverseThrust = (gBTReverseHeld || key_throttle < 0.0f) ? 1 : 0;
}
// (task #68) look-behind: hold 'V' = the pod's rear-view button.
// (The other look buttons stay unbound on the keyboard rig.)
{
extern int gBTLookBehind;
lookBehind = gBTLookBehind;
}
// CONTROL-MODE AXIS ROUTING (2026-07-13, the pod mapping): in
// BASIC the stick steers the legs (turn) and the torso auto-
// centers; in STANDARD/VETERAN the stick is the TORSO (free aim /
// twist) and the PEDALS steer -- so the keyboard's A/D feed the
// pedals and Q/E ('the stick') feed the twist. 'M' cycles modes
// (the same body the pod console button message drives, so the
// CONTROL MODE gauge tracks).
{
extern int gBTModeCycle;
extern int gBTDisplayCycle;
extern float gBTTwistAxis;
extern int gBTTorsoRecenter;
if (gBTModeCycle)
{
gBTModeCycle = 0;
CycleControlModeNow();
}
// Gitea #6: 'N' cycles the secondary screen's schematic
// (Damage -> Critical -> Heat) -- the same body the pod's
// status-info-center button message drives.
if (gBTDisplayCycle)
{
gBTDisplayCycle = 0;
CycleDisplayModeNow();
}
// TORSO ELEVATION (pitch): the pod stick's Y axis -- every
// control mode routes stickPosition.y into
// Torso::SetAnalogElevationAxis below, so the bridge feeds
// it in BOTH branches (keys R/F or a pad stick via btinput).
{
extern float gBTElevAxis;
stickPosition.y = gBTElevAxis;
}
// #124 twist-sign bench probe: is the axis routed, and in what mode?
if (getenv("BT_TORSO_LOG"))
{
static int s_mp = 0;
if ((s_mp++ % 60) == 0 && s_mp < 1200)
DEBUG_STREAM << "[mppr] mode=" << (int)controlMode
<< " twistAxis=" << gBTTwistAxis
<< " keyTurn=" << key_turn << "\n" << std::flush;
}
if (controlMode == BasicMode)
{
stickPosition.x = key_turn;
pedalsPosition = 0.0f;
}
else
{
// PORT SIGN: positive twist is CCW (left) like the turn;
// negate so stick/E-key right = torso right (manual p8:
// "pulling your joystick to the right torso twists your
// 'Mech to the right"). The earlier "MadCat twist was
// correct" call was based on the same misread screenshot
// forensics as the turn -- the user's live left-arrow
// test invalidated both.
stickPosition.x = -gBTTwistAxis; // the torso axis
pedalsPosition = key_turn; // A/D = the pedals
// 'X' recenter pulse (2026-07-13): one-frame centerCommand
// -> the sim arms recenterActive and Recenter (@004b6918)
// slews the torso home; any Q/E input cancels it sim-side.
// centerCommand is a pod BUTTON state, so the writer clears
// it while unpressed (Basic's own path re-asserts every
// frame; this branch owns it in Standard/Veteran).
{
Torso *rcTorso = (Torso *)mech->GetTorsoSubsystem();
if (rcTorso != 0)
{
if (gBTTorsoRecenter)
{
gBTTorsoRecenter = 0;
rcTorso->CommandRecenter();
}
else
{
rcTorso->ClearRecenterCommand();
}
}
}
}
}
// (stickPosition.y no longer zeroed here -- the bridge above
// feeds the elevation axis every bridged frame, 2026-07-18)
}
}
//
//------------------------------------------------------------------
// Throttle -> forward speed demand (reverse thrust inverts & rescales)
//------------------------------------------------------------------
//
// CANARY HEAL: something stomps mech->forwardThrottleScale at runtime
// nondeterministically (observed 0.14 and -1.0 across runs; ctor sets 1.0
// guarded) -- a wild raw-offset write, hunt via ba w4 on mech+0x5c0 (on the
// ledger). Until the writer is caught, restore a sane value and log.
// (band tightened: the stomp value 0.14 -- observed repeatedly, ~8 degrees
// in radians, likely an angle write landing on the wrong member -- passed
// the old 0.01..100 sanity band and the user's forward speed silently
// capped at 8.6 u/s while reverse ran full. The ctor guard currently
// always yields exactly 1.0, so heal anything else.)
if (mech->forwardThrottleScale != 1.0f)
{
DEBUG_STREAM << "[mppr] forwardThrottleScale STOMPED to "
<< mech->forwardThrottleScale << " -- healed to 1.0" << "\n" << std::flush;
mech->forwardThrottleScale = 1.0f;
}
//
// REVERSE THRUST on the DESKTOP. The authentic wiring is BTL4.RES's "L4"
// streamed record [2]: `Button Throttle1 (0x3F, the red button on the throttle
// handle) -> attr 6 ReverseThrust@0x124`, held for reverse / released for
// forward (context/pod-hardware.md, manual-verified). PadRIO publishes that
// button's hold state from EmitButton -- the one chokepoint every desktop
// source funnels through (keyboard LALT, gamepad, DirectInput joystick, glass
// panel click) -- but our streamed BUTTON mappings are not consumed yet
// (MechControlsMapper::AddOrErase is still the unreconstructed Fail stub), so
// the event never reaches attr 6 on its own. Apply it here, gated on the pad
// RIO being the live device so REAL POD hardware is untouched (there
// BTPadRIOActive() is 0 and the streamed mapping owns the flag).
//
// This is why LALT did nothing: the only writer was the keyboard bridge's
// `key_throttle < 0` test, which (a) is bypassed entirely whenever a RIO is
// operational -- which the pad RIO always is -- and (b) can never fire anyway,
// because L4PADRIO clamps the Throttle channel to [0,1].
// [T3 accommodation, marked: delete this block once streamed button mappings
// land, at which point record [2] drives attr 6 by itself.]
//
{
extern int BTPadRIOActive(void);
extern int gBTReverseHeld;
if (BTPadRIOActive())
{
reverseThrust = gBTReverseHeld ? 1 : 0;
}
}
// #78: a GIMPED mech cannot back up -- "reverse disabled". mechdmg raises
// graphicAlarm level 3 (left leg) / 4 (right leg) when a leg zone passes
// half structure, and MovementMode() IS that alarm's level (task #1). The
// pod's cue fired through the alarm's audio watchers on the level change;
// here the reverse INPUT is refused while limping (VGL Lynx's account of
// the 4.10 behavior, night 6).
{
// NB read the GRAPHIC ALARM, not MovementMode(): the port carries the
// binary's one mech+0x40 cell as TWO members (engine simulationState
// vs graphicAlarm level) and mechdmg raises the gimp states on the
// ALARM. See the split-brain note in combat-damage.md.
extern int BTMechGimpLevel(void *mech_v); // mechdmg.cpp (the TU-safe read)
int mm = BTMechGimpLevel(mech);
if ((mm == 3 || mm == 4) && reverseThrust >= 1)
{
reverseThrust = 0;
static int s_revWarned = 0;
if (!s_revWarned)
{
s_revWarned = 1;
DEBUG_STREAM << "[gimp] REVERSE DISABLED (leg damage, mode "
<< mm << ")\n" << std::flush;
}
}
}
if (reverseThrust < 1)
{
speedDemand =
mech->reverseStrideLength * throttlePosition * mech->forwardThrottleScale;
}
else
{
speedDemand = -mech->reverseStrideLength * throttlePosition;
}
// (History note: this block was briefly REMOVED on the morning of
// 2026-07-31 on the claim "the binary has NO dynamic myomer->speed
// coupling" -- an export-gap-blind conclusion, retracted the same day
// when the pod veterans testified seek 4 = 182 kph and the raw-image
// float-read sweep found the consumer inside the un-exported master-perf
// region. The restored form below is now BYTE-GROUNDED, and better than
// the pre-removal draft: MAX over the myomer chain (not first-found), NO
// 1.0 clamp (overdrive is real), and the turn-freeze at dead drive.)
{
// THE MYOMER DRIVE SCALE -- RESTORED + BYTE-GROUNDED (2026-07-31, the
// seek-audit correction). The morning's removal ("the binary has NO
// dynamic myomer->speed coupling") was EXPORT-GAP BLINDNESS: the
// master perf's un-exported region (@0x4a9cf2-0x4a9da4, raw capstone
// -- the same gap that hid the #93 crash block) walks the mech's
// MYOMER capability chain (+0x7AC) every frame, keeps the MAX
// speedEffect@0x31C in mech+0x79C, and MULTIPLIES the mapper's
// speedDemand@0x128 by it in place; when |max| <= 1e-4 (@0x4ab16c)
// it ALSO zeroes the mapper's turnDemand@0x12C -- dead/overheated
// myomers freeze BOTH speed and turn ("less than a minute at seek 4
// before overheating and freezing up" -- Oracle, who put pod seek-4
// humanoids at 182 kph: the manual's printed Super Charged figure).
// speedEffect = (gear/recGear) x heatFactor x (1 - damage), NO 1.0
// clamp: gear 4 rides ~1.43x demand into the RegisterMaxOutput gait
// cap (base x 1.4284) = SUPERCHARGE. [T1 disasm]
Scalar drive = 0.0f; // mech+0x79C accumulator
int myomers = 0;
extern Scalar BTMyomersSpeedEffectOf(void *subsystem); // myomers.cpp (-1 = not a Myomers)
int n = mech->GetSubsystemCount(); // chain-walk emulation (the
for (int i = 2; i < n; ++i) // +0x7AC fill is the SubProxy stub)
{
Scalar f = BTMyomersSpeedEffectOf(mech->GetSubsystem(i));
if (f >= -0.5f) // a Myomers (value may be 0)
{
++myomers;
if (f > drive) drive = f; // MAX over the chain (@0x4a9d20)
}
}
if (myomers > 0)
{
speedDemand *= drive; // @0x4a9d63: in-place demand scale
// The binary keeps the chain MAX in mech+0x79C (@0x4a9d00-0x4a9d39)
// -- publish it so the CROUCH gate (mech4 posture block, 2026-08-06)
// reads the same live factor (dead/overheated myomers cannot squat
// or rise; the posture selector tests |factor| <= 1e-4).
mech->myomerEffectiveness = drive;
if (fabsf(drive) <= 1.0e-4f) // @0x4a9d89 vs _DAT_004ab16c
turnDemand = 0.0f; // @0x4a9d9e: mapper+0x12C -- the FREEZE
}
extern int BTMechGimpLevel(void *mech_v); // mechdmg.cpp (the TU-safe read)
int mm = BTMechGimpLevel(mech); // the gimp cell (see above)
if (mm == 3 || mm == 4)
{
static Scalar s_gimp = -1.0f;
if (s_gimp < 0.0f)
{
const char *e = getenv("BT_GIMP_SPEED");
s_gimp = (e != 0 && *e != '\0') ? (Scalar)atof(e) : 1.0f; // 1.0 = authentic (clamp lives in the gimp SM)
if (s_gimp < 0.0f || s_gimp > 1.0f) s_gimp = 1.0f;
}
speedDemand *= s_gimp;
}
if (getenv("BT_DRIVE_LOG"))
{
static float s_dAcc = 0.0f; s_dAcc += time_slice;
if (s_dAcc >= 1.0f)
{
s_dAcc = 0.0f;
DEBUG_STREAM << "[drive] drive=" << drive << " nMyo=" << myomers
<< " mm=" << mm << " dmd=" << speedDemand
<< " mech=" << (int)mech->GetEntityID()
<< " @" << (void *)mech << "\n" << std::flush;
}
}
}
{
extern int BTMechGimpLevel(void *mech_v);
#define BTMechGimpLevelTrace BTMechGimpLevel
static int s_cTrace = -1;
if (s_cTrace < 0) { const char *e = getenv("BT_MPPR_TRACE"); s_cTrace = (e && *e != '0') ? 1 : 0; }
if (s_cTrace)
{
static float s_cAcc = 0.0f; s_cAcc += time_slice;
if (s_cAcc >= 0.5f) { s_cAcc = 0.0f;
DEBUG_STREAM << "[mppr-c] thr=" << throttlePosition << " rev=" << reverseThrust
<< " topSpd=" << mech->reverseStrideLength
<< " fScale=" << mech->forwardThrottleScale
<< " mm=" << BTMechGimpLevelTrace(mech) // #78: 3/4 = gimp
<< " -> dmd=" << speedDemand
// glass-regression verification 2026-07-20: the same trace
// carries the turn/aim scalars so one gated line proves the
// whole control surface (mode, pedals->turnDemand, stick,
// torso elevation). turnDemand prints the value computed
// BELOW this block last frame (member state) -- adequate for
// a 0.5 s cadence trace.
<< " mode=" << controlMode
<< " pedals=" << pedalsPosition
<< " stick=(" << stickPosition.x << "," << stickPosition.y << ")"
<< " turn=" << turnDemand
<< " elev=" << (torso ? torso->CurrentElevation() : 0.0f)
<< "\n" << std::flush; }
}
}
//
//------------------------------------------------------------------
// Square the stick for a soft response; cube the pedals. Preserve sign.
//------------------------------------------------------------------
//
Scalar stick_x = stickPosition.x * stickPosition.x;
Scalar stick_y = stickPosition.y * stickPosition.y;
Scalar pedal_3 = pedalsPosition * pedalsPosition * pedalsPosition;
if (stickPosition.x < 0.0f) stick_x = -stick_x; // _DAT_004b0274 == 0.0f
if (stickPosition.y < 0.0f) stick_y = -stick_y;
turnDemand = 0.0f;
if (controlMode == BasicMode)
{
//
// Basic: stick yaw drives the turn directly; stick pitch drives the
// torso pitch. The achievable turn rate is clamped down with speed.
//
turnDemand = stick_x;
if (torso)
{
torso->SetAnalogElevationAxis(stick_y); // raw torso+0x1f4 (500)
torso->SetAnalogTwistAxis(0.0f); // raw torso+0x1f0
if (getenv("BT_INPUT_LOG"))
{
static float s_eacc2 = 0.0f; s_eacc2 += time_slice;
if (s_eacc2 >= 0.5f) { s_eacc2 = 0.0f;
YawPitchRoll ypr;
ypr = mech->localOrigin.angularPosition;
DEBUG_STREAM << "[input] mppr elev in=" << stick_y
<< " torsoElev=" << torso->CurrentElevation()
<< " vel=" << torso->ElevationVelocity()
<< " mechYaw=" << (Scalar)ypr.yaw
<< "\n" << std::flush; }
}
}
if (cockpit)
{
cockpit->SetFreeAimSlew(0.0f); // raw cockpit+0x28c
}
Scalar max_turn =
(mech->reverseStrideLength - mech->walkStrideLength)
* (1.0f - Abs(turnDemand)) // _DAT_004b0278 == 1.0f
+ mech->walkStrideLength;
if (fabsf(turnDemand) > JM_CLOSE_ENOUGH) // _DAT_004b027c
{
Clamp(speedDemand, -max_turn, max_turn);
}
}
else if (controlMode == StandardMode)
{
//
// Standard: torso free-aim follows the stick yaw; pedals provide the
// turn. Turn rate clamped as in basic mode.
//
if (torso == 0 || !torso->GetHorizontalEnabled()) // raw torso+0x250 == 0
{
// (sign handled ONCE at the key bridge: positive sim twist/slew
// = CCW/left; the bridge negates so input-right = right)
if (cockpit) cockpit->SetFreeAimSlew(stick_x); // raw cockpit+0x28c
}
else
{
torso->SetAnalogTwistAxis(stick_x); // raw torso+0x1f0
}
if (torso) torso->SetAnalogElevationAxis(stick_y); // raw torso+0x1f4
if (torso && getenv("BT_INPUT_LOG"))
{
static float s_tacc = 0.0f; s_tacc += time_slice;
if (s_tacc >= 0.5f) { s_tacc = 0.0f;
DEBUG_STREAM << "[input] twist in=" << stick_x
<< " currentTwist=" << torso->CurrentTwist()
<< "\n" << std::flush; }
}
turnDemand = (pedal_3 == 0.0f) ? 0.0f : pedal_3;
Scalar max_turn =
(mech->reverseStrideLength - mech->walkStrideLength)
* (1.0f - Abs(turnDemand))
+ mech->walkStrideLength;
if (fabsf(turnDemand) > JM_CLOSE_ENOUGH)
{
Clamp(speedDemand, -max_turn, max_turn);
}
}
else if (controlMode == VeteranMode)
{
//
// Veteran: as standard, but no speed-dependent turn clamp.
//
if (torso == 0 || !torso->GetHorizontalEnabled())
{
if (cockpit) cockpit->SetFreeAimSlew(stick_x); // (sign at the bridge)
}
else
{
torso->SetAnalogTwistAxis(stick_x);
}
if (torso) torso->SetAnalogElevationAxis(stick_y);
turnDemand = (pedal_3 == 0.0f) ? 0.0f : pedal_3;
}
//
//------------------------------------------------------------------
// Look / eyepoint selection. Choose a look direction from the buttons;
// when it changes, re-aim the eyepoint and re-slave the camera children.
//------------------------------------------------------------------
//
previousLookState = lookState;
if (lookLeft > 0) lookState = LookLeftState;
else if (lookRight > 0) lookState = LookRightState;
else if (lookBehind > 0) lookState = LookBehindState;
else if (lookDown > 0) lookState = LookDownState;
else lookState = LookNone;
if (lookState != previousLookState)
{
// (task #68) the look commit is LIVE: the conflicted labels are
// arbitrated (mech+0x360 = EyepointRotation -- the eye renderable
// consumes it; +0x410 = RearFiring; the +0x7bc children = the WEAPON
// roster, not cameras). BTCommitLookState (mech4.cpp complete-type
// TU) re-aims the eyepoint and re-arms each weapon's viewFireEnable:
// forward view = the non-rear weapons, LOOK-BACK = the rear-mounted
// ones, side/down = none.
extern void BTCommitLookState(void *mech, int look_state);
BTCommitLookState((void *)mech, (int)lookState);
DEBUG_STREAM << "[mppr] look state -> " << lookState << "\n" << std::flush;
}
else if (0) // DEFERRED body (kept for the reconstruction record)
{
const int torso = 0; // shadow: the raw-offset text below predates
// the reconciliation (see TODO above)
EulerAngles eyepoint;
ChildIterator cameras(torso + 0x7bc); // FUN_004afacf
switch (lookState)
{
case LookNone:
eyepoint = EulerAngles::Identity; // DAT_004e0f8c
*(LWord *)(torso + 0x378) = 1; // re-slave eyepoint
for (int *camera; (camera = cameras.Next()) != 0; )
{
*(LWord *)(camera + 0x3e0) = (*(int *)(camera + 0x334) == 0);
}
break;
case LookLeftState:
eyepoint = EulerAngles::Identity;
eyepoint.pitch = *(Scalar *)(torso + 0x564);
*(LWord *)(torso + 0x378) = 0;
for (int *camera; (camera = cameras.Next()) != 0; )
{
*(LWord *)(camera + 0x3e0) = 0;
}
break;
case LookRightState:
eyepoint = EulerAngles::Identity;
eyepoint.pitch = *(Scalar *)(torso + 0x568);
*(LWord *)(torso + 0x378) = 0;
for (int *camera; (camera = cameras.Next()) != 0; )
{
*(LWord *)(camera + 0x3e0) = 0;
}
break;
case LookBehindState:
eyepoint = EulerAngles::Identity;
eyepoint.yaw = PI; // 0x40490fdb
eyepoint.pitch = *(Scalar *)(torso + 0x570);
*(LWord *)(torso + 0x378) = *(LWord *)(torso + 0x410);
for (int *camera; (camera = cameras.Next()) != 0; )
{
*(LWord *)(camera + 0x3e0) = *(int *)(camera + 0x334);
}
break;
case LookDownState:
eyepoint = EulerAngles::Identity;
eyepoint.pitch = *(Scalar *)(torso + 0x56c);
*(LWord *)(torso + 0x378) = 0;
for (int *camera; (camera = cameras.Next()) != 0; )
{
*(LWord *)(camera + 0x3e0) = 0;
}
break;
}
*(EulerAngles *)(torso + 0x360) = eyepoint; // commit eyepoint rotation
}
Check_Fpu();
}
//#############################################################################
// Pilot-array (other-player roster) management
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b0600 -- build the pilot roster once, lazily, from the application's
// "Players" group, counting SCORING players only. The binary's raw check
// `(entry+0x29 & 0x40) == 0` decoded (Gitea #43): +0x29 is byte 1 of the
// simulationFlags LWord @+0x28, so 0x40 there == bit 14 == Entity::NextBit ==
// Player::NonScoringPlayerFlag (PLAYER.h:392). Players START non-scoring
// (DefaultFlags) and become scoring when their mech links its owning player
// (mech.cpp:688 / btplayer.cpp:1210) -- so camera ships and unseated players
// never get a roster row. Read via the named accessor, NOT the raw offset
// (our compiled layout differs -- the raw read is the databinding trap and
// was THE #43 bug: garbage count latched the roster at 1 row in live MP).
//
void
MechControlsMapper::BuildPilotArray()
{
//
// Scoring census. The binary latches the roster on the FIRST call
// (pilotArrayBuilt, build-once) -- correct on 1995 pod hardware where
// every pod's entities exist before any mission tick. In the port the
// peers' Player/PlayerLink messages arrive over async TCP, and the rig
// proved a frame-level race (Gitea #43): the faster-loading node ticks
// InterpretControls once BEFORE the net queue creates the replicants,
// latching a 1-row roster forever. [T3 accommodation, precedent =
// the demand-latch]: re-run the build whenever the SCORING census
// changes -- late-arriving pilots get their row, and a departed peer's
// destroyed Player leaves the roster instead of dangling (the latched
// design kept a freed pointer for the rest of the mission).
//
int scoring = 0;
EntityGroup *players =
application->GetEntityManager()->FindGroup("Players"); // @0050f44b
if (players != 0)
{
ChainIteratorOf<Node*> pilots(players->groupMembers); // FUN_00421414
for (Node *entry; (entry = pilots.ReadAndNext()) != 0; )
{
if (((Player *)entry)->IsScoringPlayer()) // binary: (+0x29 & 0x40) == 0
{
++scoring;
}
}
}
// Capacity clamp: the binary reserves 10 slots (local + up to 9 remotes;
// an 8-pod game fits). pilotCount beyond that would overrun the block in
// the binary too -- clamp so the `<= pilotCount` zero-loop (which zeroes
// the local slot 0 PLUS pilotCount remotes, faithful) stays in bounds.
if (scoring > PilotArraySlots - 1)
scoring = PilotArraySlots - 1;
if (pilotArrayBuilt && scoring == pilotCount)
{
return; // steady state -- the binary's built-once path
}
pilotArrayBuilt = True;
pilotCount = scoring;
delete[] pilotIDs; // 0 on first build (ctor)
pilotIDs = new int[pilotCount > 0 ? pilotCount : 1]; // FUN_004022b0
for (int i = 0; i <= pilotCount; ++i)
{
pilotArray[i] = 0;
}
FillPilotArray(); // FUN_004b06cc
ChooseDefaultPilot(); // FUN_004b07f0
// Always-on forensic (Gitea #43): one line per census change.
DEBUG_STREAM << "[score] pilot roster built: " << pilotCount
<< " scoring pilot(s)" << std::endl;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b06cc -- slot 0 holds the local pilot; the remaining slots are filled
// from the "Players" group (paged by pilotArrayPage).
//
void
MechControlsMapper::FillPilotArray()
{
for (int i = 0; i < pilotCount; ++i)
{
pilotIDs[i] = -1;
}
//
// The local "station" (the local network player record) lives at
// application+0x6c in the binary; its pilot roster entry at +0x190.
// ENGINE DRIFT FIX (the AV that kept the mapper tick bypassed): the WinTesla
// Application exposes the local player through GetMissionPlayer() (APP.h:169)
// -- the same reconciliation SendFakeButtonEvent uses (this file's sibling in
// btl4mppr.cpp). The old draft read the 1995 byte offsets through the 2007
// object -> wild "pilot" pointer -> access violation.
//
Player *local_pilot =
(application != 0) ? application->GetMissionPlayer() : 0;
if (local_pilot == 0)
{
return;
}
pilotArray[0] = (Pilot *)local_pilot;
pilotIDs[0] = local_pilot->GetEntityID(); // raw read pilot+0x1e0 (the entity id)
EntityGroup *players =
application->GetEntityManager()->FindGroup("Players");
if (players != 0)
{
ChainIteratorOf<Node*> pilots(players->groupMembers);
// skip earlier pages
int skip = pilotArrayPage * pilotCount;
while (skip > 0 && pilots.ReadAndNext() != 0)
{
--skip;
}
int slot = 1;
for (Node *entry; (entry = pilots.ReadAndNext()) != 0; )
{
// Binary @004b0790: skip non-scoring players (the same +0x29 & 0x40
// == NonScoringPlayerFlag check as the count loop -- resolved, Gitea
// #43) and the local pilot (already slot 0).
if (!((Player *)entry)->IsScoringPlayer()
|| entry == (Node *)local_pilot)
{
continue;
}
pilotArray[slot] = (Pilot *)entry;
pilotIDs[slot] = ((Entity *)entry)->GetEntityID(); // raw entry+0x1e0
if (++slot >= pilotCount)
{
break;
}
}
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b07f0 -- pick the initial target for the local pilot: the next pilot id
// after the local one (or the first non-self pilot).
//
void
MechControlsMapper::ChooseDefaultPilot()
{
int target = 0;
if (pilotCount != 1)
{
int index = 0;
if (pilotCount == pilotIDs[0])
{
for (int i = 1; i < pilotCount; ++i)
{
if (pilotIDs[i] != 1)
{
index = i;
break;
}
}
}
else
{
for (int i = 1; i < pilotCount; ++i)
{
if (pilotIDs[i] == pilotIDs[0] + 1)
{
index = i;
break;
}
}
}
Pilot *chosen = pilotArray[index];
void *target_vehicle = (chosen != 0) ? BTPilotVehicle(chosen) : 0;
BTPilotSetObjectiveMech(pilotArray[0], target_vehicle);
(void)target;
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b049c -- set the local pilot's current target to the pilot on the given
// roster page.
//
void
MechControlsMapper::UpdateCurrentPilot(int page)
{
if (pilotArrayBuilt && pilotArray[0] != 0)
{
Pilot *chosen = pilotArray[page];
void *target_vehicle = (chosen != 0) ? BTPilotVehicle(chosen) : 0;
BTPilotSetObjectiveMech(pilotArray[0], target_vehicle);
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b04d8 -- choose the nearest living pilot (other than self) as the local
// pilot's current target.
//
void
MechControlsMapper::ChooseNearestPilot(int self_id)
{
if (!pilotArrayBuilt || pilotArray[0] == 0)
{
return;
}
int target = 0;
Scalar nearest_distance = 0.0f;
int nearest_index = 0;
Logical none_yet = True;
//
// Positions come off each pilot's VEHICLE through the bridge (the binary's
// raw `pilot + 0x100` is an Entity-layout offset that does not exist on our
// Player). A pilot with no vehicle -- dead, or not yet acquired -- has no
// position and cannot be the nearest target, so it is skipped.
//
float self_pos[3];
if (!BTPilotPosition(pilotArray[0], self_pos))
{
Check_Fpu();
return;
}
void *self_vehicle = BTPilotVehicle(pilotArray[0]);
(void)self_id; // the binary compared raw handles
for (int i = 1; i < pilotCount; ++i)
{
void *entity = BTPilotVehicle(pilotArray[i]);
float pos[3];
if (entity == 0 || entity == self_vehicle)
continue;
if (BTVehicleDestroyed(entity)) // the binary's Is_Destroyed(+0x1fc)
continue;
if (!BTPilotPosition(pilotArray[i], pos))
continue;
const float dx = pos[0] - self_pos[0];
const float dy = pos[1] - self_pos[1];
const float dz = pos[2] - self_pos[2];
Scalar distance = dx * dx + dy * dy + dz * dz;
if (none_yet || distance < nearest_distance)
{
none_yet = False;
nearest_distance = distance;
nearest_index = i;
}
}
if (!none_yet)
{
Pilot *chosen = pilotArray[nearest_index];
void *target_vehicle = (chosen != 0) ? BTPilotVehicle(chosen) : 0;
BTPilotSetObjectiveMech(pilotArray[0], target_vehicle);
}
(void)target;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b0898 -- bounds-checked roster accessor.
//
Pilot *
MechControlsMapper::GetPilot(int index)
{
if (index >= pilotCount || index < 0)
{
return 0;
}
return pilotArray[index];
}
//#############################################################################
// Configuration / event-mapping interface (secondary vtable @0050f498)
//
// These are "not overridden" defaults: a platform-specific derived mapper is
// expected to supply real implementations. The base class traps the call.
//
//
// @004b029c
//
void
MechControlsMapper::ExitConfiguration(int /*held_element*/)
{
DEBUG_STREAM << "[FAIL] ExitConfiguration not overridden (base mapper in slot 0)" << std::endl;
Fail("ExitConfiguration not overridden!\n"); // @004b029c MECHMPPR.CPP:0x245
}
//
// @004b0280 (vtable +0x38; "EnterConfiguration not overridden!" @0050f370)
//
void
MechControlsMapper::EnterConfiguration(
int /*held_element*/,
ControlsButton * /*destination*/,
Receiver * /*receiver*/,
Receiver::MessageID /*choose_message_id*/,
Receiver::MessageID /*configure_message_id*/,
Receiver::MessageID /*active_message_id*/
)
{
DEBUG_STREAM << "[FAIL] EnterConfiguration not overridden (base mapper in slot 0)" << std::endl;
Fail("EnterConfiguration not overridden!\n");
}
//
// @004b02b8 (vtable +0x40; the EVENT AddOrErase overload -- the old
// "CreateTemporaryEventMappings" name was RP drift, task #6)
//
void
MechControlsMapper::AddOrErase(
unsigned int /*button_ID*/,
Receiver * /*receiver*/,
Receiver::MessageID /*message_ID*/
)
{
DEBUG_STREAM << "[FAIL] Unhandled mapping (base AddOrErase event)" << std::endl;
Fail("Unhandled mapping!\n"); // MECHMPPR.CPP:0x24f
}
//
// @004b02d4 (vtable +0x44; the DIRECT AddOrErase overload)
//
void
MechControlsMapper::AddOrErase(
unsigned int /*button_ID*/,
ControlsButton * /*destination*/
)
{
DEBUG_STREAM << "[FAIL] Unhandled mapping (base AddOrErase direct)" << std::endl;
Fail("Unhandled mapping!\n"); // MECHMPPR.CPP:0x25a
}
//
// @004b048c -- vtable+0x48; defaults to a no-op (override for HUD feedback).
//
void
MechControlsMapper::NotifyOfControlModeChange(int /*new_mode*/)
{
}
//
// @004b0494 -- vtable+0x4c; defaults to a no-op.
//
void
MechControlsMapper::NotifyOfDisplayModeChange(int /*new_mode*/)
{
}
//
// gauge wave P2: BTResolveRosterPilot -- the bridge PilotList (btl4gau3.cpp, the
// Comm KILLS/DEATHS roster) uses to read the viewpoint mech's pilot roster. The
// roster lives on the mech's ControlsMapper (subsystemArray[0]); this complete-Mech
// TU resolves it, PilotList reads the returned pilot at raw BTPlayer offsets.
// Faithful to the binary's Execute: **(App+0x6c mech +0x128)[slot] -> GetPilot.
//
void *BTResolveRosterPilot(int slot)
{
if (application == 0)
{
return 0;
}
Mech *mech = (Mech *)application->GetViewpointEntity(); // App+0x6c local mech
if (mech == 0 || mech->GetSubsystemCount() < 1)
{
return 0;
}
MechControlsMapper *mapper = (MechControlsMapper *)mech->GetSubsystem(0); // subsystemArray[0]
if (mapper == 0)
{
return 0;
}
return (void *)mapper->GetPilot(slot); // FUN_004b0898
}
//###########################################################################
// BTMapperSpeedDemand -- complete-type bridge (task #1)
//
// Mech::WriteUpdateRecord stamps the controls mapper's live speedDemand
// (binary: *(subsystemArray[0] + 0x128)) into every record tail. mech.cpp
// sees only the forward declaration of MechControlsMapper, so the read
// lives here in the complete-type TU (the house databinding pattern).
//
//###########################################################################
// BTBuildMapperDemandLatch -- slot-0 latch (task #7)
//
// The binary leaves roster slot 0 NULL except on the viewpoint mech
// (MakeViewpointEntity installs the real device mapper via
// SetMappingSubsystem @0049fe40). Our port drives NON-viewpoint masters
// (the BT_GOTO/BT_AUTODRIVE harness) and animates replicant gait through
// mapper demand fields, so every mech gets a default-data BASE mapper as a
// demand LATCH at ctor end; SetMappingSubsystem deletes + replaces it on
// the viewpoint mech exactly as the binary replaces slot 0. [T3
// accommodation -- relocates the retired 0xBD3 squatter's role to the slot
// the binary actually reads.] Complete-type TU (the mech.cpp caller
// carries a local stub under the mapper's name).
//
Subsystem *BTBuildMapperDemandLatch(Mech *mech)
{
static MechControlsMapper::SubsystemResource latch_resource;
Str_Copy(latch_resource.subsystemName, "ControlsMapper",
sizeof(latch_resource.subsystemName));
latch_resource.classID = (RegisteredClass::ClassID)0xf; // TrivialSubsystemClassID (btl4app idiom)
latch_resource.subsystemModelSize = sizeof(latch_resource);
return new MechControlsMapper(mech, 0, &latch_resource,
MechControlsMapper::DefaultData);
}
Scalar BTMapperSpeedDemandRaw(void *mapper)
{
// void* signature: mech.cpp's TU carries a local recon stub type under the
// same name, so a typed signature mangles differently there (class/struct
// + type identity). The cast happens HERE, in the complete-type TU.
return (mapper != 0) ? ((MechControlsMapper *)mapper)->speedDemand : Scalar(0.0f);
}