Skip to content

fix the absolute throttle axis giving zero thrust when inverted - #7829

Open
Goober5000 wants to merge 2 commits into
scp-fs2open:masterfrom
Goober5000:fix/inverted_throttle
Open

Goober5000 wants to merge 2 commits into
scp-fs2open:masterfrom
Goober5000:fix/inverted_throttle

Conversation

@Goober5000

@Goober5000 Goober5000 commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Cause

With mouse flight on ("Mouse: Joy-0"), a Joy-0 axis binding also reads the mouse. In scale_invert(), the mouse's per-frame delta was added to the absolute throttle's position and inverted with the absolute rule (F1_0 - val). So a still mouse contributed F1_0 * factor (~2×10⁸), which pinned the throttle at 0% at every lever position. Relative axes were unaffected, because inverting a zero delta is still zero. New pilots have mouse flight on by default.

More broadly, a mouse on an absolute axis action was an incomplete feature: the delta was read as a position instead of being accumulated into one. Absolute actions also summed all their inputs, which doesn't make sense for positions; two levers bound as first and second were already broken this way.

Fixes #7951.

Fix

  • Inversion follows the input, not the action: joystick positions invert as F1_0 - v, and mouse deltas always invert by negating.
  • New abs_axis_state for absolute actions: it keeps the action's current value. A joystick sets the value outright and the mouse nudges it, and whichever input moved last is in control.
    • A lever that has lost control must move more than 1.25% of its range to take it back, so an idle lever's jitter doesn't fight the other inputs. This matches the analog throttle's keyboard override (1.5 of its 120 throttle-percent range). The mouse has no threshold, since a still mouse reports no movement.
    • A lever's first reading only takes control if no other lever has reported or is in control.
    • A disconnected joystick reads as centered, so it is ignored rather than treated as a lever position.
  • Rebinding or inverting mid-mission: when an absolute action's bindings change (controls screen, Lua, presets), its levers re-anchor without moving the throttle, and the lever in control keeps control and applies the new binding at once.
  • Mouse scale: it matches the mouse's feel on the relative throttle, which is the existing factor without the /frame_time. Mouse Z does nothing, because Mouse_dz is never set.
  • Only one caller updates the absolute axes: the axes can be read more than once per frame (free-look in view_modify(), and photo mode), so those callers pass update_abs_axes = false and only read_keyboard_controls() updates them.
  • Reset at mission load: a lever sets the throttle on the first frame. The analog throttle's keyboard override (override_analog_throttle, analog_throttle_last) also resets each mission; before, a throttle key pressed in one mission made the next mission ignore the lever until it moved. A mouse-only absolute throttle starts at 50%, the same as the existing analog_value default.
  • Relative actions are unchanged.

Testing

  • Unit tests: new tests for abs_axis_state in test/src/controlconfig/test_abs_axis.cpp. The full unit suite passes.
  • In-game: the absolute throttle works with mouse flight on, both normal and inverted.

🤖 Generated with Claude Code

@Goober5000 Goober5000 added this to the Release 27.0 milestone Oct 6, 2026
@Goober5000
Goober5000 requested a review from z64555 as a code owner October 6, 2026 02:18
@Goober5000 Goober5000 added fix A fix for bugs, not-a-bugs, and/or regressions. controls A feature or issue related to input devices or actions controlled/triggered by them labels Oct 6, 2026
@Goober5000
Goober5000 force-pushed the fix/inverted_throttle branch 4 times, most recently from ba10110 to 7ebec6e Compare October 7, 2026 20:43

@dustinspace217 dustinspace217 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also fixes #7951 (inverted absolute throttle stuck at zero with mouse flight on), so "Fixes #7951" in the description would close it. From reading the source and the new tests, three things:

  1. The anchors only reset at mission load. If the throttle is rebound or inverted mid-mission (the controls screen, or Lua bind->invert), a new lever is ignored until it moves 1.5%, and an inversion toggle can let an idle second lever take over. Could the anchors and active be cleared when that action's binding or inversion changes, keeping value?
  2. analog_throttle_last and override_analog_throttle (playercontrol.cpp:648-649) never reset. After a throttle key in one mission, the next mission's lever is ignored until it moves, which contradicts "a lever sets the throttle on the first frame". Reset them alongside control_reset_axes(), or reword that line?
  3. The update_abs_axes flag already stops the free-look double read, since only one caller passes true. Could the serial go? That also drops the mouse.cpp/h changes.

Smaller: the keyboard gate is about 1.25% (819 fix units), not 1.5%. Was the mouse taking control with no threshold intended? The description could say mouse Z does nothing, since Mouse_dz is never set. A test through update_abs_axis would pin the wiring.

@Goober5000
Goober5000 force-pushed the fix/inverted_throttle branch from 7ebec6e to b30bb3d Compare November 3, 2026 04:35
Goober5000 and others added 2 commits November 3, 2026 00:21
Joystick axes report positions and mouse axes report deltas, so an absolute axis action can't simply sum its inputs.  abs_axis_state keeps the action's value: a joystick sets it, the mouse nudges it, and whichever moved last is in control.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With mouse flight on, a Joy-0 axis binding also reads the mouse.  For the absolute throttle, the mouse's per-frame delta was added to the lever's position and inverted as a position (F1_0 - delta), so a still mouse added F1_0 times the mouse scale and pinned the throttle at zero.

Mouse deltas are now always inverted as relative values.  Absolute actions no longer sum their inputs: each keeps an abs_axis_state, which joysticks set and the mouse nudges, with the mouse scaled to match its feel on the relative throttle.  Since the axes can be read twice in one frame, the mouse deltas are only accumulated once per delta serial.  The state is reset at mission load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Goober5000
Goober5000 force-pushed the fix/inverted_throttle branch from b30bb3d to c3f26e6 Compare November 3, 2026 05:21
@Goober5000

Copy link
Copy Markdown
Contributor Author

@dustinspace217 Ok, points addressed and PR updated

@dustinspace217 dustinspace217 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a code-reading review of c3f26e6; nothing was built or run.

Thanks, all three earlier points are resolved:

  1. Anchors now clear when an absolute action's bindings change (controlsconfig.cpp:3159). Keeping active instead of clearing it, as I had asked, is the better choice, since an idle lever only re-anchors and the lever in control applies its new reading at once.
  2. analog_throttle_last and override_analog_throttle now reset in player_level_init(), which runs once per mission.
  3. The mouse delta serial is gone, and mouse.cpp/h are out of the diff.

The smaller points are handled too: the threshold is now F1_0 / 80 (819 fix units), matching the keyboard gate, and the description now covers the mouse threshold and mouse Z.

One new issue in the rebind path. If the mouse moved last (active == -1) and the bindings then change, clear_anchors() leaves nothing anchored and nothing in control, so the lever's next reading takes control and the throttle jumps to the lever.

That contradicts "its levers re-anchor without moving the throttle". clear_anchors_keeps_value_from_delta stops right after clear_anchors(), and going by the source, adding state.update_position(0, 20000); ASSERT_EQ(25000, state.value); would fail on the current code.

One fix is a flag set by any reading, cleared by reset() and kept by clear_anchors(), so a first reading after a rebind only anchors. Narrowing that line of the description would also work.

Still open from last time: the tests only cover abs_axis_state, so the mouse invert type in update_abs_axis, the disconnected joystick guard and the rebind check can each be reverted with the suite staying green.

Smaller: no test moves a lever down to retake control or pins the 1.25% value, so dropping abs() or zeroing TAKEOVER_THRESHOLD would also pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

controls A feature or issue related to input devices or actions controlled/triggered by them fix A fix for bugs, not-a-bugs, and/or regressions.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

P2: An inverted absolute throttle on the first joystick is stuck at zero while mouse flight is on

2 participants