diff --git a/code/controlconfig/controlsconfig.cpp b/code/controlconfig/controlsconfig.cpp index 3b6a4938cce..d43827d4402 100644 --- a/code/controlconfig/controlsconfig.cpp +++ b/code/controlconfig/controlsconfig.cpp @@ -1531,6 +1531,11 @@ const char *control_config_tooltip_handler(const char *str) return NULL; } +bool control_config_special_mode() +{ + return (Binding_mode || Search_mode); +} + void control_config_init(bool API_Access) { int i; diff --git a/code/controlconfig/controlsconfig.h b/code/controlconfig/controlsconfig.h index 24e538d6882..5f659afa916 100644 --- a/code/controlconfig/controlsconfig.h +++ b/code/controlconfig/controlsconfig.h @@ -740,6 +740,11 @@ void control_config_common_init(); */ void control_config_common_close(); +/*! + * @brief detect whether control config is in search or binding modes + */ +bool control_config_special_mode(); + /*! * @brief init config menu */ diff --git a/code/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/cutscene/movie.cpp b/code/cutscene/movie.cpp index 0ac531a81db..53bd352ed30 100644 --- a/code/cutscene/movie.cpp +++ b/code/cutscene/movie.cpp @@ -28,6 +28,7 @@ #include "tracing/tracing.h" #include "io/timer.h" #include "io/key.h" +#include "io/gamepad.h" #include "mod_table/mod_table.h" #include "network/multi.h" #include "scripting/global_hooks.h" @@ -244,6 +245,10 @@ void movie_display_loop(Player* player, PlaybackState* state) { processEvents(); + if (io::gamepad::action_or_cancel()) { + state->playing = false; + } + // NOTE: This does not update mission time! If movies get enabled in places // other than through cutscenes then some refactoring should be done // to account for normal time progression rather than just timestamps diff --git a/code/io/gamepad.cpp b/code/io/gamepad.cpp new file mode 100644 index 00000000000..eaab3b3166e --- /dev/null +++ b/code/io/gamepad.cpp @@ -0,0 +1,605 @@ +/* + * Copyright (C) Volition, Inc. 1999. All rights reserved. + * + * All source code herein is the property of Volition, Inc. You may not sell + * or otherwise commercially exploit the source or things you created based on the + * source. + * + */ + +#include "globalincs/pstypes.h" +#include "io/gamepad.h" +#include "io/mouse.h" +#include "io/cursor.h" +#include "io/key.h" +#include "osapi/osapi.h" +#include "gamesequence/gamesequence.h" +#include "options/Option.h" + +#include "imgui.h" + + +using namespace io::gamepad; + +namespace { + +bool initialized = false; + +typedef std::unique_ptr GamepadPtr; + +SCP_vector gamepads; + +constexpr uint32_t KEY_CHECK_INTERVAL_MS = 150; +constexpr uint32_t CURSOR_UPDATE_INTERVAL_MS = 20; // 50 Hz + +// config options ---- +bool NavEnabled = true; +bool SwapActionCancel = false; +int CursorSpeed = 6; +// ------------------- + +// NOTE: never return true from here since we want these events to cascade to +// other places +bool event_handler(const SDL_Event &evt) +{ + const auto id = evt.gdevice.which; + + auto gamepad = std::find_if(gamepads.begin(), gamepads.end(), + [id](GamepadPtr &p) { return p->getId() == id; }); + + if (evt.type == SDL_EVENT_GAMEPAD_ADDED) { + // this event can fire more than once for the same device so we need to + // check for duplicates + if (gamepad == gamepads.end()) { + gamepads.push_back(GamepadPtr(new Gamepad(id))); + } + + return false; + } else if (evt.type == SDL_EVENT_GAMEPAD_REMOVED) { + if (gamepad != gamepads.end()) { + std::swap(*gamepad, gamepads.back()); + gamepads.pop_back(); + } + + return false; + } + + if (gamepad == gamepads.end()) { + return false; + } + + // skip these events if control config is in bind or search mode + if (control_config_special_mode()) { + return false; + } + + switch (evt.type) { + case SDL_EVENT_GAMEPAD_BUTTON_DOWN: + case SDL_EVENT_GAMEPAD_BUTTON_UP: + (*gamepad)->mark_button(evt.gbutton.button, evt.gbutton.down); + break; + + case SDL_EVENT_GAMEPAD_AXIS_MOTION: { + bool down = (evt.gaxis.value > 10000); + + if (evt.gaxis.axis == SDL_GAMEPAD_AXIS_LEFT_TRIGGER) { + (*gamepad)->mark_button(GAMEPAD_BUTTON_LEFT_TRIGGER, down); + } else if (evt.gaxis.axis == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) { + (*gamepad)->mark_button(GAMEPAD_BUTTON_RIGHT_TRIGGER, down); + } + + break; + } + } + + return false; +} + +bool change_gamepad_nav_func(float new_val, bool initial) +{ + NavEnabled = new_val; + + if ( !initial ) { + if (new_val) { + io::gamepad::init(); + } else { + io::gamepad::shutdown(); + } + } + + return true; +} + +void parse_gamepad_nav_func() +{ + bool value; + stuff_boolean(&value); + + NavEnabled = value; +} + +// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton +auto GamepadNavOption __UNUSED = options::OptionBuilder("Input.GamepadNav", + std::pair{"Gamepad Navigation", 1932}, + std::pair{"Enable or disable gamepad UI control", 1933}) + .category(std::make_pair("Input", 1827)) + .level(options::ExpertLevel::Beginner) + .importance(2) + .default_func([]() { return NavEnabled; }) + .change_listener(change_gamepad_nav_func) + .parser(parse_gamepad_nav_func) + .finish(); + +void parse_gamepad_swap_func() +{ + bool value; + stuff_boolean(&value); + + SwapActionCancel = value; +} + +// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton +auto GamepadSwapOption __UNUSED = options::OptionBuilder("Input.GamepadSwapActionCancel", + std::pair{"Swap Action/Cancel Buttons", 1934}, + std::pair{"Swap gamepad buttons used for action and cancel", 1935}) + .category(std::make_pair("Input", 1827)) + .level(options::ExpertLevel::Beginner) + .importance(1) + .default_func([]() { return SwapActionCancel; }) + .bind_to(&SwapActionCancel) + .parser(parse_gamepad_swap_func) + .finish(); + +// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton +auto GamepadCursorSpeed __UNUSED = options::OptionBuilder("Input.GamepadCursorSpeed", + std::pair{"Gamepad Cursor Speed", 1936}, + std::pair{"Movement speed of cursor from gamepad inputs", 1937}) + .category(std::make_pair("Input", 1827)) + .level(options::ExpertLevel::Beginner) + .importance(0) + .range(1, 10) + .default_func([]() { return CursorSpeed; }) + .flags({options::OptionFlags::RangeTypeInteger}) + .bind_to(&CursorSpeed) + .finish(); + +} // namespace + + +namespace io::gamepad { + +Gamepad::Gamepad(SDL_JoystickID _id) : + m_id(_id) +{ + m_gamepad = SDL_OpenGamepad(m_id); + m_button_state.fill(false); +} + +Gamepad::~Gamepad() +{ + if (m_gamepad) { + SDL_CloseGamepad(m_gamepad); + } +} + +bool Gamepad::action() +{ + auto button = SwapActionCancel ? SDL_GAMEPAD_BUTTON_EAST : SDL_GAMEPAD_BUTTON_SOUTH; + + auto state = m_button_state[button]; + m_button_state[button] = false; // we only want this to be true once + + return state; +} + +bool Gamepad::cancel() +{ + auto button = SwapActionCancel ? SDL_GAMEPAD_BUTTON_SOUTH : SDL_GAMEPAD_BUTTON_EAST; + + auto state = m_button_state[button]; + m_button_state[button] = false; // we only want this to be true once + + return state; +} + +int Gamepad::get_key() +{ + int k = 0; + + if (cancel()) { + k = KEY_ESC; + // resets automatically + } else if (m_button_state[SDL_GAMEPAD_BUTTON_DPAD_DOWN]) { + k = KEY_DOWN; + // key repeats + } else if (m_button_state[SDL_GAMEPAD_BUTTON_DPAD_UP]) { + k = KEY_UP; + // key repeats + } else if (m_button_state[SDL_GAMEPAD_BUTTON_DPAD_LEFT]) { + k = KEY_LEFT; + // key repeats + } else if (m_button_state[SDL_GAMEPAD_BUTTON_DPAD_RIGHT]) { + k = KEY_RIGHT; + // key repeats + } else if (m_button_state[SDL_GAMEPAD_BUTTON_LEFT_SHOULDER]) { + k = KEY_SHIFTED | KEY_TAB; + // reset after use + m_button_state[SDL_GAMEPAD_BUTTON_LEFT_SHOULDER] = false; + } else if (m_button_state[SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER]) { + k = KEY_TAB; + // reset after use + m_button_state[SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER] = false; + } else if (m_button_state[GAMEPAD_BUTTON_LEFT_TRIGGER]) { + k = SDLK_PAGEDOWN; + // reset after use + m_button_state[GAMEPAD_BUTTON_LEFT_TRIGGER] = false; + } else if (m_button_state[GAMEPAD_BUTTON_RIGHT_TRIGGER]) { + k = SDLK_PAGEUP; + // reset after use + m_button_state[GAMEPAD_BUTTON_RIGHT_TRIGGER] = false; + } + + return k; +} + +bool Gamepad::update_mouse_pos() +{ + if ( !m_gamepad ) { + return false; + } + + // we poll directly here in order to get smooth movement + int gx = SDL_GetGamepadAxis(m_gamepad, SDL_GAMEPAD_AXIS_LEFTX); + int gy = SDL_GetGamepadAxis(m_gamepad, SDL_GAMEPAD_AXIS_LEFTY); + + // ignore possible stick drift + if (abs(gx) < DEAD_ZONE) gx = 0; + if (abs(gy) < DEAD_ZONE) gy = 0; + + if ( !gx && !gy ) { + return false; + } + + // scales delta to a range between 4 (slow, accurate) and 32 (fast, inaccurate) + const float sensitivity = 8000.f - (CursorSpeed - 1.f) * (7000.f / 9.f); + + float dx = gx / sensitivity; + float dy = gy / sensitivity; + + int x = 0; + int y = 0; + + mouse_get_real_pos(&x, &y); + + // update pos and deltas + mouse_update_pos_scaled(static_cast(x+dx), static_cast(y+dy), dx, dy); + + // now change position + SDL_HideCursor(); // prevents cursor getting stuck as non-game one + SDL_WarpMouseInWindow(os::getSDLMainWindow(), x+dx, y+dy); + SDL_ShowCursor(); + + return true; +} + +void Gamepad::mark_mouse_button(Uint8 button) +{ + const int left_button = SwapActionCancel ? SDL_GAMEPAD_BUTTON_EAST : SDL_GAMEPAD_BUTTON_SOUTH; + uint m_button = 0; + + if (button == left_button) { + // "A" or "B" (if swapped) + m_button = MOUSE_LEFT_BUTTON; + } else if (button == SDL_GAMEPAD_BUTTON_WEST) { + // "X" + m_button = MOUSE_RIGHT_BUTTON; + } + + if ( !m_button ) { + return; + } + + mouse_mark_button(m_button, m_button_state[button] ? 1 : 0); +} + +void Gamepad::mark_button(Uint8 button, bool down) +{ + if (button >= m_button_state.size()) { + return; + } + + m_button_state[button] = down; + + mark_mouse_button(button); +} + +bool Gamepad::get_camera_vals(int *dx, int *dy, int *dz, bool *lmb_down, bool *rmb_down, uint *key_flags) +{ + if ( !m_gamepad ) { + return false; + } + + bool rval = false; // whether or not we're using inputs from this gamepad + + // we poll directly here in order to get smooth movement + int gx = SDL_GetGamepadAxis(m_gamepad, SDL_GAMEPAD_AXIS_RIGHTX); + int gy = SDL_GetGamepadAxis(m_gamepad, SDL_GAMEPAD_AXIS_RIGHTY); + + // ignore possible stick drift + if (abs(gx) < DEAD_ZONE) gx = 0; + if (abs(gy) < DEAD_ZONE) gy = 0; + + if ( !gx && !gy ) { + return rval; + } + + const bool LeftTrigger = m_button_state[GAMEPAD_BUTTON_LEFT_TRIGGER]; + const bool RightTrigger = m_button_state[GAMEPAD_BUTTON_RIGHT_TRIGGER]; + + if (LeftTrigger) { + if (rmb_down) *rmb_down = true; + rval = true; + + if (RightTrigger) { + if (key_flags) *key_flags |= KEY_SHIFTED; + } + } else if ( !RightTrigger ) { + if (lmb_down) *lmb_down = true; + rval = true; + } + + // scales delta to +/- 3 (slow, accurate) + const float sensitivity = 10000.f; + + float mdx = gx / sensitivity; + float mdy = gy / sensitivity; + + // we should only set dz or dx/dy, not both, and don't set dz if "shifted" + if (RightTrigger && !LeftTrigger) { + if (mdy) { + if (dz) *dz = (mdy > 0.f) ? -1 : 1; + } + } else { + if (dx) *dx = fl2i(mdx); + if (dy) *dy = fl2i(mdy); + } + + return rval; +} + + +// Initialize gamepad leanback +// NOTE: This should work independently of io::joystick! +void init() +{ + if (initialized) { + return; + } + + if (Is_standalone) { + return; + } + + if ( !Using_in_game_options ) { + NavEnabled = os_config_read_uint("Input", "GamepadNav", 1) == 1; + SwapActionCancel = os_config_read_uint("Input", "GamepadSwapActionCancel", 0) == 1; + CursorSpeed = os_config_read_uint("Input", "GamepadCursorSpeed", 6); + CLAMP(CursorSpeed, 1, 10); + } + + if ( !NavEnabled ) { + return; + } + + if ( !SDL_InitSubSystem(SDL_INIT_GAMEPAD) ) { + return; + } + + // TODO: SDL3 => It might be nice to eventually add touchpad support here + // for more precise cursor control. + + initialized = true; + + auto pads = SDL_GetGamepads(nullptr); + + if (pads) { + for (int i = 0; pads[i]; ++i) { + gamepads.push_back(GamepadPtr(new Gamepad(pads[i]))); + } + + SDL_free(pads); + pads = nullptr; + } + + if (ImGui::GetCurrentContext()) { + auto &io = ImGui::GetIO(); + + // enable gamepad nav for imgui + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + + io.ConfigNavSwapGamepadButtons = SwapActionCancel; + io.ConfigNavMoveSetMousePos = true; + } + + // These events should *not* be consumed, so that the joystick code can also + // make use of them + os::events::addEventListener(SDL_EVENT_GAMEPAD_ADDED, -1, event_handler); + os::events::addEventListener(SDL_EVENT_GAMEPAD_REMOVED, -1, event_handler); + os::events::addEventListener(SDL_EVENT_GAMEPAD_AXIS_MOTION, -1, event_handler); + os::events::addEventListener(SDL_EVENT_GAMEPAD_BUTTON_DOWN, -1, event_handler); + os::events::addEventListener(SDL_EVENT_GAMEPAD_BUTTON_UP, -1, event_handler); +} + +void shutdown() +{ + if ( !initialized ) { + return; + } + + if (ImGui::GetCurrentContext()) { + auto &io = ImGui::GetIO(); + + io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableGamepad; + + io.ConfigNavSwapGamepadButtons = false; + io.ConfigNavMoveSetMousePos = false; + } + + gamepads.clear(); + gamepads.shrink_to_fit(); + + SDL_QuitSubSystem(SDL_INIT_GAMEPAD); + + initialized = false; +} + +bool action() +{ + if ( !navActive() ) { + return false; + } + + // this should always work (cursor on/off, imgui, etc.) + + for (GamepadPtr &p : gamepads) { + if (p->action()) { + return true; + } + } + + return false; +} + +bool cancel() +{ + if ( !navActive() ) { + return false; + } + + // this should always work (cursor on/off, imgui, etc.) + + for (GamepadPtr &p : gamepads) { + if (p->cancel()) { + return true; + } + } + + return false; +} + +bool action_or_cancel() +{ + if ( !navActive() ) { + return false; + } + + // this should always work (cursor on/off, imgui, etc.) + + for (GamepadPtr &p : gamepads) { + if (p->action_or_cancel()) { + return true; + } + } + + return false; +} + +int get_key() +{ + static Uint64 key_check_time = 0; + + if ( !navActive() ) { + return 0; + } + + // skip if we aren't doing ui stuff + if ( !io::mouse::CursorManager::get()->isCursorShown() ) { + return 0; + } + + // if imgui should be controlling the gamepad nav then let it + const auto game_state = gameseq_get_state(); + + if ((game_state == GS_STATE_LAB) || (game_state == GS_STATE_INGAME_OPTIONS)) { + return 0; + } + + if (SDL_GetTicks() < key_check_time) { + return 0; + } + + key_check_time = SDL_GetTicks() + KEY_CHECK_INTERVAL_MS; + + for (GamepadPtr &p : gamepads) { + auto k = p->get_key(); + + if (k) { + return k; + } + } + + return 0; +} + +void do_frame() +{ + static Uint64 next_update = 0; + + if ( !navActive() ) { + return; + } + + // skip if we aren't doing ui stuff + if ( !io::mouse::CursorManager::get()->isCursorShown() ) { + return; + } + + // if imgui should be controlling the gamepad nav then let it + const auto game_state = gameseq_get_state(); + + if ((game_state == GS_STATE_LAB) || (game_state == GS_STATE_INGAME_OPTIONS)) { + return; + } + + // limit updates so that we aren't zooming all over the place at higher fps + if (next_update > SDL_GetTicks()) { + return; + } + + next_update = SDL_GetTicks() + CURSOR_UPDATE_INTERVAL_MS; + + for (GamepadPtr &p : gamepads) { + if (p->update_mouse_pos()) { + return; + } + } +} + +bool get_camera_vals(int *dx, int *dy, int *dz, bool *lmb_down, bool *rmb_down, uint *key_flags) +{ + if ( !navActive() ) { + return false; + } + + if (dx) *dx = 0; + if (dy) *dy = 0; + if (dz) *dz = 0; + if (lmb_down) *lmb_down = false; + if (rmb_down) *rmb_down = false; + if (key_flags) *key_flags = 0; + + for (GamepadPtr &p : gamepads) { + if (p->get_camera_vals(dx, dy, dz, lmb_down, rmb_down, key_flags)) { + return true; + } + } + + return false; +} + +bool navActive() +{ + return (initialized && NavEnabled && !gamepads.empty()); +} + +} // namespace io::gamepad diff --git a/code/io/gamepad.h b/code/io/gamepad.h new file mode 100644 index 00000000000..63d65581adf --- /dev/null +++ b/code/io/gamepad.h @@ -0,0 +1,158 @@ +/* + * Copyright (C) Volition, Inc. 1999. All rights reserved. + * + * All source code herein is the property of Volition, Inc. You may not sell + * or otherwise commercially exploit the source or things you created based on the + * source. + * + */ + +#ifndef __GAMEPAD_H__ +#define __GAMEPAD_H__ + +namespace io::gamepad { + +constexpr Uint8 GAMEPAD_BUTTON_LEFT_TRIGGER = SDL_GAMEPAD_BUTTON_COUNT; +constexpr Uint8 GAMEPAD_BUTTON_RIGHT_TRIGGER = SDL_GAMEPAD_BUTTON_COUNT+1; + + +class Gamepad { +private: + /** + * @brief Minimum dead zone to avoid reading stick drift + * @note This is the generally recommended value, but not necessarily accurate for all gamepads + */ + static constexpr int DEAD_ZONE = 8000; + + /** + * @brief State of buttons on gamepad, plus 2 entries for the triggers + */ + std::array m_button_state; + + SDL_JoystickID m_id; //!< SDL joystick/gamepad id + SDL_Gamepad *m_gamepad; //!< SDL gamepad handle + + /** + * @brief Translates a gamepad button press into a mouse button press + * @param button Uint8 representation of SDL_GAMEPAD_BUTTON_* + */ + void mark_mouse_button(Uint8 button); + +public: + Gamepad(SDL_JoystickID _id); + ~Gamepad(); + + SDL_JoystickID getId() const { return m_id; } + + /** + * @brief Determines if the action button has been pressed (similar to left mouse button) + * @note Typically the A button on Xbox style gamepads + * + * @return @c true if the action button is pressed, @c false otherwise + */ + bool action(); + + /** + * @brief Determines if the cancel button has been pressed (similar to Esc key) + * @note Typically the B button on Xbox style gamepads + * + * @return @c true if the cancel button is pressed, @c false otherwise + */ + bool cancel(); + + /** + * @brief Determines if the action or cancel buttons have been pressed + * + * @return @c true if the action or cancel button is pressed, @c false otherwise + */ + bool action_or_cancel() { return (action() || cancel()); } + + /** + * @brief Get the keyboard equivalent of a gamepad button + * + * @return KEY_* value corresponding to gamepad button + */ + int get_key(); + + /** + * @brief Gets the down time of the given hat and position + * @param[in] button Uint8 representation of SDL_GAMEPAD_BUTTON_* + * @param[in] down @c true if button is pressed, @c false if button is released + */ + void mark_button(Uint8 button, bool down); + + /** + * @brief Update mouse/cursor position based on gamepad stick movement + */ + bool update_mouse_pos(); + + /** + * @brief Translate gamepad state into mouse states for camera/object movement + * + * @details These controls were chosen due to ImGui not using them by default. If those defaults + * change an alternate mapping may be required. + * + * @param[out] dx x-axis delta of right stick + * @param[out] dy y-axis delta of right stick + * @param[out] dz z-axis delta (mouse wheel, only if right trigger pressed) + * @param[out] lmb_down @c true if left mouse button down ( dx/dy has value and no triggers pressed) + * @param[out] rmb_down @c true if riight mouse button down (dx/dy has value and left trigger pressed) + * @param[out] key_flags will have @c KEY_SHIFTED set if both triggers are pressed + * + * @returns @c true if any of the values were set + */ + bool get_camera_vals(int *dx, int *dy, int *dz, bool *lmb_down, bool *rmb_down, uint *key_flags); +}; + +void init(); +void shutdown(); +void do_frame(); + +/** + * @brief Returns @c true if gamepad navigation is enabled and active + */ +bool navActive(); + +/** + * @brief Returns @c true if the defined Action button has been pressed + */ +bool action(); + +/** + * @brief Returns @c true if the defined Cancel button has been pressed + */ +bool cancel(); + +/** + * @brief Returns @c true if the defined Action or Cancel buttons have been pressed + */ +bool action_or_cancel(); + +/** + * @brief Get the keyboard equivalent of any pressed gamepad buttons + * + * @return KEY_* value corresponding to a gamepad button + */ +int get_key(); + +/** + * @brief Translate gamepad state into mouse states for camera/object movement + * + * @details These controls were chosen due to ImGui not using them by default. If those defaults + * change an alternate mapping may be required. + * + * @param[out] dx x-axis delta of right stick + * @param[out] dy y-axis delta of right stick + * @param[out] dz z-axis delta (mouse wheel, only if right trigger pressed) + * @param[out] lmb_down @c true if left mouse button down ( dx/dy has value and no triggers pressed) + * @param[out] rmb_down @c true if riight mouse button down (dx/dy has value and left trigger pressed) + * @param[out] key_flags will have @c KEY_SHIFTED set if both triggers are pressed + * + * @returns @c true if any of the values were set to something other than defaults + * + * @warning This function sets all passed arguments to default values if gamepad navigation is active! + */ +bool get_camera_vals(int *dx, int *dy, int *dz, bool *lmb_down, bool *rmb_down, uint *key_flags); +} + +#endif diff --git a/code/io/joy-sdl.cpp b/code/io/joy-sdl.cpp index 729ecb62d82..8be7e89ed9a 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 8-pos timestamp + // 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 _hat[i].DownTimestamp8[hatval] = ui_timestamp(); } } @@ -876,30 +920,58 @@ namespace joystick } } - SDL_Joystick *Joystick::getDevice() + void Joystick::flush() + { + for (auto &b : _button) { + b.DownTimestamp = UI_TIMESTAMP::invalid(); + b.DownCount = 0; + } + } + + 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 +984,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 +1036,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 +1185,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 +1217,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 +1311,7 @@ namespace joystick // Automatically frees joystick resources joysticks.clear(); - SDL_QuitSubSystem(SDL_INIT_JOYSTICK); + SDL_QuitSubSystem(SDL_INIT_GAMEPAD); } json_t* getJsonArray() { @@ -1299,3 +1464,35 @@ 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); +} + +void joy_flush() +{ + for (auto pJoy : pJoystick) { + if (pJoy) { + pJoy->flush(); + } + } +} diff --git a/code/io/joy.h b/code/io/joy.h index ede8dfab364..e7dcc9c4dc5 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 @@ -248,6 +254,11 @@ namespace io */ json_t* getJSON(); + /** + * @brief Clears internal button state + */ + void flush(); + private: Joystick(const Joystick &); Joystick &operator=(const Joystick &); @@ -262,8 +273,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,10 +388,13 @@ 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 */ bool joy_present(short cid); +void joy_flush(); #endif /* __JOY_H__ */ diff --git a/code/io/joy_ff.cpp b/code/io/joy_ff.cpp new file mode 100644 index 00000000000..4e55154e8ee --- /dev/null +++ b/code/io/joy_ff.cpp @@ -0,0 +1,400 @@ +/* + * 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) + .importance(25) + .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) + .importance(24) + .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 relative strength of Force Feedback effects", 1757}) + .category(std::make_pair("Input", 1827)) + .level(options::ExpertLevel::Beginner) + .importance(23) + .range(0, 100) + .default_val(100) + .flags({options::OptionFlags::RangeTypeInteger}) + .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/io/mouse.cpp b/code/io/mouse.cpp index 044327e8b18..7f079487bc0 100644 --- a/code/io/mouse.cpp +++ b/code/io/mouse.cpp @@ -720,3 +720,16 @@ short bit_distance(short x) { return i; } + +// update mouse with position which is already scaled for max_w/max_h +void mouse_update_pos_scaled(int x, int y, float dx, float dy) +{ + CAP(x, 0, gr_screen.max_w-1); + CAP(y, 0, gr_screen.max_h-1); + + Mouse_x = x; + Mouse_y = y; + + Mouse_dx += fl2i(dx); + Mouse_dy += fl2i(dy); +} diff --git a/code/io/mouse.h b/code/io/mouse.h index 3a2fa7c29ee..ceaa37b2fe3 100644 --- a/code/io/mouse.h +++ b/code/io/mouse.h @@ -124,4 +124,7 @@ extern void mouse_force_pos(float x, float y); */ short bit_distance(short x); +// update mouse with position which is already scaled for max_w/max_h +void mouse_update_pos_scaled(int x, int y, float dx, float dy); + #endif diff --git a/code/lab/dialogs/lab_ui.cpp b/code/lab/dialogs/lab_ui.cpp index a534a02aef4..8185c7a9b1e 100644 --- a/code/lab/dialogs/lab_ui.cpp +++ b/code/lab/dialogs/lab_ui.cpp @@ -15,6 +15,7 @@ #include "mission/missionload.h" #include "prop/prop.h" #include "controlconfig/controlsconfig.h" +#include "io/gamepad.h" using namespace ImGui; @@ -84,7 +85,7 @@ void LabUi::build_species_entry(const species_info &species_def, int species_idx TreeNodeEx(node_label.c_str(), ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen, "%s", class_def.name); - if (IsItemClicked() && !IsItemToggledOpen()) { + if (IsItemActivated() && !IsItemToggledOpen()) { getLabManager()->changeDisplayedObject(LabMode::Ship, ship_info_idx); } } @@ -121,7 +122,7 @@ void LabUi::build_weapon_subtype_list() const ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen, "%s", class_def.name); - if (IsItemClicked() && !IsItemToggledOpen()) { + if (IsItemActivated() && !IsItemToggledOpen()) { getLabManager()->changeDisplayedObject(LabMode::Weapon, weapon_idx); } } @@ -147,7 +148,7 @@ void LabUi::build_prop_subtype_list() "%s", class_def.name.c_str()); - if (IsItemClicked() && !IsItemToggledOpen()) { + if (IsItemActivated() && !IsItemToggledOpen()) { getLabManager()->changeDisplayedObject(LabMode::Prop, prop_idx); } } @@ -179,7 +180,7 @@ void LabUi::build_asteroid_list() info.name, subtype.type_name.c_str()); - if (IsItemClicked() && !IsItemToggledOpen()) { + if (IsItemActivated() && !IsItemToggledOpen()) { getLabManager()->changeDisplayedObject(LabMode::Asteroid, asteroid_idx, subtype_idx); } @@ -210,7 +211,7 @@ void LabUi::build_debris_list() "%s", info.name); - if (IsItemClicked() && !IsItemToggledOpen()) { + if (IsItemActivated() && !IsItemToggledOpen()) { getLabManager()->changeDisplayedObject(LabMode::Asteroid, debris_idx, 0); // Debris subtype is always 0 } @@ -275,7 +276,7 @@ void LabUi::build_background_list() ImGuiTreeNodeFlags node_flags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; TreeNodeEx(LAB_MISSION_NONE_STRING, node_flags, LAB_MISSION_NONE_STRING); - if (IsItemClicked() && !IsItemToggledOpen()) { + if (IsItemActivated() && !IsItemToggledOpen()) { getLabManager()->Renderer->useBackground(LAB_MISSION_NONE_STRING); } @@ -291,7 +292,7 @@ void LabUi::build_background_list() for (const auto& mission_name : directory.second) { TreeNodeEx(mission_name.c_str(), node_flags, "%s", mission_name.c_str()); - if (IsItemClicked() && !IsItemToggledOpen()) { + if (IsItemActivated() && !IsItemToggledOpen()) { getLabManager()->Renderer->useBackground(mission_name); } } @@ -397,6 +398,15 @@ void LabUi::show_controls_reference() TextWrapped("Rotation axis limits and rotation speed apply only to object orientation (LMB), not " "camera controls (RMB)."); + if (io::gamepad::navActive()) { + Separator(); + TextWrapped("Gamepad controls"); + controls_reference_entry("RStick", "Orient the displayed object."); + controls_reference_entry("RStick + LTrigger", "Rotate the camera."); + controls_reference_entry("RStick + LTrigger + RTrigger", "Pan the camera on the X/Y plane."); + controls_reference_entry("RStick + RTrigger", "Zoom the camera in or out."); + } + Separator(); TextWrapped("Keyboard shortcuts"); controls_reference_entry("R", "Cycle object orientation (LMB) axis mode (Yaw, Pitch, Roll, or Both)."); diff --git a/code/lab/manager/lab_manager.cpp b/code/lab/manager/lab_manager.cpp index fd1775d645a..f9ca485a14a 100644 --- a/code/lab/manager/lab_manager.cpp +++ b/code/lab/manager/lab_manager.cpp @@ -19,6 +19,7 @@ #include "freespace.h" #include "io/mouse.h" +#include "io/gamepad.h" #undef LOCAL #include "extensions/ImGuizmo.h" @@ -131,20 +132,33 @@ void LabManager::onFrame(float frametime) { int mouse_y = 0; mouse_get_pos(&mouse_x, &mouse_y); - const bool lmb_down = mouse_down(MOUSE_LEFT_BUTTON) != 0; + auto key_flags = key_get_shift_status(); + + bool lmb_down = mouse_down(MOUSE_LEFT_BUTTON) != 0; + bool rmb_down = mouse_down(MOUSE_RIGHT_BUTTON) != 0; + bool lmb_pressed = lmb_down && !LastLmbDown; - LastLmbDown = lmb_down; - if (lmb_pressed && ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow)) { - lmb_pressed = false; - } + // don't let gamepad nav override mouse control + if (io::gamepad::navActive() && !lmb_down && !rmb_down) { + io::gamepad::get_camera_vals(&dx, &dy, &dz, &lmb_down, &rmb_down, &key_flags); + lmb_pressed = lmb_down && !LastLmbDown; + } else if (ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow)) { + // NOTE: when using gamepad nav a window will always be hovered + if (dz != 0) { + dz = 0; + } - if (dz != 0 && ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow)) { - dz = 0; + if (lmb_pressed) { + lmb_pressed = false; + } } + + LastLmbDown = lmb_down; + auto& current_camera = Renderer->getCurrentCamera(); current_camera->handleInput( - dx, dy, dz, lmb_down, lmb_pressed, mouse_down(MOUSE_RIGHT_BUTTON) != 0, key_get_shift_status(), mouse_x, mouse_y); + dx, dy, dz, lmb_down, lmb_pressed, rmb_down, key_flags, mouse_x, mouse_y); if (!current_camera->handlesObjectPlacement()) { const bool over_camera_overlay = Renderer->getShowOrientationWidget() && current_camera->isOverlayHit(mouse_x, mouse_y); diff --git a/code/localization/localize.cpp b/code/localization/localize.cpp index 2387709d4c9..d6968d14287 100644 --- a/code/localization/localize.cpp +++ b/code/localization/localize.cpp @@ -65,7 +65,7 @@ bool *Lcl_unexpected_tstring_check = nullptr; // NOTE: with map storage of XSTR strings, the indexes no longer need to be contiguous, // but internal strings should still increment XSTR_SIZE to avoid collisions. // retail XSTR_SIZE = 1570 -// #define XSTR_SIZE 1932 // This is the next available ID +// #define XSTR_SIZE 1937 // This is the next available ID // struct to allow for strings.tbl-determined x offset // offset is 0 for english, by default diff --git a/code/source_groups.cmake b/code/source_groups.cmake index 70dd601d39a..508aca0d20c 100644 --- a/code/source_groups.cmake +++ b/code/source_groups.cmake @@ -745,9 +745,15 @@ 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 + io/gamepad.cpp + io/gamepad.h ) # jpgutils files diff --git a/freespace2/freespace.cpp b/freespace2/freespace.cpp index c4e1e53c5dc..3722b498266 100644 --- a/freespace2/freespace.cpp +++ b/freespace2/freespace.cpp @@ -80,6 +80,7 @@ #include "hud/hudtargetbox.h" #include "iff_defs/iff_defs.h" #include "io/cursor.h" +#include "io/gamepad.h" #include "io/joy.h" #include "io/joy_ff.h" #include "io/key.h" @@ -2108,6 +2109,7 @@ void game_init() // standalone's don't use the joystick and it seems to sometimes cause them to not get shutdown properly if(!Is_standalone){ + io::gamepad::init(); io::joystick::init(); } @@ -4693,6 +4695,7 @@ void game_flush() { key_flush(); mouse_flush(); + joy_flush(); snazzy_flush(); Joymouse_button_status = 0; @@ -4716,6 +4719,10 @@ int game_check_key() if ((k & KEY_MASK) == KEY_PADENTER) k = (k & ~KEY_MASK) | KEY_ENTER; + if ( !k ) { + k = io::gamepad::get_key(); + } + return k; } @@ -4753,7 +4760,8 @@ int game_poll() int k = key_inkey(); // Move the mouse cursor with the joystick. Currently uses Joystick0 - if (os_foreground() && !io::mouse::CursorManager::get()->isCursorShown() && (Use_joy_mouse)) { + // (NOTE: ignore this when using gamepad navigation to avoid a mess) + if (Use_joy_mouse && os_foreground() && io::mouse::CursorManager::get()->isCursorShown() && !io::gamepad::navActive()) { // Move the mouse cursor with the joystick int mx, my; @@ -6453,6 +6461,8 @@ void game_do_state_common(int state,int no_networking) #endif Last_frame_ui_timestamp = ui_timestamp(); + io::gamepad::do_frame(); + io::mouse::CursorManager::doFrame(); // determine if to draw the mouse this frame snd_do_frame(); // update sound system event_music_do_frame(); // music needs to play across many states @@ -7130,6 +7140,7 @@ void game_shutdown(void) control_config_common_close(); io::joystick::shutdown(); + io::gamepad::shutdown(); audiostream_close(); snd_close();