diff --git a/game/reconstructed/hud.cpp b/game/reconstructed/hud.cpp index f5b55df..cd0b5a4 100644 --- a/game/reconstructed/hud.cpp +++ b/game/reconstructed/hud.cpp @@ -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); +} diff --git a/game/reconstructed/hud.hpp b/game/reconstructed/hud.hpp index e251e0e..44f83dd 100644 --- a/game/reconstructed/hud.hpp +++ b/game/reconstructed/hud.hpp @@ -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); diff --git a/game/reconstructed/mechmppr.cpp b/game/reconstructed/mechmppr.cpp index 4cf55fa..8cffb05 100644 --- a/game/reconstructed/mechmppr.cpp +++ b/game/reconstructed/mechmppr.cpp @@ -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=): 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. diff --git a/game/reconstructed/torso.cpp b/game/reconstructed/torso.cpp index 98ddab6..a3842ce 100644 --- a/game/reconstructed/torso.cpp +++ b/game/reconstructed/torso.cpp @@ -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) diff --git a/game/reconstructed/torso.hpp b/game/reconstructed/torso.hpp index 2e85cfc..fb53f00 100644 --- a/game/reconstructed/torso.hpp +++ b/game/reconstructed/torso.hpp @@ -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 diff --git a/scratchpad/night13/modecycle.sh b/scratchpad/night13/modecycle.sh new file mode 100644 index 0000000..f378522 --- /dev/null +++ b/scratchpad/night13/modecycle.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# ========================================================================= +# Sauron: "toggled through advanced controls from standard to advanced and +# back to standard -- lost torso control." +# +# THE MECHANISM (@004afbe0, the binary's CycleControlModeMessageHandler): +# the mode cycles 0 Basic -> 1 Standard -> 2 Veteran -> WRAPS TO BASIC. So +# getting from Veteran/"advanced" back to Standard PASSES THROUGH BASIC, and +# the Basic arm re-centres the torso. +# +# The port set that re-centre with CommandRecenter() -> centerCommand (@0x208). +# 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. One visit to Basic pinned it at 1 +# forever. Digital twist commands are processed BEFORE the centerCommand +# block, so they were overridden as fast as they were applied = "lost torso +# control". The binary writes recenterActive (@0x274) instead: a ONE-SHOT that +# self-clears on settle and is cancelled by any twist input. +# +# THE MEASUREMENT. BT_MODECYCLE_EVERY= cycles the control mode every n mapper ticks +# frames from that frame. BT_TORSO_LOG's gate probe now prints the two cells: +# [torso] ... ctrCmd= recen= vLim=(lo..hi) +# +# PASS: ctrCmd stays 0 across every cycle (the one-shot is used instead), and +# vLim SWAPS between the Basic pair and the assisted pair as the mode +# changes -- proving the elevation-limit swap (@0x228/@0x22C vs +# @0x230/@0x234) that the port previously never implemented. +# FAIL: ctrCmd latches to 1 after the first pass through Basic and never +# returns to 0 -> the torso re-centres forever. +# +# Single node: this is entirely local control state, no peer needed. +# ========================================================================= +set -x +. /c/git/bt411/scratchpad/night6/bench_common.sh +cd /c/git/bt411/content || exit 1 +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 2 +rm -f mc_a.log +bt_expert_egg MP.EGG MC.EGG +sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" MC.EGG + +( export BT_MODECYCLE_EVERY=400 + export BT_TORSO_LOG=1 BT_KEY_NOFOCUS=1 BT_KEY_BRIDGE=1 ${LEGACY:+BT_LEGACY_MODE_RECENTER=1} + bt_launch mc_a.log MC.EGG 0x03 ) +sleep 150 +bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe > /dev/null 2>&1; sleep 3 + +echo "=================== CONTROL-MODE TORSO STATE ===================" +echo "--- the mode cycles that happened ---" +grep -a "\[mode\] control mode" mc_a.log | head -10 +echo +echo "--- centerCommand must NEVER latch (ctrCmd=1 with no button = the bug) ---" +echo -n " samples with ctrCmd=1 : "; grep -ao "ctrCmd=[0-9]*" mc_a.log | grep -c "ctrCmd=1" +echo -n " samples with ctrCmd=0 : "; grep -ao "ctrCmd=[0-9]*" mc_a.log | grep -c "ctrCmd=0" +echo +echo "--- the elevation-limit SWAP (should differ between Basic and assisted) ---" +grep -ao "vLim=([^)]*)" mc_a.log | sort | uniq -c | sort -rn | head -5 +echo +echo "--- torso state around each mode change ---" +grep -aE "\[mode\] control mode|ctrCmd=" mc_a.log | grep -aA1 "\[mode\]" | head -12