From 390d4ddce71def3feb3de127da03e98a0e17356b Mon Sep 17 00:00:00 2001 From: arcattack Date: Thu, 23 Jul 2026 23:40:27 -0500 Subject: [PATCH] Issue #36: direct-axis RELEASE EDGE -- centering an axis now releases its channel Night-2 (RajelAran): 'the game is not seeing axis release for turn -- the mech may keep turning when control is released.' Root cause: the direct-mode axis write was gated on raw != 0, so an axis returning INSIDE the deadzone just stopped writing and the channel LATCHED at the last deflection -- fatal on channels with no keyboard spring (the Turn composite in the twin-stick profile). Fix: per-binding previous-value tracking (pad + joystick paths); the outside->inside transition writes 0 exactly once, then the centered axis leaves the channel to other inputs (composition preserved; slew bindings excluded by design -- a lever stays where slewed). Plus the #24 rule, axis edition: a pad/stick that DIES while deflected releases its direct channels (throttle keeps the last lever position -- safer than an all-stop mid-fight). Verified: build + 45s glass mission with an idle XInput pad (no spurious writes, no regression). The deflect-release cycle itself needs a human hand -- awaiting RajelAran's next session. Co-Authored-By: Claude Opus 4.8 (1M context) --- engine/MUNGA_L4/L4PADRIO.cpp | 52 +++++++++++++++++++++++++++++++++++- engine/MUNGA_L4/L4PADRIO.h | 14 ++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/engine/MUNGA_L4/L4PADRIO.cpp b/engine/MUNGA_L4/L4PADRIO.cpp index ab80179..859d97f 100644 --- a/engine/MUNGA_L4/L4PADRIO.cpp +++ b/engine/MUNGA_L4/L4PADRIO.cpp @@ -130,6 +130,8 @@ PadRIO::PadRIO(): memset(previousKeyHeld, 0, sizeof(previousKeyHeld)); memset(previousJoyButtonHeld, 0, sizeof(previousJoyButtonHeld)); memset(previousJoyHatHeld, 0, sizeof(previousJoyHatHeld)); + memset(previousPadAxisRaw, 0, sizeof(previousPadAxisRaw)); + memset(previousJoyAxisRaw, 0, sizeof(previousJoyAxisRaw)); memset(channelValue, 0, sizeof(channelValue)); memset(lampState, 0, sizeof(lampState)); @@ -439,6 +441,19 @@ void } } previousPadButtons = 0; + // + // Issue #36 (the #24 rule, axis edition): a pad that dies while + // DEFLECTED must release its direct channels too -- otherwise + // the mech keeps turning/aiming on a dead controller. + // + for (int a = 0; a < bindings.padAxisBindingCount; ++a) + { + if (previousPadAxisRaw[a] != 0.0f) + { + channelValue[bindings.padAxisBindings[a].channel] = 0.0f; + previousPadAxisRaw[a] = 0.0f; + } + } } } if (padIndex < 0 && (lastPadProbeMilliseconds == 0 || @@ -672,6 +687,18 @@ void // channelValue[binding.channel] = raw; } + else if (previousPadAxisRaw[a] != 0.0f) + { + // + // RELEASE EDGE (issue #36): the axis just came back inside + // the deadzone -- write 0 ONCE so its deflection lets go + // (channels without a keyboard spring, e.g. Turn, latched + // at the last value forever). After this single write the + // centered axis leaves the channel to other inputs again. + // + channelValue[binding.channel] = 0.0f; + } + previousPadAxisRaw[a] = binding.slew ? 0.0f : raw; } } @@ -756,7 +783,21 @@ void const BTJoyDeviceState *state = slotState[binding.device]; if (state == 0) { - continue; // detached: leave the channel alone + // + // Issue #36 (the #24 rule): a stick that detaches while + // deflected releases its direct channel once. (Throttle + // keeps its last lever position -- freezing the lever is + // safer than an all-stop on an unplug mid-fight.) + // + if (previousJoyAxisRaw[a] != 0.0f) + { + if (binding.channel != PadBindingProfile::ChannelThrottle) + { + channelValue[binding.channel] = 0.0f; + } + previousJoyAxisRaw[a] = 0.0f; + } + continue; } float raw = state->axis[binding.axis]; if (binding.deadzone > 0.0f) @@ -797,6 +838,15 @@ void { channelValue[binding.channel] = raw; } + else if (previousJoyAxisRaw[a] != 0.0f) + { + // + // RELEASE EDGE (issue #36): outside -> inside the deadzone + // writes 0 once -- see the pad-axis twin above. + // + channelValue[binding.channel] = 0.0f; + } + previousJoyAxisRaw[a] = binding.slew ? 0.0f : raw; } } diff --git a/engine/MUNGA_L4/L4PADRIO.h b/engine/MUNGA_L4/L4PADRIO.h index bdf644e..70425a7 100644 --- a/engine/MUNGA_L4/L4PADRIO.h +++ b/engine/MUNGA_L4/L4PADRIO.h @@ -119,6 +119,20 @@ protected: previousJoyButtonHeld[48]; unsigned char previousJoyHatHeld[16]; + // + // Direct-axis RELEASE EDGES (issue #36, night-2: "the mech may keep + // turning when control is released"). A direct-mode axis write was + // gated on raw != 0, so an axis returning INSIDE the deadzone simply + // stopped writing -- the channel LATCHED at the last deflection (fatal + // on channels with no keyboard spring, e.g. the Turn composite). Track + // the previous post-deadzone value per binding: the outside->inside + // transition writes 0 exactly once, then leaves the channel to other + // inputs (the composition model is preserved). + // + float + previousPadAxisRaw[12]; // parallel to padAxisBindings + float + previousJoyAxisRaw[24]; // parallel to joyAxisBindings float channelValue[PadBindingProfile::ChannelCount]; float