diff --git a/code/controlconfig/controlsconfig.cpp b/code/controlconfig/controlsconfig.cpp index 3b6a4938cce..d43827d4402 100644 --- a/code/controlconfig/controlsconfig.cpp +++ b/code/controlconfig/controlsconfig.cpp @@ -1531,6 +1531,11 @@ const char *control_config_tooltip_handler(const char *str) return NULL; } +bool control_config_special_mode() +{ + return (Binding_mode || Search_mode); +} + void control_config_init(bool API_Access) { int i; diff --git a/code/controlconfig/controlsconfig.h b/code/controlconfig/controlsconfig.h index 24e538d6882..5f659afa916 100644 --- a/code/controlconfig/controlsconfig.h +++ b/code/controlconfig/controlsconfig.h @@ -740,6 +740,11 @@ void control_config_common_init(); */ void control_config_common_close(); +/*! + * @brief detect whether control config is in search or binding modes + */ +bool control_config_special_mode(); + /*! * @brief init config menu */ diff --git a/code/cutscene/movie.cpp b/code/cutscene/movie.cpp index 0ac531a81db..53bd352ed30 100644 --- a/code/cutscene/movie.cpp +++ b/code/cutscene/movie.cpp @@ -28,6 +28,7 @@ #include "tracing/tracing.h" #include "io/timer.h" #include "io/key.h" +#include "io/gamepad.h" #include "mod_table/mod_table.h" #include "network/multi.h" #include "scripting/global_hooks.h" @@ -244,6 +245,10 @@ void movie_display_loop(Player* player, PlaybackState* state) { processEvents(); + if (io::gamepad::action_or_cancel()) { + state->playing = false; + } + // NOTE: This does not update mission time! If movies get enabled in places // other than through cutscenes then some refactoring should be done // to account for normal time progression rather than just timestamps diff --git a/code/globalincs/toolchain/clang.h b/code/globalincs/toolchain/clang.h index 06952ddf43b..3a19f325838 100644 --- a/code/globalincs/toolchain/clang.h +++ b/code/globalincs/toolchain/clang.h @@ -104,6 +104,7 @@ #define PUSH_SUPPRESS_WARNINGS \ _Pragma("clang diagnostic push") \ _Pragma("clang diagnostic ignored \"-Wattributes\"") \ +_Pragma("clang diagnostic ignored \"-Wshadow\"") \ /** * @brief Restored previous warning settings diff --git a/code/globalincs/toolchain/gcc.h b/code/globalincs/toolchain/gcc.h index 121424b630c..5f83a53be4f 100644 --- a/code/globalincs/toolchain/gcc.h +++ b/code/globalincs/toolchain/gcc.h @@ -96,6 +96,7 @@ #define PUSH_SUPPRESS_WARNINGS \ _Pragma("GCC diagnostic push") \ _Pragma("GCC diagnostic ignored \"-Wattributes\"") \ +_Pragma("GCC diagnostic ignored \"-Wshadow\"") \ /** * @brief Restored previous warning settings diff --git a/code/globalincs/toolchain/mingw.h b/code/globalincs/toolchain/mingw.h index cf6f14eb76d..92d73e0babb 100644 --- a/code/globalincs/toolchain/mingw.h +++ b/code/globalincs/toolchain/mingw.h @@ -92,6 +92,7 @@ #define PUSH_SUPPRESS_WARNINGS \ _Pragma("GCC diagnostic push") \ _Pragma("GCC diagnostic ignored \"-Wattributes\"") \ +_Pragma("GCC diagnostic ignored \"-Wshadow\"") \ /** * @brief Restored previous warning settings diff --git a/code/io/gamepad.cpp b/code/io/gamepad.cpp new file mode 100644 index 00000000000..ce567baecbf --- /dev/null +++ b/code/io/gamepad.cpp @@ -0,0 +1,709 @@ +/* + * 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 "scpui/rocket_ui.h" + +#include "imgui.h" + +// Our Assert conflicts with the definitions inside libRocket +#pragma push_macro("Assert") +#undef Assert + +PUSH_SUPPRESS_WARNINGS +#include +#include +POP_SUPPRESS_WARNINGS + +#pragma pop_macro("Assert") + + + +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) +{ + if ( !os::events::isWindowEvent(evt, os::getSDLMainWindow()) ) { + return false; + } + + 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", 1933}, + std::pair{"Enable or disable gamepad UI control", 1934}) + .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", 1935}, + std::pair{"Swap gamepad buttons used for action and cancel", 1936}) + .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", 1937}, + std::pair{"Movement speed of cursor from gamepad inputs", 1938}) + .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..4 (slow, accurate) and -32..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; + + // skip this if we're doing scpui events + if (scpui::getContext()) { + return; + } + + 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::scpui_translate_button(Uint8 button) +{ + // skip this if we aren't doing scpui inputs + auto input_context = scpui::getContext(); + + if ( !input_context ) { + return; + } + + using namespace Rocket::Core; + + // mouse button events + const int left_button = SwapActionCancel ? SDL_GAMEPAD_BUTTON_EAST : SDL_GAMEPAD_BUTTON_SOUTH; + int mouse_button = -1; + + if (button == left_button) { + mouse_button = 0; + } else if (button == SDL_GAMEPAD_BUTTON_WEST) { + mouse_button = 1; + } + + if (mouse_button >= 0) { + if (m_button_state[button]) { + input_context->ProcessMouseButtonDown(mouse_button, 0); + } else { + input_context->ProcessMouseButtonUp(mouse_button, 0); + } + + // reset down state to avoid repeats + m_button_state[button] = false; + + return; + } + + // keyboard events + Input::KeyIdentifier key = Input::KI_UNKNOWN; + int mod = 0; + bool reset_state = true; + + if (cancel()) { + key = Input::KI_ESCAPE; + } else if (button == SDL_GAMEPAD_BUTTON_DPAD_UP) { + key = Input::KI_UP; + reset_state = false; + } else if (button == SDL_GAMEPAD_BUTTON_DPAD_DOWN) { + key = Input::KI_DOWN; + reset_state = false; + } else if (button == SDL_GAMEPAD_BUTTON_DPAD_LEFT) { + key = Input::KI_LEFT; + reset_state = false; + } else if (button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT) { + key = Input::KI_RIGHT; + reset_state = false; + } else if (button == SDL_GAMEPAD_BUTTON_LEFT_SHOULDER) { + key = Input::KI_TAB; + mod = Input::KM_SHIFT; + } else if (button == SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER) { + key = Input::KI_TAB; + } else if (button == GAMEPAD_BUTTON_LEFT_TRIGGER) { + key = Input::KI_NEXT; + } else if (button == GAMEPAD_BUTTON_RIGHT_TRIGGER) { + key = Input::KI_PRIOR; + } + + if (key != Input::KI_UNKNOWN) { + if (m_button_state[button]) { + input_context->ProcessKeyDown(key, mod); + + // reset down state to avoid repeats + if (reset_state) { + m_button_state[button] = false; + } + } else { + input_context->ProcessKeyUp(key, mod); + } + } +} + +void Gamepad::mark_button(Uint8 button, bool down) +{ + if (button >= m_button_state.size()) { + return; + } + + m_button_state[button] = down; + + mark_mouse_button(button); + scpui_translate_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; + } + + // 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 false; + } + + 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; + + if (RightTrigger) { + if (key_flags) *key_flags |= KEY_SHIFTED; + } + } else if ( !RightTrigger ) { + if (lmb_down) *lmb_down = true; + } + + // scales delta to -3..3 (slow, accurate) + // NOTE: using purposefully slow scale due to dz speed issues + const float sensitivity = 10000.f; + + auto mdx = fl2i(gx / sensitivity); + auto mdy = fl2i(gy / sensitivity); + + // we should only set dz or dx/dy, not both, and don't set dz if "shifted" + if (RightTrigger && !LeftTrigger) { + if (mdy && dz) { + *dz = (mdy > 0) ? -1 : 1; + } + } else { + if (dx) *dx = fl2i(mdx); + if (dy) *dy = fl2i(mdy); + } + + return true; +} + + +// 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 + using namespace os::events; + addEventListener(SDL_EVENT_GAMEPAD_ADDED, DEFAULT_LISTENER_WEIGHT - 5, event_handler); + addEventListener(SDL_EVENT_GAMEPAD_REMOVED, DEFAULT_LISTENER_WEIGHT - 5, event_handler); + addEventListener(SDL_EVENT_GAMEPAD_AXIS_MOTION, DEFAULT_LISTENER_WEIGHT - 5, event_handler); + addEventListener(SDL_EVENT_GAMEPAD_BUTTON_DOWN, DEFAULT_LISTENER_WEIGHT - 5, event_handler); + addEventListener(SDL_EVENT_GAMEPAD_BUTTON_UP, DEFAULT_LISTENER_WEIGHT - 5, 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 (auto &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 (auto &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 (auto &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; + } + + // also skip this if we're doing scpui events + if (scpui::getContext()) { + return 0; + } + + if (SDL_GetTicks() < key_check_time) { + return 0; + } + + key_check_time = SDL_GetTicks() + KEY_CHECK_INTERVAL_MS; + + for (auto &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 (auto &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 (auto &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..47963449deb --- /dev/null +++ b/code/io/gamepad.h @@ -0,0 +1,164 @@ +/* + * 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); + + /** + * @brief Translates a gamepad button press into a SCPUI mouse or keyboard input + * @param button Uint8 representation of SDL_GAMEPAD_BUTTON_* + */ + void scpui_translate_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 44921aa550b..f2c232d51ef 100644 --- a/code/io/joy-sdl.cpp +++ b/code/io/joy-sdl.cpp @@ -921,6 +921,14 @@ namespace joystick } } + void Joystick::flush() + { + for (auto &b : _button) { + b.DownTimestamp = UI_TIMESTAMP::invalid(); + b.DownCount = 0; + } + } + SDL_Joystick *Joystick::getJoystick() { return _joystick; @@ -1480,3 +1488,12 @@ short joy_get_button_axis(short cid, short btn) return static_cast(SDL_GAMEPAD_AXIS_LEFT_TRIGGER + axis_button); } + +void joy_flush() +{ + for (auto pJoy : pJoystick) { + if (pJoy) { + pJoy->flush(); + } + } +} diff --git a/code/io/joy.h b/code/io/joy.h index 3b68a1be954..e7dcc9c4dc5 100644 --- a/code/io/joy.h +++ b/code/io/joy.h @@ -254,6 +254,11 @@ namespace io */ json_t* getJSON(); + /** + * @brief Clears internal button state + */ + void flush(); + private: Joystick(const Joystick &); Joystick &operator=(const Joystick &); @@ -390,5 +395,6 @@ short joy_get_button_axis(const short cid, short btn); */ bool joy_present(short cid); +void joy_flush(); #endif /* __JOY_H__ */ diff --git a/code/io/joy_ff.cpp b/code/io/joy_ff.cpp index c856f1c1395..4e55154e8ee 100644 --- a/code/io/joy_ff.cpp +++ b/code/io/joy_ff.cpp @@ -33,6 +33,7 @@ static auto ForceFeedbackOption = options::OptionBuilder("Input.ForceFeedb std::pair{"Enable or disable force feedback", 1729}) .category(std::make_pair("Input", 1827)) .level(options::ExpertLevel::Beginner) + .importance(25) .default_val(true) .change_listener([](bool val, bool) { if (val) joy_ff_init(); @@ -47,6 +48,7 @@ static auto HitEffectOption = options::OptionBuilder("Input.HitEffect", std::pair{"Enable or disable the directional hit effect", 1731}) .category(std::make_pair("Input", 1827)) .level(options::ExpertLevel::Beginner) + .importance(24) .default_val(true) .change_listener([](bool val, bool) { Joy_ff_directional_hit_effect_enabled = val; @@ -57,9 +59,10 @@ static auto HitEffectOption = options::OptionBuilder("Input.HitEffect", // coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton static auto ForceFeedbackStrength = options::OptionBuilder("Input.FFStrength", std::pair{"Force Feedback Strength", 1756}, - std::pair{"The realtive strength of Force Feedback effects", 1757}) + std::pair{"The relative strength of Force Feedback effects", 1757}) .category(std::make_pair("Input", 1827)) .level(options::ExpertLevel::Beginner) + .importance(23) .range(0, 100) .default_val(100) .flags({options::OptionFlags::RangeTypeInteger}) diff --git a/code/io/mouse.cpp b/code/io/mouse.cpp index 185b2da8dff..32e0991bffe 100644 --- a/code/io/mouse.cpp +++ b/code/io/mouse.cpp @@ -721,3 +721,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 24560967a3a..22607ca8309 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" @@ -129,20 +130,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 17bd9d20588..22b616f0c8f 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 1933 // This is the next available ID +// #define XSTR_SIZE 1939 // This is the next available ID // struct to allow for strings.tbl-determined x offset // offset is 0 for english, by default diff --git a/code/source_groups.cmake b/code/source_groups.cmake index 913aeff5543..508aca0d20c 100644 --- a/code/source_groups.cmake +++ b/code/source_groups.cmake @@ -752,6 +752,8 @@ add_file_folder("Io" io/joy_rumble.cpp io/spacemouse.cpp io/spacemouse.h + io/gamepad.cpp + io/gamepad.h ) # jpgutils files diff --git a/freespace2/freespace.cpp b/freespace2/freespace.cpp index c4e1e53c5dc..3722b498266 100644 --- a/freespace2/freespace.cpp +++ b/freespace2/freespace.cpp @@ -80,6 +80,7 @@ #include "hud/hudtargetbox.h" #include "iff_defs/iff_defs.h" #include "io/cursor.h" +#include "io/gamepad.h" #include "io/joy.h" #include "io/joy_ff.h" #include "io/key.h" @@ -2108,6 +2109,7 @@ void game_init() // standalone's don't use the joystick and it seems to sometimes cause them to not get shutdown properly if(!Is_standalone){ + io::gamepad::init(); io::joystick::init(); } @@ -4693,6 +4695,7 @@ void game_flush() { key_flush(); mouse_flush(); + joy_flush(); snazzy_flush(); Joymouse_button_status = 0; @@ -4716,6 +4719,10 @@ int game_check_key() if ((k & KEY_MASK) == KEY_PADENTER) k = (k & ~KEY_MASK) | KEY_ENTER; + if ( !k ) { + k = io::gamepad::get_key(); + } + return k; } @@ -4753,7 +4760,8 @@ int game_poll() int k = key_inkey(); // Move the mouse cursor with the joystick. Currently uses Joystick0 - if (os_foreground() && !io::mouse::CursorManager::get()->isCursorShown() && (Use_joy_mouse)) { + // (NOTE: ignore this when using gamepad navigation to avoid a mess) + if (Use_joy_mouse && os_foreground() && io::mouse::CursorManager::get()->isCursorShown() && !io::gamepad::navActive()) { // Move the mouse cursor with the joystick int mx, my; @@ -6453,6 +6461,8 @@ void game_do_state_common(int state,int no_networking) #endif Last_frame_ui_timestamp = ui_timestamp(); + io::gamepad::do_frame(); + io::mouse::CursorManager::doFrame(); // determine if to draw the mouse this frame snd_do_frame(); // update sound system event_music_do_frame(); // music needs to play across many states @@ -7130,6 +7140,7 @@ void game_shutdown(void) control_config_common_close(); io::joystick::shutdown(); + io::gamepad::shutdown(); audiostream_close(); snd_close();