diff --git a/common/util/FileUtil.cpp b/common/util/FileUtil.cpp index cd50d820ee..c836a06b90 100644 --- a/common/util/FileUtil.cpp +++ b/common/util/FileUtil.cpp @@ -42,7 +42,7 @@ #include #include -#include +#include "mach-o/dyld.h" #endif namespace file_util { diff --git a/game/graphics/pipelines/opengl.cpp b/game/graphics/pipelines/opengl.cpp index 8afe9ddd04..518c06735a 100644 --- a/game/graphics/pipelines/opengl.cpp +++ b/game/graphics/pipelines/opengl.cpp @@ -316,7 +316,7 @@ GLDisplay::GLDisplay(SDL_Window* window, SDL_GLContext gl_context, bool is_main) m_display_manager(std::make_shared(window)), m_input_manager(std::make_shared()) { m_main = is_main; - + m_display_manager->set_input_manager(m_input_manager); // Register commands m_input_manager->register_command(CommandBinding::Source::KEYBOARD, CommandBinding(SDLK_F12, [&]() { @@ -466,13 +466,9 @@ void GLDisplay::process_sdl_events() { ImGui_ImplSDL2_ProcessEvent(&evt); } } - ImGuiIO& io = ImGui::GetIO(); { - auto p = scoped_prof("sdl-input-monitor"); - // Ignore relevant inputs from the window if ImGUI is capturing them - // On the first frame, this will clear any current inputs in an attempt to reduce unexpected - // behaviour - m_input_manager->process_sdl_event(evt, io.WantCaptureMouse, io.WantCaptureKeyboard); + auto p = scoped_prof("sdl-input-monitor-process-event"); + m_input_manager->process_sdl_event(evt); } } } @@ -481,7 +477,27 @@ void GLDisplay::process_sdl_events() { * Main function called to render graphics frames. This is called in a loop. */ void GLDisplay::render() { - // Process SDL Events + // Before we process the current frames SDL events we for keyboard/mouse button inputs. + // + // This technically means that keyboard/mouse button inputs will be a frame behind but the + // event-based code is buggy and frankly not worth stressing over. Leaving this as a note incase + // someone complains. Binding handling is still taken care of by the event code though. + { + auto p = scoped_prof("sdl-input-monitor-poll-for-kb-mouse"); + ImGuiIO& io = ImGui::GetIO(); + if (io.WantCaptureKeyboard) { + m_input_manager->clear_keyboard_actions(); + } else { + m_input_manager->poll_keyboard_data(); + } + if (io.WantCaptureMouse) { + m_input_manager->clear_mouse_actions(); + } else { + m_input_manager->poll_mouse_data(); + } + m_input_manager->finish_polling(); + } + // Now process SDL Events process_sdl_events(); // Also process any display related events received from the EE (the game) // this is done here so they run from the perspective of the graphics thread diff --git a/game/system/hid/devices/game_controller.cpp b/game/system/hid/devices/game_controller.cpp index 8868586ced..29e31fdaf4 100644 --- a/game/system/hid/devices/game_controller.cpp +++ b/game/system/hid/devices/game_controller.cpp @@ -66,8 +66,7 @@ GameController::GameController(int sdl_device_id, void GameController::process_event(const SDL_Event& event, const CommandBindingGroups& commands, std::shared_ptr data, - std::optional& bind_assignment, - bool ignore_inputs) { + std::optional& bind_assignment) { if (event.type == SDL_CONTROLLERAXISMOTION && event.caxis.which == m_sdl_instance_id) { // https://wiki.libsdl.org/SDL2/SDL_GameControllerAxis if ((int)event.caxis.axis <= SDL_CONTROLLER_AXIS_INVALID || diff --git a/game/system/hid/devices/game_controller.h b/game/system/hid/devices/game_controller.h index a88c2bce38..84df0e8a32 100644 --- a/game/system/hid/devices/game_controller.h +++ b/game/system/hid/devices/game_controller.h @@ -11,8 +11,7 @@ class GameController : public InputDevice { void process_event(const SDL_Event& event, const CommandBindingGroups& commands, std::shared_ptr data, - std::optional& bind_assignment, - bool ignore_inputs = false) override; + std::optional& bind_assignment) override; void close_device() override; int update_rumble(const u8 low_rumble, const u8 high_rumble); std::string get_name() const { return m_device_name; } diff --git a/game/system/hid/devices/input_device.h b/game/system/hid/devices/input_device.h index d786b5a2b8..2c4ba78b5b 100644 --- a/game/system/hid/devices/input_device.h +++ b/game/system/hid/devices/input_device.h @@ -19,8 +19,7 @@ class InputDevice { virtual void process_event(const SDL_Event& event, const CommandBindingGroups& commands, std::shared_ptr data, - std::optional& bind_assignment, - bool ignore_inputs = false) = 0; + std::optional& bind_assignment) = 0; virtual void close_device() = 0; bool is_loaded() const { return m_loaded; }; }; diff --git a/game/system/hid/devices/keyboard.cpp b/game/system/hid/devices/keyboard.cpp index 311b974a3b..d7343cad78 100644 --- a/game/system/hid/devices/keyboard.cpp +++ b/game/system/hid/devices/keyboard.cpp @@ -6,19 +6,95 @@ KeyboardDevice::KeyboardDevice(std::shared_ptr set m_settings = settings; } +// I don't trust SDL's key repeat stuff, do it myself to avoid bug reports...(or cause more) +bool KeyboardDevice::is_action_already_active(const u32 sdl_keycode) { + for (const auto& action : m_active_actions) { + if (action.sdl_keycode == sdl_keycode) { + return true; + } + } + return false; +} + +void KeyboardDevice::poll_state(std::shared_ptr data) { + auto& binds = m_settings->keyboard_binds; + const auto keyboard_state = SDL_GetKeyboardState(NULL); + const auto keyboard_modifier_state = SDL_GetModState(); + + // Iterate binds, see if there are any new actions we need to track + // - Normal Buttons + for (const auto& [sdl_keycode, bind_list] : binds.buttons) { + for (const auto& bind : bind_list) { + if (keyboard_state[SDL_GetScancodeFromKey(sdl_keycode)] && + bind.modifiers.has_necessary_modifiers(keyboard_modifier_state) && + !is_action_already_active(sdl_keycode)) { + data->button_data.at(bind.pad_data_index) = true; // press the button + m_active_actions.push_back( + {sdl_keycode, bind, [](std::shared_ptr data, InputBinding bind) { + data->button_data.at(bind.pad_data_index) = false; // let go of the button + }}); + } + } + } + // - Analog Buttons + for (const auto& [sdl_keycode, bind_list] : binds.button_axii) { + for (const auto& bind : bind_list) { + if (keyboard_state[SDL_GetScancodeFromKey(sdl_keycode)] && + bind.modifiers.has_necessary_modifiers(keyboard_modifier_state) && + !is_action_already_active(sdl_keycode)) { + data->button_data.at(bind.pad_data_index) = true; // press the button + m_active_actions.push_back( + {sdl_keycode, bind, [](std::shared_ptr data, InputBinding bind) { + data->button_data.at(bind.pad_data_index) = false; // let go of the button + }}); + } + } + } + // - Analog Sticks + for (const auto& [sdl_keycode, bind_list] : binds.analog_axii) { + for (const auto& bind : bind_list) { + if (keyboard_state[SDL_GetScancodeFromKey(sdl_keycode)] && + bind.modifiers.has_necessary_modifiers(keyboard_modifier_state) && + !is_action_already_active(sdl_keycode)) { + data->analog_data.at(bind.pad_data_index) += bind.minimum_in_range ? -127 : 127; + data->update_analog_sim_tracker(false); + m_active_actions.push_back( + {sdl_keycode, bind, [](std::shared_ptr data, InputBinding bind) { + data->analog_data.at(bind.pad_data_index) += bind.minimum_in_range ? 127 : -127; + data->update_analog_sim_tracker(true); + }}); + } + } + } + + // Check if any previously tracked actions are now invalidated by the new state of the keyboard + // if so, we'll run their revert code and remove them + for (auto it = m_active_actions.begin(); it != m_active_actions.end();) { + // Modifiers are easy, if the action required one and it's not pressed anymore, evict it + // Alternatively, was the primary key released + if (!keyboard_state[SDL_GetScancodeFromKey(it->sdl_keycode)] || + !it->binding.modifiers.has_necessary_modifiers(keyboard_modifier_state)) { + it->revert_action(data, it->binding); + it = m_active_actions.erase(it); + } else { + it++; + } + } +} + +void KeyboardDevice::clear_actions(std::shared_ptr data) { + for (auto it = m_active_actions.begin(); it != m_active_actions.end();) { + it->revert_action(data, it->binding); + it = m_active_actions.erase(it); + } +} + void KeyboardDevice::process_event(const SDL_Event& event, const CommandBindingGroups& commands, std::shared_ptr data, - std::optional& bind_assignment, - bool ignore_inputs) { - if (ignore_inputs) { - return; - } + std::optional& bind_assignment) { if (event.type == SDL_KEYDOWN || event.type == SDL_KEYUP) { const auto key_event = event.key; - if (key_event.repeat != 0) { - return; - } if (m_ignore_key_on_keyup && m_ignore_key_on_keyup.value() == (u32)key_event.keysym.sym && event.type == SDL_KEYUP) { m_ignore_key_on_keyup = std::nullopt; @@ -32,7 +108,6 @@ void KeyboardDevice::process_event(const SDL_Event& event, if (bind_assignment->device_type != InputDeviceType::KEYBOARD) { return; } - // A normal key down event (a new key was pressed) and it's not a modifier if (event.type == SDL_KEYDOWN && !sdl_util::is_modifier_key(key_event.keysym.sym)) { if (bind_assignment->for_analog) { @@ -54,50 +129,19 @@ void KeyboardDevice::process_event(const SDL_Event& event, return; } } + } + // otherwise, set the bind + if (bind_assignment->for_analog) { + binds.assign_analog_bind(key_event.keysym.sym, bind_assignment.value(), + InputModifiers(key_event.keysym.mod)); } else { - // otherwise, set the bind - if (bind_assignment->for_analog) { - binds.assign_analog_bind(key_event.keysym.sym, bind_assignment.value(), - InputModifiers(key_event.keysym.mod)); - } else { - binds.assign_button_bind(key_event.keysym.sym, bind_assignment.value(), false, - InputModifiers(key_event.keysym.mod)); - } + binds.assign_button_bind(key_event.keysym.sym, bind_assignment.value(), false, + InputModifiers(key_event.keysym.mod)); } } return; } - // Normal Buttons - if (binds.buttons.find(key_event.keysym.sym) != binds.buttons.end()) { - for (const auto& bind : binds.buttons.at(key_event.keysym.sym)) { - if (bind.modifiers.has_necessary_modifiers(key_event.keysym.mod)) { - data->button_data.at(bind.pad_data_index) = event.type == SDL_KEYDOWN; - } - } - } - // Analog Buttons (useless for keyboards, but here for completeness) - if (binds.button_axii.find(key_event.keysym.sym) != binds.button_axii.end()) { - for (const auto& bind : binds.button_axii.at(key_event.keysym.sym)) { - if (bind.modifiers.has_necessary_modifiers(key_event.keysym.mod)) { - data->button_data.at(bind.pad_data_index) = event.type == SDL_KEYDOWN; - } - } - } - // Analog Sticks simulating - if (binds.analog_axii.find(key_event.keysym.sym) != binds.analog_axii.end()) { - for (const auto& bind : binds.analog_axii.at(key_event.keysym.sym)) { - if (bind.modifiers.has_necessary_modifiers(key_event.keysym.mod)) { - int analog_val = bind.minimum_in_range ? 127 : -127; - if (event.type == SDL_KEYDOWN) { - analog_val = bind.minimum_in_range ? -127 : 127; - } - data->analog_data.at(bind.pad_data_index) += analog_val; - data->update_analog_sim_tracker(event.type != SDL_KEYDOWN); - } - } - } - // Check for commands if (event.type == SDL_KEYDOWN && commands.keyboard_binds.find(key_event.keysym.sym) != commands.keyboard_binds.end()) { diff --git a/game/system/hid/devices/keyboard.h b/game/system/hid/devices/keyboard.h index 399dc47ac7..32fa80c67b 100644 --- a/game/system/hid/devices/keyboard.h +++ b/game/system/hid/devices/keyboard.h @@ -2,22 +2,44 @@ #include "input_device.h" +// A simple way to store the current state of the keyboard +// if we are polling every frame, we need a point of reference to know +// how the PadData has to be modified +// +// For example if the keyboard no longer has "W" pressed is it: +// - because it was pressed before and the user let go of it (adjust analog value) +// - because it was never pressed at all (noop!) +// +// Likewise, when modifiers are let go, we can use this to enumerate through to see what has to be +// invalidated +struct ActiveKeyboardAction { + u32 sdl_keycode; + InputBinding binding; + std::function, InputBinding bind)> revert_action; +}; + class KeyboardDevice : public InputDevice { public: KeyboardDevice(){}; KeyboardDevice(std::shared_ptr settings); ~KeyboardDevice() {} + // Polls the entire state of the keyboard to set the PadData + void poll_state(std::shared_ptr data); + void clear_actions(std::shared_ptr data); void process_event(const SDL_Event& event, const CommandBindingGroups& commands, std::shared_ptr data, - std::optional& bind_assignment, - bool ignore_inputs = false) override; + std::optional& bind_assignment) override; void close_device() override{ // there is nothing to close }; + private: + std::vector m_active_actions = {}; // When we assign a bind on a keydown, we have to ignore it's keyup event // for analog binds, or it will adjust the position std::optional m_ignore_key_on_keyup; + + bool is_action_already_active(const u32 sdl_keycode); }; diff --git a/game/system/hid/devices/mouse.cpp b/game/system/hid/devices/mouse.cpp index b7aa85e57a..dae83828fc 100644 --- a/game/system/hid/devices/mouse.cpp +++ b/game/system/hid/devices/mouse.cpp @@ -7,20 +7,109 @@ MouseDevice::MouseDevice(std::shared_ptr settings) enable_relative_mode(m_control_camera); } +// I don't trust SDL's key repeat stuff, do it myself to avoid bug reports...(or cause more) +bool MouseDevice::is_action_already_active(const u32 sdl_code, const bool player_movement) { + for (const auto& action : m_active_actions) { + if (!player_movement && action.sdl_mouse_button == sdl_code) { + return true; + } else if (player_movement && action.player_movement) { + return true; + } + } + return false; +} + +void MouseDevice::poll_state(std::shared_ptr data) { + auto& binds = m_settings->mouse_binds; + const auto mouse_state = SDL_GetMouseState(NULL, NULL); + const auto keyboard_modifier_state = SDL_GetModState(); + + // Iterate binds, see if there are any new actions we need to track + // - Normal Buttons + for (const auto& [sdl_code, bind_list] : binds.buttons) { + for (const auto& bind : bind_list) { + if (mouse_state & SDL_BUTTON(sdl_code) && + bind.modifiers.has_necessary_modifiers(keyboard_modifier_state) && + !is_action_already_active(sdl_code, false)) { + data->button_data.at(bind.pad_data_index) = true; // press the button + m_active_actions.push_back( + {sdl_code, bind, false, [](std::shared_ptr data, InputBinding bind) { + data->button_data.at(bind.pad_data_index) = false; // let go of the button + }}); + } + } + } + // - Analog Buttons (useless for keyboards, but here for completeness) + for (const auto& [sdl_code, bind_list] : binds.button_axii) { + for (const auto& bind : bind_list) { + if (mouse_state & SDL_BUTTON(sdl_code) && + bind.modifiers.has_necessary_modifiers(keyboard_modifier_state) && + !is_action_already_active(sdl_code, false)) { + data->button_data.at(bind.pad_data_index) = true; // press the button + m_active_actions.push_back( + {sdl_code, bind, false, [](std::shared_ptr data, InputBinding bind) { + data->button_data.at(bind.pad_data_index) = false; // let go of the button + }}); + } + } + } + // No analog stick simulation, not support via the mouse instead we allow for only this: + // WoW style mouse movement, if you have both buttons held down, you will move forward + if (m_control_movement && !is_action_already_active(0, true) && + (mouse_state & SDL_BUTTON_LMASK && mouse_state & SDL_BUTTON_RMASK)) { + data->analog_data.at(1) += -127; // move forward + data->update_analog_sim_tracker(false); + ActiveMouseAction action; + action.player_movement = true; + action.revert_action = [](std::shared_ptr data, InputBinding bind) { + data->analog_data.at(1) += 127; // stop moving forward + data->update_analog_sim_tracker(true); + }; + m_active_actions.push_back(action); + } + + // Check if any previously tracked actions are now invalidated by the new state of the keyboard + // if so, we'll run their revert code and remove them + for (auto it = m_active_actions.begin(); it != m_active_actions.end();) { + // Modifiers are easy, if the action required one and it's not pressed anymore, evict it + // Alternatively, was the primary key released + // Alternatively, alternatively the special case'd mouse movement + if (it->player_movement) { + if (!(mouse_state & SDL_BUTTON_LMASK) || !(mouse_state & SDL_BUTTON_RMASK)) { + it->revert_action(data, it->binding); + it = m_active_actions.erase(it); + } else { + it++; + } + } else { + if (!(mouse_state & SDL_BUTTON(it->sdl_mouse_button)) || + !it->binding.modifiers.has_necessary_modifiers(keyboard_modifier_state)) { + it->revert_action(data, it->binding); + it = m_active_actions.erase(it); + } else { + it++; + } + } + } +} + +void MouseDevice::clear_actions(std::shared_ptr data) { + for (auto it = m_active_actions.begin(); it != m_active_actions.end();) { + it->revert_action(data, it->binding); + it = m_active_actions.erase(it); + } +} + void MouseDevice::process_event(const SDL_Event& event, const CommandBindingGroups& commands, std::shared_ptr data, - std::optional& bind_assignment, - bool ignore_inputs) { + std::optional& bind_assignment) { + // We still want to keep track of the cursor location even if we aren't using it for inputs + // return early if (event.type == SDL_MOUSEMOTION) { // https://wiki.libsdl.org/SDL2/SDL_MouseMotionEvent m_xcoord = event.motion.x; m_ycoord = event.motion.y; - if (ignore_inputs) { - // We still want to keep track of the cursor location even if we aren't using it for inputs - // return early - return; - } if (m_control_camera) { const auto xadjust = std::clamp(127 + int(float(event.motion.xrel) * m_xsens), 0, 255); const auto yadjust = std::clamp(127 + int(float(event.motion.yrel) * m_ysens), 0, 255); @@ -31,7 +120,7 @@ void MouseDevice::process_event(const SDL_Event& event, // Mouse Button Events // https://wiki.libsdl.org/SDL2/SDL_MouseButtonEvent const auto button_event = event.button; - // Always update the internal button tracker, this is for GOAL reasons. + // Update the internal mouse tracking, this is for GOAL reasons. switch (button_event.button) { case SDL_BUTTON_LEFT: m_button_status.left = event.type == SDL_MOUSEBUTTONDOWN; @@ -50,10 +139,6 @@ void MouseDevice::process_event(const SDL_Event& event, break; } - if (ignore_inputs) { - return; - } - auto& binds = m_settings->mouse_binds; // Binding re-assignment @@ -65,49 +150,6 @@ void MouseDevice::process_event(const SDL_Event& event, return; } - // Normal Buttons - if (binds.buttons.find(button_event.button) != binds.buttons.end()) { - for (const auto& bind : binds.buttons.at(button_event.button)) { - if (bind.modifiers.has_necessary_modifiers(SDL_GetModState())) { - data->button_data.at(bind.pad_data_index) = event.type == SDL_MOUSEBUTTONDOWN; - } - } - } - // Analog Buttons (useless for mice, but here for completeness) - if (binds.button_axii.find(button_event.button) != binds.button_axii.end()) { - for (const auto& bind : binds.button_axii.at(button_event.button)) { - data->button_data.at(bind.pad_data_index) = event.type == SDL_MOUSEBUTTONDOWN; - } - } - // Analog Sticks simulating - if (binds.analog_axii.find(button_event.button) != binds.analog_axii.end()) { - for (const auto& bind : binds.analog_axii.at(button_event.button)) { - if (bind.modifiers.has_necessary_modifiers(SDL_GetModState())) { - int analog_val = event.type == SDL_MOUSEBUTTONDOWN ? 255 : 127; - if (event.type == SDL_MOUSEBUTTONDOWN && bind.minimum_in_range) { - analog_val = 0; - } - data->analog_data.at(bind.pad_data_index) = analog_val; - } - } - } - - if (m_control_movement) { - // WoW style mouse movement, if you have both buttons held down, you will move forward - const auto mouse_state = SDL_GetMouseState(NULL, NULL); - if (!m_was_moving_with_mouse && event.type == SDL_MOUSEBUTTONDOWN && - (mouse_state & SDL_BUTTON_LMASK && mouse_state & SDL_BUTTON_RMASK)) { - data->analog_data.at(1) += -127; - m_was_moving_with_mouse = true; - data->update_analog_sim_tracker(false); - } else if (m_was_moving_with_mouse && event.type == SDL_MOUSEBUTTONUP && - ((mouse_state & SDL_BUTTON_LMASK) == 0 || (mouse_state & SDL_BUTTON_RMASK) == 0)) { - data->analog_data.at(1) += 127; - m_was_moving_with_mouse = false; - data->update_analog_sim_tracker(true); - } - } - // Check for commands if (event.type == SDL_MOUSEBUTTONDOWN && commands.mouse_binds.find(button_event.button) != commands.mouse_binds.end()) { diff --git a/game/system/hid/devices/mouse.h b/game/system/hid/devices/mouse.h index 26455f3dfa..8d2b1a9ac6 100644 --- a/game/system/hid/devices/mouse.h +++ b/game/system/hid/devices/mouse.h @@ -2,6 +2,15 @@ #include "input_device.h" +// See ActiveKeyboardAction, same idea slightly different data +struct ActiveMouseAction { + u32 sdl_mouse_button; + InputBinding binding; + bool player_movement = + false; // A special one for the mouse, no related binding, hard-coded behaviour! + std::function, InputBinding bind)> revert_action; +}; + class MouseDevice : public InputDevice { public: struct MouseButtonStatus { @@ -16,11 +25,12 @@ class MouseDevice : public InputDevice { MouseDevice(std::shared_ptr settings); ~MouseDevice() {} + void poll_state(std::shared_ptr data); + void clear_actions(std::shared_ptr data); void process_event(const SDL_Event& event, const CommandBindingGroups& commands, std::shared_ptr data, - std::optional& bind_assignment, - bool ignore_inputs = false) override; + std::optional& bind_assignment) override; void close_device() override{ // there is nothing to close }; @@ -31,15 +41,20 @@ class MouseDevice : public InputDevice { std::pair get_mouse_pos() const { return {m_xcoord, m_ycoord}; } MouseButtonStatus get_mouse_button_status() const { return m_button_status; } void set_camera_sens(const float xsens, const float ysens); + bool is_camera_being_controlled() { return m_control_camera; } private: + std::vector m_active_actions = {}; + + // Track the state of mouse for Game reasons int m_xcoord = 0; int m_ycoord = 0; MouseButtonStatus m_button_status; bool m_control_camera = false; bool m_control_movement = false; - bool m_was_moving_with_mouse = false; float m_xsens = -15.0; float m_ysens = 10.0; + + bool is_action_already_active(const u32 sdl_keycode, const bool player_movement); }; diff --git a/game/system/hid/display_manager.cpp b/game/system/hid/display_manager.cpp index fd6adee73a..ffd9638e4a 100644 --- a/game/system/hid/display_manager.cpp +++ b/game/system/hid/display_manager.cpp @@ -88,6 +88,14 @@ void DisplayManager::process_sdl_event(const SDL_Event& event) { // framerate we don't handle that update_curr_display_info(); break; + case SDL_WINDOWEVENT_ENTER: + if (m_input_manager && m_input_manager.value()->auto_hiding_cursor()) { + m_input_manager.value()->hide_cursor(true); + } + break; + case SDL_WINDOWEVENT_LEAVE: + m_input_manager.value()->hide_cursor(false); + break; } } else if (event_type == SDL_DISPLAYEVENT) { // https://wiki.libsdl.org/SDL2/SDL_DisplayEventID diff --git a/game/system/hid/display_manager.h b/game/system/hid/display_manager.h index 13cad17634..50cf92ec33 100644 --- a/game/system/hid/display_manager.h +++ b/game/system/hid/display_manager.h @@ -8,6 +8,7 @@ #include #include "game/settings/settings.h" +#include "game/system/hid/input_manager.h" #include "third-party/SDL/include/SDL.h" @@ -89,9 +90,14 @@ class DisplayManager { void enqueue_set_window_display_mode(WindowDisplayMode mode); void enqueue_set_fullscreen_display_id(int display_id); + void set_input_manager(std::shared_ptr input_manager) { + m_input_manager = input_manager; + } + private: SDL_Window* m_window; game_settings::DisplaySettings m_display_settings; + std::optional> m_input_manager; std::mutex event_queue_mtx; std::queue ee_event_queue; diff --git a/game/system/hid/input_bindings.cpp b/game/system/hid/input_bindings.cpp index 9ef78c12f9..2da9c6a40c 100644 --- a/game/system/hid/input_bindings.cpp +++ b/game/system/hid/input_bindings.cpp @@ -193,28 +193,11 @@ std::vector InputBindingGroups::lookup_button_binds(PadData::B return entry; } -void InputBindingGroups::assign_analog_bind(u32 sdl_idx, - InputBindAssignmentMeta& bind_meta, - const std::optional modifiers) { - // Find out if the PS2 input is already bound, if it is we will do a swap so no input is ever left - // unmapped - const auto current_binds = - lookup_analog_binds((PadData::AnalogIndex)bind_meta.pad_idx, bind_meta.for_analog_minimum); - if (analog_axii.find(sdl_idx) != analog_axii.end()) { - const auto existing_binds = analog_axii.at(sdl_idx); - analog_axii[sdl_idx] = {InputBinding((PadData::AnalogIndex)bind_meta.pad_idx, - bind_meta.for_analog_minimum, modifiers)}; - // there already a bind, so swap (as long as it's not the same key) - if (!current_binds.empty() && (u32)current_binds.front().sdl_idx != sdl_idx) { - analog_axii[current_binds.front().sdl_idx] = existing_binds; - } - } else { - analog_axii[sdl_idx] = {InputBinding((PadData::AnalogIndex)bind_meta.pad_idx, - bind_meta.for_analog_minimum, modifiers)}; - } - // The underlying data structures support multiple binds for the same input, but the UX doesn't - // so we have to wipe out any shared bindings after an assignment - for (auto it = analog_axii.begin(); it != analog_axii.end();) { +void InputBindingGroups::remove_multiple_binds( + u32 sdl_idx, + InputBindAssignmentMeta& bind_meta, + std::unordered_map>& bind_map) { + for (auto it = bind_map.begin(); it != bind_map.end();) { if (it->first == sdl_idx) { it++; continue; @@ -225,7 +208,7 @@ void InputBindingGroups::assign_analog_bind(u32 sdl_idx, bind.minimum_in_range != bind_meta.for_analog_minimum) { continue; } - it = analog_axii.erase(it); + it = bind_map.erase(it); found_match = true; break; } @@ -233,19 +216,99 @@ void InputBindingGroups::assign_analog_bind(u32 sdl_idx, it++; } } +} +std::optional> InputBindingGroups::find_button_bind_from_sdl_idx( + u32 sdl_idx, + const std::optional modifiers) { + // We need to check that there isn't a shared SDL key on the button group side + for (const auto& [sdl_code, binds] : buttons) { + if (sdl_code == sdl_idx) { + for (const auto bind : binds) { + if (!modifiers || bind.modifiers == modifiers.value()) { + std::pair result = {bind, false}; + return result; + } + } + } + } + for (const auto& [sdl_code, binds] : button_axii) { + if (sdl_code == sdl_idx) { + for (const auto bind : binds) { + if (!modifiers || bind.modifiers == modifiers.value()) { + std::pair result = {bind, true}; + return result; + } + } + } + } + return {}; +} + +void InputBindingGroups::assign_analog_bind(u32 sdl_idx, + InputBindAssignmentMeta& bind_meta, + const std::optional modifiers) { + // Find out if the PS2 input is already bound, if it is we will do a swap so no input is ever left + // unmapped + const auto current_analog_binds = + lookup_analog_binds((PadData::AnalogIndex)bind_meta.pad_idx, bind_meta.for_analog_minimum); + const auto current_button_bind = find_button_bind_from_sdl_idx(sdl_idx, modifiers); + if (analog_axii.find(sdl_idx) != analog_axii.end()) { + const auto existing_binds = analog_axii.at(sdl_idx); + analog_axii[sdl_idx] = {InputBinding((PadData::AnalogIndex)bind_meta.pad_idx, + bind_meta.for_analog_minimum, modifiers)}; + // there already a bind, so swap (as long as it's not the same key) + if (!current_analog_binds.empty() && (u32)current_analog_binds.front().sdl_idx != sdl_idx) { + analog_axii[current_analog_binds.front().sdl_idx] = existing_binds; + } + } else if (!current_analog_binds.empty() && current_button_bind.has_value()) { + analog_axii[sdl_idx] = {InputBinding((PadData::AnalogIndex)bind_meta.pad_idx, + bind_meta.for_analog_minimum, modifiers)}; + if (current_button_bind->second) { + // button_axii + button_axii.erase(sdl_idx); + button_axii[current_analog_binds.front().sdl_idx] = {current_button_bind->first}; + } else { + // button + buttons.erase(sdl_idx); + buttons[current_analog_binds.front().sdl_idx] = {current_button_bind->first}; + } + } else { + analog_axii[sdl_idx] = {InputBinding((PadData::AnalogIndex)bind_meta.pad_idx, + bind_meta.for_analog_minimum, modifiers)}; + } + remove_multiple_binds(sdl_idx, bind_meta, analog_axii); // Invalidate lookup cache m_analog_lookup.clear(); + m_button_lookup.clear(); bind_meta.assigned = true; } +std::optional> InputBindingGroups::find_analog_bind_from_sdl_idx( + u32 sdl_idx, + const std::optional modifiers) { + // We need to check that there isn't a shared SDL key on the button group side + for (const auto& [sdl_code, binds] : analog_axii) { + if (sdl_code == sdl_idx) { + for (const auto bind : binds) { + if (!modifiers || bind.modifiers == modifiers.value()) { + std::pair result = {bind, false}; + return result; + } + } + } + } + return {}; +} + void InputBindingGroups::assign_button_bind(u32 sdl_idx, InputBindAssignmentMeta& bind_meta, const bool analog_button, const std::optional modifiers) { // Find out if the PS2 input is already bound, if it is we will do a swap so no input is ever left // unmapped - const auto current_binds = lookup_button_binds((PadData::ButtonIndex)bind_meta.pad_idx); + const auto current_button_binds = lookup_button_binds((PadData::ButtonIndex)bind_meta.pad_idx); + const auto current_analog_bind = find_analog_bind_from_sdl_idx(sdl_idx, modifiers); if (buttons.find(sdl_idx) != buttons.end()) { const auto existing_binds = buttons.at(sdl_idx); if (analog_button) { @@ -254,13 +317,21 @@ void InputBindingGroups::assign_button_bind(u32 sdl_idx, buttons[sdl_idx] = {InputBinding((PadData::ButtonIndex)bind_meta.pad_idx, modifiers)}; } // there already a bind, so swap (as long as it's not the same key) - if (!current_binds.empty() && current_binds.front().sdl_idx != (s32)sdl_idx) { - if (current_binds.front().analog_button) { - button_axii[current_binds.front().sdl_idx] = existing_binds; + if (!current_button_binds.empty() && current_button_binds.front().sdl_idx != (s32)sdl_idx) { + if (current_button_binds.front().analog_button) { + button_axii[current_button_binds.front().sdl_idx] = existing_binds; } else { - buttons[current_binds.front().sdl_idx] = existing_binds; + buttons[current_button_binds.front().sdl_idx] = existing_binds; } } + } else if (!current_button_binds.empty() && current_analog_bind.has_value()) { + if (analog_button) { + button_axii[sdl_idx] = {InputBinding((PadData::ButtonIndex)bind_meta.pad_idx, modifiers)}; + } else { + buttons[sdl_idx] = {InputBinding((PadData::ButtonIndex)bind_meta.pad_idx, modifiers)}; + } + analog_axii.erase(sdl_idx); + analog_axii[current_button_binds.front().sdl_idx] = {current_analog_bind->first}; } else { if (analog_button) { button_axii[sdl_idx] = {InputBinding((PadData::ButtonIndex)bind_meta.pad_idx, modifiers)}; @@ -268,47 +339,11 @@ void InputBindingGroups::assign_button_bind(u32 sdl_idx, buttons[sdl_idx] = {InputBinding((PadData::ButtonIndex)bind_meta.pad_idx, modifiers)}; } } - // The underlying data structures support multiple binds for the same input, but the UX doesn't - // so we have to wipe out any shared bindings after an assignment - for (auto it = buttons.begin(); it != buttons.end();) { - if (it->first == sdl_idx) { - it++; - continue; - } - bool found_match = false; - for (const auto& bind : it->second) { - if (bind.pad_data_index != bind_meta.pad_idx) { - continue; - } - it = buttons.erase(it); - found_match = true; - break; - } - if (!found_match) { - it++; - } - } - for (auto it = button_axii.begin(); it != button_axii.end();) { - if (it->first == sdl_idx) { - it++; - continue; - } - bool found_match = false; - for (const auto& bind : it->second) { - if (bind.pad_data_index != bind_meta.pad_idx) { - continue; - } - it = button_axii.erase(it); - found_match = true; - break; - } - if (!found_match) { - it++; - } - } - + remove_multiple_binds(sdl_idx, bind_meta, buttons); + remove_multiple_binds(sdl_idx, bind_meta, button_axii); // Invalidate lookup cache m_button_lookup.clear(); + m_analog_lookup.clear(); bind_meta.assigned = true; } diff --git a/game/system/hid/input_bindings.h b/game/system/hid/input_bindings.h index 1728fb0477..20650849e3 100644 --- a/game/system/hid/input_bindings.h +++ b/game/system/hid/input_bindings.h @@ -155,6 +155,14 @@ struct InputModifiers { bool need_alt = false; bool has_necessary_modifiers(const u16 key_modifiers) const; + + bool operator==(const InputModifiers& other) const { + if (need_shift == other.need_shift && need_ctrl == other.need_ctrl && + need_meta == other.need_meta && need_alt == other.need_alt) { + return true; + } + return false; + } }; void to_json(json& j, const InputModifiers& obj); @@ -287,6 +295,17 @@ struct InputBindingGroups { // correspond with the PS2 value. Such as when remapping a key so you can unbind overlapping binds std::unordered_map, hash_name> m_analog_lookup; std::unordered_map, hash_name> m_button_lookup; + // The underlying data structures support multiple binds for the same input, but the UX doesn't + // so we have to wipe out any shared bindings after an assignment + void remove_multiple_binds(u32 sdl_idx, + InputBindAssignmentMeta& bind_meta, + std::unordered_map>& bind_map); + std::optional> find_button_bind_from_sdl_idx( + u32 sdl_idx, + const std::optional modifiers); + std::optional> find_analog_bind_from_sdl_idx( + u32 sdl_idx, + const std::optional modifiers); }; void to_json(json& j, const InputBindingGroups& obj); @@ -306,6 +325,16 @@ extern const InputBindingGroups DEFAULT_MOUSE_BINDS; /// an arbitrary lambda (with no return value) when triggered. /// /// An example of these would be taking a screenshot or save-state actions +// TODO - there is currently a bad UX if commands overlap with user bindings. For example if "F2" +// is for screenshots and the user binds that to "X" it will work, but you're going to take a +// screenshot everytime you jump. +// +// We probably don't want that but fundamentally this is a problem because the commands are +// hard-coded and not customizable so even if we prevented such binds -- there would not be a good +// user-facing reason why the bind failed to take. +// +// So there are some potential solutions but this doesn't feel high priority and this was always an +// issue. struct CommandBinding { enum Source { CONTROLLER, KEYBOARD, MOUSE }; diff --git a/game/system/hid/input_manager.cpp b/game/system/hid/input_manager.cpp index 5d5ba3caa4..9e30c8f693 100644 --- a/game/system/hid/input_manager.cpp +++ b/game/system/hid/input_manager.cpp @@ -124,6 +124,8 @@ void InputManager::hide_cursor(const bool hide_cursor) { if (hide_cursor == m_mouse_currently_hidden) { return; } + // NOTE - seems like an SDL bug, but the cursor will be visible / locked to the center of the + // screen if you use the 'start menu' to exit the window / return to it (atleast in windowed mode) auto ok = SDL_ShowCursor(hide_cursor ? SDL_DISABLE : SDL_ENABLE); if (ok < 0) { sdl_util::log_error("Unable to show/hide mouse cursor"); @@ -132,9 +134,7 @@ void InputManager::hide_cursor(const bool hide_cursor) { } } -void InputManager::process_sdl_event(const SDL_Event& event, - const bool ignore_mouse, - const bool ignore_kb) { +void InputManager::process_sdl_event(const SDL_Event& event) { // Detect controller connections and disconnects if (sdl_util::is_any_event_type(event.type, {SDL_CONTROLLERDEVICEADDED, SDL_CONTROLLERDEVICEREMOVED})) { @@ -142,18 +142,11 @@ void InputManager::process_sdl_event(const SDL_Event& event, refresh_device_list(); } - if (!m_ignored_device_last_frame && (ignore_mouse || ignore_kb)) { - clear_inputs(); - m_ignored_device_last_frame = true; - } else if (m_ignored_device_last_frame && !ignore_mouse && !ignore_kb) { - m_ignored_device_last_frame = false; - } - if (m_data.find(m_keyboard_and_mouse_port) != m_data.end()) { m_keyboard.process_event(event, m_command_binds, m_data.at(m_keyboard_and_mouse_port), - m_waiting_for_bind, ignore_kb || !m_keyboard_enabled); + m_waiting_for_bind); m_mouse.process_event(event, m_command_binds, m_data.at(m_keyboard_and_mouse_port), - m_waiting_for_bind, ignore_mouse || !m_mouse_enabled); + m_waiting_for_bind); } // Send event to active controller device @@ -168,11 +161,15 @@ void InputManager::process_sdl_event(const SDL_Event& event, // Clear the binding assignment if we got one if (m_waiting_for_bind && m_waiting_for_bind->assigned) { stop_waiting_for_bind(); + // NOTE - this is a total hack, but it's to prevent immediately re-assigning the "confirmation" + // bind if you use a source that is polled + // TODO: There's a correct way to do this....figure it out eventually + m_skip_polling_for_n_frames = 60; } // Adjust mouse cursor visibility if (m_auto_hide_mouse) { - if (event.type == SDL_MOUSEMOTION) { + if (event.type == SDL_MOUSEMOTION && !m_mouse.is_camera_being_controlled()) { hide_cursor(false); } else if (event.type == SDL_KEYDOWN || event.type == SDL_CONTROLLERBUTTONDOWN) { hide_cursor(true); @@ -180,6 +177,45 @@ void InputManager::process_sdl_event(const SDL_Event& event, } } +void InputManager::poll_keyboard_data() { + if (m_keyboard_enabled && m_skip_polling_for_n_frames <= 0 && !m_waiting_for_bind) { + if (m_data.find(m_keyboard_and_mouse_port) != m_data.end()) { + m_keyboard.poll_state(m_data.at(m_keyboard_and_mouse_port)); + } + } +} + +void InputManager::clear_keyboard_actions() { + if (m_keyboard_enabled) { + if (m_data.find(m_keyboard_and_mouse_port) != m_data.end()) { + m_keyboard.clear_actions(m_data.at(m_keyboard_and_mouse_port)); + } + } +} + +void InputManager::poll_mouse_data() { + if (m_mouse_enabled && m_skip_polling_for_n_frames <= 0 && !m_waiting_for_bind) { + if (m_data.find(m_keyboard_and_mouse_port) != m_data.end()) { + m_mouse.poll_state(m_data.at(m_keyboard_and_mouse_port)); + } + } +} + +void InputManager::clear_mouse_actions() { + if (m_mouse_enabled && !m_waiting_for_bind) { + if (m_data.find(m_keyboard_and_mouse_port) != m_data.end()) { + m_mouse.clear_actions(m_data.at(m_keyboard_and_mouse_port)); + } + } +} + +void InputManager::finish_polling() { + m_skip_polling_for_n_frames--; + if (m_skip_polling_for_n_frames < 0) { + m_skip_polling_for_n_frames = 0; + } +} + void InputManager::process_ee_events() { const std::lock_guard lock(m_event_queue_mtx); // Fully process any events from the EE diff --git a/game/system/hid/input_manager.h b/game/system/hid/input_manager.h index f96df9c461..5a7d4bf6d3 100644 --- a/game/system/hid/input_manager.h +++ b/game/system/hid/input_manager.h @@ -46,7 +46,13 @@ class InputManager { ~InputManager(); // Propagate and handle the SDL event, ignored it if it's not relevant - void process_sdl_event(const SDL_Event& event, const bool ignore_mouse, const bool ignore_kb); + void process_sdl_event(const SDL_Event& event); + void poll_keyboard_data(); + void clear_keyboard_actions(); + void poll_mouse_data(); + void clear_mouse_actions(); + // Any cleanup that should happen after polling has completed for this frame + void finish_polling(); /// Any event coming from the EE thread that interacts directly with SDL should be enqueued as an /// event so it can be ran from the proper thread context (the graphics thread) void process_ee_events(); @@ -92,6 +98,8 @@ class InputManager { void stop_waiting_for_bind() { m_waiting_for_bind = std::nullopt; } void set_camera_sens(const float xsens, const float ysens); void reset_input_bindings_to_defaults(const int port, const InputDeviceType device_type); + bool auto_hiding_cursor() { return m_auto_hide_mouse || m_mouse.is_camera_being_controlled(); } + void hide_cursor(const bool hide_cursor); private: std::mutex m_event_queue_mtx; @@ -119,10 +127,9 @@ class InputManager { /// Collection of arbitrary commands to run on user actions CommandBindingGroups m_command_binds; - bool m_ignored_device_last_frame = false; - bool m_keyboard_enabled = true; bool m_mouse_enabled = false; + int m_skip_polling_for_n_frames = 0; bool m_auto_hide_mouse = true; bool m_mouse_currently_hidden = false; bool m_ignore_background_controller_events = false; @@ -134,7 +141,6 @@ class InputManager { std::optional m_waiting_for_bind = std::nullopt; void refresh_device_list(); - void hide_cursor(const bool hide_cursor); void clear_inputs(); void ignore_background_controller_events(const bool ignore);