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..44921aa550b 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,13 @@ 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 { + _gamepad = nullptr; + _joystick = SDL_OpenJoystick(id); + } if (_joystick == nullptr) { SCP_stringstream msg; @@ -590,14 +601,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 +628,7 @@ namespace joystick std::swap(_id, other._id); std::swap(_joystick, other._joystick); + std::swap(_gamepad, other._gamepad); fillValues(); @@ -619,7 +637,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 +799,35 @@ namespace joystick void Joystick::fillValues() { + // To avoid some weirdness and build compatibility 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 +848,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 +895,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 +921,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 +977,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 +1029,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 +1178,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 +1210,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 +1304,7 @@ namespace joystick // Automatically frees joystick resources joysticks.clear(); - SDL_QuitSubSystem(SDL_INIT_JOYSTICK); + SDL_QuitSubSystem(SDL_INIT_GAMEPAD); } json_t* getJsonArray() { @@ -1299,3 +1457,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 )