Files
BT411/game/reconstructed/mechmppr.cpp
T
arcattackandClaude Opus 5 2dd4ee3910 ROOT CAUSE: target designation wrote a Mech* into deathPending, disabling respawn (#57/#48)
MEASURED our compiled BTPlayer layout (new one-shot [layout] ctor diagnostic):
  sizeof=652 (0x28c)  killCount@0x270  pad_0x280@0x274
  objectiveMech@0x278  deathPending@0x284

The three target-designation sites wrote RAW at pilotArray[0]+0x284 intending the
BINARY's objectiveMech.  On our object 0x284 is deathPending -- the death-cycle
latch.  So EVERY target designation (the Comm bank 0x30-0x37, or the typed
t/y/u/i/o keys) stored a Mech pointer into deathPending, and a non-zero
deathPending makes VehicleDeadMessageHandler take its dedup early-return, which
skips the warp, ++deathCount, the PLAYER_DEAD record AND the drop-zone hunt.

=> designating a target silently disabled your respawn for the rest of the
   process.  That is #57's missing first cause: it explains David's very first
   death being swallowed with no prior death, and the 3-of-3 swallowed deaths in
   the final round.  It also explains the operator's persistent observation that
   the trouble began when the comms panel went live -- before the pilot list
   populated, there was nothing to designate.
   And the designation never worked either: the value never reached
   objectiveMech (input-audit finding #1).

FIX: all three sites now use named-member bridges in the complete-BTPlayer TU --
BTPilotSetObjectiveMech / BTPilotObjectiveMech / BTPilotVehicle / BTPilotPosition
/ BTVehicleDestroyed.  Also retires the raw +0x1fc (playerVehicle) reads and the
raw +0x100 position reads in ChooseNearestPilot, and routes its destroyed-check
through the real Mech predicate instead of the Is_Destroyed stub.

LAYOUT LOCKS: static_asserts on objectiveMech@0x278, deathPending@0x284,
killCount@0x270 and sizeof==0x28c, in the friend fn BTPlayerLayoutSelfCheck, so
a member shift fails the BUILD instead of silently clobbering the latch again.

Rigs: scratchpad/commstest.py (drives designation + captures panels),
valvetest.py, lampgallery.py (428-bitmap gallery for #48 visual search).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg
2026-07-25 08:33:22 -05:00

1602 lines
56 KiB
C++

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