Files
BT411/game/reconstructed/mechmppr.cpp
T
arcattackandClaude Opus 4.8 0060a3e1ca gauge wave P2b: build the Comm KILLS/DEATHS pilot roster (PilotList)
PilotList (keyword "pilotList") was PROSE-ONLY, so the Comm surface showed only
the baked btcomm.pcx labels.  Reconstructed all 7 functions from part_014.c:
3156-3434 (Make/ctor/dtor/BecameActive/TestInstance/Execute/DrawMechIcon), with
the 8x(x,y,layoutMode) layout table DAT_0051af88 PE-parsed exact from BTL4OPT.EXE.
Draws ONE roster slot/frame (round-robin) from the viewpoint mech's cockpit-mapper
pilot roster.

- Roster FEED: new BTResolveRosterPilot(slot) bridge in mechmppr.cpp (a complete-Mech
  TU) resolves the viewpoint mech's ControlsMapper (subsystemArray[0]) -> GetPilot;
  btl4gau3.cpp reads the returned pilot at raw BTPlayer offsets.  The mapper's
  FillPilotArray already fills the roster (pilotArray[0]=GetMissionPlayer,
  [1..]=FindGroup("Players")).
- KILLS = killCount@0x27c (real; ScoreMessageHandler increments it in combat).
- DEATHS: the binary reads pad_0x280 which has NO writer anywhere (a shipped dead
  field -> perpetual 0).  FIX per "if it doesn't work, fix it": read the real
  deaths counter Player::deathCount@0x200 (VehicleDeadMessageHandler increments it)
  so DEATHS is meaningful in MP.
- DrawMechIcon: App+0xC8 name-bitmap cache is unwired (same deferral PlayerStatus
  uses) -> LookupPlayerNameBitmap returns NULL -> the tinted name box (never an AV).

Dropped the bogus x,y from the header ctor (positions come from DAT_0051af88).
/FORCE-safe (all vtable slots real; link log clean, no unresolved PilotList/
BTResolveRosterPilot).  Verified DBASE+dev gauges: the Comm surface now renders the
live local pilot row (KILLS 0 DEATHS 0 + name box; 0/0 authentic in solo -- the
combat scoring feed that moves them is Phase 3), combat un-regressed (DESTROYED),
0 crashes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 17:24:24 -05:00

1206 lines
37 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;
//
// 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;
pilotArray[0] = 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.
// The decomp body for this thunk was not captured; per the VTVControlsMapper
// pattern it routes the press through the configuration / mapping interface.
// BEST-EFFORT.
//
void
MechControlsMapper::ConfigureMappableMessageHandler(
ReceiverDataMessageOf<ControlsButton> *message
)
{
Check(this);
Check(message);
if (message->dataContents > 0)
{
// TODO: verify against @004afbc4 -- expected to add or erase a
// temporary mapping for the pressed mappable button.
AddOrErase(message->dataContents, (ControlsButton *)0);
}
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// @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_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 poweredSubsystems --
// 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;
}
}
}
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()
{
Fail("ExitConfiguration not overridden!\n"); // MECHMPPR.CPP:0x245
}
//
// (companion to the above; "EnterConfiguration not overridden!" @0050f370)
//
void
MechControlsMapper::EnterConfiguration(
ControlsButton * /*destination*/,
Receiver * /*receiver*/,
Receiver::MessageID /*activation_message_id*/,
Receiver::MessageID /*configuration_message_id*/
)
{
Fail("EnterConfiguration not overridden!\n");
}
//
// @004b02b8
//
void
MechControlsMapper::CreateTemporaryEventMappings(
Receiver * /*receiver*/,
Receiver::MessageID /*config_message_id*/
)
{
Fail("Unhandled mapping!\n"); // MECHMPPR.CPP:0x24f
}
//
// @004b02d4
//
void
MechControlsMapper::AddOrErase(
unsigned int /*button_ID*/,
ControlsButton * /*destination*/
)
{
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
}