control mode: Basic re-centre used the STICKY held-button cell -- and the elevation-limit swap was never ported
Sauron: "toggled through advanced controls from standard to advanced and back
to standard -- lost torso control."
The cycle is 0 Basic -> 1 Standard -> 2 Veteran -> WRAPS TO BASIC, so getting
from "advanced" back to Standard PASSES THROUGH BASIC, whose arm re-centres the
torso. @004afbe0 is a complete spec and the port got three things wrong:
iVar1 = mech+0x438 (TORSO) iVar2 = mech+0x5b4 (HUD)
if (mode == 0) { // BASIC
*(iVar1 + 0x1f0) = 0; // analogTwistAxis
*(iVar1 + 0x274) = 1; // recenterActive
*(iVar1 + 0x220) = *(iVar1 + 0x228); // vertLimitTop
*(iVar1 + 0x224) = *(iVar1 + 0x22c); // vertLimitBottom
*(iVar2 + 0x2a0) = 1; // HUD flickerActive
} else if (mode - 1U < 2) { // STANDARD/VETERAN
*(iVar1 + 0x220) = *(iVar1 + 0x230);
*(iVar1 + 0x224) = *(iVar1 + 0x234);
}
1. WRONG CELL. Basic called CommandRecenter() -> centerCommand (@0x208), the
HELD-BUTTON cell: TorsoSimulation re-arms recenterActive from it EVERY frame
it is non-zero, and only the input path clears it -- a mode switch has no
button release to follow. Digital twist commands are processed BEFORE the
centerCommand block, so while it is set they are overridden as fast as they
are applied: the torso stops responding. The binary sets recenterActive
(@0x274) directly -- a ONE-SHOT that self-clears on settle
(`recenterActive = Recenter(dt)`) and is cancelled by any twist input.
2. THE ELEVATION-LIMIT SWAP WAS MISSING ENTIRELY. Two authored pairs exist --
BASIC @0x228/@0x22C (full top, HALF bottom) vs STANDARD/VETERAN @0x230/@0x234
(the full pair) -- and all four members were ctor-written and read by NOTHING.
Basic never restricted downward travel; the assisted modes never restored it.
3. Basic also raises the HUD's flickerActive (@0x2A0) so the horizon re-settles
with the torso it just re-centred. Not ported. (New BTSetHudFlickerActive
bridge in hud.cpp -- mechmppr sees Subsystem*, not HUD.)
Also removed an invented SetAnalogElevationAxis(0); the binary zeroes only 0x1F0.
MEASURED A/B (scratchpad/night13/modecycle.sh, LEGACY=1 for the old path;
BT_LEGACY_MODE_RECENTER=1 is the revert switch):
ctrCmd=1 samples legacy 26 fixed 0
vLim pairs fixed run shows BOTH -- (-0.698..0.349) = -40..20 deg
assisted, and (-0.349..0.349) = -20..20 deg Basic.
Before this commit only the ctor pair ever appeared.
WHAT IS *NOT* PROVEN. I did not reproduce Sauron's PERMANENT loss. In this
bench the legacy latch is periodic, not sticky:
..........LLLL......LLLL......LLLL......LLLL......LLLL......LL
because the desktop key bridge writes centerCommand every frame and zeroes it
when no button is held, so it self-recovers. The torso IS locked while the cell
is set, which is the symptom -- but whether it stays locked depends on the input
path OWNING that cell. On the glass/pad route (Sauron's) nothing may clear it,
which would make it permanent. So: mechanism fixed and binary-grounded, exact
field persistence unverified. Field-verify by cycling modes on a pad build.
Probe: the BT_TORSO_LOG gate line now carries ctrCmd / recen / vLim.
Bench hook: BT_MODECYCLE_EVERY=<n> cycles the mode from the mapper (the pod's
own route is console key 0x13d -- not a RIO button, so BT_BTNTEST cannot press
it, and mech4's BT_MODECYCLE_TEST counter did not advance in a solo run).
NOTE the bench needs BT_KEY_BRIDGE=1: with a PadRIO present the key-bridge
block that consumes the cycle is skipped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
This commit is contained in:
co-authored by
Claude Opus 5
parent
43777569f9
commit
4ccc2a7eec
@@ -505,3 +505,20 @@ void
|
||||
{
|
||||
ResetToInitialState(reset_command != 0); // @004b77bc
|
||||
}
|
||||
|
||||
//
|
||||
// BTSetHudFlickerActive -- complete-type bridge for the CONTROL-MODE switch
|
||||
// (mechmppr.cpp treats the mech's subsystems as opaque pointers, so it cannot
|
||||
// touch HUD members directly; same pattern as torso.cpp's BTGetTorsoTwistAddr).
|
||||
//
|
||||
// @004afbe0's BASIC arm ends in `*(mech+0x5b4 + 0x2a0) = 1` -- mech+0x5b4 is the
|
||||
// HUD subsystem cache and +0x2A0 is flickerActive. Basic mode re-centres the
|
||||
// torso, so the HUD horizon is kicked into its settle animation to follow it
|
||||
// (UpdateFlicker @004b7ed4 decays horizontalTorsoOffset and reports whether it
|
||||
// is still moving). The port never made this call.
|
||||
//
|
||||
void BTSetHudFlickerActive(Subsystem *hud)
|
||||
{
|
||||
if (hud != 0)
|
||||
((HUD *)hud)->SetFlickerActive(1);
|
||||
}
|
||||
|
||||
@@ -196,6 +196,12 @@
|
||||
// Simulation Support
|
||||
//
|
||||
public:
|
||||
// @0x2A0 -- raised by the CONTROL-MODE switch's BASIC arm
|
||||
// (`*(mech+0x5b4 + 0x2a0) = 1`, @004afbe0) so the HUD horizon re-settles
|
||||
// with the torso that Basic just re-centred. Reached from mechmppr via
|
||||
// hud.cpp's BTSetHudFlickerActive bridge (that TU sees Subsystem*, not HUD).
|
||||
void SetFlickerActive(int on) { Check(this); flickerActive = on; }
|
||||
|
||||
typedef void
|
||||
(HUD::*Performance)(Scalar time_slice);
|
||||
|
||||
|
||||
@@ -508,25 +508,69 @@ void
|
||||
}
|
||||
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).
|
||||
// TYPED torso reconfiguration. The raw block this replaces wrote the
|
||||
// BINARY's offsets straight onto OUR compiled Torso (the databinding
|
||||
// trap); the typed rewrite that followed then got the SEMANTICS wrong in
|
||||
// three ways. Corrected 2026-08-08 against @004afbe0, which is a
|
||||
// complete spec:
|
||||
//
|
||||
// iVar1 = mech+0x438 (TORSO) iVar2 = mech+0x5b4 (HUD)
|
||||
// if (mode == 0) { // BASIC
|
||||
// *(iVar1 + 0x1f0) = 0; // analogTwistAxis
|
||||
// *(iVar1 + 0x274) = 1; // recenterActive
|
||||
// *(iVar1 + 0x220) = *(iVar1 + 0x228); // vertLimitTop
|
||||
// *(iVar1 + 0x224) = *(iVar1 + 0x22c); // vertLimitBottom
|
||||
// *(iVar2 + 0x2a0) = 1; // HUD flickerActive
|
||||
// } else if (mode - 1U < 2) { // STANDARD/VETERAN
|
||||
// *(iVar1 + 0x220) = *(iVar1 + 0x230);
|
||||
// *(iVar1 + 0x224) = *(iVar1 + 0x234);
|
||||
// }
|
||||
//
|
||||
// (1) THE BUG Sauron hit. Basic set `centerCommand` (@0x208) via
|
||||
// CommandRecenter(). That is the HELD-BUTTON cell: TorsoSimulation
|
||||
// re-arms recenterActive from it EVERY frame it is non-zero, and only
|
||||
// the input path clears it -- and a MODE SWITCH has no button release
|
||||
// to follow. So one visit to Basic pinned it at 1 forever, the torso
|
||||
// re-centred every frame, and the digital twist commands (processed
|
||||
// BEFORE the centerCommand block) were overridden as fast as they were
|
||||
// applied. Cycling Standard -> Veteran -> (wraps through BASIC) ->
|
||||
// Standard is enough to trigger it, which is exactly the reported
|
||||
// "toggled to advanced and back, lost torso control". The binary sets
|
||||
// recenterActive (@0x274) directly: a ONE-SHOT that self-clears on
|
||||
// settle (`recenterActive = Recenter(dt)`) and is cancelled by any
|
||||
// twist input.
|
||||
// (2) The ELEVATION LIMIT SWAP was missing entirely. Two authored pairs
|
||||
// exist -- BASIC @0x228/@0x22C (full top, HALF bottom) vs
|
||||
// STANDARD/VETERAN @0x230/@0x234 (the full pair) -- and all four were
|
||||
// ctor-written and never read by anything. So Basic never restricted
|
||||
// downward travel and the assisted modes never restored it.
|
||||
// (3) Basic also raises the HUD's flickerActive (@0x2A0) so the horizon
|
||||
// re-settles with the torso. Not ported.
|
||||
// Also: the binary zeroes ONLY analogTwistAxis (@0x1F0). The extra
|
||||
// SetAnalogElevationAxis(0) was invented; removed.
|
||||
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();
|
||||
torso->SetAnalogTwistAxis(0.0f); // @0x1F0
|
||||
// BT_LEGACY_MODE_RECENTER=1 restores the defective pre-2026-08-08
|
||||
// behaviour (the sticky centerCommand) for A/B measurement.
|
||||
static const int s_legacyRecenter =
|
||||
getenv("BT_LEGACY_MODE_RECENTER") ? 1 : 0;
|
||||
if (s_legacyRecenter)
|
||||
torso->CommandRecenter(); // @0x208 STICKY -- the bug
|
||||
else
|
||||
torso->BeginRecenterOnce(); // @0x274 (NOT centerCommand)
|
||||
torso->ApplyBasicElevationLimits(); // @0x220/@0x224 <- @0x228/@0x22C
|
||||
extern void BTSetHudFlickerActive(Subsystem *hud);
|
||||
BTSetHudFlickerActive(mech->GetHudSubsystem()); // HUD @0x2A0 = 1
|
||||
}
|
||||
else // StandardMode / VeteranMode -- `mode - 1U < 2` in the binary
|
||||
{
|
||||
torso->ApplyAssistedElevationLimits(); // @0x220/@0x224 <- @0x230/@0x234
|
||||
}
|
||||
// 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;
|
||||
@@ -824,6 +868,23 @@ void
|
||||
gBTModeCycle = 0;
|
||||
CycleControlModeNow();
|
||||
}
|
||||
// BENCH (BT_MODECYCLE_EVERY=<n>): cycle the control mode every n
|
||||
// InterpretControls calls. mech4's BT_MODECYCLE_TEST hook sits in
|
||||
// a scope whose frame counter did not advance in a solo run, and
|
||||
// the pod's own route is console key 0x13d -- not a RIO button, so
|
||||
// BT_BTNTEST cannot press it. This drives the SAME body the key
|
||||
// and the console button drive. Dev-only; default off.
|
||||
{
|
||||
static const char *s_mcEvery = getenv("BT_MODECYCLE_EVERY");
|
||||
if (s_mcEvery != 0)
|
||||
{
|
||||
static int s_mcN = 0;
|
||||
int period = atoi(s_mcEvery);
|
||||
if (period < 1) period = 300;
|
||||
if (++s_mcN % period == 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.
|
||||
|
||||
@@ -646,6 +646,13 @@ void
|
||||
<< " limits=(" << horizontalLimitRight << ".." << horizontalLimitLeft << ")"
|
||||
<< " axis=" << analogTwistAxis
|
||||
<< " twist=" << currentTwist
|
||||
// control-mode recenter state. centerCommand (@0x208) is the
|
||||
// HELD-button cell -- if it reads 1 with no button down, the
|
||||
// torso re-arms recenterActive every frame and digital twist is
|
||||
// dead (Sauron's "lost torso control" after cycling modes).
|
||||
<< " ctrCmd=" << centerCommand
|
||||
<< " recen=" << recenterActive
|
||||
<< " vLim=(" << verticalLimitBottom << ".." << verticalLimitTop << ")"
|
||||
<< " wIdx=" << watchedSubsystem
|
||||
<< " w=" << (void*)w
|
||||
<< " wElec=" << (w ? w->electricalStateAlarm.GetLevel() : -1)
|
||||
|
||||
@@ -235,8 +235,30 @@ class Joint; // engine skeleton node (JOINT.h); the twist target
|
||||
// Controls (@0x1F0 twist, @0x1F4 elevation); proportional, no button ramp.
|
||||
void SetAnalogTwistAxis(Scalar v) { analogTwistAxis = v; }
|
||||
void SetAnalogElevationAxis(Scalar v) { analogElevationAxis = v; }
|
||||
void CommandRecenter() { centerCommand = 1; } // @0x208 (Basic-mode re-center)
|
||||
void CommandRecenter() { centerCommand = 1; } // @0x208 HELD button -- writer MUST clear it
|
||||
void ClearRecenterCommand() { centerCommand = 0; } // button released (writer-owned state)
|
||||
|
||||
// ⚠ centerCommand (@0x208) is a HELD-BUTTON cell: TorsoSimulation re-arms
|
||||
// `recenterActive` from it EVERY frame it is non-zero, and only the input
|
||||
// path clears it. Do NOT use CommandRecenter() for a one-shot recenter --
|
||||
// nothing releases it and the torso re-centres forever, which reads to the
|
||||
// pilot as "lost torso control" (Sauron, control-mode cycle).
|
||||
//
|
||||
// The one-shot the mode switch actually wants is recenterActive (@0x274)
|
||||
// itself: TorsoSimulation runs `recenterActive = Recenter(dt)`, so it
|
||||
// SELF-CLEARS on settle, and any twist input cancels it. This is exactly
|
||||
// what the binary writes -- `*(torso + 0x274) = 1` @004afbe0.
|
||||
void BeginRecenterOnce() { recenterActive = 1; } // @0x274 one-shot (@004afbe0)
|
||||
|
||||
// The TWO authored elevation-limit pairs the control mode swaps between
|
||||
// (@004afbe0). BASIC gets @0x228/@0x22C (full top, HALF bottom -- reduced
|
||||
// downward travel); STANDARD/VETERAN get @0x230/@0x234 (the full authored
|
||||
// pair). Before 2026-08-08 all four were written by the ctor and never
|
||||
// read by anything -- the port simply never implemented the swap.
|
||||
void ApplyBasicElevationLimits()
|
||||
{ verticalLimitTop = elevationCenter; verticalLimitBottom = elevationHalfBottom; }
|
||||
void ApplyAssistedElevationLimits()
|
||||
{ verticalLimitTop = twistCenterHigh; verticalLimitBottom = twistCenterLow; }
|
||||
Logical GetHorizontalEnabled() const { return horizontalEnabled; } // @0x250 (mapper free-aim gate @004afd10)
|
||||
|
||||
// Reachable horizontal (yaw) half-arc the guns can be brought to bear by
|
||||
|
||||
Reference in New Issue
Block a user