Files
BT411/game/reconstructed/mechmppr.cpp
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

1281 lines
41 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);
//
// Mech::GetHorizontalFiringReach -- the reachable horizontal firing half-arc
// (radians) the mech's torso can bring its guns to bear off dead-ahead. Weapons
// carry no arc field (the .SUB resources have none); the TORSO mount is what lets
// a mech point its guns to the side, so the authentic per-mech weapon traverse IS
// the torso's horizontal twist range (0 for a fixed torso like the Blackhawk).
// Defined here (not mech.cpp) because the cast to the full Torso type needs its
// header, which mech.cpp deliberately does not include (subsystem-stub collision).
//
Scalar
Mech::GetHorizontalFiringReach()
{
Torso *torso = (Torso *)GetTorsoSubsystem(); // @0x438 cache (real Torso or 0)
return (torso != 0) ? torso->GetHorizontalReach() : 0.0f;
}
//
// ChildIterator -- walks the eyepoint / camera child list hung off the torso
// articulation block (engine FUN_004afacf/FUN_004afb0d). The real type is a
// scene-graph DCS child iterator; the recovered InterpretControls() touches
// the list head as a raw address and the entries as raw int* records, so the
// iterator is reconstructed here as a minimal intrusive-list walk. BEST-EFFORT.
//
namespace {
struct ChildIterator {
int *node;
explicit ChildIterator(int list_head)
: node(*(int **)list_head)
{}
int *Next() {
int *current = node;
if (current != 0) {
node = *(int **)current; // next-link assumed at offset 0
}
return current;
}
};
}
//
// Owner (Mech) sub-object offsets touched by this mapper. The exact Mech
// layout is not recoverable from the pseudo-C; these accessors document the
// observed byte offsets and are flagged best-effort. A human should fold them
// back onto the real Mech / TorsoArticulation / Cockpit accessors.
//
// mech + 0x438 -> torso articulation block
// +0x220/+0x224 current torso yaw / pitch demand
// +0x228/+0x22c neutral (centered) yaw / pitch
// +0x230/+0x234 torso yaw / pitch travel limits
// +0x1f0 torso free-aim demand
// +0x274 torso auto-center flag
// +0x1f4 (500) torso pitch demand
// +0x250 free-aim-enabled flag
// +0x34c maximum yaw rate (also reverse scale)
// +0x534 minimum (high-speed) yaw rate
// +0x5c0 forward throttle scale
// +0x360 eyepoint rotation (EulerAngles)
// +0x378 eyepoint slave flag
// +0x410 eyepoint slave amount
// +0x564/+0x568/+0x56c/+0x570 look L/R/down/behind eyepoint pitches
// +0x7bc eyepoint / camera child list
// mech + 0x5b4 -> cockpit block (+0x28c torso free-aim demand, +0x2a0 flag)
//
//
// Pilot record offsets (entries of pilotArray):
// pilot + 0x100 world position (Vector3D)
// pilot + 0x1e0 pilot id (network ordinal)
// pilot + 0x1fc linked entity handle
// pilot + 0x284 current target handle (written on the LOCAL pilot)
//
// Player drive input, owned by the launcher (btbuild/btl4main.cpp). Consumed by
// the dev-box key bridge at the top of MechControlsMapper::InterpretControls.
struct BTDriveInput { float throttle; float turn; int forced; int fire; int fireForced; float forcedThrottle;
int keyFwd; int keyBack; int keyLeft; int keyRight; int allStop; };
extern BTDriveInput gBTDrive;
//###########################################################################
//###########################################################################
// MechControlsMapper
//###########################################################################
//###########################################################################
//#############################################################################
// Shared Data Support
//
MechControlsMapper::SharedData
MechControlsMapper::DefaultData(
MechControlsMapper::GetClassDerivations(),
MechControlsMapper::GetMessageHandlers(),
MechControlsMapper::GetAttributeIndex(),
MechControlsMapper::StateCount
);
Derivation*
MechControlsMapper::GetClassDerivations() // @0050ee10
{
static Derivation classDerivations(
Subsystem::GetClassDerivations(),
"MechControlsMapper" // @0050f173
);
return &classDerivations;
}
//#############################################################################
// Messaging Support
//
// Message table @0050ee40. IDs 3..0x13 share the single ConfigureMappable
// handler; the last three drive the dedicated cycle / toggle handlers. The
// engine MESSAGE_ENTRY macro forces handler-name == message-name, so the rows
// that fan into the shared handler are written out explicitly.
//
#define MAPPABLE_ENTRY(message) \
{ \
MechControlsMapper::message##MessageID, \
#message, \
(Receiver::Handler) \
&MechControlsMapper::ConfigureMappableMessageHandler \
}
const MechControlsMapper::HandlerEntry
MechControlsMapper::MessageHandlerEntries[]=
{
MAPPABLE_ENTRY(Aux1Quad),
MAPPABLE_ENTRY(Aux1Eng1),
MAPPABLE_ENTRY(Aux1Eng2),
MAPPABLE_ENTRY(Aux1Eng3),
MAPPABLE_ENTRY(Aux1Eng4),
MAPPABLE_ENTRY(Aux2Quad),
MAPPABLE_ENTRY(Aux2Eng1),
MAPPABLE_ENTRY(Aux2Eng2),
MAPPABLE_ENTRY(Aux2Eng3),
MAPPABLE_ENTRY(Aux2Eng4),
MAPPABLE_ENTRY(Aux3Quad),
MAPPABLE_ENTRY(Aux3Eng1),
MAPPABLE_ENTRY(Aux3Eng2),
MAPPABLE_ENTRY(Aux3Eng3),
MAPPABLE_ENTRY(Aux3Eng4),
MAPPABLE_ENTRY(ZoomIn),
MAPPABLE_ENTRY(ZoomOut),
MESSAGE_ENTRY(MechControlsMapper, CycleControlMode),
MESSAGE_ENTRY(MechControlsMapper, CycleDisplayMode),
MESSAGE_ENTRY(MechControlsMapper, ToggleVoiceAssist)
};
#undef MAPPABLE_ENTRY
MechControlsMapper::MessageHandlerSet&
MechControlsMapper::GetMessageHandlers()
{
static MessageHandlerSet messageHandlers(
ELEMENTS(MechControlsMapper::MessageHandlerEntries),
MechControlsMapper::MessageHandlerEntries,
Subsystem::GetMessageHandlers()
);
return messageHandlers;
}
//#############################################################################
// Attribute Support
//
// Attribute table @0050efd0. (Recorded offsets carry the engine scalar tag,
// e.g. 0x115 -> stickPosition @0x114.)
//
const MechControlsMapper::IndexEntry
MechControlsMapper::AttributePointers[]=
{
ATTRIBUTE_ENTRY(MechControlsMapper, StickPosition, stickPosition), // 0x114
ATTRIBUTE_ENTRY(MechControlsMapper, ThrottlePosition, throttlePosition), // 0x11C
ATTRIBUTE_ENTRY(MechControlsMapper, PedalsPosition, pedalsPosition), // 0x120
ATTRIBUTE_ENTRY(MechControlsMapper, ReverseThrust, reverseThrust), // 0x124
ATTRIBUTE_ENTRY(MechControlsMapper, SpeedDemand, speedDemand), // 0x128
ATTRIBUTE_ENTRY(MechControlsMapper, TurnDemand, turnDemand), // 0x12C
ATTRIBUTE_ENTRY(MechControlsMapper, LookForward, lookForward), // 0x130
ATTRIBUTE_ENTRY(MechControlsMapper, LookLeft, lookLeft), // 0x134
ATTRIBUTE_ENTRY(MechControlsMapper, LookRight, lookRight), // 0x138
ATTRIBUTE_ENTRY(MechControlsMapper, LookBehind, lookBehind), // 0x13C
ATTRIBUTE_ENTRY(MechControlsMapper, LookDown, lookDown), // 0x140
ATTRIBUTE_ENTRY(MechControlsMapper, TorsoUp, torsoUp), // 0x144
ATTRIBUTE_ENTRY(MechControlsMapper, TorsoDown, torsoDown), // 0x148
ATTRIBUTE_ENTRY(MechControlsMapper, TorsoLeft, torsoLeft), // 0x14C
ATTRIBUTE_ENTRY(MechControlsMapper, TorsoRight, torsoRight), // 0x150
ATTRIBUTE_ENTRY(MechControlsMapper, TorsoCenter, torsoCenter), // 0x154
ATTRIBUTE_ENTRY(MechControlsMapper, ControlMode, controlMode), // 0x190
ATTRIBUTE_ENTRY(MechControlsMapper, DisplayMode, displayMode), // 0x194
ATTRIBUTE_ENTRY(MechControlsMapper, PilotArrayPage, pilotArrayPage), // 0x158
ATTRIBUTE_ENTRY(MechControlsMapper, PilotArray, pilotArray) // 0x15C
};
MechControlsMapper::AttributeIndexSet&
MechControlsMapper::GetAttributeIndex()
{
static AttributeIndexSet attributeIndex(
ELEMENTS(MechControlsMapper::AttributePointers),
MechControlsMapper::AttributePointers,
Subsystem::GetAttributeIndex()
);
return attributeIndex;
}
//#############################################################################
// Construction and Destruction
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b02f0
//
MechControlsMapper::MechControlsMapper(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
Subsystem( // FUN_004ac530(...,0,0)
owner,
subsystem_ID,
subsystem_resource,
shared_data
)
{
Check(owner);
//
// This mapper is always the active "Performance" of its Mech; the engine
// installs the default InterpretControls member-pointer (vtable @0050f120)
// into the simulation slot (this[7..9]).
//
SetPerformance(&MechControlsMapper::InterpretControls);
//
// Clear all published control inputs / demands.
//
stickPosition.x = 0.0f;
stickPosition.y = 0.0f;
throttlePosition = 0.0f;
pedalsPosition = 0.0f;
speedDemand = 0.0f;
turnDemand = 0.0f;
reverseThrust = 0;
lookForward = 0;
lookLeft = 0;
lookRight = 0;
lookDown = 0;
lookBehind = 0;
lookState = LookNone;
torsoUp = 0;
torsoDown = 0;
torsoLeft = 0;
torsoRight = 0;
torsoCenter = 0;
controlMode = BasicMode;
displayMode = 0;
pilotArrayPage = 0;
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;
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)
{
controlMode = (ControlMode)(controlMode + 1);
if (controlMode > VeteranMode)
{
controlMode = BasicMode;
}
NotifyOfControlModeChange(controlMode); // vtable+0x48
Mech *mech = GetMech();
int torso = *(int *)((int)mech + 0x438);
int cockpit = *(int *)((int)mech + 0x5b4);
if (controlMode == BasicMode)
{
*(LWord *)(torso + 0x1f0) = 0; // no free aim
*(LWord *)(torso + 0x274) = 1; // auto-center on
*(LWord *)(torso + 0x220) = *(LWord *)(torso + 0x228); // recenter yaw
*(LWord *)(torso + 0x224) = *(LWord *)(torso + 0x22c); // recenter pitch
*(LWord *)(cockpit + 0x2a0) = 1;
}
else if ((unsigned)(controlMode - 1) < 2) // Standard / Veteran
{
*(LWord *)(torso + 0x220) = *(LWord *)(torso + 0x230); // yaw -> limit
*(LWord *)(torso + 0x224) = *(LWord *)(torso + 0x234); // pitch -> limit
}
}
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004afcac -- cycle the HUD display mode (0 -> 1 -> 2 -> 0).
//
void
MechControlsMapper::CycleDisplayModeMessageHandler(
ReceiverDataMessageOf<ControlsButton> *message
)
{
Check(this);
Check(message);
if (message->dataContents > 0)
{
displayMode = displayMode + 1;
if (displayMode > 2)
{
displayMode = 0;
}
NotifyOfDisplayModeChange(displayMode); // vtable+0x4c
}
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004afce8 -- toggle voice assist on the Mech's pilot subsystem.
//
void
MechControlsMapper::ToggleVoiceAssistMessageHandler(
ReceiverDataMessageOf<ControlsButton> *message
)
{
Check(this);
Check(message);
if (message->dataContents > 0)
{
// FUN_004bff74(mech->subsystem(0x190)) -- toggles the voice-assist flag.
ToggleVoiceAssist(*(int *)((int)GetMech() + 0x190));
}
Check_Fpu();
}
//#############################################################################
// Model Support -- InterpretControls
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// Owner accessor. The mapper is always owned by a Mech; the decomp used the
// stored owner pointer directly (this[?]). Routed here through the engine's
// Subsystem::GetEntity().
//
Mech*
MechControlsMapper::GetMech()
{
Check(this);
return (Mech *)GetEntity();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004afd10 -- the active master-Mech performance. Reads the raw stick,
// throttle, pedals and look buttons and writes the locomotion demands
// (speedDemand / turnDemand), the torso aim and the eyepoint pose. The
// per-control-mode steering/torso math is faithful to the decomp; the Mech
// sub-object offsets are documented at the top of this file. BEST-EFFORT on
// the precise Mech-field semantics.
//
void
MechControlsMapper::InterpretControls(Scalar time_slice)
{
Check(this);
//
// OFFSET RECONCILIATION (the revival fix): the earlier draft read the owner
// at RAW binary offsets, including TYPED-POINTER arithmetic on Mech*
// ("*(Scalar *)(mech + 0x34c)" == mech + 0x34c*sizeof(Mech) -- a wild pointer,
// the AV that kept this tick bypassed). Every owner access now goes through
// the reconciled members: mech+0x34c -> reverseStrideLength (naming caveat:
// LoadLocomotionClips measures it from the rr* clips -- it is the TOP/run
// cycle speed the throttle scales), mech+0x534 -> walkStrideLength,
// mech+0x5c0 -> forwardThrottleScale, mech+0x438 -> sinkSourceSubsystem (the
// re-based binary-exact Torso), mech+0x5b4 -> hudSubsystem (the HUD -- raw
// factory part_012.c:10164; MechTech's 0x104 alloc cannot hold +0x28c).
// Torso/HUD writes go through their real members (analog axes @0x1F0/0x1F4,
// horizontalEnabled @0x250, freeAimSlew @0x28C). Null-guards are bring-up
// safety for mechs without those subsystems (the binary trusts the data).
//
Mech *mech = GetMech();
Torso *torso = (Torso *)mech->GetTorsoSubsystem(); // @0x438 (raw iVar5)
HUD *cockpit= (HUD *)mech->GetHudSubsystem(); // @0x5b4 (raw iVar3)
//
// BRING-UP INPUT BRIDGE (dev box; env BT_KEY_BRIDGE=0 to disable on pods).
// The engine controls push refreshes the registered input attributes from
// DEVICE elements every frame BEFORE this Performance runs; on a keyboard
// dev box the RIO scalar throttle channel doesn't exist and its element
// reads a constant (observed: throttlePosition forced back to 1.0 each
// frame -> the mech could never stop). So key state is written HERE --
// after the push, immediately before interpretation -- making the keyboard
// authoritative on the dev box. Interpretation below stays 100% authentic.
//
{
static const int s_keyBridge =
(getenv("BT_KEY_BRIDGE") == 0 || *getenv("BT_KEY_BRIDGE") != '0');
if (s_keyBridge && mech != 0 && application != 0
&& (Entity *)mech == application->GetViewpointEntity())
{
float key_throttle = gBTDrive.forced
? gBTDrive.forcedThrottle : gBTDrive.throttle;
float key_turn = gBTDrive.forced ? 0.0f : gBTDrive.turn;
// Headless harness (forced mode only): BT_FORCE_TURN holds a steering
// demand; BT_FORCE_SECONDS releases the forced throttle after n
// sim-seconds while KEEPING the turn (the turn-in-place repro).
if (gBTDrive.forced)
{
static float s_hClock = 0.0f, s_hLimit = -1.0f, s_hTurn = -999.0f;
if (s_hLimit < 0.0f)
{
const char *fs = getenv("BT_FORCE_SECONDS");
s_hLimit = fs ? (float)atof(fs) : 0.0f;
}
if (s_hTurn < -900.0f)
{
const char *ft = getenv("BT_FORCE_TURN");
s_hTurn = ft ? (float)atof(ft) : 0.0f;
}
key_turn = s_hTurn;
// BT_GOTO beeline (DEBUG harness): the turn demand is computed in
// mech4.cpp's drive block (where position/heading are in scope)
// and published through gBTGotoTurn/gBTGotoActive.
{
extern int gBTGotoActive;
extern float gBTGotoTurn;
extern float gBTGotoThrottle;
if (gBTGotoActive)
{
key_turn = gBTGotoTurn;
key_throttle = key_throttle * gBTGotoThrottle;
}
}
// BT_FORCE_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_FLIP=<t>: invert the forced throttle after t sim-seconds
// (the mid-stride direction-change repro).
static float s_hFlip = -1.0f;
if (s_hFlip < 0.0f)
{
const char *ff = getenv("BT_FORCE_FLIP");
s_hFlip = ff ? (float)atof(ff) : 0.0f;
}
if (s_hLimit > 0.0f || s_hFlip > 0.0f)
{
s_hClock += time_slice;
if (s_hFlip > 0.0f && s_hClock > s_hFlip)
{
key_throttle = -key_throttle;
}
else if (s_hLimit > 0.0f && s_hClock > s_hLimit)
{
key_throttle = 0.0f;
}
}
}
throttlePosition = (key_throttle >= 0.0f) ? key_throttle : -key_throttle;
reverseThrust = (key_throttle < 0.0f) ? 1 : 0;
stickPosition.x = key_turn;
stickPosition.y = 0.0f;
}
}
//
//------------------------------------------------------------------
// Throttle -> forward speed demand (reverse thrust inverts & rescales)
//------------------------------------------------------------------
//
// CANARY HEAL: something stomps mech->forwardThrottleScale at runtime
// nondeterministically (observed 0.14 and -1.0 across runs; ctor sets 1.0
// guarded) -- a wild raw-offset write, hunt via ba w4 on mech+0x5c0 (on the
// ledger). Until the writer is caught, restore a sane value and log.
// (band tightened: the stomp value 0.14 -- observed repeatedly, ~8 degrees
// in radians, likely an angle write landing on the wrong member -- passed
// the old 0.01..100 sanity band and the user's forward speed silently
// capped at 8.6 u/s while reverse ran full. The ctor guard currently
// always yields exactly 1.0, so heal anything else.)
if (mech->forwardThrottleScale != 1.0f)
{
DEBUG_STREAM << "[mppr] forwardThrottleScale STOMPED to "
<< mech->forwardThrottleScale << " -- healed to 1.0" << "\n" << std::flush;
mech->forwardThrottleScale = 1.0f;
}
if (reverseThrust < 1)
{
speedDemand =
mech->reverseStrideLength * throttlePosition * mech->forwardThrottleScale;
}
else
{
speedDemand = -mech->reverseStrideLength * throttlePosition;
}
{
static int s_cTrace = -1;
if (s_cTrace < 0) { const char *e = getenv("BT_MPPR_TRACE"); s_cTrace = (e && *e != '0') ? 1 : 0; }
if (s_cTrace)
{
static float s_cAcc = 0.0f; s_cAcc += time_slice;
if (s_cAcc >= 0.5f) { s_cAcc = 0.0f;
DEBUG_STREAM << "[mppr-c] thr=" << throttlePosition << " rev=" << reverseThrust
<< " topSpd=" << mech->reverseStrideLength
<< " fScale=" << mech->forwardThrottleScale
<< " -> dmd=" << speedDemand << "\n" << std::flush; }
}
}
//
//------------------------------------------------------------------
// Square the stick for a soft response; cube the pedals. Preserve sign.
//------------------------------------------------------------------
//
Scalar stick_x = stickPosition.x * stickPosition.x;
Scalar stick_y = stickPosition.y * stickPosition.y;
Scalar pedal_3 = pedalsPosition * pedalsPosition * pedalsPosition;
if (stickPosition.x < 0.0f) stick_x = -stick_x; // _DAT_004b0274 == 0.0f
if (stickPosition.y < 0.0f) stick_y = -stick_y;
turnDemand = 0.0f;
if (controlMode == BasicMode)
{
//
// Basic: stick yaw drives the turn directly; stick pitch drives the
// torso pitch. The achievable turn rate is clamped down with speed.
//
turnDemand = stick_x;
if (torso)
{
torso->SetAnalogElevationAxis(stick_y); // raw torso+0x1f4 (500)
torso->SetAnalogTwistAxis(0.0f); // raw torso+0x1f0
}
if (cockpit)
{
cockpit->SetFreeAimSlew(0.0f); // raw cockpit+0x28c
}
Scalar max_turn =
(mech->reverseStrideLength - mech->walkStrideLength)
* (1.0f - Abs(turnDemand)) // _DAT_004b0278 == 1.0f
+ mech->walkStrideLength;
if (fabsf(turnDemand) > JM_CLOSE_ENOUGH) // _DAT_004b027c
{
Clamp(speedDemand, -max_turn, max_turn);
}
}
else if (controlMode == StandardMode)
{
//
// Standard: torso free-aim follows the stick yaw; pedals provide the
// turn. Turn rate clamped as in basic mode.
//
if (torso == 0 || !torso->GetHorizontalEnabled()) // raw torso+0x250 == 0
{
if (cockpit) cockpit->SetFreeAimSlew(stick_x); // raw cockpit+0x28c
}
else
{
torso->SetAnalogTwistAxis(stick_x); // raw torso+0x1f0
}
if (torso) torso->SetAnalogElevationAxis(stick_y); // raw torso+0x1f4
turnDemand = (pedal_3 == 0.0f) ? 0.0f : pedal_3;
Scalar max_turn =
(mech->reverseStrideLength - mech->walkStrideLength)
* (1.0f - Abs(turnDemand))
+ mech->walkStrideLength;
if (fabsf(turnDemand) > JM_CLOSE_ENOUGH)
{
Clamp(speedDemand, -max_turn, max_turn);
}
}
else if (controlMode == VeteranMode)
{
//
// Veteran: as standard, but no speed-dependent turn clamp.
//
if (torso == 0 || !torso->GetHorizontalEnabled())
{
if (cockpit) cockpit->SetFreeAimSlew(stick_x);
}
else
{
torso->SetAnalogTwistAxis(stick_x);
}
if (torso) torso->SetAnalogElevationAxis(stick_y);
turnDemand = (pedal_3 == 0.0f) ? 0.0f : pedal_3;
}
//
//------------------------------------------------------------------
// Look / eyepoint selection. Choose a look direction from the buttons;
// when it changes, re-aim the eyepoint and re-slave the camera children.
//------------------------------------------------------------------
//
previousLookState = lookState;
if (lookLeft > 0) lookState = LookLeftState;
else if (lookRight > 0) lookState = LookRightState;
else if (lookBehind > 0) lookState = LookBehindState;
else if (lookDown > 0) lookState = LookDownState;
else lookState = LookNone;
if (lookState != previousLookState)
{
// TODO(look-eyepoint): the commit block below writes the eyepoint pose at
// binary offsets whose Mech-member mapping is CONFLICTED (mech+0x360 is
// claimed by mechName, +0x410 by stateFlags, +0x7bc by weaponRoster --
// and the old draft even routed them through the TORSO pointer). Arbitrate
// those labels against the raw before enabling; the port has no eyepoint
// consumer yet. Look-state SELECTION above stays live (this-only).
DEBUG_STREAM << "[mppr] look state -> " << lookState
<< " (eyepoint commit deferred)\n" << std::flush;
}
else if (0) // DEFERRED body (kept for the reconstruction record)
{
const int torso = 0; // shadow: the raw-offset text below predates
// the reconciliation (see TODO above)
EulerAngles eyepoint;
ChildIterator cameras(torso + 0x7bc); // FUN_004afacf
switch (lookState)
{
case LookNone:
eyepoint = EulerAngles::Identity; // DAT_004e0f8c
*(LWord *)(torso + 0x378) = 1; // re-slave eyepoint
for (int *camera; (camera = cameras.Next()) != 0; )
{
*(LWord *)(camera + 0x3e0) = (*(int *)(camera + 0x334) == 0);
}
break;
case LookLeftState:
eyepoint = EulerAngles::Identity;
eyepoint.pitch = *(Scalar *)(torso + 0x564);
*(LWord *)(torso + 0x378) = 0;
for (int *camera; (camera = cameras.Next()) != 0; )
{
*(LWord *)(camera + 0x3e0) = 0;
}
break;
case LookRightState:
eyepoint = EulerAngles::Identity;
eyepoint.pitch = *(Scalar *)(torso + 0x568);
*(LWord *)(torso + 0x378) = 0;
for (int *camera; (camera = cameras.Next()) != 0; )
{
*(LWord *)(camera + 0x3e0) = 0;
}
break;
case LookBehindState:
eyepoint = EulerAngles::Identity;
eyepoint.yaw = PI; // 0x40490fdb
eyepoint.pitch = *(Scalar *)(torso + 0x570);
*(LWord *)(torso + 0x378) = *(LWord *)(torso + 0x410);
for (int *camera; (camera = cameras.Next()) != 0; )
{
*(LWord *)(camera + 0x3e0) = *(int *)(camera + 0x334);
}
break;
case LookDownState:
eyepoint = EulerAngles::Identity;
eyepoint.pitch = *(Scalar *)(torso + 0x56c);
*(LWord *)(torso + 0x378) = 0;
for (int *camera; (camera = cameras.Next()) != 0; )
{
*(LWord *)(camera + 0x3e0) = 0;
}
break;
}
*(EulerAngles *)(torso + 0x360) = eyepoint; // commit eyepoint rotation
}
Check_Fpu();
}
//#############################################################################
// Pilot-array (other-player roster) management
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b0600 -- build the pilot roster once, lazily, from the application's
// "Players" group (skipping spectators flagged 0x40).
//
void
MechControlsMapper::BuildPilotArray()
{
if (pilotArrayBuilt)
{
return;
}
pilotArrayBuilt = True;
pilotCount = 0;
EntityGroup *players =
application->GetEntityManager()->FindGroup("Players"); // @0050f44b
if (players != 0)
{
ChainIteratorOf<Node*> pilots(players->groupMembers); // FUN_00421414
for (Node *entry; (entry = pilots.ReadAndNext()) != 0; )
{
if ((*(byte *)((int)entry + 0x29) & 0x40) == 0) // not a spectator
{
++pilotCount;
}
}
}
// 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 (pilotCount > PilotArraySlots - 1)
pilotCount = PilotArraySlots - 1;
pilotIDs = new int[pilotCount]; // FUN_004022b0
for (int i = 0; i <= pilotCount; ++i)
{
pilotArray[i] = 0;
}
FillPilotArray(); // FUN_004b06cc
ChooseDefaultPilot(); // FUN_004b07f0
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b06cc -- slot 0 holds the local pilot; the remaining slots are filled
// from the "Players" group (paged by pilotArrayPage).
//
void
MechControlsMapper::FillPilotArray()
{
for (int i = 0; i < pilotCount; ++i)
{
pilotIDs[i] = -1;
}
//
// The local "station" (the local network player record) lives at
// application+0x6c in the binary; its pilot roster entry at +0x190.
// ENGINE DRIFT FIX (the AV that kept the mapper tick bypassed): the WinTesla
// Application exposes the local player through GetMissionPlayer() (APP.h:169)
// -- the same reconciliation SendFakeButtonEvent uses (this file's sibling in
// btl4mppr.cpp). The old draft read the 1995 byte offsets through the 2007
// object -> wild "pilot" pointer -> access violation.
//
Player *local_pilot =
(application != 0) ? application->GetMissionPlayer() : 0;
if (local_pilot == 0)
{
return;
}
pilotArray[0] = (Pilot *)local_pilot;
pilotIDs[0] = local_pilot->GetEntityID(); // raw read pilot+0x1e0 (the entity id)
EntityGroup *players =
application->GetEntityManager()->FindGroup("Players");
if (players != 0)
{
ChainIteratorOf<Node*> pilots(players->groupMembers);
// skip earlier pages
int skip = pilotArrayPage * pilotCount;
while (skip > 0 && pilots.ReadAndNext() != 0)
{
--skip;
}
int slot = 1;
for (Node *entry; (entry = pilots.ReadAndNext()) != 0; )
{
// TODO(mp-roster): the binary skips spectators via a flag byte at
// entry+0x29 (bit 0x40) -- its WinTesla home is unmapped; solo has no
// spectators, so the check is omitted until the flag is reconciled.
if (entry == (Node *)local_pilot) // already slot 0
{
continue;
}
pilotArray[slot] = (Pilot *)entry;
pilotIDs[slot] = ((Entity *)entry)->GetEntityID(); // raw entry+0x1e0
if (++slot >= pilotCount)
{
break;
}
}
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b07f0 -- pick the initial target for the local pilot: the next pilot id
// after the local one (or the first non-self pilot).
//
void
MechControlsMapper::ChooseDefaultPilot()
{
int target = 0;
if (pilotCount != 1)
{
int index = 0;
if (pilotCount == pilotIDs[0])
{
for (int i = 1; i < pilotCount; ++i)
{
if (pilotIDs[i] != 1)
{
index = i;
break;
}
}
}
else
{
for (int i = 1; i < pilotCount; ++i)
{
if (pilotIDs[i] == pilotIDs[0] + 1)
{
index = i;
break;
}
}
}
Pilot *chosen = pilotArray[index];
if (chosen != 0)
{
target = *(int *)((int)chosen + 0x1fc);
}
*(int *)((int)pilotArray[0] + 0x284) = target;
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b049c -- set the local pilot's current target to the pilot on the given
// roster page.
//
void
MechControlsMapper::UpdateCurrentPilot(int page)
{
if (pilotArrayBuilt && pilotArray[0] != 0)
{
int target = 0;
Pilot *chosen = pilotArray[page];
if (chosen != 0)
{
target = *(int *)((int)chosen + 0x1fc);
}
*(int *)((int)pilotArray[0] + 0x284) = target;
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b04d8 -- choose the nearest living pilot (other than self) as the local
// pilot's current target.
//
void
MechControlsMapper::ChooseNearestPilot(int self_id)
{
if (!pilotArrayBuilt || pilotArray[0] == 0)
{
return;
}
int target = 0;
Scalar nearest_distance = 0.0f;
int nearest_index = 0;
Logical none_yet = True;
for (int i = 1; i < pilotCount; ++i)
{
int entity = *(int *)((int)pilotArray[i] + 0x1fc);
Vector3D delta;
delta.Subtract( // FUN_00408644
*(Vector3D *)((int)pilotArray[i] + 0x100),
*(Vector3D *)((int)pilotArray[0] + 0x100)
);
Scalar distance = delta.x * delta.x + delta.y * delta.y + delta.z * delta.z;
if (none_yet)
{
if (!Is_Destroyed(entity) && entity != self_id)
{
none_yet = False;
nearest_distance = distance;
nearest_index = i;
}
}
else if (!Is_Destroyed(entity) && entity != self_id
&& distance < nearest_distance)
{
nearest_distance = distance;
nearest_index = i;
}
}
if (!none_yet)
{
Pilot *chosen = pilotArray[nearest_index];
if (chosen != 0)
{
target = *(int *)((int)chosen + 0x1fc);
}
*(int *)((int)pilotArray[0] + 0x284) = target;
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @004b0898 -- bounds-checked roster accessor.
//
Pilot *
MechControlsMapper::GetPilot(int index)
{
if (index >= pilotCount || index < 0)
{
return 0;
}
return pilotArray[index];
}
//#############################################################################
// Configuration / event-mapping interface (secondary vtable @0050f498)
//
// These are "not overridden" defaults: a platform-specific derived mapper is
// expected to supply real implementations. The base class traps the call.
//
//
// @004b029c
//
void
MechControlsMapper::ExitConfiguration(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);
}