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) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-23 23:40:27 -05:00
co-authored by Claude Opus 4.8
parent 0d1507027c
commit 390d4ddce7
2 changed files with 65 additions and 1 deletions
+51 -1
View File
@@ -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;
}
}
+14
View File
@@ -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