Files
TeslaRel410/restoration/source410/BT/MECHMPPR.CPP
T
CydandClaude Fable 5 79aba77309 BT410 5.3.127-128: the mech can stand on its heels, and it knows when it is about to fall over -- the super stop, the instability model and the telemetry rings
THE MODEL RESOURCE NAMED THESE BLOCKS.  The ctor loads (part_012.c:15798)
tie the master perf's anonymous offsets to field names our
Mech__ModelResource already carried, and the names confirm the arithmetic
as surely as the arithmetic confirms the names:

  rec+0x48 -> +0x5bc  superStopAcceleration
  rec+0x80 -> +0x784  maxUnstableAcceleration
  rec+0x84 -> +0x788  unstableAccelerationEffect
  rec+0x88 -> +0x78c  unstableGunTheEngineEffect
  rec+0x94 -> +0x798  unstableStopedTurnEffect

THE SUPER STOP (@0x4aa353): hauling the throttle negative while the mech
still rolls forward is a hard brake, not a reverse.  The edge swaps the
gait's acceleration rate to the authored superStopAcceleration, marks the
update, and throws ONE random-signed lurch through both gyro channels;
while it holds above a walk it repeats a pitch kick every 0.4s -- the
shudder of a hundred tons standing on its heels.  A chassis authoring
-1.1 disables the system entirely (the sentinel must survive the model
read unguarded).

THE INSTABILITY MODEL (tail @0x4aab8e): three weighted terms -- filtered
acceleration (squared), gunning the engine, and turning on the spot --
clamped to 1 into unstablePercentage, which was ALREADY a published
cockpit attribute holding a staged zero, and republished to the gyro
(whose tip/sway machinery @004b275c consumes it; that's the next gyro
brick).

THE TELEMETRY RINGS (@0x4aafba): the binary's five rings ARE the engine's
AverageOf<Scalar>, and our telemetryFilter[5] has been sized 15 and unfed
since the layout wave.  FUN_0043ae47 is CalculateOlympicAverage -- a
judge-style trimmed mean, min and max discarded.  Two oddities kept
verbatim and flagged: the vel.y ring is never reduced, and the derived
acceleration's components come from a mongrel basis (only its length is
consumed).

CORRECTION to 5.3.122: 'realMaxSpeed' was a duplicate member for
mech+0x7a0, which our tree already carried as runSpeedMax; merged.  And
the +0x7a4 'computed my own cap' flag is set by the ctor's MYOMERS sweep
-- a mech with myomers broadcasts its cap, one without adopts the
streamed value.

Soaks (pod_render_superstop / pod_render_telem, BT_SUPERSTOP_SOAK cycling
+0.8 for 6s then -0.5 for 4s): the super stop engages on the correct edge
(speed 35.6, demand -28.0) and releases cleanly, repeatedly; the rings
fill and the instability sweeps its whole range to the 1.0 clamp; zero
faults.  The new gauge immediately earned its keep as a diagnostic -- it
pins high while walking because the mech demands 35.9 u/s and the
reconstructed gait delivers 7.0, which is now a measurable open question
rather than an invisible one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 00:46:25 -05:00

383 lines
12 KiB
C++

//===========================================================================//
// File: mechmppr.cpp //
// Project: BattleTech //
// Contents: Implementation details for the mech controls mapper //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. //
// All Rights reserved worldwide //
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
//===========================================================================//
#include <bt.hpp>
#pragma hdrstop
#if !defined(MECHMPPR_HPP)
# include <mechmppr.hpp>
#endif
#if !defined(MECH_HPP)
# include <mech.hpp>
#endif
#if !defined(TORSO_HPP)
# include <torso.hpp>
#endif
Derivation
MechControlsMapper::ClassDerivations(
Subsystem::ClassDerivations,
"MechControlsMapper"
);
//
//#############################################################################
// Published control-input attributes. The streamed control mappings bind (by
// name) to these; the pad entry fills the chain-vs-id gap at id 2.
//#############################################################################
//
const MechControlsMapper::IndexEntry
MechControlsMapper::AttributePointers[]=
{
{ (int)MechControlsMapper::MechControlsMapperPadFirstAttributeID,
"MechControlsMapperPad02",
(Simulation::AttributePointer)&MechControlsMapper::throttlePosition },
ATTRIBUTE_ENTRY(MechControlsMapper, StickPosition, stickPosition),
ATTRIBUTE_ENTRY(MechControlsMapper, ThrottlePosition, throttlePosition),
ATTRIBUTE_ENTRY(MechControlsMapper, PedalsPosition, pedalsPosition),
ATTRIBUTE_ENTRY(MechControlsMapper, ReverseThrust, reverseThrust),
ATTRIBUTE_ENTRY(MechControlsMapper, SpeedDemand, speedDemand),
ATTRIBUTE_ENTRY(MechControlsMapper, TurnDemand, turnDemand),
ATTRIBUTE_ENTRY(MechControlsMapper, LookForward, lookForward),
ATTRIBUTE_ENTRY(MechControlsMapper, LookLeft, lookLeft),
ATTRIBUTE_ENTRY(MechControlsMapper, LookRight, lookRight),
ATTRIBUTE_ENTRY(MechControlsMapper, LookBehind, lookBehind),
ATTRIBUTE_ENTRY(MechControlsMapper, LookDown, lookDown),
ATTRIBUTE_ENTRY(MechControlsMapper, TorsoUp, torsoUp),
ATTRIBUTE_ENTRY(MechControlsMapper, TorsoDown, torsoDown),
ATTRIBUTE_ENTRY(MechControlsMapper, TorsoLeft, torsoLeft),
ATTRIBUTE_ENTRY(MechControlsMapper, TorsoRight, torsoRight),
ATTRIBUTE_ENTRY(MechControlsMapper, TorsoCenter, torsoCenter),
ATTRIBUTE_ENTRY(MechControlsMapper, ControlMode, controlMode),
ATTRIBUTE_ENTRY(MechControlsMapper, DisplayMode, displayMode),
ATTRIBUTE_ENTRY(MechControlsMapper, PilotArrayPage, pilotArrayPage),
ATTRIBUTE_ENTRY(MechControlsMapper, PilotArray, pilotArray),
ATTRIBUTE_ENTRY(MechControlsMapper, TargetRangeExponent,
targetRangeExponent)
};
MechControlsMapper::AttributeIndexSet
MechControlsMapper::AttributeIndex(
ELEMENTS(MechControlsMapper::AttributePointers),
MechControlsMapper::AttributePointers,
Subsystem::AttributeIndex
);
MechControlsMapper::SharedData
MechControlsMapper::DefaultData(
MechControlsMapper::ClassDerivations,
Subsystem::MessageHandlers,
MechControlsMapper::AttributeIndex,
1
);
MechControlsMapper::MechControlsMapper(
Mech *owner,
int subsystem_ID,
CString subsystem_name,
RegisteredClass::ClassID class_ID,
SharedData &shared_data
):
Subsystem(
(Entity *)owner,
subsystem_ID,
subsystem_name,
class_ID,
shared_data
)
{
duckCommand = 0;
ejectLatch = 0;
Check(owner);
//
// Prime the published control inputs to neutral. Per-frame interpretation
// (InterpretControls) is reconstructed with the mech2/3/4 sim (phase 5.3).
//
stickPosition.x = 0.0f;
stickPosition.y = 0.0f;
throttlePosition = 0.0f;
pedalsPosition = 0.0f;
reverseThrust = 0;
speedDemand = 0.0f;
turnDemand = 0.0f;
lookForward = 0;
lookLeft = 0;
lookRight = 0;
lookBehind = 0;
lookDown = 0;
torsoUp = 0;
torsoDown = 0;
torsoLeft = 0;
torsoRight = 0;
torsoCenter = 0;
controlMode = BasicMode;
displayMode = 0;
pilotArrayPage = 0;
pilotArray = 0;
targetRangeExponent = 0.0f; // staged: bound by an authored watcher
lookState = LookNone;
previousLookState = LookNone;
{
//
// ELEMENTS(), never a literal -- this said 22 against reserved[21]
// and wrote 4 bytes off the end of every mapper. Found by a sweep
// after the same mistake surfaced in Torso (5.3.78).
//
int
i;
for (i = 0; i < (int)ELEMENTS(reserved); ++i)
{
reserved[i] = 0;
}
}
//
// Install the per-frame control-interpretation Performance.
//
SetPerformance(&MechControlsMapper::InterpretControls);
static int s_mpprLog = -1;
if (s_mpprLog < 0) { s_mpprLog = (getenv("BT_MECH_LOG") != NULL); }
if (s_mpprLog)
{
DEBUG_STREAM << "[mapper] ctor id=" << subsystem_ID
<< " throttleAttr=" << GetAttributePointer(ThrottlePositionAttributeID)
<< " controlModeAttr=" << GetAttributePointer(ControlModeAttributeID)
<< endl << flush;
}
Check_Fpu();
}
MechControlsMapper::~MechControlsMapper()
{
}
//
//#############################################################################
// InterpretControls -- the mapper's per-frame Performance. Reads the raw input
// attributes (the engine controls push refreshes throttle/stick/pedals/buttons
// from the RIO/keyboard before this runs) and publishes the locomotion demands
// (speedDemand in world-u/s, turnDemand [-1..1]) the mech drive consumes.
//
// Reconstructs the authentic demand math (mechmppr.cpp @004afd10):
// * throttle -> forward speed: speedDemand = topSpeed * throttle * fwdScale
// (reverse thrust inverts and drops the forward scale);
// * soft stick response: square the yaw (sign preserved), cube the pedals;
// * Basic: stick yaw = turn; Standard/Veteran: pedals = turn;
// * speed is clamped down while turning hard (max_turn ramp).
//
// DEV hook (headless verification, no RIO/keyboard): BT_FORCE_THROTTLE /
// BT_FORCE_TURN override the pushed raw inputs so the demand math + mech drive
// are exercisable without hardware. The torso-aim / free-look interpretation
// (torso analog axes, look/eyepoint commit) is the aiming wave, deferred.
//#############################################################################
//
void
MechControlsMapper::InterpretControls(Scalar time_slice)
{
Check(this);
Mech *mech = GetMech();
Check(mech);
//
// DEV forced-input override. Parsed ONCE -- this block used to run four
// getenv scans plus atof parses EVERY FRAME in the control path, part of
// why the build lagged the shipped exe.
//
{
static int s_forceParsed = 0;
static int s_hasThrottle = 0, s_hasTurn = 0, s_hasElev = 0, s_hasLook = 0;
static Scalar s_throttle = 0.0f, s_turn = 0.0f, s_elev = 0.0f;
static int s_look = 0;
if (!s_forceParsed)
{
s_forceParsed = 1;
const char *e;
e = getenv("BT_FORCE_THROTTLE");
if (e != NULL) { s_hasThrottle = 1; s_throttle = (Scalar)atof(e); }
e = getenv("BT_FORCE_TURN");
if (e != NULL) { s_hasTurn = 1; s_turn = (Scalar)atof(e); }
e = getenv("BT_FORCE_ELEV");
if (e != NULL) { s_hasElev = 1; s_elev = (Scalar)atof(e); }
e = getenv("BT_FORCE_LOOK");
if (e != NULL) { s_hasLook = 1; s_look = atoi(e); }
}
//
// BT_SUPERSTOP_SOAK=1: run forward 6s, then haul the throttle
// negative for 4s, forever -- the edge, the shudder cadence and the
// release, headlessly. Overrides BT_FORCE_THROTTLE while set.
//
{
static int s_soak = -1;
static Scalar s_soakClock = 0.0f;
if (s_soak < 0)
{
s_soak = (getenv("BT_SUPERSTOP_SOAK") != NULL) ? 1 : 0;
}
if (s_soak)
{
s_soakClock += time_slice;
if (s_soakClock >= 10.0f)
{
s_soakClock = 0.0f;
}
s_hasThrottle = 1;
s_throttle = (s_soakClock < 6.0f) ? 0.8f : -0.5f;
}
}
if (s_hasThrottle)
{
throttlePosition = (s_throttle >= 0.0f) ? s_throttle : -s_throttle;
reverseThrust = (s_throttle < 0.0f) ? 1 : 0;
}
if (s_hasTurn)
{
stickPosition.x = s_turn;
}
if (s_hasElev)
{
stickPosition.y = s_elev;
}
if (s_hasLook)
{
int state = s_look;
lookLeft = (state == (int)LookLeftState) ? 1 : 0;
lookRight = (state == (int)LookRightState) ? 1 : 0;
lookBehind = (state == (int)LookBehindState) ? 1 : 0;
lookDown = (state == (int)LookDownState) ? 1 : 0;
}
}
Torso *torso = (Torso *)mech->GetTorsoSubsystem();
Scalar topSpeed = mech->GetReverseStrideLength();
Scalar walkSpeed = mech->GetWalkStrideLength();
//
// Throttle -> forward speed demand.
//
if (reverseThrust < 1)
{
speedDemand = topSpeed * throttlePosition * mech->GetForwardThrottleScale();
}
else
{
speedDemand = -topSpeed * throttlePosition;
}
//
// Soft response: square the stick yaw (sign preserved), cube the pedals.
//
Scalar stick_x = stickPosition.x * stickPosition.x;
if (stickPosition.x < 0.0f)
{
stick_x = -stick_x;
}
Scalar stick_y = stickPosition.y * stickPosition.y;
if (stickPosition.y < 0.0f)
{
stick_y = -stick_y;
}
Scalar pedal_3 = pedalsPosition * pedalsPosition * pedalsPosition;
turnDemand = 0.0f;
//
// Torso aim. Every control mode routes the stick pitch (stick_y) into the
// torso weapon-elevation axis. In Basic the stick yaw is the turn (legs);
// in Standard/Veteran the stick yaw is the torso twist (free aim) and the
// pedals steer. (The HUD free-aim slew + look/eyepoint commit are the
// aiming/camera wave, deferred.)
//
if (controlMode == BasicMode)
{
turnDemand = stick_x;
if (torso != NULL)
{
torso->SetAnalogElevationAxis(stick_y);
torso->SetAnalogTwistAxis(0.0f);
}
}
else
{
turnDemand = pedal_3;
if (torso != NULL)
{
torso->SetAnalogElevationAxis(stick_y);
torso->SetAnalogTwistAxis(stick_x);
}
}
//
// Clamp forward speed down while turning hard (except VeteranMode, which has
// no speed-dependent turn clamp). max_turn ramps from topSpeed (no turn) to
// walkSpeed (full turn).
//
if (controlMode != VeteranMode)
{
Scalar turn_mag = (turnDemand < 0.0f) ? -turnDemand : turnDemand;
if (turn_mag > 0.001f)
{
Scalar max_turn = (topSpeed - walkSpeed) * (1.0f - turn_mag) + walkSpeed;
if (speedDemand > max_turn) speedDemand = max_turn;
if (speedDemand < -max_turn) speedDemand = -max_turn;
}
}
//
// Look / eyepoint selection. Choose a look direction from the buttons;
// when it changes, commit it -- the mech re-aims the eyepoint from its
// authored look angles (and, once the weapon wave lands, re-arms the
// per-view weapon fire enables through the same commit).
//
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)
{
mech->CommitLookState(lookState);
}
static int s_mpprLog = -1;
if (s_mpprLog < 0) { s_mpprLog = (getenv("BT_MECH_LOG") != NULL); }
if (s_mpprLog)
{
static Scalar reportAccum = 0.0f;
reportAccum += time_slice;
if (reportAccum >= 1.0f)
{
reportAccum = 0.0f;
DEBUG_STREAM << "[mppr] thr=" << throttlePosition
<< " rev=" << reverseThrust
<< " stickX=" << stickPosition.x
<< " stickY=" << stickPosition.y
<< " mode=" << controlMode
<< " -> speedDemand=" << speedDemand
<< " turnDemand=" << turnDemand
<< " torsoElev=" << (torso ? torso->CurrentElevation() : 0.0f)
<< endl << flush;
}
}
Check_Fpu();
}
Logical
MechControlsMapper::TestInstance() const
{
return IsDerivedFrom(ClassDerivations);
}