From 98e7074bdbddba6b55679370913b9fbf480678eb Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Sat, 12 Jul 2025 12:33:16 -0400 Subject: [PATCH 1/2] add proper gamepad support Use the SDL gamepad API to add full gamepad support. Adds rumble for supported gamepads and changes the FF in-game options to take immediate effect instead of requiring a restart. Additionally this makes changes to allow gamepad triggers to be used as both an axis and a button. This support is specific to gamepads since we can explicitly identify the trigger with the API and act on it accordingly. FSO control config doesn't currently support such an action directly however so we have to hack it in just a bit. Due to this the control binding will show the trigger axis name bound to a button control only while a gamepad is connected, otherwise it shows a button name associated with the trigger. --- code/controlconfig/controlsconfigcommon.cpp | 51 ++- code/io/joy-sdl.cpp | 304 ++++++++++--- code/io/joy.h | 14 +- code/io/joy_ff.cpp | 397 ++++++++++++++++ code/io/joy_ff.h | 5 +- code/io/{joy_ff-sdl.cpp => joy_haptic.cpp} | 472 ++++++++------------ code/io/joy_haptic.h | 36 ++ code/io/joy_rumble.cpp | 162 +++++++ code/io/joy_rumble.h | 37 ++ code/source_groups.cmake | 6 +- 10 files changed, 1125 insertions(+), 359 deletions(-) create mode 100644 code/io/joy_ff.cpp rename code/io/{joy_ff-sdl.cpp => joy_haptic.cpp} (61%) create mode 100644 code/io/joy_haptic.h create mode 100644 code/io/joy_rumble.cpp create mode 100644 code/io/joy_rumble.h diff --git a/code/controlconfig/controlsconfigcommon.cpp b/code/controlconfig/controlsconfigcommon.cpp index 03241a6fc2d..5a01816a19c 100644 --- a/code/controlconfig/controlsconfigcommon.cpp +++ b/code/controlconfig/controlsconfigcommon.cpp @@ -2611,25 +2611,51 @@ void CC_bind::invert_toggle() { flags ^= CCF_INVERTED; } + +static bool compare_btn(short cid, const CC_bind& A, char& A_flags, const CC_bind& B, char& B_flags) +{ + auto A_btn = A.get_btn(); + auto B_btn = B.get_btn(); + + auto current = io::joystick::getPlayerJoystick(cid); + + if (current && current->isGamepad()) { + if (A_btn >= SDL_GAMEPAD_BUTTON_COUNT) { + A_btn = (A_btn - SDL_GAMEPAD_BUTTON_COUNT) + SDL_GAMEPAD_AXIS_LEFT_TRIGGER; + A_flags |= CCF_AXIS_BTN; + } + + if (B_btn >= SDL_GAMEPAD_BUTTON_COUNT) { + B_btn = (B_btn - SDL_GAMEPAD_BUTTON_COUNT) + SDL_GAMEPAD_AXIS_LEFT_TRIGGER; + B_flags |= CCF_AXIS_BTN; + } + } + + return (A_btn == B_btn); +} + bool CC_bind::conflicts_with(const CC_bind& B) const { + char A_flags = flags; + char B_flags = B.flags; + // Bail early if CID or btn are not the same - if ((cid != B.cid) || (btn != B.btn)) { + if ((cid != B.cid) || !compare_btn(cid, *this, A_flags, B, B_flags)) { return false; } // Check if A is an Axis or Axis Button, and if B is an Axis or Axis Button char mask = (CCF_AXIS_BTN | CCF_AXIS); - if ((flags & mask) && (B.flags & mask)) { + if ((A_flags & mask) && (B_flags & mask)) { return true; } // Check if Hat - if (flags & B.flags & CCF_HAT) { + if (A_flags & B_flags & CCF_HAT) { return true; } // Check if Ball - if (flags & B.flags & CCF_BALL) { + if (A_flags & B_flags & CCF_BALL) { return true; } @@ -2641,7 +2667,7 @@ bool CC_bind::conflicts_with(const CC_bind& B) const { // First off, check if A or B is NOT a button, as according to the mask. Buttons do not have a flag, so we check // if they are any of the other input types // Next, we return the inverse of the result. Negative of a Negative = Positive. Not Not a button = Is a button - return !((flags | B.flags) & mask); + return !((A_flags | B_flags) & mask); } bool CC_bind::is_inverted() const { @@ -2732,10 +2758,19 @@ SCP_string CC_bind::textify() const { case CID_JOY0: case CID_JOY1: case CID_JOY2: - case CID_JOY3: - Assert((btn >= 0) && (btn < JOY_TOTAL_BUTTONS)); - retval = SCP_string(textify_button(btn)); + case CID_JOY3: { + auto axis = joy_get_button_axis(cid, btn); + + if (axis < 0) { + Assert((btn >= 0) && (btn < JOY_TOTAL_BUTTONS)); + retval = SCP_string(textify_button(btn)); + } else { + Assert((axis >= 0) && (axis < NUM_AXIS_TEXT)); + retval = SCP_string(Axis_text[axis]); + } + break; + } case CID_NONE: default: diff --git a/code/io/joy-sdl.cpp b/code/io/joy-sdl.cpp index 729ecb62d82..562e8afe575 100644 --- a/code/io/joy-sdl.cpp +++ b/code/io/joy-sdl.cpp @@ -224,6 +224,11 @@ void setPlayerJoystick(Joystick* stick, short cid) if (pJoystick[cid] != nullptr) { mprintf((" Using '%s' as Joy-%i\n", pJoystick[cid]->getName().c_str(), cid)); mprintf(("\n")); + mprintf((" Is gamepad: %s\n", pJoystick[cid]->isGamepad() ? "YES" : "NO")); + if (pJoystick[cid]->isGamepad()) { + auto type_str = SDL_GetGamepadStringForType(SDL_GetGamepadType(pJoystick[cid]->getGamepad())); + mprintf((" Gamepad type: %s\n", type_str ? type_str : "")); + } mprintf((" Number of axes: %d\n", pJoystick[cid]->numAxes())); mprintf((" Number of buttons: %d\n", pJoystick[cid]->numButtons())); mprintf((" Number of hats: %d\n", pJoystick[cid]->numHats())); @@ -578,7 +583,12 @@ namespace joystick Joystick::Joystick(int id) : _id(id) { - _joystick = SDL_OpenJoystick(id); + if (SDL_IsGamepad(id)) { + _gamepad = SDL_OpenGamepad(id); + _joystick = SDL_GetGamepadJoystick(_gamepad); + } else { + _joystick = SDL_OpenJoystick(id); + } if (_joystick == nullptr) { SCP_stringstream msg; @@ -590,14 +600,20 @@ namespace joystick } Joystick::Joystick(Joystick &&other) noexcept : - _joystick(nullptr) + _joystick(nullptr), _gamepad(nullptr) { *this = std::move(other); } Joystick::~Joystick() { - if (_joystick != nullptr) + if (_gamepad != nullptr) + { + SDL_CloseGamepad(_gamepad); // also closes joystick + _gamepad = nullptr; + _joystick = nullptr; + } + else if (_joystick != nullptr) { SDL_CloseJoystick(_joystick); _joystick = nullptr; @@ -611,6 +627,7 @@ namespace joystick std::swap(_id, other._id); std::swap(_joystick, other._joystick); + std::swap(_gamepad, other._gamepad); fillValues(); @@ -619,7 +636,7 @@ namespace joystick bool Joystick::isAttached() const { - return SDL_JoystickConnected(_joystick); + return isGamepad() ? SDL_GamepadConnected(_gamepad) : SDL_JoystickConnected(_joystick); } bool Joystick::isHaptic() const @@ -781,22 +798,35 @@ namespace joystick void Joystick::fillValues() { + // To avoid some weirdness and build compatiblity issues we always use + // _joystick here rather than comparable _gamepad functions + _name.assign(SDL_GetJoystickName(_joystick)); _guidStr = getJoystickGUID(_joystick); _isHaptic = SDL_IsJoystickHaptic(_joystick); _isGamepad = SDL_IsGamepad(_id); // Initialize values of the axes - auto numSticks = SDL_GetNumJoystickAxes(_joystick); - if (numSticks >= 0) { - _axisValues.resize(static_cast(numSticks)); - for (auto i = 0; i < numSticks; ++i) { - _axisValues[i] = SDL_GetJoystickAxis(_joystick, i); + if (_isGamepad) { + // gamepads may not have all axes, but they don't necessarily match + // the number or index of what's reported by the joystick api either + _axisValues.resize(static_cast(SDL_GAMEPAD_AXIS_COUNT)); + for (size_t i = 0; i < _axisValues.size(); ++i) { + // will return 0 (center) if axis not supported + _axisValues[i] = SDL_GetGamepadAxis(_gamepad, static_cast(i)); } - } else { - _axisValues.resize(0); - mprintf(("Failed to get number of axes for joystick %s: %s\n", _name.c_str(), SDL_GetError())); + auto numSticks = SDL_GetNumJoystickAxes(_joystick); + if (numSticks >= 0) { + _axisValues.resize(static_cast(numSticks)); + for (auto i = 0; i < numSticks; ++i) { + _axisValues[i] = SDL_GetJoystickAxis(_joystick, i); + } + + } else { + _axisValues.resize(0); + mprintf(("Failed to get number of axes for joystick %s: %s\n", _name.c_str(), SDL_GetError())); + } } // Initialize ball values @@ -817,30 +847,42 @@ namespace joystick } // Initialize buttons - auto buttonNum = SDL_GetNumJoystickButtons(_joystick); - if (buttonNum >= 0) { - _button.resize(static_cast(buttonNum)); - for (auto i = 0; i < buttonNum; ++i) { - if (SDL_GetJoystickButton(_joystick, i)) { + if (_isGamepad) { + // gamepads may not support all buttons, but they don't necessarily match + // the number or index of what's reported by the joystick api either + // NOTE: +2 added so we can map left & right triggers to a button + _button.resize(static_cast(SDL_GAMEPAD_BUTTON_COUNT + 2)); + for (size_t i = 0; i < _button.size(); ++i) { + if (SDL_GetGamepadButton(_gamepad, static_cast(i))) { _button[i].DownTimestamp = ui_timestamp(); - } else { _button[i].DownTimestamp = UI_TIMESTAMP::invalid(); } } - } else { - _button.resize(0); - mprintf(("Failed to get number of buttons for joystick %s: %s\n", _name.c_str(), SDL_GetError())); + auto buttonNum = SDL_GetNumJoystickButtons(_joystick); + if (buttonNum >= 0) { + _button.resize(static_cast(buttonNum)); + for (auto i = 0; i < buttonNum; ++i) { + if (SDL_GetJoystickButton(_joystick, i)) { + _button[i].DownTimestamp = ui_timestamp(); + } else { + _button[i].DownTimestamp = UI_TIMESTAMP::invalid(); + } + } + + } else { + _button.resize(0); + mprintf(("Failed to get number of buttons for joystick %s: %s\n", _name.c_str(), SDL_GetError())); + } } - // Initialize hats - auto hatNum = SDL_GetNumJoystickHats(_joystick); + // Initialize hats (consider gamepads to always have one hat) + auto hatNum = _isGamepad ? 1 : SDL_GetNumJoystickHats(_joystick); if (hatNum >= 0) { _hat.resize(static_cast(hatNum)); for (auto i = 0; i < hatNum; ++i) { - std::bitset<4> hatset = SDL_GetJoystickHat(_joystick, i); - auto hatval = convertSDLHat(SDL_GetJoystickHat(_joystick, i)); + auto hatval = _isGamepad ? HAT_CENTERED : convertSDLHat(SDL_GetJoystickHat(_joystick, i)); _hat[i].Value = hatval; // Reset timestampts @@ -852,21 +894,23 @@ namespace joystick } if (_hat[i].Value != HAT_CENTERED) { - // Set the 4-pos timestamp(s) - if ((hatset[HAT_DOWN])) { - _hat[i].DownTimestamp4[HAT_DOWN] = ui_timestamp(); - } - if ((hatset[HAT_UP])) { - _hat[i].DownTimestamp4[HAT_UP] = ui_timestamp(); - } - if ((hatset[HAT_LEFT])) { - _hat[i].DownTimestamp4[HAT_LEFT] = ui_timestamp(); - } - if ((hatset[HAT_RIGHT])) { - _hat[i].DownTimestamp4[HAT_RIGHT] = ui_timestamp(); - } + std::bitset<4> hatset = SDL_GetJoystickHat(_joystick, i); + + // Set the 4-pos timestamp(s) + if ((hatset[HAT_DOWN])) { + _hat[i].DownTimestamp4[HAT_DOWN] = ui_timestamp(); + } + if ((hatset[HAT_UP])) { + _hat[i].DownTimestamp4[HAT_UP] = ui_timestamp(); + } + if ((hatset[HAT_LEFT])) { + _hat[i].DownTimestamp4[HAT_LEFT] = ui_timestamp(); + } + if ((hatset[HAT_RIGHT])) { + _hat[i].DownTimestamp4[HAT_RIGHT] = ui_timestamp(); + } - // Set the 8-pos timestamp + // Set the 8-pos timestamp _hat[i].DownTimestamp8[hatval] = ui_timestamp(); } } @@ -876,30 +920,50 @@ namespace joystick } } - SDL_Joystick *Joystick::getDevice() + SDL_Joystick *Joystick::getJoystick() { return _joystick; } + SDL_Gamepad *Joystick::getGamepad() + { + return _gamepad; + } + void Joystick::handleJoyEvent(const SDL_Event &evt) { - switch (evt.type) - { - case SDL_EVENT_JOYSTICK_AXIS_MOTION: - handleAxisEvent(evt.jaxis); - break; - case SDL_EVENT_JOYSTICK_BALL_MOTION: - handleBallEvent(evt.jball); - break; - case SDL_EVENT_JOYSTICK_BUTTON_DOWN: - case SDL_EVENT_JOYSTICK_BUTTON_UP: - handleButtonEvent(evt.jbutton); - break; - case SDL_EVENT_JOYSTICK_HAT_MOTION: - handleHatEvent(evt.jhat); - break; - default: - break; + // gamepads also get joy events, so make sure we ignore those + if (isGamepad()) { + switch (evt.type) { + case SDL_EVENT_GAMEPAD_AXIS_MOTION: + handleAxisEvent(evt.gaxis); + break; + case SDL_EVENT_GAMEPAD_BUTTON_DOWN: + case SDL_EVENT_GAMEPAD_BUTTON_UP: + handleButtonEvent(evt.gbutton); + break; + default: + break; + } + } else { + switch (evt.type) + { + case SDL_EVENT_JOYSTICK_AXIS_MOTION: + handleAxisEvent(evt.jaxis); + break; + case SDL_EVENT_JOYSTICK_BALL_MOTION: + handleBallEvent(evt.jball); + break; + case SDL_EVENT_JOYSTICK_BUTTON_DOWN: + case SDL_EVENT_JOYSTICK_BUTTON_UP: + handleButtonEvent(evt.jbutton); + break; + case SDL_EVENT_JOYSTICK_HAT_MOTION: + handleHatEvent(evt.jhat); + break; + default: + break; + } } } @@ -912,6 +976,43 @@ namespace joystick _axisValues[axis] = evt.value; } + // gamepad version of event + void Joystick::handleAxisEvent(const SDL_GamepadAxisEvent &evt) + { + auto axis = evt.axis; + auto value = evt.value; + + Assertion(axis < numAxes(), "SDL event contained invalid axis index!"); + + if ((axis == SDL_GAMEPAD_AXIS_LEFT_TRIGGER) || (axis == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER)) { + // Triggers range from 0..32767 so we need to scale the value for FSO + // since it expects a full -32768..32767 range. Note that precision is + // lost in the scaling so only scale if it's not a min/max trigger value + if (value == 0) { + value = -32768; + } else if (value < 32767) { + value = static_cast((value * 2) - 32768); + } + + // We can also take this opportunity to map triggers to a button + int button = SDL_GAMEPAD_BUTTON_COUNT + (axis - SDL_GAMEPAD_AXIS_LEFT_TRIGGER); + bool down = (value >= 0); + + Assertion(button < numButtons(), "Gamepad trigger button is out of bounds!"); + + // We also need to avoid bumping DownCount for every tiny axis movement. + if ( !down || (_button[button].DownCount == 0) ) { + _button[button].DownTimestamp = down ? ui_timestamp() : UI_TIMESTAMP::invalid(); + + if (down) { + ++_button[button].DownCount; + } + } + } + + _axisValues[axis] = value; + } + void Joystick::handleButtonEvent(const SDL_JoyButtonEvent &evt) { auto button = evt.button; @@ -927,6 +1028,58 @@ namespace joystick } } + // gamepad version of event (we deal with dpad->hat translation here too) + void Joystick::handleButtonEvent(const SDL_GamepadButtonEvent &evt) + { + auto button = evt.button; + auto down = evt.down; + + Assertion(button < numButtons(), "SDL event contained invalid button index!"); + + // treat dpad as hat + if (button >= SDL_GAMEPAD_BUTTON_DPAD_UP && button <= SDL_GAMEPAD_BUTTON_DPAD_RIGHT) { + HatPosition hatpos; + + if (numHats() != 1) { + return; + } + + switch (button) { + case SDL_GAMEPAD_BUTTON_DPAD_UP: + hatpos = HAT_UP; + break; + case SDL_GAMEPAD_BUTTON_DPAD_DOWN: + hatpos = HAT_DOWN; + break; + case SDL_GAMEPAD_BUTTON_DPAD_LEFT: + hatpos = HAT_LEFT; + break; + case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: + hatpos = HAT_RIGHT; + break; + default: + return; + } + + // Set current values + _hat[0].Value = hatpos; + + _hat[0].DownTimestamp4[hatpos] = down ? ui_timestamp() : UI_TIMESTAMP::invalid(); + _hat[0].DownTimestamp8[hatpos] = down ? ui_timestamp() : UI_TIMESTAMP::invalid(); + + if (down) { + ++_hat[0].DownCount4[hatpos]; + ++_hat[0].DownCount8[hatpos]; + } + } else { + _button[button].DownTimestamp = down ? ui_timestamp() : UI_TIMESTAMP::invalid(); + + if (down) { + ++_button[button].DownCount; + } + } + } + void Joystick::handleHatEvent(const SDL_JoyHatEvent &evt) { auto hat = evt.hat; @@ -1024,15 +1177,13 @@ namespace joystick mprintf(("Initializing Joystick...\n")); - if ( !SDL_InitSubSystem(SDL_INIT_JOYSTICK) ) + // NOTE: gamepad depends on joystick, so this handles both + if ( !SDL_InitSubSystem(SDL_INIT_GAMEPAD) ) { mprintf((" Could not initialize joystick: %s\n", SDL_GetError())); return false; } - // enable event processing of the joystick - SDL_SetJoystickEventsEnabled(true); - if ( !SDL_HasJoystick() ) { mprintf((" No joysticks found\n")); @@ -1058,6 +1209,12 @@ namespace joystick addEventListener(SDL_EVENT_JOYSTICK_ADDED, DEFAULT_LISTENER_WEIGHT, device_event_handler); addEventListener(SDL_EVENT_JOYSTICK_REMOVED, DEFAULT_LISTENER_WEIGHT, device_event_handler); + // Gamepad events. NOTE: This is on top of joystick events, so both will be fired for gamepads! + // (we can ignore add/remove events here since the normal joystick ones will do it) + addEventListener(SDL_EVENT_GAMEPAD_AXIS_MOTION, DEFAULT_LISTENER_WEIGHT, axis_event_handler); + addEventListener(SDL_EVENT_GAMEPAD_BUTTON_DOWN, DEFAULT_LISTENER_WEIGHT, button_event_handler); + addEventListener(SDL_EVENT_GAMEPAD_BUTTON_UP, DEFAULT_LISTENER_WEIGHT, button_event_handler); + // Search for the correct stick if (Using_in_game_options) { @@ -1146,7 +1303,7 @@ namespace joystick // Automatically frees joystick resources joysticks.clear(); - SDL_QuitSubSystem(SDL_INIT_JOYSTICK); + SDL_QuitSubSystem(SDL_INIT_GAMEPAD); } json_t* getJsonArray() { @@ -1299,3 +1456,26 @@ bool joy_present(short cid) { return pJoystick[cid] != nullptr; } + +// Check for special trigger buttons and return axis for button +// This is primarily for easier UI/UX in controlconfig (like conflict detection) +// returns axis or -1 if not an axis button +short joy_get_button_axis(short cid, short btn) +{ + auto current = io::joystick::getPlayerJoystick(cid); + + // gamepads only (for now?) + if ( !current || !current->isGamepad() ) { + return -1; + } + + // the special trigger buttons are extended on normal buttons so we need to + // remove those and sort out whether this is the left (0) or right (1) trigger + auto axis_button = btn - SDL_GAMEPAD_BUTTON_COUNT; + + if (axis_button < 0 || axis_button > 1) { + return -1; + } + + return static_cast(SDL_GAMEPAD_AXIS_LEFT_TRIGGER + axis_button); +} diff --git a/code/io/joy.h b/code/io/joy.h index ede8dfab364..3b68a1be954 100644 --- a/code/io/joy.h +++ b/code/io/joy.h @@ -228,7 +228,13 @@ namespace io * @brief The SDL joystick handle * @return The handle */ - SDL_Joystick* getDevice(); + SDL_Joystick* getJoystick(); + + /** + * @brief The SDL gampad handle + * @return The handle + */ + SDL_Gamepad* getGamepad(); /** * @brief Handles a SDL joystick event @@ -262,8 +268,12 @@ namespace io void handleButtonEvent(const SDL_JoyButtonEvent& evt); void handleHatEvent(const SDL_JoyHatEvent& evt); + void handleAxisEvent(const SDL_GamepadAxisEvent& evt); + void handleButtonEvent(const SDL_GamepadButtonEvent& evt); + SDL_JoystickID _id; //!< The instance ID SDL_Joystick *_joystick; //!< The SDL joystick handle + SDL_Gamepad *_gamepad; //!< The SDL gamepad handle SCP_string _guidStr; //!< The GUID string SCP_string _name; //!< The joystick name @@ -373,6 +383,8 @@ int joy_down_count(const CC_bind &bind, int reset_count); int joy_down(const CC_bind &bind); +short joy_get_button_axis(const short cid, short btn); + /** * Checks if the given joystick is present or not */ diff --git a/code/io/joy_ff.cpp b/code/io/joy_ff.cpp new file mode 100644 index 00000000000..c856f1c1395 --- /dev/null +++ b/code/io/joy_ff.cpp @@ -0,0 +1,397 @@ +/* + * Copyright (C) Volition, Inc. 1999. All rights reserved. + * + * All source code herein is the property of Volition, Inc. You may not sell + * or otherwise commercially exploit the source or things you created based on the + * source. + * +*/ + +#include "globalincs/pstypes.h" +#include "options/Option.h" +#include "io/joy.h" +#include "io/joy_ff.h" +#include "io/joy_haptic.h" +#include "io/joy_rumble.h" + + +static bool Joy_ff_enabled = false; +static bool Joy_ff_acquired = false; +static bool Joy_ff_directional_hit_effect_enabled = true; +static bool Joy_ff_afterburning = false; +static bool Using_rumble = false; + + +static bool joy_ff_can_play() +{ + return (Joy_ff_enabled && Joy_ff_acquired); +} + +// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton +static auto ForceFeedbackOption = options::OptionBuilder("Input.ForceFeedback", + std::pair{"Force Feedback", 1728}, + std::pair{"Enable or disable force feedback", 1729}) + .category(std::make_pair("Input", 1827)) + .level(options::ExpertLevel::Beginner) + .default_val(true) + .change_listener([](bool val, bool) { + if (val) joy_ff_init(); + else joy_ff_shutdown(); + return true; + }) + .finish(); + +// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton +static auto HitEffectOption = options::OptionBuilder("Input.HitEffect", + std::pair{"Directional Hit", 1730}, + std::pair{"Enable or disable the directional hit effect", 1731}) + .category(std::make_pair("Input", 1827)) + .level(options::ExpertLevel::Beginner) + .default_val(true) + .change_listener([](bool val, bool) { + Joy_ff_directional_hit_effect_enabled = val; + return true; + }) + .finish(); + +// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton +static auto ForceFeedbackStrength = options::OptionBuilder("Input.FFStrength", + std::pair{"Force Feedback Strength", 1756}, + std::pair{"The realtive strength of Force Feedback effects", 1757}) + .category(std::make_pair("Input", 1827)) + .level(options::ExpertLevel::Beginner) + .range(0, 100) + .default_val(100) + .flags({options::OptionFlags::RangeTypeInteger}) + .change_listener([](int val, bool) { + joy_haptic_set_gain(val); + joy_rumble_set_gain(val); + return true; + }) + .finish(); + + +bool joy_ff_init() +{ + bool ff_enabled; + int ff_strength; + + if (Joy_ff_enabled) { + return true; + } + + auto Joy = io::joystick::getPlayerJoystick(CID_JOY0); + + if ( !Joy ) { + return false; + } + + if (Using_in_game_options) { + ff_enabled = ForceFeedbackOption->getValue(); + } else { + ff_enabled = os_config_read_uint(nullptr, "EnableJoystickFF", 1) != 0; + } + + if ( !ff_enabled ) { + return false; + } + + if (Using_in_game_options) { + Joy_ff_directional_hit_effect_enabled = HitEffectOption->getValue(); + } else { + Joy_ff_directional_hit_effect_enabled = os_config_read_uint(nullptr, "EnableHitEffect", 1) != 0; + } + + if (Using_in_game_options) { + ff_strength = (int)ForceFeedbackStrength->getValue(); + } else { + ff_strength = os_config_read_uint("ForceFeedback", "Strength", 100); + } + + if (Joy->isGamepad()) { + Using_rumble = joy_rumble_init(); + + if ( !Using_rumble ) { + joy_ff_shutdown(); + return false; + } + + joy_rumble_set_gain(ff_strength); + } else { + if ( !joy_haptic_init() ) { + joy_ff_shutdown(); + return false; + } + + joy_haptic_set_gain(ff_strength); + } + + Joy_ff_enabled = true; + Joy_ff_acquired = true; + + return true; +} + +void joy_ff_shutdown() +{ + if ( !Joy_ff_enabled ) { + return; + } + + joy_ff_afterburn_off(); + + joy_haptic_shutdown(); + joy_rumble_shutdown(); + + Joy_ff_enabled = false; + Joy_ff_acquired = false; + Using_rumble = false; +} + +bool joy_ff_reinit() +{ + if (Joy_ff_enabled) { + joy_ff_shutdown(); + } + + return joy_ff_init(); +} + +bool joy_ff_hit_effect_enabled() +{ + return Joy_ff_directional_hit_effect_enabled; +} + +void joy_ff_stop_effects() +{ + if ( !joy_ff_can_play() ) { + return; + } + + if (Using_rumble) { + joy_rumble_stop_effects(); + } else { + joy_haptic_stop_effects(); + } +} + +void joy_ff_mission_init(vec3d v) +{ + if ( !joy_ff_can_play() ) { + return; + } + + Joy_ff_afterburning = false; + + if (Using_rumble) { + joy_rumble_mission_init(&v); + } else { + joy_haptic_mission_init(&v); + } +} + +void joy_reacquire_ff() +{ + if ( !Joy_ff_enabled ) { + return; + } + + if (Joy_ff_acquired) { + return; + } + + Joy_ff_acquired = true; +} + +void joy_unacquire_ff() +{ + if ( !Joy_ff_enabled ) { + return; + } + + if ( !Joy_ff_acquired ) { + return; + } + + joy_ff_stop_effects(); + + Joy_ff_acquired = false; +} + +void joy_ff_play_vector_effect(vec3d *v, float scaler) +{ + if ( !joy_ff_can_play() ) { + return; + } + + if (Using_rumble) { + joy_rumble_play_vector_effect(v, scaler); + } else { + joy_haptic_play_vector_effect(v, scaler); + } +} + +void joy_ff_play_dir_effect(float x, float y) +{ + if ( !joy_ff_can_play() ) { + return; + } + + if (Using_rumble) { + joy_rumble_play_dir_effect(x, y); + } else { + joy_haptic_play_dir_effect(x, y); + } +} + +void joy_ff_play_primary_shoot(int gain) +{ + if ( !joy_ff_can_play() ) { + return; + } + + if (Using_rumble) { + joy_rumble_play_primary_shoot(gain); + } else { + joy_haptic_play_primary_shoot(gain); + } +} + +void joy_ff_play_secondary_shoot(int gain) +{ + if ( !joy_ff_can_play() ) { + return; + } + + if (Using_rumble) { + joy_rumble_play_secondary_shoot(gain); + } else { + joy_haptic_play_secondary_shoot(gain); + } +} + +void joy_ff_adjust_handling(int speed) +{ + if ( !joy_ff_can_play() ) { + return; + } + + if (Using_rumble) { + joy_rumble_adjust_handling(speed); + } else { + joy_haptic_adjust_handling(speed); + } +} + +void joy_ff_docked() +{ + if ( !joy_ff_can_play() ) { + return; + } + + if (Using_rumble) { + joy_rumble_docked(); + } else { + joy_haptic_docked(); + } +} + +void joy_ff_play_reload_effect() +{ + if ( !joy_ff_can_play() ) { + return; + } + + if (Using_rumble) { + joy_rumble_play_reload_effect(); + } else { + joy_haptic_play_reload_effect(); + } +} + +void joy_ff_afterburn_on() +{ + if ( !joy_ff_can_play() ) { + return; + } + + if (Joy_ff_afterburning) { + return; + } + + if (Using_rumble) { + joy_rumble_afterburn_on(); + } else { + joy_haptic_afterburn_on(); + } + + Joy_ff_afterburning = true; +} + +void joy_ff_afterburn_off() +{ + if ( !joy_ff_can_play() ) { + return; + } + + if ( !Joy_ff_afterburning ) { + return; + } + + if (Using_rumble) { + joy_rumble_afterburn_off(); + } else { + joy_haptic_afterburn_off(); + } + + Joy_ff_afterburning = false; +} + +void joy_ff_explode() +{ + if ( !joy_ff_can_play() ) { + return; + } + + joy_ff_afterburn_off(); + + if (Using_rumble) { + joy_rumble_explode(); + } else { + joy_haptic_explode(); + } +} + +void joy_ff_fly_by(int mag) +{ + if ( !joy_ff_can_play() ) { + return; + } + + if (Joy_ff_afterburning) { + return; + } + + if (Using_rumble) { + joy_rumble_fly_by(mag); + } else { + joy_haptic_fly_by(mag); + } +} + +void joy_ff_deathroll() +{ + if ( !joy_ff_can_play() ) { + return; + } + + joy_ff_afterburn_off(); + + if (Using_rumble) { + joy_rumble_deathroll(); + } else { + joy_haptic_deathroll(); + } + + Joy_ff_afterburning = true; +} diff --git a/code/io/joy_ff.h b/code/io/joy_ff.h index 9f78eb80d7f..ec487fc7614 100644 --- a/code/io/joy_ff.h +++ b/code/io/joy_ff.h @@ -14,9 +14,10 @@ #include "globalincs/pstypes.h" -int joy_ff_init(); +bool joy_ff_init(); void joy_ff_shutdown(); -int joy_ff_reinit(); +bool joy_ff_reinit(); +bool joy_ff_hit_effect_enabled(); void joy_ff_stop_effects(); void joy_ff_mission_init(vec3d v); void joy_reacquire_ff(); diff --git a/code/io/joy_ff-sdl.cpp b/code/io/joy_haptic.cpp similarity index 61% rename from code/io/joy_ff-sdl.cpp rename to code/io/joy_haptic.cpp index 85114bcfdf2..df4ab1967d3 100644 --- a/code/io/joy_ff-sdl.cpp +++ b/code/io/joy_haptic.cpp @@ -10,23 +10,14 @@ #include "globalincs/pstypes.h" #include "io/joy.h" #include "io/joy_ff.h" +#include "io/joy_haptic.h" #include "math/vecmat.h" -#include "mod_table/mod_table.h" -#include "options/Option.h" -#include "osapi/osapi.h" -#include "osapi/osregistry.h" -#ifndef SDL_INIT_HAPTIC -#define SDL_INIT_HAPTIC 0x00001000 -#endif - -static int Joy_ff_enabled = 0; -static int Joy_ff_acquired = 0; -static SDL_Haptic *haptic = NULL; -static int joy_ff_handling_scaler = 0; -static bool Joy_ff_directional_hit_effect_enabled = true; -static int Joy_ff_afterburning = 0; +static SDL_Haptic *haptic = nullptr; +static int joy_haptic_handling_scaler = 0; +static bool Gain_adjust = false; +static float Gain = 1.0f; typedef struct haptic_effect_t { SDL_HapticEffect eff; @@ -51,37 +42,11 @@ static haptic_effect_t pSecShootEffect; static haptic_effect_t pSpring; static haptic_effect_t pDock; -// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton -static auto ForceFeedbackOption = options::OptionBuilder("Input.ForceFeedback", - std::pair{"Force Feedback", 1728}, - std::pair{"Enable or disable force feedback", 1729}) - .category(std::make_pair("Input", 1827)) - .level(options::ExpertLevel::Beginner) - .default_val(false) - .finish(); - -// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton -static auto HitEffectOption = options::OptionBuilder("Input.HitEffect", - std::pair{"Directional Hit", 1730}, - std::pair{"Enable or disable the directional hit effect", 1731}) - .category(std::make_pair("Input", 1827)) - .level(options::ExpertLevel::Beginner) - .default_val(false) - .finish(); - -// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton -static auto ForceFeedbackStrength = options::OptionBuilder("Input.FFStrength", - std::pair{"Force Feedback Strength", 1756}, - std::pair{"The realtive strength of Force Feedback effects", 1757}) - .category(std::make_pair("Input", 1827)) - .level(options::ExpertLevel::Beginner) - .range(0, 100) - .default_val(100) - .finish(); - -static bool joy_ff_create_effects(); -static void joy_ff_start_effect(haptic_effect_t *eff, const char *name); -static void joy_ff_stop_effect(haptic_effect_t *eff, const char *name); + +static bool joy_haptic_create_effects(); +static void joy_haptic_start_effect(haptic_effect_t *eff, const char *name); +static void joy_haptic_update_effect(haptic_effect_t *eff, const char *name); +static void joy_haptic_stop_effect(haptic_effect_t *eff, const char *name); static void check_and_print_haptic_feature(uint32_t flags, uint32_t check_flag, const char* description) { auto has_flag = (flags & check_flag) == check_flag; @@ -111,34 +76,12 @@ static void print_haptic_support() { check_and_print_haptic_feature(supported, SDL_HAPTIC_PAUSE, "Pause effects"); } -int joy_ff_init() +bool joy_haptic_init() { - bool ff_enabled; - - if (Joy_ff_enabled) { - return 0; - } - auto Joy = io::joystick::getPlayerJoystick(CID_JOY0); if ( !Joy || !Joy->isHaptic() ) { - return 0; - } - - if (Using_in_game_options) { - ff_enabled = ForceFeedbackOption->getValue(); - } else { - ff_enabled = os_config_read_uint(nullptr, "EnableJoystickFF", 1) != 0; - } - - if ( !ff_enabled ) { - return 0; - } - - // ignore gamepads - if ( Joy->isGamepad() ) { - // TODO: add support for gamepad API and rumble effects - return 0; + return false; } mprintf(("\n")); @@ -146,7 +89,7 @@ int joy_ff_init() if ( !SDL_InitSubSystem(SDL_INIT_HAPTIC) ) { mprintf((" ERROR: Could not initialize Haptic subsystem: %s\n", SDL_GetError())); - return -1; + return false; } #ifndef NDEBUG @@ -165,46 +108,18 @@ int joy_ff_init() } #endif - haptic = SDL_OpenHapticFromJoystick(Joy->getDevice()); + haptic = SDL_OpenHapticFromJoystick(Joy->getJoystick()); if (haptic == NULL) { mprintf((" ERROR: Unable to open haptic joystick: %s\n", SDL_GetError())); SDL_QuitSubSystem(SDL_INIT_HAPTIC); - return -1; + return false; } - Joy_ff_enabled = 1; - Joy_ff_acquired = 1; - - if ( !joy_ff_create_effects() ) { + if ( !joy_haptic_create_effects() ) { mprintf((" ERROR: Unable to create effects\n")); - joy_ff_shutdown(); - return -1; - } - - if (Using_in_game_options) { - Joy_ff_directional_hit_effect_enabled = HitEffectOption->getValue(); - } else { - Joy_ff_directional_hit_effect_enabled = os_config_read_uint(nullptr, "EnableHitEffect", 1) != 0; - } - // ForceFeedback.Strength lets the user specify how strong the effects should be. This uses SDL_HapticSetGain which - // needs to be supported by the haptic device - int ff_strength; - - if (Using_in_game_options) { - ff_strength = (int)ForceFeedbackStrength->getValue(); - } else { - ff_strength = os_config_read_uint("ForceFeedback", "Strength", 100); - } - CLAMP(ff_strength, 0, 100); - if (SDL_GetHapticFeatures(haptic) & SDL_HAPTIC_GAIN) { - SDL_SetHapticGain(haptic, ff_strength); - } else { - if (ff_strength != 100) { - ReleaseWarning(LOCATION, "The configuration file is configured with a force feedback strength value of %d%% " - "but your haptic device does not support setting the global strength of the effects. All effects will be" - "played with full strength.", ff_strength); - } + joy_haptic_shutdown(); + return false; } mprintf(("\n")); @@ -215,37 +130,101 @@ int joy_ff_init() mprintf((" ... Haptic successfully initialized!\n")); - return 0; + return true; } -void joy_ff_shutdown() +void joy_haptic_shutdown() { - if ( !Joy_ff_enabled ) { + if ( !haptic ) { return; } - joy_ff_afterburn_off(); + joy_haptic_afterburn_off(); SDL_StopHapticEffects(haptic); SDL_CloseHaptic(haptic); - haptic = NULL; + haptic = nullptr; SDL_QuitSubSystem(SDL_INIT_HAPTIC); +} - Joy_ff_enabled = 0; - Joy_ff_acquired = 0; +static bool joy_haptic_can_play() +{ + return (haptic != nullptr); } -int joy_ff_reinit() +void joy_haptic_set_gain(int gain) { - if (Joy_ff_enabled) { - joy_ff_shutdown(); + if ( !joy_haptic_can_play() ) { + return; } - return joy_ff_init(); + CLAMP(gain, 0, 100); + + if (SDL_GetHapticFeatures(haptic) & SDL_HAPTIC_GAIN) { + SDL_SetHapticGain(haptic, gain); + } else { + Gain = gain / 100.0f; + Gain_adjust = (gain < 100); + + // force update spring effect now since it only changes at mission start + joy_haptic_update_effect(&pSpring, "Spring"); + } } -static bool joy_ff_create_effects() +static void haptic_adjust_effect_strength(SDL_HapticEffect *eff) +{ + if ( !Gain_adjust ) { + return; + } + + switch (eff->type) { + case SDL_HAPTIC_SINE: + case SDL_HAPTIC_SQUARE: + case SDL_HAPTIC_TRIANGLE: + case SDL_HAPTIC_SAWTOOTHUP: + case SDL_HAPTIC_SAWTOOTHDOWN: + eff->periodic.magnitude = Sint16(eff->periodic.magnitude * Gain); + break; + + case SDL_HAPTIC_CONSTANT: + eff->constant.level = Sint16(eff->constant.level * Gain); + break; + + // special case for spring + case SDL_HAPTIC_SPRING: { + auto val = Uint16(0x7FFF * Gain); + + for (size_t i = 0; i < SDL_arraysize(eff->condition.right_sat); i++) { + eff->condition.right_sat[i] = val; + eff->condition.left_sat[i] = val; + } + + break; + } + + default: + break; + } +} + +static bool haptic_create_effect(haptic_effect_t *effect) +{ + // manually adjust effect strength if necessary + haptic_adjust_effect_strength(&effect->eff); + + effect->id = SDL_CreateHapticEffect(haptic, &effect->eff); + + if (effect->id < 0) { + return false; + } + + effect->loaded = true; + + return true; +} + +static bool joy_haptic_create_effects() { int num_loaded = 0; @@ -273,12 +252,9 @@ static bool joy_ff_create_effects() pSpring.eff.condition.left_coeff[i] = 0x147; } - pSpring.id = SDL_CreateHapticEffect(haptic, &pSpring.eff); - - if (pSpring.id < 0) { + if ( !haptic_create_effect(&pSpring) ) { mprintf((" Spring effect failed to load:\n %s\n", SDL_GetError())); } else { - pSpring.loaded = true; ++num_loaded; } } @@ -296,12 +272,9 @@ static bool joy_ff_create_effects() pShootEffect.eff.periodic.magnitude = 0x7FFF; pShootEffect.eff.periodic.fade_length = 120; - pShootEffect.id = SDL_CreateHapticEffect(haptic, &pShootEffect.eff); - - if (pShootEffect.id < 0) { + if ( !haptic_create_effect(&pShootEffect) ) { mprintf((" Fire primary effect failed to load:\n %s\n", SDL_GetError())); } else { - pShootEffect.loaded = true; ++num_loaded; } } @@ -321,12 +294,9 @@ static bool joy_ff_create_effects() pSecShootEffect.eff.constant.fade_length = 100; pSecShootEffect.eff.constant.fade_level = 1; - pSecShootEffect.id = SDL_CreateHapticEffect(haptic, &pSecShootEffect.eff); - - if (pSecShootEffect.id < 0) { + if ( !haptic_create_effect(&pSecShootEffect) ) { mprintf((" Fire secondary effect failed to load:\n %s\n", SDL_GetError())); } else { - pSecShootEffect.loaded = true; ++num_loaded; } } @@ -343,12 +313,9 @@ static bool joy_ff_create_effects() pAfterburn1.eff.periodic.period = 20; pAfterburn1.eff.periodic.magnitude = 0x3332; - pAfterburn1.id = SDL_CreateHapticEffect(haptic, &pAfterburn1.eff); - - if (pAfterburn1.id < 0) { + if ( !haptic_create_effect(&pAfterburn1) ) { mprintf((" Afterburn effect 1 failed to load:\n %s\n", SDL_GetError())); } else { - pAfterburn1.loaded = true; ++num_loaded; } @@ -363,12 +330,9 @@ static bool joy_ff_create_effects() pAfterburn2.eff.periodic.period = 100; pAfterburn2.eff.periodic.magnitude = 0x1999; - pAfterburn2.id = SDL_CreateHapticEffect(haptic, &pAfterburn2.eff); - - if (pAfterburn2.id < 0) { + if ( !haptic_create_effect(&pAfterburn2) ) { mprintf((" Afterburn effect 2 failed to load:\n %s\n", SDL_GetError())); } else { - pAfterburn2.loaded = true; ++num_loaded; } } @@ -388,12 +352,9 @@ static bool joy_ff_create_effects() pHitEffect1.eff.constant.fade_length = 120; pHitEffect1.eff.constant.fade_level = 1; - pHitEffect1.id = SDL_CreateHapticEffect(haptic, &pHitEffect1.eff); - - if (pHitEffect1.id < 0) { + if ( !haptic_create_effect(&pHitEffect1) ) { mprintf((" Hit effect 1 failed to load:\n %s\n", SDL_GetError())); } else { - pHitEffect1.loaded = true; ++num_loaded; } } @@ -412,12 +373,9 @@ static bool joy_ff_create_effects() pHitEffect2.eff.periodic.attack_length = 100; pHitEffect2.eff.periodic.fade_length = 100; - pHitEffect2.id = SDL_CreateHapticEffect(haptic, &pHitEffect2.eff); - - if (pHitEffect2.id < 0) { + if ( !haptic_create_effect(&pHitEffect2) ) { mprintf((" Hit effect 2 failed to load:\n %s\n", SDL_GetError())); } else { - pHitEffect2.loaded = true; ++num_loaded; } } @@ -434,12 +392,9 @@ static bool joy_ff_create_effects() pDock.eff.periodic.period = 100; pDock.eff.periodic.magnitude = 0x3332; - pDock.id = SDL_CreateHapticEffect(haptic, &pDock.eff); - - if (pDock.id < 0) { + if ( !haptic_create_effect(&pDock) ) { mprintf((" Dock effect failed to load:\n %s\n", SDL_GetError())); } else { - pDock.loaded = true; ++num_loaded; } } @@ -447,12 +402,7 @@ static bool joy_ff_create_effects() return (num_loaded > 0); } -static bool joy_ff_can_play() -{ - return (Joy_ff_enabled && Joy_ff_acquired); -} - -static void joy_ff_start_effect(haptic_effect_t *eff, const char* /* name */) +static void joy_haptic_start_effect(haptic_effect_t *eff, const char* /* name */) { if ( !eff->loaded ) { return; @@ -465,12 +415,15 @@ static void joy_ff_start_effect(haptic_effect_t *eff, const char* /* name */) } } -static void joy_ff_update_effect(haptic_effect_t *eff, const char* /* name */) +static void joy_haptic_update_effect(haptic_effect_t *eff, const char* /* name */) { if ( !eff->loaded ) { return; } + // manually adjust effect strength if necessary + haptic_adjust_effect_strength(&eff->eff); + // nprintf(("Joystick", "FF: Updating effect %s\n", name)); if ( !SDL_UpdateHapticEffect(haptic, eff->id, &eff->eff) ) { @@ -478,7 +431,7 @@ static void joy_ff_update_effect(haptic_effect_t *eff, const char* /* name */) } } -static void joy_ff_stop_effect(haptic_effect_t *eff, const char* /* name */) +static void joy_haptic_stop_effect(haptic_effect_t *eff, const char* /* name */) { if ( !eff->loaded ) { return; @@ -491,38 +444,36 @@ static void joy_ff_stop_effect(haptic_effect_t *eff, const char* /* name */) } } -void joy_ff_stop_effects() +void joy_haptic_stop_effects() { - if ( !Joy_ff_enabled ) { + if ( !joy_haptic_can_play() ) { return; } - joy_ff_afterburn_off(); + joy_haptic_afterburn_off(); // stop everything *except* spring - joy_ff_stop_effect(&pHitEffect1, "HitEffect1"); - joy_ff_stop_effect(&pHitEffect2, "HitEffect2"); - joy_ff_stop_effect(&pAfterburn1, "Afterburn1"); - joy_ff_stop_effect(&pAfterburn2, "Afterburn2"); - joy_ff_stop_effect(&pShootEffect, "ShootEffect"); - joy_ff_stop_effect(&pSecShootEffect, "SecShootEffect"); - joy_ff_stop_effect(&pDock, "Dock"); + joy_haptic_stop_effect(&pHitEffect1, "HitEffect1"); + joy_haptic_stop_effect(&pHitEffect2, "HitEffect2"); + joy_haptic_stop_effect(&pAfterburn1, "Afterburn1"); + joy_haptic_stop_effect(&pAfterburn2, "Afterburn2"); + joy_haptic_stop_effect(&pShootEffect, "ShootEffect"); + joy_haptic_stop_effect(&pSecShootEffect, "SecShootEffect"); + joy_haptic_stop_effect(&pDock, "Dock"); } -void joy_ff_mission_init(vec3d v) +void joy_haptic_mission_init(vec3d *v) { - if ( !Joy_ff_enabled ) { + if ( !joy_haptic_can_play() ) { return; } - v.xyz.z = 0.0f; - - joy_ff_handling_scaler = (int) ((vm_vec_mag(&v) + 1.3f) * 5.0f); + v->xyz.z = 0.0f; - joy_ff_adjust_handling(0); - joy_ff_start_effect(&pSpring, "Spring"); + joy_haptic_handling_scaler = (int) ((vm_vec_mag(v) + 1.3f) * 5.0f); - Joy_ff_afterburning = 0; + joy_haptic_adjust_handling(0); + joy_haptic_start_effect(&pSpring, "Spring"); // reset afterburn effects to default values @@ -531,14 +482,14 @@ void joy_ff_mission_init(vec3d v) pAfterburn1.eff.periodic.magnitude = 0x3332; pAfterburn1.eff.periodic.attack_length = 0; - joy_ff_update_effect(&pAfterburn1, "Afterburn1 (init)"); + joy_haptic_update_effect(&pAfterburn1, "Afterburn1 (init)"); pAfterburn2.eff.periodic.length = SDL_HAPTIC_INFINITY; pAfterburn2.eff.periodic.period = 100; pAfterburn2.eff.periodic.magnitude = 0x1999; pAfterburn2.eff.periodic.attack_length = 0; - joy_ff_update_effect(&pAfterburn2, "Afterburn2 (init)"); + joy_haptic_update_effect(&pAfterburn2, "Afterburn2 (init)"); // reset primary shoot effect to default values @@ -546,46 +497,15 @@ void joy_ff_mission_init(vec3d v) pShootEffect.eff.periodic.length = 160; pShootEffect.eff.periodic.fade_length = 120; - joy_ff_update_effect(&pShootEffect, "ShootEffect (init)"); + joy_haptic_update_effect(&pShootEffect, "ShootEffect (init)"); } -void joy_reacquire_ff() -{ - if ( !Joy_ff_enabled ) { - return; - } - - if (Joy_ff_acquired) { - return; - } - - Joy_ff_acquired = 1; - - joy_ff_start_effect(&pSpring, "Spring"); -} - -void joy_unacquire_ff() -{ - if ( !Joy_ff_enabled ) { - return; - } - - if ( !Joy_ff_acquired ) { - return; - } - - joy_ff_afterburn_off(); - SDL_StopHapticEffects(haptic); - - Joy_ff_acquired = 0; -} - -void joy_ff_play_vector_effect(vec3d *v, float scaler) +void joy_haptic_play_vector_effect(vec3d *v, float scaler) { vec3d vf; float x, y; - if ( !joy_ff_can_play() ) { + if ( !joy_haptic_can_play() ) { return; } @@ -600,16 +520,16 @@ void joy_ff_play_vector_effect(vec3d *v, float scaler) y = vm_vec_mag(&vf); } - joy_ff_play_dir_effect(-x, -y); + joy_haptic_play_dir_effect(-x, -y); } -void joy_ff_play_dir_effect(float x, float y) +void joy_haptic_play_dir_effect(float x, float y) { static UI_TIMESTAMP hit_timeout; int idegs, imag; float degs; - if ( !joy_ff_can_play() ) { + if ( !joy_haptic_can_play() ) { return; } @@ -625,7 +545,7 @@ void joy_ff_play_dir_effect(float x, float y) hit_timeout = ui_timestamp(pHitEffect1.eff.condition.length); - if (Joy_ff_directional_hit_effect_enabled) { + if (joy_ff_hit_effect_enabled()) { if (x > 8000.0f) { x = 8000.0f; } else if (x < -8000.0f) { @@ -656,7 +576,7 @@ void joy_ff_play_dir_effect(float x, float y) pHitEffect1.eff.constant.direction.dir[0] = idegs; pHitEffect1.eff.constant.level = (Sint16)fl2i(0x7FFF * (imag / 10000.0f)); - joy_ff_update_effect(&pHitEffect1, "HitEffect1"); + joy_haptic_update_effect(&pHitEffect1, "HitEffect1"); idegs += 9000; if (idegs >= 36000) @@ -665,16 +585,16 @@ void joy_ff_play_dir_effect(float x, float y) pHitEffect2.eff.periodic.direction.dir[0] = idegs; pHitEffect2.eff.periodic.magnitude = (Sint16)fl2i(0x7FFF * (imag / 10000.0f)); - joy_ff_update_effect(&pHitEffect2, "HitEffect2"); + joy_haptic_update_effect(&pHitEffect2, "HitEffect2"); } - joy_ff_start_effect(&pHitEffect1, "HitEffect1"); - joy_ff_start_effect(&pHitEffect2, "HitEffect2"); + joy_haptic_start_effect(&pHitEffect1, "HitEffect1"); + joy_haptic_start_effect(&pHitEffect2, "HitEffect2"); } -void joy_ff_play_primary_shoot(int gain) +void joy_haptic_play_primary_shoot(int gain) { - if ( !joy_ff_can_play() ) { + if ( !joy_haptic_can_play() ) { return; } @@ -689,17 +609,17 @@ void joy_ff_play_primary_shoot(int gain) if (gain != primary_ff_level) { pShootEffect.eff.periodic.magnitude = (Sint16) fl2i(0x7FFF * (gain / 10000.0f)); - joy_ff_update_effect(&pShootEffect, "ShootEffect"); + joy_haptic_update_effect(&pShootEffect, "ShootEffect"); primary_ff_level = gain; } - joy_ff_start_effect(&pShootEffect, "ShootEffect"); + joy_haptic_start_effect(&pShootEffect, "ShootEffect"); } -void joy_ff_play_secondary_shoot(int gain) +void joy_haptic_play_secondary_shoot(int gain) { - if ( !joy_ff_can_play() ) { + if ( !joy_haptic_can_play() ) { return; } @@ -717,21 +637,21 @@ void joy_ff_play_secondary_shoot(int gain) pSecShootEffect.eff.constant.level = (Sint16) fl2i(0x7FFF * (gain / 10000.0f)); pSecShootEffect.eff.constant.length = (150000 + gain * 25) / 1000; - joy_ff_update_effect(&pSecShootEffect, "SecShootEffect"); + joy_haptic_update_effect(&pSecShootEffect, "SecShootEffect"); secondary_ff_level = gain; // nprintf(("Joystick", "FF: Secondary force = 0x%04x\n", pSecShootEffect.eff.constant.level)); } - joy_ff_start_effect(&pSecShootEffect, "SecShootEffect"); + joy_haptic_start_effect(&pSecShootEffect, "SecShootEffect"); } -void joy_ff_adjust_handling(int speed) +void joy_haptic_adjust_handling(int speed) { int v; short coeff = 0; - if ( !joy_ff_can_play() ) { + if ( !joy_haptic_can_play() ) { return; } @@ -747,9 +667,9 @@ void joy_ff_adjust_handling(int speed) last_speed = speed; - v = speed * joy_ff_handling_scaler * 2 / 3; -// v += joy_ff_handling_scaler * joy_ff_handling_scaler * 6 / 7 + 250; - v += joy_ff_handling_scaler * 45 - 500; + v = speed * joy_haptic_handling_scaler * 2 / 3; +// v += joy_haptic_handling_scaler * joy_haptic_handling_scaler * 6 / 7 + 250; + v += joy_haptic_handling_scaler * 45 - 500; CLAMP(v, 0, 10000); @@ -762,12 +682,12 @@ void joy_ff_adjust_handling(int speed) // nprintf(("Joystick", "FF: New handling force = 0x%04x\n", coeff)); - joy_ff_update_effect(&pSpring, "Spring"); + joy_haptic_update_effect(&pSpring, "Spring"); } -void joy_ff_docked() +void joy_haptic_docked() { - if ( !joy_ff_can_play() ) { + if ( !joy_haptic_can_play() ) { return; } @@ -777,13 +697,13 @@ void joy_ff_docked() pDock.eff.periodic.magnitude = 0x3332; - joy_ff_update_effect(&pDock, "Dock"); - joy_ff_start_effect(&pDock, "Dock"); + joy_haptic_update_effect(&pDock, "Dock"); + joy_haptic_start_effect(&pDock, "Dock"); } -void joy_ff_play_reload_effect() +void joy_haptic_play_reload_effect() { - if ( !Joy_ff_enabled ) { + if ( !joy_haptic_can_play() ) { return; } @@ -793,89 +713,73 @@ void joy_ff_play_reload_effect() pDock.eff.periodic.magnitude = 0x1999; - joy_ff_update_effect(&pDock, "Dock (Reload)"); - joy_ff_start_effect(&pDock, "Dock (Reload)"); + joy_haptic_update_effect(&pDock, "Dock (Reload)"); + joy_haptic_start_effect(&pDock, "Dock (Reload)"); } -void joy_ff_afterburn_on() +void joy_haptic_afterburn_on() { - if ( !joy_ff_can_play() ) { - return; - } - - if (Joy_ff_afterburning) { + if ( !joy_haptic_can_play() ) { return; } pAfterburn1.eff.periodic.length = SDL_HAPTIC_INFINITY; pAfterburn1.eff.periodic.magnitude = 0x3332; - joy_ff_update_effect(&pAfterburn1, "Afterburn1"); + joy_haptic_update_effect(&pAfterburn1, "Afterburn1"); pAfterburn2.eff.periodic.length = SDL_HAPTIC_INFINITY; pAfterburn2.eff.periodic.magnitude = 0x1999; - joy_ff_update_effect(&pAfterburn2, "Afterburn2"); + joy_haptic_update_effect(&pAfterburn2, "Afterburn2"); - joy_ff_start_effect(&pAfterburn1, "Afterburn1"); - joy_ff_start_effect(&pAfterburn2, "Afterburn2"); + joy_haptic_start_effect(&pAfterburn1, "Afterburn1"); + joy_haptic_start_effect(&pAfterburn2, "Afterburn2"); // nprintf(("Joystick", "FF: Afterburn started\n")); - - Joy_ff_afterburning = 1; } -void joy_ff_afterburn_off() +void joy_haptic_afterburn_off() { - if ( !joy_ff_can_play() ) { - return; - } - - if ( !Joy_ff_afterburning ) { + if ( !joy_haptic_can_play() ) { return; } - joy_ff_stop_effect(&pAfterburn1, "Afterburn1"); - joy_ff_stop_effect(&pAfterburn2, "Afterburn2"); - - Joy_ff_afterburning = 0; + joy_haptic_stop_effect(&pAfterburn1, "Afterburn1"); + joy_haptic_stop_effect(&pAfterburn2, "Afterburn2"); // nprintf(("Joystick", "FF: Afterburn stopped\n")); } -void joy_ff_explode() +void joy_haptic_explode() { - if ( !joy_ff_can_play() ) { + if ( !joy_haptic_can_play() ) { return; } - joy_ff_afterburn_off(); + joy_haptic_afterburn_off(); // reuse shoot effect here to save device memory if (pShootEffect.loaded) { // direction changes so an effect stop is needed before udpate - joy_ff_stop_effect(&pShootEffect, "ShootEffect (Explode)"); + joy_haptic_stop_effect(&pShootEffect, "ShootEffect (Explode)"); pShootEffect.eff.periodic.direction.dir[0] = 9000; pShootEffect.eff.periodic.length = 500; pShootEffect.eff.periodic.magnitude = 0x7FFF; pShootEffect.eff.periodic.fade_length = 500; - joy_ff_update_effect(&pShootEffect, "ShootEffect (Explode)"); + joy_haptic_update_effect(&pShootEffect, "ShootEffect (Explode)"); - joy_ff_start_effect(&pShootEffect, "ShootEffect (Explode)"); + joy_haptic_start_effect(&pShootEffect, "ShootEffect (Explode)"); } } -void joy_ff_fly_by(int mag) +void joy_haptic_fly_by(int mag) { int gain; - if ( !joy_ff_can_play() ) { - return; - } - - if (Joy_ff_afterburning) { + if ( !joy_haptic_can_play() ) { return; } @@ -886,20 +790,20 @@ void joy_ff_fly_by(int mag) pAfterburn1.eff.periodic.length = (6000 * mag + 400000) / 1000; pAfterburn1.eff.periodic.magnitude = (Sint16) fl2i(0x7FFF * (gain / 10000.0f)); - joy_ff_update_effect(&pAfterburn1, "Afterburn1 (Fly by)"); + joy_haptic_update_effect(&pAfterburn1, "Afterburn1 (Fly by)"); pAfterburn2.eff.periodic.length = (6000 * mag + 400000) / 1000; pAfterburn2.eff.periodic.magnitude = (Sint16) fl2i(0x7FFF * (gain / 10000.0f)); - joy_ff_update_effect(&pAfterburn2, "Afterburn2 (Fly by)"); + joy_haptic_update_effect(&pAfterburn2, "Afterburn2 (Fly by)"); - joy_ff_start_effect(&pAfterburn1, "Afterburn1 (Fly by)"); - joy_ff_start_effect(&pAfterburn2, "Afterburn2 (Fly by)"); + joy_haptic_start_effect(&pAfterburn1, "Afterburn1 (Fly by)"); + joy_haptic_start_effect(&pAfterburn2, "Afterburn2 (Fly by)"); } -void joy_ff_deathroll() +void joy_haptic_deathroll() { - if ( !joy_ff_can_play() ) { + if ( !joy_haptic_can_play() ) { return; } @@ -914,17 +818,15 @@ void joy_ff_deathroll() pAfterburn1.eff.periodic.magnitude = 0x7FFF; pAfterburn1.eff.periodic.attack_length = 200; - joy_ff_update_effect(&pAfterburn1, "Afterburn1 (Death Roll)"); + joy_haptic_update_effect(&pAfterburn1, "Afterburn1 (Death Roll)"); pAfterburn2.eff.periodic.length = SDL_HAPTIC_INFINITY; pAfterburn2.eff.periodic.period = 200; pAfterburn2.eff.periodic.magnitude = 0x7FFF; pAfterburn2.eff.periodic.attack_length = 200; - joy_ff_update_effect(&pAfterburn2, "Afterburn2 (Death Roll)"); - - joy_ff_start_effect(&pAfterburn1, "Afterburn1 (Death Roll)"); - joy_ff_start_effect(&pAfterburn2, "Afterburn2 (Death Roll)"); + joy_haptic_update_effect(&pAfterburn2, "Afterburn2 (Death Roll)"); - Joy_ff_afterburning = 1; // to prevent flyby effect + joy_haptic_start_effect(&pAfterburn1, "Afterburn1 (Death Roll)"); + joy_haptic_start_effect(&pAfterburn2, "Afterburn2 (Death Roll)"); } diff --git a/code/io/joy_haptic.h b/code/io/joy_haptic.h new file mode 100644 index 00000000000..3158b32ffa9 --- /dev/null +++ b/code/io/joy_haptic.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) Volition, Inc. 1999. All rights reserved. + * + * All source code herein is the property of Volition, Inc. You may not sell + * or otherwise commercially exploit the source or things you created based on the + * source. + * +*/ + + + +#ifndef __JOY_HAPTIC_H__ +#define __JOY_HAPTIC_H__ + +#include "globalincs/pstypes.h" + +bool joy_haptic_init(); +void joy_haptic_shutdown(); +void joy_haptic_set_gain(int gain); +void joy_haptic_stop_effects(); +void joy_haptic_mission_init(vec3d *v); +void joy_haptic_play_vector_effect(vec3d *v, float scaler); +void joy_haptic_play_dir_effect(float x, float y); +void joy_haptic_play_primary_shoot(int gain); +void joy_haptic_play_secondary_shoot(int gain); +void joy_haptic_adjust_handling(int speed); +void joy_haptic_docked(); +void joy_haptic_play_reload_effect(); +void joy_haptic_afterburn_on(); +void joy_haptic_afterburn_off(); +void joy_haptic_explode(); +void joy_haptic_fly_by(int mag); +void joy_haptic_deathroll(); + +#endif + diff --git a/code/io/joy_rumble.cpp b/code/io/joy_rumble.cpp new file mode 100644 index 00000000000..4c0cbe0d9e8 --- /dev/null +++ b/code/io/joy_rumble.cpp @@ -0,0 +1,162 @@ +/* + * Copyright (C) Volition, Inc. 1999. All rights reserved. + * + * All source code herein is the property of Volition, Inc. You may not sell + * or otherwise commercially exploit the source or things you created based on the + * source. + * + */ + +#include "globalincs/pstypes.h" +#include "io/joy.h" +#include "io/joy_ff.h" +#include "io/joy_rumble.h" + + +static SDL_Gamepad *gamepad = nullptr; +static bool Gain_adjust = false; +static float Gain = 1.0f; + +static ushort adjust_gain(ushort val) +{ + if (Gain_adjust) { + val = static_cast(val * Gain); + } + + return val; +} + +bool joy_rumble_init() +{ + auto Joy = io::joystick::getPlayerJoystick(CID_JOY0); + + if ( !Joy || !Joy->isGamepad() ) { + return false; + } + + mprintf(("\n")); + mprintf((" Initializing Rumble...\n")); + + gamepad = Joy->getGamepad(); + + auto props = SDL_GetGamepadProperties(gamepad); + + if ( !SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_RUMBLE_BOOLEAN, false) ) { + mprintf((" ERROR: Rumble not supported\n")); + gamepad = nullptr; + return false; + } + + // TODO: SDL3 => figure out how best to use trigger rumble + bool rumble_triggers = SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_TRIGGER_RUMBLE_BOOLEAN, false); + (void)rumble_triggers; + + mprintf((" Supports trigger rumble: %s\n", rumble_triggers ? "true" : "false")); + + mprintf((" ... Rumble successfully initialized!\n")); + + return true; +} + +void joy_rumble_shutdown() +{ + if (gamepad) { + SDL_RumbleGamepad(gamepad, 0, 0, 0); + gamepad = nullptr; + } +} + +void joy_rumble_set_gain(int gain) +{ + CAP(gain, 1, 100); + + Gain = gain / 100.0f; + Gain_adjust = (gain < 100); +} + +void joy_rumble_stop_effects() +{ + SDL_RumbleGamepad(gamepad, 0, 0, 0); +} + +void joy_rumble_mission_init(vec3d *v __UNUSED) +{ +} + +void joy_rumble_play_vector_effect(vec3d *v __UNUSED, float scaler __UNUSED) +{ + SDL_RumbleGamepad(gamepad, adjust_gain(0x2fff), adjust_gain(0x4fff), 300); +} + +void joy_rumble_play_dir_effect(float x __UNUSED, float y __UNUSED) +{ + SDL_RumbleGamepad(gamepad, adjust_gain(0x2fff), adjust_gain(0x4fff), 300); +} + +void joy_rumble_play_primary_shoot(int gain) +{ + CAP(gain, 1, 10000); + + auto freq = ushort(0x7fff * (gain / 10000.0f)); + freq = adjust_gain(freq); + + SDL_RumbleGamepad(gamepad, freq, freq/2, 100); +} + +void joy_rumble_play_secondary_shoot(int gain) +{ + gain = gain * 100 + 2500; + + CAP(gain, 1, 10000); + + int duration = (150000 + gain * 25) / 1000; + auto freq = ushort(0xffff * (gain / 10000.0f)); + freq = adjust_gain(freq); + + SDL_RumbleGamepad(gamepad, freq, freq/2, duration); +} + +void joy_rumble_adjust_handling(int speed __UNUSED) +{ +} + +void joy_rumble_docked() +{ + SDL_RumbleGamepad(gamepad, adjust_gain(0xffff), 0, 150); +} + +void joy_rumble_play_reload_effect() +{ + SDL_RumbleGamepad(gamepad, adjust_gain(0x1fff), adjust_gain(0x7fff), 50); +} + +void joy_rumble_afterburn_on() +{ + SDL_RumbleGamepad(gamepad, adjust_gain(0x7fff), 0, 1500); +} + +void joy_rumble_afterburn_off() +{ + SDL_RumbleGamepad(gamepad, 0, 0, 0); +} + +void joy_rumble_explode() +{ + SDL_RumbleGamepad(gamepad, adjust_gain(0xffff), adjust_gain(0x7fff), 500); +} + +void joy_rumble_fly_by(int mag) +{ + int gain = mag * 120 + 4000; + + CAP(gain, 1, 10000); + + int duration = (6000 * mag + 400000) / 1000; + + SDL_RumbleGamepad(gamepad, adjust_gain(0x3fff), adjust_gain(0x7fff), duration); +} + +void joy_rumble_deathroll() +{ + SDL_RumbleGamepad(gamepad, adjust_gain(0x4fff), adjust_gain(0x1fff), 4000); +} diff --git a/code/io/joy_rumble.h b/code/io/joy_rumble.h new file mode 100644 index 00000000000..fdf67be8ff5 --- /dev/null +++ b/code/io/joy_rumble.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) Volition, Inc. 1999. All rights reserved. + * + * All source code herein is the property of Volition, Inc. You may not sell + * or otherwise commercially exploit the source or things you created based on the + * source. + * +*/ + + + +#ifndef __JOY_RUMBLE_H__ +#define __JOY_RUMBLE_H__ + +#include "globalincs/pstypes.h" + +bool joy_rumble_init(); +void joy_rumble_shutdown(); +void joy_rumble_set_gain(int gain); +void joy_rumble_stop_effects(); +void joy_rumble_mission_init(vec3d *v); +void joy_rumble_play_vector_effect(vec3d *v, float scaler); +void joy_rumble_play_dir_effect(float x, float y); +void joy_rumble_play_primary_shoot(int gain); +void joy_rumble_play_secondary_shoot(int gain); +void joy_rumble_adjust_handling(int speed); +void joy_rumble_docked(); +void joy_rumble_play_reload_effect(); +void joy_rumble_afterburn_on(); +void joy_rumble_afterburn_off(); +void joy_rumble_explode(); +void joy_rumble_fly_by(int mag); +void joy_rumble_deathroll(); + +#endif + + diff --git a/code/source_groups.cmake b/code/source_groups.cmake index 70dd601d39a..913aeff5543 100644 --- a/code/source_groups.cmake +++ b/code/source_groups.cmake @@ -745,7 +745,11 @@ add_file_folder("Io" io/joy.h io/joy-sdl.cpp io/joy_ff.h - io/joy_ff-sdl.cpp + io/joy_ff.cpp + io/joy_haptic.h + io/joy_haptic.cpp + io/joy_rumble.h + io/joy_rumble.cpp io/spacemouse.cpp io/spacemouse.h ) From 21d975704cb13cad142a80f5a27e1bd105dd6a5f Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Fri, 24 Jul 2026 17:24:46 -0400 Subject: [PATCH 2/2] add gamepad navigation support Gamepad navigation is enabled by default but can be disabled with settings. As the retail FreeSpace UI system doesn't lend itself all that well to proper gamepad mechanics, the gamepad inputs instead mimic a mouse and keyboard. Gamepad navigation is only active when at least one gamepad is connected and the mouse cursor is set to visible in the game. One exception is gamepad camera controls, where no cursor check is performed. A gamepad does NOT need to be set as a current joystick in order to work for navigation. When gamepad navigation is initialized it also enables Dear ImGui gamepad navigation. While in the Lab or In-game Settings the ImGui gamepad navigation layout will be used instead of the normal FreeSpace-style navigation layout. FreeSpace gamepad navigation layout: Mouse -------- The cursor is controlled via the Left Stick. Left click is simulated via the Action button (A / Cross, by default) Right click is simulated via the X / Square button Keyboard --------- Although they are used inconsistently, with usage and purpose occasionally varying between different menus, the retail UI code generally uses the following keyboard keys for navigation: The ESC key is simulated via the Cancel button (B / Circle, by default) The Arrow keys are simulated via D-Pad The TAB key is simulated via the Right Shoulder button The Shift-TAB combo is simulated via the Left Shoulder button The Page Down key is simulated via the Left Trigger The Page Up key is simulated via the Right Trigger Dear ImGui gamepad navigation: The gamepad is supported directly without simulating Mouse/Keyboard inputs. ImGui control layout infographics are available at: https://www.dearimgui.com/controls_sheets/ Camera controls (used in the Lab & chosen to not conflict with ImGui): Right Stick: Orient the displayed object Right Stick + Left Trigger: Rotate the camera Right Stick + Right Trigger: Zoom the camera in or out Right Stick + Both Triggers: Pan the camera on the X/Y plane --- code/controlconfig/controlsconfig.cpp | 5 + code/controlconfig/controlsconfig.h | 5 + code/cutscene/movie.cpp | 5 + code/io/gamepad.cpp | 605 ++++++++++++++++++++++++++ code/io/gamepad.h | 158 +++++++ code/io/joy-sdl.cpp | 17 + code/io/joy.h | 6 + code/io/joy_ff.cpp | 5 +- code/io/mouse.cpp | 13 + code/io/mouse.h | 3 + code/lab/dialogs/lab_ui.cpp | 24 +- code/lab/manager/lab_manager.cpp | 30 +- code/localization/localize.cpp | 2 +- code/source_groups.cmake | 2 + freespace2/freespace.cpp | 13 +- 15 files changed, 875 insertions(+), 18 deletions(-) create mode 100644 code/io/gamepad.cpp create mode 100644 code/io/gamepad.h diff --git a/code/controlconfig/controlsconfig.cpp b/code/controlconfig/controlsconfig.cpp index 3b6a4938cce..d43827d4402 100644 --- a/code/controlconfig/controlsconfig.cpp +++ b/code/controlconfig/controlsconfig.cpp @@ -1531,6 +1531,11 @@ const char *control_config_tooltip_handler(const char *str) return NULL; } +bool control_config_special_mode() +{ + return (Binding_mode || Search_mode); +} + void control_config_init(bool API_Access) { int i; diff --git a/code/controlconfig/controlsconfig.h b/code/controlconfig/controlsconfig.h index 24e538d6882..5f659afa916 100644 --- a/code/controlconfig/controlsconfig.h +++ b/code/controlconfig/controlsconfig.h @@ -740,6 +740,11 @@ void control_config_common_init(); */ void control_config_common_close(); +/*! + * @brief detect whether control config is in search or binding modes + */ +bool control_config_special_mode(); + /*! * @brief init config menu */ diff --git a/code/cutscene/movie.cpp b/code/cutscene/movie.cpp index 0ac531a81db..53bd352ed30 100644 --- a/code/cutscene/movie.cpp +++ b/code/cutscene/movie.cpp @@ -28,6 +28,7 @@ #include "tracing/tracing.h" #include "io/timer.h" #include "io/key.h" +#include "io/gamepad.h" #include "mod_table/mod_table.h" #include "network/multi.h" #include "scripting/global_hooks.h" @@ -244,6 +245,10 @@ void movie_display_loop(Player* player, PlaybackState* state) { processEvents(); + if (io::gamepad::action_or_cancel()) { + state->playing = false; + } + // NOTE: This does not update mission time! If movies get enabled in places // other than through cutscenes then some refactoring should be done // to account for normal time progression rather than just timestamps diff --git a/code/io/gamepad.cpp b/code/io/gamepad.cpp new file mode 100644 index 00000000000..eaab3b3166e --- /dev/null +++ b/code/io/gamepad.cpp @@ -0,0 +1,605 @@ +/* + * Copyright (C) Volition, Inc. 1999. All rights reserved. + * + * All source code herein is the property of Volition, Inc. You may not sell + * or otherwise commercially exploit the source or things you created based on the + * source. + * + */ + +#include "globalincs/pstypes.h" +#include "io/gamepad.h" +#include "io/mouse.h" +#include "io/cursor.h" +#include "io/key.h" +#include "osapi/osapi.h" +#include "gamesequence/gamesequence.h" +#include "options/Option.h" + +#include "imgui.h" + + +using namespace io::gamepad; + +namespace { + +bool initialized = false; + +typedef std::unique_ptr GamepadPtr; + +SCP_vector gamepads; + +constexpr uint32_t KEY_CHECK_INTERVAL_MS = 150; +constexpr uint32_t CURSOR_UPDATE_INTERVAL_MS = 20; // 50 Hz + +// config options ---- +bool NavEnabled = true; +bool SwapActionCancel = false; +int CursorSpeed = 6; +// ------------------- + +// NOTE: never return true from here since we want these events to cascade to +// other places +bool event_handler(const SDL_Event &evt) +{ + const auto id = evt.gdevice.which; + + auto gamepad = std::find_if(gamepads.begin(), gamepads.end(), + [id](GamepadPtr &p) { return p->getId() == id; }); + + if (evt.type == SDL_EVENT_GAMEPAD_ADDED) { + // this event can fire more than once for the same device so we need to + // check for duplicates + if (gamepad == gamepads.end()) { + gamepads.push_back(GamepadPtr(new Gamepad(id))); + } + + return false; + } else if (evt.type == SDL_EVENT_GAMEPAD_REMOVED) { + if (gamepad != gamepads.end()) { + std::swap(*gamepad, gamepads.back()); + gamepads.pop_back(); + } + + return false; + } + + if (gamepad == gamepads.end()) { + return false; + } + + // skip these events if control config is in bind or search mode + if (control_config_special_mode()) { + return false; + } + + switch (evt.type) { + case SDL_EVENT_GAMEPAD_BUTTON_DOWN: + case SDL_EVENT_GAMEPAD_BUTTON_UP: + (*gamepad)->mark_button(evt.gbutton.button, evt.gbutton.down); + break; + + case SDL_EVENT_GAMEPAD_AXIS_MOTION: { + bool down = (evt.gaxis.value > 10000); + + if (evt.gaxis.axis == SDL_GAMEPAD_AXIS_LEFT_TRIGGER) { + (*gamepad)->mark_button(GAMEPAD_BUTTON_LEFT_TRIGGER, down); + } else if (evt.gaxis.axis == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) { + (*gamepad)->mark_button(GAMEPAD_BUTTON_RIGHT_TRIGGER, down); + } + + break; + } + } + + return false; +} + +bool change_gamepad_nav_func(float new_val, bool initial) +{ + NavEnabled = new_val; + + if ( !initial ) { + if (new_val) { + io::gamepad::init(); + } else { + io::gamepad::shutdown(); + } + } + + return true; +} + +void parse_gamepad_nav_func() +{ + bool value; + stuff_boolean(&value); + + NavEnabled = value; +} + +// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton +auto GamepadNavOption __UNUSED = options::OptionBuilder("Input.GamepadNav", + std::pair{"Gamepad Navigation", 1932}, + std::pair{"Enable or disable gamepad UI control", 1933}) + .category(std::make_pair("Input", 1827)) + .level(options::ExpertLevel::Beginner) + .importance(2) + .default_func([]() { return NavEnabled; }) + .change_listener(change_gamepad_nav_func) + .parser(parse_gamepad_nav_func) + .finish(); + +void parse_gamepad_swap_func() +{ + bool value; + stuff_boolean(&value); + + SwapActionCancel = value; +} + +// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton +auto GamepadSwapOption __UNUSED = options::OptionBuilder("Input.GamepadSwapActionCancel", + std::pair{"Swap Action/Cancel Buttons", 1934}, + std::pair{"Swap gamepad buttons used for action and cancel", 1935}) + .category(std::make_pair("Input", 1827)) + .level(options::ExpertLevel::Beginner) + .importance(1) + .default_func([]() { return SwapActionCancel; }) + .bind_to(&SwapActionCancel) + .parser(parse_gamepad_swap_func) + .finish(); + +// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton +auto GamepadCursorSpeed __UNUSED = options::OptionBuilder("Input.GamepadCursorSpeed", + std::pair{"Gamepad Cursor Speed", 1936}, + std::pair{"Movement speed of cursor from gamepad inputs", 1937}) + .category(std::make_pair("Input", 1827)) + .level(options::ExpertLevel::Beginner) + .importance(0) + .range(1, 10) + .default_func([]() { return CursorSpeed; }) + .flags({options::OptionFlags::RangeTypeInteger}) + .bind_to(&CursorSpeed) + .finish(); + +} // namespace + + +namespace io::gamepad { + +Gamepad::Gamepad(SDL_JoystickID _id) : + m_id(_id) +{ + m_gamepad = SDL_OpenGamepad(m_id); + m_button_state.fill(false); +} + +Gamepad::~Gamepad() +{ + if (m_gamepad) { + SDL_CloseGamepad(m_gamepad); + } +} + +bool Gamepad::action() +{ + auto button = SwapActionCancel ? SDL_GAMEPAD_BUTTON_EAST : SDL_GAMEPAD_BUTTON_SOUTH; + + auto state = m_button_state[button]; + m_button_state[button] = false; // we only want this to be true once + + return state; +} + +bool Gamepad::cancel() +{ + auto button = SwapActionCancel ? SDL_GAMEPAD_BUTTON_SOUTH : SDL_GAMEPAD_BUTTON_EAST; + + auto state = m_button_state[button]; + m_button_state[button] = false; // we only want this to be true once + + return state; +} + +int Gamepad::get_key() +{ + int k = 0; + + if (cancel()) { + k = KEY_ESC; + // resets automatically + } else if (m_button_state[SDL_GAMEPAD_BUTTON_DPAD_DOWN]) { + k = KEY_DOWN; + // key repeats + } else if (m_button_state[SDL_GAMEPAD_BUTTON_DPAD_UP]) { + k = KEY_UP; + // key repeats + } else if (m_button_state[SDL_GAMEPAD_BUTTON_DPAD_LEFT]) { + k = KEY_LEFT; + // key repeats + } else if (m_button_state[SDL_GAMEPAD_BUTTON_DPAD_RIGHT]) { + k = KEY_RIGHT; + // key repeats + } else if (m_button_state[SDL_GAMEPAD_BUTTON_LEFT_SHOULDER]) { + k = KEY_SHIFTED | KEY_TAB; + // reset after use + m_button_state[SDL_GAMEPAD_BUTTON_LEFT_SHOULDER] = false; + } else if (m_button_state[SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER]) { + k = KEY_TAB; + // reset after use + m_button_state[SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER] = false; + } else if (m_button_state[GAMEPAD_BUTTON_LEFT_TRIGGER]) { + k = SDLK_PAGEDOWN; + // reset after use + m_button_state[GAMEPAD_BUTTON_LEFT_TRIGGER] = false; + } else if (m_button_state[GAMEPAD_BUTTON_RIGHT_TRIGGER]) { + k = SDLK_PAGEUP; + // reset after use + m_button_state[GAMEPAD_BUTTON_RIGHT_TRIGGER] = false; + } + + return k; +} + +bool Gamepad::update_mouse_pos() +{ + if ( !m_gamepad ) { + return false; + } + + // we poll directly here in order to get smooth movement + int gx = SDL_GetGamepadAxis(m_gamepad, SDL_GAMEPAD_AXIS_LEFTX); + int gy = SDL_GetGamepadAxis(m_gamepad, SDL_GAMEPAD_AXIS_LEFTY); + + // ignore possible stick drift + if (abs(gx) < DEAD_ZONE) gx = 0; + if (abs(gy) < DEAD_ZONE) gy = 0; + + if ( !gx && !gy ) { + return false; + } + + // scales delta to a range between 4 (slow, accurate) and 32 (fast, inaccurate) + const float sensitivity = 8000.f - (CursorSpeed - 1.f) * (7000.f / 9.f); + + float dx = gx / sensitivity; + float dy = gy / sensitivity; + + int x = 0; + int y = 0; + + mouse_get_real_pos(&x, &y); + + // update pos and deltas + mouse_update_pos_scaled(static_cast(x+dx), static_cast(y+dy), dx, dy); + + // now change position + SDL_HideCursor(); // prevents cursor getting stuck as non-game one + SDL_WarpMouseInWindow(os::getSDLMainWindow(), x+dx, y+dy); + SDL_ShowCursor(); + + return true; +} + +void Gamepad::mark_mouse_button(Uint8 button) +{ + const int left_button = SwapActionCancel ? SDL_GAMEPAD_BUTTON_EAST : SDL_GAMEPAD_BUTTON_SOUTH; + uint m_button = 0; + + if (button == left_button) { + // "A" or "B" (if swapped) + m_button = MOUSE_LEFT_BUTTON; + } else if (button == SDL_GAMEPAD_BUTTON_WEST) { + // "X" + m_button = MOUSE_RIGHT_BUTTON; + } + + if ( !m_button ) { + return; + } + + mouse_mark_button(m_button, m_button_state[button] ? 1 : 0); +} + +void Gamepad::mark_button(Uint8 button, bool down) +{ + if (button >= m_button_state.size()) { + return; + } + + m_button_state[button] = down; + + mark_mouse_button(button); +} + +bool Gamepad::get_camera_vals(int *dx, int *dy, int *dz, bool *lmb_down, bool *rmb_down, uint *key_flags) +{ + if ( !m_gamepad ) { + return false; + } + + bool rval = false; // whether or not we're using inputs from this gamepad + + // we poll directly here in order to get smooth movement + int gx = SDL_GetGamepadAxis(m_gamepad, SDL_GAMEPAD_AXIS_RIGHTX); + int gy = SDL_GetGamepadAxis(m_gamepad, SDL_GAMEPAD_AXIS_RIGHTY); + + // ignore possible stick drift + if (abs(gx) < DEAD_ZONE) gx = 0; + if (abs(gy) < DEAD_ZONE) gy = 0; + + if ( !gx && !gy ) { + return rval; + } + + const bool LeftTrigger = m_button_state[GAMEPAD_BUTTON_LEFT_TRIGGER]; + const bool RightTrigger = m_button_state[GAMEPAD_BUTTON_RIGHT_TRIGGER]; + + if (LeftTrigger) { + if (rmb_down) *rmb_down = true; + rval = true; + + if (RightTrigger) { + if (key_flags) *key_flags |= KEY_SHIFTED; + } + } else if ( !RightTrigger ) { + if (lmb_down) *lmb_down = true; + rval = true; + } + + // scales delta to +/- 3 (slow, accurate) + const float sensitivity = 10000.f; + + float mdx = gx / sensitivity; + float mdy = gy / sensitivity; + + // we should only set dz or dx/dy, not both, and don't set dz if "shifted" + if (RightTrigger && !LeftTrigger) { + if (mdy) { + if (dz) *dz = (mdy > 0.f) ? -1 : 1; + } + } else { + if (dx) *dx = fl2i(mdx); + if (dy) *dy = fl2i(mdy); + } + + return rval; +} + + +// Initialize gamepad leanback +// NOTE: This should work independently of io::joystick! +void init() +{ + if (initialized) { + return; + } + + if (Is_standalone) { + return; + } + + if ( !Using_in_game_options ) { + NavEnabled = os_config_read_uint("Input", "GamepadNav", 1) == 1; + SwapActionCancel = os_config_read_uint("Input", "GamepadSwapActionCancel", 0) == 1; + CursorSpeed = os_config_read_uint("Input", "GamepadCursorSpeed", 6); + CLAMP(CursorSpeed, 1, 10); + } + + if ( !NavEnabled ) { + return; + } + + if ( !SDL_InitSubSystem(SDL_INIT_GAMEPAD) ) { + return; + } + + // TODO: SDL3 => It might be nice to eventually add touchpad support here + // for more precise cursor control. + + initialized = true; + + auto pads = SDL_GetGamepads(nullptr); + + if (pads) { + for (int i = 0; pads[i]; ++i) { + gamepads.push_back(GamepadPtr(new Gamepad(pads[i]))); + } + + SDL_free(pads); + pads = nullptr; + } + + if (ImGui::GetCurrentContext()) { + auto &io = ImGui::GetIO(); + + // enable gamepad nav for imgui + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + + io.ConfigNavSwapGamepadButtons = SwapActionCancel; + io.ConfigNavMoveSetMousePos = true; + } + + // These events should *not* be consumed, so that the joystick code can also + // make use of them + os::events::addEventListener(SDL_EVENT_GAMEPAD_ADDED, -1, event_handler); + os::events::addEventListener(SDL_EVENT_GAMEPAD_REMOVED, -1, event_handler); + os::events::addEventListener(SDL_EVENT_GAMEPAD_AXIS_MOTION, -1, event_handler); + os::events::addEventListener(SDL_EVENT_GAMEPAD_BUTTON_DOWN, -1, event_handler); + os::events::addEventListener(SDL_EVENT_GAMEPAD_BUTTON_UP, -1, event_handler); +} + +void shutdown() +{ + if ( !initialized ) { + return; + } + + if (ImGui::GetCurrentContext()) { + auto &io = ImGui::GetIO(); + + io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableGamepad; + + io.ConfigNavSwapGamepadButtons = false; + io.ConfigNavMoveSetMousePos = false; + } + + gamepads.clear(); + gamepads.shrink_to_fit(); + + SDL_QuitSubSystem(SDL_INIT_GAMEPAD); + + initialized = false; +} + +bool action() +{ + if ( !navActive() ) { + return false; + } + + // this should always work (cursor on/off, imgui, etc.) + + for (GamepadPtr &p : gamepads) { + if (p->action()) { + return true; + } + } + + return false; +} + +bool cancel() +{ + if ( !navActive() ) { + return false; + } + + // this should always work (cursor on/off, imgui, etc.) + + for (GamepadPtr &p : gamepads) { + if (p->cancel()) { + return true; + } + } + + return false; +} + +bool action_or_cancel() +{ + if ( !navActive() ) { + return false; + } + + // this should always work (cursor on/off, imgui, etc.) + + for (GamepadPtr &p : gamepads) { + if (p->action_or_cancel()) { + return true; + } + } + + return false; +} + +int get_key() +{ + static Uint64 key_check_time = 0; + + if ( !navActive() ) { + return 0; + } + + // skip if we aren't doing ui stuff + if ( !io::mouse::CursorManager::get()->isCursorShown() ) { + return 0; + } + + // if imgui should be controlling the gamepad nav then let it + const auto game_state = gameseq_get_state(); + + if ((game_state == GS_STATE_LAB) || (game_state == GS_STATE_INGAME_OPTIONS)) { + return 0; + } + + if (SDL_GetTicks() < key_check_time) { + return 0; + } + + key_check_time = SDL_GetTicks() + KEY_CHECK_INTERVAL_MS; + + for (GamepadPtr &p : gamepads) { + auto k = p->get_key(); + + if (k) { + return k; + } + } + + return 0; +} + +void do_frame() +{ + static Uint64 next_update = 0; + + if ( !navActive() ) { + return; + } + + // skip if we aren't doing ui stuff + if ( !io::mouse::CursorManager::get()->isCursorShown() ) { + return; + } + + // if imgui should be controlling the gamepad nav then let it + const auto game_state = gameseq_get_state(); + + if ((game_state == GS_STATE_LAB) || (game_state == GS_STATE_INGAME_OPTIONS)) { + return; + } + + // limit updates so that we aren't zooming all over the place at higher fps + if (next_update > SDL_GetTicks()) { + return; + } + + next_update = SDL_GetTicks() + CURSOR_UPDATE_INTERVAL_MS; + + for (GamepadPtr &p : gamepads) { + if (p->update_mouse_pos()) { + return; + } + } +} + +bool get_camera_vals(int *dx, int *dy, int *dz, bool *lmb_down, bool *rmb_down, uint *key_flags) +{ + if ( !navActive() ) { + return false; + } + + if (dx) *dx = 0; + if (dy) *dy = 0; + if (dz) *dz = 0; + if (lmb_down) *lmb_down = false; + if (rmb_down) *rmb_down = false; + if (key_flags) *key_flags = 0; + + for (GamepadPtr &p : gamepads) { + if (p->get_camera_vals(dx, dy, dz, lmb_down, rmb_down, key_flags)) { + return true; + } + } + + return false; +} + +bool navActive() +{ + return (initialized && NavEnabled && !gamepads.empty()); +} + +} // namespace io::gamepad diff --git a/code/io/gamepad.h b/code/io/gamepad.h new file mode 100644 index 00000000000..63d65581adf --- /dev/null +++ b/code/io/gamepad.h @@ -0,0 +1,158 @@ +/* + * Copyright (C) Volition, Inc. 1999. All rights reserved. + * + * All source code herein is the property of Volition, Inc. You may not sell + * or otherwise commercially exploit the source or things you created based on the + * source. + * + */ + +#ifndef __GAMEPAD_H__ +#define __GAMEPAD_H__ + +namespace io::gamepad { + +constexpr Uint8 GAMEPAD_BUTTON_LEFT_TRIGGER = SDL_GAMEPAD_BUTTON_COUNT; +constexpr Uint8 GAMEPAD_BUTTON_RIGHT_TRIGGER = SDL_GAMEPAD_BUTTON_COUNT+1; + + +class Gamepad { +private: + /** + * @brief Minimum dead zone to avoid reading stick drift + * @note This is the generally recommended value, but not necessarily accurate for all gamepads + */ + static constexpr int DEAD_ZONE = 8000; + + /** + * @brief State of buttons on gamepad, plus 2 entries for the triggers + */ + std::array m_button_state; + + SDL_JoystickID m_id; //!< SDL joystick/gamepad id + SDL_Gamepad *m_gamepad; //!< SDL gamepad handle + + /** + * @brief Translates a gamepad button press into a mouse button press + * @param button Uint8 representation of SDL_GAMEPAD_BUTTON_* + */ + void mark_mouse_button(Uint8 button); + +public: + Gamepad(SDL_JoystickID _id); + ~Gamepad(); + + SDL_JoystickID getId() const { return m_id; } + + /** + * @brief Determines if the action button has been pressed (similar to left mouse button) + * @note Typically the A button on Xbox style gamepads + * + * @return @c true if the action button is pressed, @c false otherwise + */ + bool action(); + + /** + * @brief Determines if the cancel button has been pressed (similar to Esc key) + * @note Typically the B button on Xbox style gamepads + * + * @return @c true if the cancel button is pressed, @c false otherwise + */ + bool cancel(); + + /** + * @brief Determines if the action or cancel buttons have been pressed + * + * @return @c true if the action or cancel button is pressed, @c false otherwise + */ + bool action_or_cancel() { return (action() || cancel()); } + + /** + * @brief Get the keyboard equivalent of a gamepad button + * + * @return KEY_* value corresponding to gamepad button + */ + int get_key(); + + /** + * @brief Gets the down time of the given hat and position + * @param[in] button Uint8 representation of SDL_GAMEPAD_BUTTON_* + * @param[in] down @c true if button is pressed, @c false if button is released + */ + void mark_button(Uint8 button, bool down); + + /** + * @brief Update mouse/cursor position based on gamepad stick movement + */ + bool update_mouse_pos(); + + /** + * @brief Translate gamepad state into mouse states for camera/object movement + * + * @details These controls were chosen due to ImGui not using them by default. If those defaults + * change an alternate mapping may be required. + * + * @param[out] dx x-axis delta of right stick + * @param[out] dy y-axis delta of right stick + * @param[out] dz z-axis delta (mouse wheel, only if right trigger pressed) + * @param[out] lmb_down @c true if left mouse button down ( dx/dy has value and no triggers pressed) + * @param[out] rmb_down @c true if riight mouse button down (dx/dy has value and left trigger pressed) + * @param[out] key_flags will have @c KEY_SHIFTED set if both triggers are pressed + * + * @returns @c true if any of the values were set + */ + bool get_camera_vals(int *dx, int *dy, int *dz, bool *lmb_down, bool *rmb_down, uint *key_flags); +}; + +void init(); +void shutdown(); +void do_frame(); + +/** + * @brief Returns @c true if gamepad navigation is enabled and active + */ +bool navActive(); + +/** + * @brief Returns @c true if the defined Action button has been pressed + */ +bool action(); + +/** + * @brief Returns @c true if the defined Cancel button has been pressed + */ +bool cancel(); + +/** + * @brief Returns @c true if the defined Action or Cancel buttons have been pressed + */ +bool action_or_cancel(); + +/** + * @brief Get the keyboard equivalent of any pressed gamepad buttons + * + * @return KEY_* value corresponding to a gamepad button + */ +int get_key(); + +/** + * @brief Translate gamepad state into mouse states for camera/object movement + * + * @details These controls were chosen due to ImGui not using them by default. If those defaults + * change an alternate mapping may be required. + * + * @param[out] dx x-axis delta of right stick + * @param[out] dy y-axis delta of right stick + * @param[out] dz z-axis delta (mouse wheel, only if right trigger pressed) + * @param[out] lmb_down @c true if left mouse button down ( dx/dy has value and no triggers pressed) + * @param[out] rmb_down @c true if riight mouse button down (dx/dy has value and left trigger pressed) + * @param[out] key_flags will have @c KEY_SHIFTED set if both triggers are pressed + * + * @returns @c true if any of the values were set to something other than defaults + * + * @warning This function sets all passed arguments to default values if gamepad navigation is active! + */ +bool get_camera_vals(int *dx, int *dy, int *dz, bool *lmb_down, bool *rmb_down, uint *key_flags); +} + +#endif diff --git a/code/io/joy-sdl.cpp b/code/io/joy-sdl.cpp index 562e8afe575..8be7e89ed9a 100644 --- a/code/io/joy-sdl.cpp +++ b/code/io/joy-sdl.cpp @@ -920,6 +920,14 @@ namespace joystick } } + void Joystick::flush() + { + for (auto &b : _button) { + b.DownTimestamp = UI_TIMESTAMP::invalid(); + b.DownCount = 0; + } + } + SDL_Joystick *Joystick::getJoystick() { return _joystick; @@ -1479,3 +1487,12 @@ short joy_get_button_axis(short cid, short btn) return static_cast(SDL_GAMEPAD_AXIS_LEFT_TRIGGER + axis_button); } + +void joy_flush() +{ + for (auto pJoy : pJoystick) { + if (pJoy) { + pJoy->flush(); + } + } +} diff --git a/code/io/joy.h b/code/io/joy.h index 3b68a1be954..e7dcc9c4dc5 100644 --- a/code/io/joy.h +++ b/code/io/joy.h @@ -254,6 +254,11 @@ namespace io */ json_t* getJSON(); + /** + * @brief Clears internal button state + */ + void flush(); + private: Joystick(const Joystick &); Joystick &operator=(const Joystick &); @@ -390,5 +395,6 @@ short joy_get_button_axis(const short cid, short btn); */ bool joy_present(short cid); +void joy_flush(); #endif /* __JOY_H__ */ diff --git a/code/io/joy_ff.cpp b/code/io/joy_ff.cpp index c856f1c1395..4e55154e8ee 100644 --- a/code/io/joy_ff.cpp +++ b/code/io/joy_ff.cpp @@ -33,6 +33,7 @@ static auto ForceFeedbackOption = options::OptionBuilder("Input.ForceFeedb std::pair{"Enable or disable force feedback", 1729}) .category(std::make_pair("Input", 1827)) .level(options::ExpertLevel::Beginner) + .importance(25) .default_val(true) .change_listener([](bool val, bool) { if (val) joy_ff_init(); @@ -47,6 +48,7 @@ static auto HitEffectOption = options::OptionBuilder("Input.HitEffect", std::pair{"Enable or disable the directional hit effect", 1731}) .category(std::make_pair("Input", 1827)) .level(options::ExpertLevel::Beginner) + .importance(24) .default_val(true) .change_listener([](bool val, bool) { Joy_ff_directional_hit_effect_enabled = val; @@ -57,9 +59,10 @@ static auto HitEffectOption = options::OptionBuilder("Input.HitEffect", // coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton static auto ForceFeedbackStrength = options::OptionBuilder("Input.FFStrength", std::pair{"Force Feedback Strength", 1756}, - std::pair{"The realtive strength of Force Feedback effects", 1757}) + std::pair{"The relative strength of Force Feedback effects", 1757}) .category(std::make_pair("Input", 1827)) .level(options::ExpertLevel::Beginner) + .importance(23) .range(0, 100) .default_val(100) .flags({options::OptionFlags::RangeTypeInteger}) diff --git a/code/io/mouse.cpp b/code/io/mouse.cpp index 044327e8b18..7f079487bc0 100644 --- a/code/io/mouse.cpp +++ b/code/io/mouse.cpp @@ -720,3 +720,16 @@ short bit_distance(short x) { return i; } + +// update mouse with position which is already scaled for max_w/max_h +void mouse_update_pos_scaled(int x, int y, float dx, float dy) +{ + CAP(x, 0, gr_screen.max_w-1); + CAP(y, 0, gr_screen.max_h-1); + + Mouse_x = x; + Mouse_y = y; + + Mouse_dx += fl2i(dx); + Mouse_dy += fl2i(dy); +} diff --git a/code/io/mouse.h b/code/io/mouse.h index 3a2fa7c29ee..ceaa37b2fe3 100644 --- a/code/io/mouse.h +++ b/code/io/mouse.h @@ -124,4 +124,7 @@ extern void mouse_force_pos(float x, float y); */ short bit_distance(short x); +// update mouse with position which is already scaled for max_w/max_h +void mouse_update_pos_scaled(int x, int y, float dx, float dy); + #endif diff --git a/code/lab/dialogs/lab_ui.cpp b/code/lab/dialogs/lab_ui.cpp index a534a02aef4..8185c7a9b1e 100644 --- a/code/lab/dialogs/lab_ui.cpp +++ b/code/lab/dialogs/lab_ui.cpp @@ -15,6 +15,7 @@ #include "mission/missionload.h" #include "prop/prop.h" #include "controlconfig/controlsconfig.h" +#include "io/gamepad.h" using namespace ImGui; @@ -84,7 +85,7 @@ void LabUi::build_species_entry(const species_info &species_def, int species_idx TreeNodeEx(node_label.c_str(), ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen, "%s", class_def.name); - if (IsItemClicked() && !IsItemToggledOpen()) { + if (IsItemActivated() && !IsItemToggledOpen()) { getLabManager()->changeDisplayedObject(LabMode::Ship, ship_info_idx); } } @@ -121,7 +122,7 @@ void LabUi::build_weapon_subtype_list() const ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen, "%s", class_def.name); - if (IsItemClicked() && !IsItemToggledOpen()) { + if (IsItemActivated() && !IsItemToggledOpen()) { getLabManager()->changeDisplayedObject(LabMode::Weapon, weapon_idx); } } @@ -147,7 +148,7 @@ void LabUi::build_prop_subtype_list() "%s", class_def.name.c_str()); - if (IsItemClicked() && !IsItemToggledOpen()) { + if (IsItemActivated() && !IsItemToggledOpen()) { getLabManager()->changeDisplayedObject(LabMode::Prop, prop_idx); } } @@ -179,7 +180,7 @@ void LabUi::build_asteroid_list() info.name, subtype.type_name.c_str()); - if (IsItemClicked() && !IsItemToggledOpen()) { + if (IsItemActivated() && !IsItemToggledOpen()) { getLabManager()->changeDisplayedObject(LabMode::Asteroid, asteroid_idx, subtype_idx); } @@ -210,7 +211,7 @@ void LabUi::build_debris_list() "%s", info.name); - if (IsItemClicked() && !IsItemToggledOpen()) { + if (IsItemActivated() && !IsItemToggledOpen()) { getLabManager()->changeDisplayedObject(LabMode::Asteroid, debris_idx, 0); // Debris subtype is always 0 } @@ -275,7 +276,7 @@ void LabUi::build_background_list() ImGuiTreeNodeFlags node_flags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; TreeNodeEx(LAB_MISSION_NONE_STRING, node_flags, LAB_MISSION_NONE_STRING); - if (IsItemClicked() && !IsItemToggledOpen()) { + if (IsItemActivated() && !IsItemToggledOpen()) { getLabManager()->Renderer->useBackground(LAB_MISSION_NONE_STRING); } @@ -291,7 +292,7 @@ void LabUi::build_background_list() for (const auto& mission_name : directory.second) { TreeNodeEx(mission_name.c_str(), node_flags, "%s", mission_name.c_str()); - if (IsItemClicked() && !IsItemToggledOpen()) { + if (IsItemActivated() && !IsItemToggledOpen()) { getLabManager()->Renderer->useBackground(mission_name); } } @@ -397,6 +398,15 @@ void LabUi::show_controls_reference() TextWrapped("Rotation axis limits and rotation speed apply only to object orientation (LMB), not " "camera controls (RMB)."); + if (io::gamepad::navActive()) { + Separator(); + TextWrapped("Gamepad controls"); + controls_reference_entry("RStick", "Orient the displayed object."); + controls_reference_entry("RStick + LTrigger", "Rotate the camera."); + controls_reference_entry("RStick + LTrigger + RTrigger", "Pan the camera on the X/Y plane."); + controls_reference_entry("RStick + RTrigger", "Zoom the camera in or out."); + } + Separator(); TextWrapped("Keyboard shortcuts"); controls_reference_entry("R", "Cycle object orientation (LMB) axis mode (Yaw, Pitch, Roll, or Both)."); diff --git a/code/lab/manager/lab_manager.cpp b/code/lab/manager/lab_manager.cpp index fd1775d645a..f9ca485a14a 100644 --- a/code/lab/manager/lab_manager.cpp +++ b/code/lab/manager/lab_manager.cpp @@ -19,6 +19,7 @@ #include "freespace.h" #include "io/mouse.h" +#include "io/gamepad.h" #undef LOCAL #include "extensions/ImGuizmo.h" @@ -131,20 +132,33 @@ void LabManager::onFrame(float frametime) { int mouse_y = 0; mouse_get_pos(&mouse_x, &mouse_y); - const bool lmb_down = mouse_down(MOUSE_LEFT_BUTTON) != 0; + auto key_flags = key_get_shift_status(); + + bool lmb_down = mouse_down(MOUSE_LEFT_BUTTON) != 0; + bool rmb_down = mouse_down(MOUSE_RIGHT_BUTTON) != 0; + bool lmb_pressed = lmb_down && !LastLmbDown; - LastLmbDown = lmb_down; - if (lmb_pressed && ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow)) { - lmb_pressed = false; - } + // don't let gamepad nav override mouse control + if (io::gamepad::navActive() && !lmb_down && !rmb_down) { + io::gamepad::get_camera_vals(&dx, &dy, &dz, &lmb_down, &rmb_down, &key_flags); + lmb_pressed = lmb_down && !LastLmbDown; + } else if (ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow)) { + // NOTE: when using gamepad nav a window will always be hovered + if (dz != 0) { + dz = 0; + } - if (dz != 0 && ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow)) { - dz = 0; + if (lmb_pressed) { + lmb_pressed = false; + } } + + LastLmbDown = lmb_down; + auto& current_camera = Renderer->getCurrentCamera(); current_camera->handleInput( - dx, dy, dz, lmb_down, lmb_pressed, mouse_down(MOUSE_RIGHT_BUTTON) != 0, key_get_shift_status(), mouse_x, mouse_y); + dx, dy, dz, lmb_down, lmb_pressed, rmb_down, key_flags, mouse_x, mouse_y); if (!current_camera->handlesObjectPlacement()) { const bool over_camera_overlay = Renderer->getShowOrientationWidget() && current_camera->isOverlayHit(mouse_x, mouse_y); diff --git a/code/localization/localize.cpp b/code/localization/localize.cpp index 2387709d4c9..d6968d14287 100644 --- a/code/localization/localize.cpp +++ b/code/localization/localize.cpp @@ -65,7 +65,7 @@ bool *Lcl_unexpected_tstring_check = nullptr; // NOTE: with map storage of XSTR strings, the indexes no longer need to be contiguous, // but internal strings should still increment XSTR_SIZE to avoid collisions. // retail XSTR_SIZE = 1570 -// #define XSTR_SIZE 1932 // This is the next available ID +// #define XSTR_SIZE 1937 // This is the next available ID // struct to allow for strings.tbl-determined x offset // offset is 0 for english, by default diff --git a/code/source_groups.cmake b/code/source_groups.cmake index 913aeff5543..508aca0d20c 100644 --- a/code/source_groups.cmake +++ b/code/source_groups.cmake @@ -752,6 +752,8 @@ add_file_folder("Io" io/joy_rumble.cpp io/spacemouse.cpp io/spacemouse.h + io/gamepad.cpp + io/gamepad.h ) # jpgutils files diff --git a/freespace2/freespace.cpp b/freespace2/freespace.cpp index c4e1e53c5dc..3722b498266 100644 --- a/freespace2/freespace.cpp +++ b/freespace2/freespace.cpp @@ -80,6 +80,7 @@ #include "hud/hudtargetbox.h" #include "iff_defs/iff_defs.h" #include "io/cursor.h" +#include "io/gamepad.h" #include "io/joy.h" #include "io/joy_ff.h" #include "io/key.h" @@ -2108,6 +2109,7 @@ void game_init() // standalone's don't use the joystick and it seems to sometimes cause them to not get shutdown properly if(!Is_standalone){ + io::gamepad::init(); io::joystick::init(); } @@ -4693,6 +4695,7 @@ void game_flush() { key_flush(); mouse_flush(); + joy_flush(); snazzy_flush(); Joymouse_button_status = 0; @@ -4716,6 +4719,10 @@ int game_check_key() if ((k & KEY_MASK) == KEY_PADENTER) k = (k & ~KEY_MASK) | KEY_ENTER; + if ( !k ) { + k = io::gamepad::get_key(); + } + return k; } @@ -4753,7 +4760,8 @@ int game_poll() int k = key_inkey(); // Move the mouse cursor with the joystick. Currently uses Joystick0 - if (os_foreground() && !io::mouse::CursorManager::get()->isCursorShown() && (Use_joy_mouse)) { + // (NOTE: ignore this when using gamepad navigation to avoid a mess) + if (Use_joy_mouse && os_foreground() && io::mouse::CursorManager::get()->isCursorShown() && !io::gamepad::navActive()) { // Move the mouse cursor with the joystick int mx, my; @@ -6453,6 +6461,8 @@ void game_do_state_common(int state,int no_networking) #endif Last_frame_ui_timestamp = ui_timestamp(); + io::gamepad::do_frame(); + io::mouse::CursorManager::doFrame(); // determine if to draw the mouse this frame snd_do_frame(); // update sound system event_music_do_frame(); // music needs to play across many states @@ -7130,6 +7140,7 @@ void game_shutdown(void) control_config_common_close(); io::joystick::shutdown(); + io::gamepad::shutdown(); audiostream_close(); snd_close();