Files
BT412/game/reconstructed/mechmppr.hpp
T
arcattackandClaude Fable 5 a110041a70 Fix "can't turn in MP": pilotArray[1] overrun stomped controlMode
User report from the first live 2-window session: throttle worked, A/D turn
did nothing.  [mppr] showed the input REACHING the mapper (stickX=-1) but
turnDemand=0 and controlMode = pointer garbage (73583232 / 217478632 --
different per instance), from frame 1, MP only (solo mode=0).

Hunted with a cdb hardware write-breakpoint (ba w4 armed at ctor time on
&controlMode, compiled offset +0x128 from the [mode-ctor] probe).  Caught the
writers red-handed: MechControlsMapper::BuildPilotArray / FillPilotArray.

ROOT CAUSE -- a shrunk-span array: the binary reserves a FIXED 10-slot pilot
block @0x15C..0x183 ((pilotArrayBuilt@0x184 - 0x15C)/4 = 10); the recon
declared `Pilot *pilotArray[1]` ("variable length"), so every write past
slot 0 stomps the members declared after it.  In MP, FillPilotArray wrote the
PEER's Player pointer into slot 1 = over controlMode -> the turn shaping
dispatched on pointer garbage -> turnDemand 0.  MASKED in solo: the zero-loop
overrun wrote 0 there, and 0 == BasicMode.  This was ALSO the real cause of
task #48's "turnDemand zeroes out in -net" observation (worked around then
with the direct goto-turn; attribution corrected in the KB).

Fix: pilotArray sized to the binary's own 10-slot span; ctor zeroes all
slots; BuildPilotArray clamps pilotCount to capacity (local + 9 remotes; an
8-pod game fits).  Diagnostics kept (BT_MODE_LOG ctor probe; [mppr] line now
prints mapper/&mode).

VERIFIED: MP session holds mode=0 throughout, turnDemand flows nonzero, the
mech turns (heading -1.33) and closes on the peer; solo kill chain intact.
KB: new "shrunk-span array" gotcha (reconstruction-gotchas 5) with the cdb
ba-w4 recipe; multiplayer.md task-#48 note corrected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 18:16:46 -05:00

309 lines
11 KiB
C++

//===========================================================================//
// File: mechmppr.hpp //
// 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 (Ghidra pseudo-C, cluster
// @004afbe0-@004b08c0) cross-referenced against the Red Planet sibling
// RP\VTVMPPR.cpp / VTVMPPR.h (VTVControlsMapper) -- same engine, same
// mapper pattern. The class name "MechControlsMapper" is recovered verbatim
// from the Derivation name string @0050f173. Attribute and message names are
// recovered from the class string pool @0050f180-@0050f367 and the attribute
// table @0050efd0; the configuration / event-mapping stub strings come from
// @0050f370-@0050f44a.
//
// Field offsets in comments are the byte offsets observed in the decompiled
// object (e.g. "@0x114" == this[0x45]). Names follow VTVControlsMapper where
// the role is identical; Mech-specific members (torso, pilot array) are named
// from the attribute table and flagged best-effort where uncertain.
//
// This is the BattleTech analog of Red Planet's VTVControlsMapper. Like that
// class it is the active "Performance" of the master, dynamic Mech: each
// simulation frame InterpretControls() reads the raw stick/throttle/pedals and
// the look/torso buttons and produces speedDemand / turnDemand plus the torso
// and eyepoint pose used by the locomotion and view code.
//
#if !defined(MECHMPPR_HPP)
# define MECHMPPR_HPP
#if !defined(SUBSYSTM_HPP)
# include <subsystm.hpp>
#endif
#if !defined(CONTROLS_HPP)
# include <controls.hpp> // ControlsButton, ControlsJoystick
#endif
//##################### Forward Class Declarations #######################
class Mech;
class Pilot;
//###########################################################################
//######################### MechControlsMapper ##########################
//###########################################################################
//
// Primary vtable @0050f45c (slot 0 = scalar-deleting destructor @004b044c;
// slots 1-13 inherited from Subsystem; slots 18/19 -- vtable+0x48/+0x4c --
// are the NotifyOf* hooks below). A secondary "controls mapper /
// configurator" interface vtable @0050f498 supplies the EnterConfiguration /
// ExitConfiguration / event-mapping stubs (all "not overridden" by default).
//
class MechControlsMapper:
public Subsystem
{
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Shared Data Support
//
public:
// WinTesla engine refactored the static derivation/handler/attribute
// objects behind Get* accessors (avoids static-init ordering bugs).
static Derivation *GetClassDerivations(); // @0050ee10
static SharedData DefaultData;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Messaging Support
//
// Message IDs and handler names recovered from the message table
// @0050ee40 (string pool @0050f180). IDs 3..0x13 (the auxiliary-equipment
// and zoom buttons) are routed through the shared ConfigureMappable
// handler @004afbc4; the last three drive dedicated handlers.
//
public:
enum {
Aux1QuadMessageID = Subsystem::NextMessageID, // 3
Aux1Eng1MessageID, Aux1Eng2MessageID,
Aux1Eng3MessageID, Aux1Eng4MessageID,
Aux2QuadMessageID,
Aux2Eng1MessageID, Aux2Eng2MessageID,
Aux2Eng3MessageID, Aux2Eng4MessageID,
Aux3QuadMessageID,
Aux3Eng1MessageID, Aux3Eng2MessageID,
Aux3Eng3MessageID, Aux3Eng4MessageID,
ZoomInMessageID, // 0x12
ZoomOutMessageID, // 0x13
CycleControlModeMessageID, // 0x14
CycleDisplayModeMessageID, // 0x15
ToggleVoiceAssistMessageID, // 0x16
NextMessageID
};
void
ConfigureMappableMessageHandler( // @004afbc4 (shared)
ReceiverDataMessageOf<ControlsButton> *message
);
void
CycleControlModeMessageHandler( // @004afbe0
ReceiverDataMessageOf<ControlsButton> *message
);
void
CycleDisplayModeMessageHandler( // @004afcac
ReceiverDataMessageOf<ControlsButton> *message
);
void
ToggleVoiceAssistMessageHandler( // @004afce8
ReceiverDataMessageOf<ControlsButton> *message
);
private:
static const HandlerEntry MessageHandlerEntries[]; // @0050ee40
protected:
static MessageHandlerSet& GetMessageHandlers();
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Attribute Support
//
// Attribute IDs / names / member offsets recovered from the attribute
// table @0050efd0. (The recorded member offsets carry the engine's
// low-bit "scalar" tag, e.g. 0x115 -> &stickPosition @0x114.)
//
public:
enum {
StickPositionAttributeID = Subsystem::NextAttributeID, // 3
ThrottlePositionAttributeID, // 4
PedalsPositionAttributeID, // 5
ReverseThrustAttributeID, // 6
SpeedDemandAttributeID, // 7
TurnDemandAttributeID, // 8
LookForwardAttributeID, // 9
LookLeftAttributeID, // 0xa
LookRightAttributeID, // 0xb
LookBehindAttributeID, // 0xc
LookDownAttributeID, // 0xd
TorsoUpAttributeID, // 0xe
TorsoDownAttributeID, // 0xf
TorsoLeftAttributeID, // 0x10
TorsoRightAttributeID, // 0x11
TorsoCenterAttributeID, // 0x12
ControlModeAttributeID, // 0x13
DisplayModeAttributeID, // 0x14
PilotArrayPageAttributeID, // 0x15
PilotArrayAttributeID, // 0x16
NextAttributeID
};
private:
static const IndexEntry AttributePointers[]; // @0050efd0
protected:
static AttributeIndexSet& GetAttributeIndex();
//
// Published attributes.
//
public:
ControlsJoystick stickPosition; // @0x114 (x @0x114, y @0x118)
Scalar throttlePosition; // @0x11C
Scalar pedalsPosition; // @0x120
ControlsButton reverseThrust; // @0x124
Scalar speedDemand; // @0x128 (output)
Scalar turnDemand; // @0x12C (output)
ControlsButton lookForward; // @0x130
ControlsButton lookLeft; // @0x134
ControlsButton lookRight; // @0x138
ControlsButton lookBehind; // @0x13C
ControlsButton lookDown; // @0x140
ControlsButton torsoUp; // @0x144
ControlsButton torsoDown; // @0x148
ControlsButton torsoLeft; // @0x14C
ControlsButton torsoRight; // @0x150
ControlsButton torsoCenter; // @0x154
int pilotArrayPage; // @0x158 (which "page" of pilots)
// FIXED 10-SLOT block, the binary's own reservation: 0x15C..0x183 =
// (pilotArrayBuilt@0x184 - 0x15C)/4 = 10 pointers ([0]=local, [1..9]
// remote; an 8-pod game + the local fits). The old `[1]` "variable
// length" declaration made EVERY write past slot 0 stomp the members
// declared after it: in MP, FillPilotArray wrote the PEER's Player
// pointer over controlMode (the turn shaping dispatched on pointer
// garbage -> turnDemand 0 -> "I can't turn"); in solo the zero-loop
// wrote 0 there, masked only because 0 == BasicMode. Caught live via
// cdb `ba w4` on &controlMode (task #51).
enum { PilotArraySlots = 10 };
Pilot *pilotArray[PilotArraySlots]; // @0x15C..0x183
enum ControlMode {
BasicMode = 0,
StandardMode = 1,
VeteranMode = 2
} controlMode; // @0x190
int displayMode; // @0x194 (0..2, cycles)
int lookState; // @0x198 eyepoint look selection
int previousLookState; // @0x19C
protected:
Logical pilotArrayBuilt; // @0x184 built-once flag
int *pilotIDs; // @0x188 parallel id table
int pilotCount; // @0x18C
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Model Support
//
public:
typedef void
(MechControlsMapper::*Performance)(Scalar time_slice);
void
SetPerformance(Performance performance)
{
Check(this);
activePerformance = (Simulation::Performance)performance;
}
// The active master-Mech performance (vtable default @0050f120).
void
InterpretControls(Scalar time_slice); // @004afd10
// Owner accessor -- the mapper's owning entity is always a Mech.
// (Subsystem::GetEntity() returns the generic Entity*; the decomp
// pseudo-C used the owner pointer directly.)
Mech*
GetMech();
//
// Look / torso eyepoint selection.
//
public:
enum LookState {
LookNone = 0,
LookLeftState = 1,
LookRightState = 2,
LookBehindState = 3,
LookDownState = 4
};
//
// Pilot-array (other-player roster) management. Built lazily on first
// use from the "Players" group of the application object (@0050f44b).
//
public:
void
BuildPilotArray(); // @004b0600
Pilot *
GetPilot(int index); // @004b0898
void
ChooseNearestPilot(int self_id); // @004b04d8
void
ChooseDefaultPilot(); // @004b07f0
void
UpdateCurrentPilot(int page); // @004b049c
protected:
void
FillPilotArray(); // @004b06cc
//
// Configuration / event-mapping interface (the secondary vtable @0050f498).
// Platform-specific derived mappers override these; the base mapper traps
// any unrouted call.
//
public:
virtual void
EnterConfiguration(
ControlsButton *destination,
Receiver *receiver,
Receiver::MessageID activation_message_id,
Receiver::MessageID configuration_message_id
);
virtual void
ExitConfiguration(); // @004b029c
virtual void
CreateTemporaryEventMappings( // @004b02b8
Receiver *receiver,
Receiver::MessageID config_message_id
);
virtual void
AddOrErase( // @004b02d4
unsigned int button_ID,
ControlsButton *destination
);
virtual void
NotifyOfControlModeChange(int new_mode); // @004b048c (vtable+0x48)
virtual void
NotifyOfDisplayModeChange(int new_mode); // @004b0494 (vtable+0x4c)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Construction and Destruction
//
public:
MechControlsMapper( // @004b02f0
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data = DefaultData
);
~MechControlsMapper(); // @004b044c
Logical
TestInstance() const; // @004b08c0
};
#endif