Skip to content

Refactor input to define hotkey interaction types#311

Closed
oznogon wants to merge 18 commits into
daid:masterfrom
oznogon:hotkeys-remapping-with-dialog
Closed

Refactor input to define hotkey interaction types#311
oznogon wants to merge 18 commits into
daid:masterfrom
oznogon:hotkeys-remapping-with-dialog

Conversation

@oznogon

@oznogon oznogon commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

SP's input system passes input values down state, up state, and normalized values to a game, and the game defines how each hotkey interacts with game systems.

Before the move to SDL, SFML limitations indirectly delineated different interaction types (keyboards sent OS-defined repeating down events if enabled, buttons sent high-low values, axes sent variable values). After the move to SDL's more polished input system, keyboard repeat interactions were lost. However, players relied on this behavior.

SDL additionally differentiates between different controller and axis types in ways SFML did not. SDL can interpret and send values from both monodirectional gamecontroller axes (triggers, pressure-sensitive buttons) that normalize from 0 to 1 and rest at 0; bidirectional joysticks that normalize from -1 to 1 and rest at 0; and digital axes (gamepads, hats) that normalize across -1 to 1 like bidirectional axes but behave like buttons.

Redefine input to use these SDL features and allow hotkeys to define supported and default interactions. This allows hotkeys to decide whether to treat inputs as:

  • discrete interactions, as high-low button presses that send one action upon hitting their threshold
  • continuous interactions, which act based on their value every tick
  • repeating interactions, which simulate SFML's passing of OS-defined keyboard repeating behavior but allow the game to customize timings
  • monodirectional axis interactions (axis0), which rest at 0 as a minimum and don't return negative values; this represents triggers with an 0-1 axis, or half of a joystick's bidirectional axis
  • bidirectional axis interactions (axis1, which rest at 0 as a center value between min (-1) and max (1) values

This PR provides an API for the game to use to define which interaction types a hotkey supports. For example, a hotkey representing an on-screen toggle button would support only discrete interactions, while combat manuevers would support continuous interactions (for accumulation via digital input), bidirectional axes (for strafe), and monodirectional axes (for boost).

This PR also provides a function to define which interaction type is its default, if multiple types are supported. This allows the game to simplify hotkey binding by defaulting to assigning inputs to the defined default type, while still providing flexibility for more advanced configurations that support unusual hardware, such as keyboard-emulating encoders; HOTAS/HOSAS monodirectional throttles, multi-axis sticks, digital and analog hats, pots/sliders, and encoder/pot knobs and dials; and MIDI-to-HID sliders, knobs, and pressure-sensitive buttons.

THIS SHOULD NOT CHANGE, AND DOES NOT REMOVE, EXISTING BEHAVIORS. getDown, getUp, and getValue remain in place. All hotkeys continue to work in EE with this PR as they would without it.

Example implementations in EE

While initializing the Keys class, define a simple button-style hotkey as Discrete-only by invoking its setSupportedInteractions(sp::io::Keybinding::Interaction::Discrete):

 void Keys::init()
 {
     pause.setLabel(tr("hotkey_menu", "General"), tr("hotkey_General", "Pause game"));
+    pause.setSupportedInteractions(sp::io::Keybinding::Interaction::Discrete);

     ...

setSupportedInteractions takes a mask, allowing multiple interactions to be supported at once:

    zoom_in.setSupportedInteractions(
        sp::io::Keybinding::Interaction::Discrete |
        sp::io::Keybinding::Interaction::Repeating |
        sp::io::Keybinding::Interaction::Continuous |
        sp::io::Keybinding::Interaction::Axis0 |
        sp::io::Keybinding::Interaction::Axis1
    );

When a hotkey supports multiple interactions, define one as the default. If no default is explicitly defined, Continuous (representing the existing behavior without this PR) is assumed.

    zoom_in.setDefaultInteraction(sp::io::Keybinding::Interaction::Repeating);

If a different interaction type should be the default for a different input type, optionally define that to override the global default interaction:

    zoom_in.setDefaultInteraction(sp::io::Keybinding::Type::JoystickAxis, sp::io::Keybinding::Interaction::Axis1);
    zoom_in.setDefaultInteraction(sp::io::Keybinding::Type::ControllerAxis, sp::io::Keybinding::Interaction::Axis1);

Helms slider behaviors, the most commonly debated action regarding the SFML/SDL changes in interactions:

    helms_increase_impulse.setLabel(tr("hotkey_menu", "Helms"), tr("hotkey_Helms", "Increase impulse"));
    helms_increase_impulse.setSupportedInteractions(
        sp::io::Keybinding::Interaction::Discrete |
        sp::io::Keybinding::Interaction::Repeating |
        sp::io::Keybinding::Interaction::Continuous |
        sp::io::Keybinding::Interaction::Axis0 |
        sp::io::Keybinding::Interaction::Axis1
    );
    helms_increase_impulse.setDefaultInteraction(sp::io::Keybinding::Interaction::Repeating);
    helms_increase_impulse.setDefaultInteraction(sp::io::Keybinding::Type::JoystickAxis, sp::io::Keybinding::Interaction::Axis1);
    helms_increase_impulse.setDefaultInteraction(sp::io::Keybinding::Type::ControllerAxis, sp::io::Keybinding::Interaction::Axis1);

The above:

  • Sets the helms increase keybind to repeating SFML-like behavior by default
  • Allows binding the input as discrete (for encoders), repeating (for keyboards), continuous (for button/digital axis), axis0 (gamepad left/right trigger), and axis1 (joystick)
  • Changes the default to axis1 for joysticks and game controllers

Where the hotkey action is implemented in impulse controls, use the hotkey's new functions for receiving values for each supported interaction type.

  • isDiscreteStepDown() returns true if the hotkey is activated
  • isRepeatReady() returns true if the hotkey is activated and the timeout hasn't started, the timeout (i.e. 500ms) has elapsed, or the delay period (i.e. every 5ms after the timeout) has elapsed
  • getContinuousValue() returns 1.0 if the hotkey is activated
  • getAxis0Value() and getAxis1Value() return a normalized value
void GuiImpulseControls::onUpdate()
{
    ...
    float change = keys.helms_increase_impulse.getContinuousValue() - keys.helms_decrease_impulse.getContinuousValue()
        - keys.helms_increase_impulse.getAxis0Value() + keys.helms_decrease_impulse.getAxis0Value()
        - keys.helms_increase_impulse.getAxis1Value() + keys.helms_decrease_impulse.getAxis1Value();
    if (change != 0.0f)
        my_player_info->commandImpulse(std::clamp(slider->getValue() + change * 0.01f, -1.0f, 1.0f));
    if (keys.helms_increase_impulse.isDiscreteStepDown() || keys.helms_increase_impulse.isRepeatReady())
        my_player_info->commandImpulse(std::min(1.0f, slider->getValue() + 0.1f));

oznogon added 6 commits April 1, 2026 15:22
discrete_step_size, threshold, repeat_delay, repeat_interval, and
sensitivity were declared static, so all keybindings shared one value.

Make them non-static instance members, with defaults matching the static
initializers.
setValue() compares raw this->value against the threshold, but
threshold_value is fabs(new_value). For negative axes, -0.8 < 0.5 is
always true, so down_event fires every frame. Likewise, -0.8 >= 0.5 is
never true, so up_event never fires.

Store the absolute threshold_value so the next comparison is consistent.
Set the UI to show and commit a stale pending rebind.
loadKeybindings() checked entry.contains() for keys like "discrete",
"repeat", "continuous", but saveKeybindings() writes "discrete_step",
"repeat_delay", "continuous_sensitivity", etc. Per-binding properties
were likewise saved but could never be loaded.

Fix the keys.
@oznogon oznogon closed this Apr 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant