From eb42dde90298e6907c78b50df1f8c66953c09e94 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 61b272a11ac3eb84ec59d138145e6cebc334ca2a Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Mon, 3 Aug 2026 15:14:54 -0400 Subject: [PATCH 2/2] address feedback --- code/io/joy-sdl.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/io/joy-sdl.cpp b/code/io/joy-sdl.cpp index 562e8afe575..44921aa550b 100644 --- a/code/io/joy-sdl.cpp +++ b/code/io/joy-sdl.cpp @@ -587,6 +587,7 @@ namespace joystick _gamepad = SDL_OpenGamepad(id); _joystick = SDL_GetGamepadJoystick(_gamepad); } else { + _gamepad = nullptr; _joystick = SDL_OpenJoystick(id); } @@ -798,7 +799,7 @@ namespace joystick void Joystick::fillValues() { - // To avoid some weirdness and build compatiblity issues we always use + // To avoid some weirdness and build compatibility issues we always use // _joystick here rather than comparable _gamepad functions _name.assign(SDL_GetJoystickName(_joystick));