input: Hopefully make keyboard/mouse handling more consistent (#2807)

The current event-based approach is very difficult to get right, and it
depends on no events ever being missed. This changes the keyboard/mouse
handling code to a polling-based approach.

Other fixes:
- an issue where modifier keys were not able to be successfully bound
(like Left Shift to `X`)
- improves cursor hiding (except when you use the start menu, this seems
like an SDL issue, see comment)
- Better discarding of kb/mouse inputs when imgui intercepts input
- properly swap bindings when an already set key is assigned, even if it
crosses the distinction of an analog vs normal button

Fixes #2800
This commit is contained in:
Tyler Wilding 2023-07-08 09:45:56 -05:00 committed by GitHub
parent c9330a6951
commit 6faa7530f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 462 additions and 206 deletions

View File

@ -42,7 +42,7 @@
#include <crt_externs.h>
#include <limits.h>
#include <mach-o/dyld.h>
#include "mach-o/dyld.h"
#endif
namespace file_util {

View File

@ -316,7 +316,7 @@ GLDisplay::GLDisplay(SDL_Window* window, SDL_GLContext gl_context, bool is_main)
m_display_manager(std::make_shared<DisplayManager>(window)),
m_input_manager(std::make_shared<InputManager>()) {
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

View File

@ -66,8 +66,7 @@ GameController::GameController(int sdl_device_id,
void GameController::process_event(const SDL_Event& event,
const CommandBindingGroups& commands,
std::shared_ptr<PadData> data,
std::optional<InputBindAssignmentMeta>& bind_assignment,
bool ignore_inputs) {
std::optional<InputBindAssignmentMeta>& 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 ||

View File

@ -11,8 +11,7 @@ class GameController : public InputDevice {
void process_event(const SDL_Event& event,
const CommandBindingGroups& commands,
std::shared_ptr<PadData> data,
std::optional<InputBindAssignmentMeta>& bind_assignment,
bool ignore_inputs = false) override;
std::optional<InputBindAssignmentMeta>& 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; }

View File

@ -19,8 +19,7 @@ class InputDevice {
virtual void process_event(const SDL_Event& event,
const CommandBindingGroups& commands,
std::shared_ptr<PadData> data,
std::optional<InputBindAssignmentMeta>& bind_assignment,
bool ignore_inputs = false) = 0;
std::optional<InputBindAssignmentMeta>& bind_assignment) = 0;
virtual void close_device() = 0;
bool is_loaded() const { return m_loaded; };
};

View File

@ -6,19 +6,95 @@ KeyboardDevice::KeyboardDevice(std::shared_ptr<game_settings::InputSettings> 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<PadData> 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<PadData> 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<PadData> 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<PadData> 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<PadData> 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<PadData> data,
std::optional<InputBindAssignmentMeta>& bind_assignment,
bool ignore_inputs) {
if (ignore_inputs) {
return;
}
std::optional<InputBindAssignmentMeta>& 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()) {

View File

@ -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<void(std::shared_ptr<PadData>, InputBinding bind)> revert_action;
};
class KeyboardDevice : public InputDevice {
public:
KeyboardDevice(){};
KeyboardDevice(std::shared_ptr<game_settings::InputSettings> settings);
~KeyboardDevice() {}
// Polls the entire state of the keyboard to set the PadData
void poll_state(std::shared_ptr<PadData> data);
void clear_actions(std::shared_ptr<PadData> data);
void process_event(const SDL_Event& event,
const CommandBindingGroups& commands,
std::shared_ptr<PadData> data,
std::optional<InputBindAssignmentMeta>& bind_assignment,
bool ignore_inputs = false) override;
std::optional<InputBindAssignmentMeta>& bind_assignment) override;
void close_device() override{
// there is nothing to close
};
private:
std::vector<ActiveKeyboardAction> 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<u32> m_ignore_key_on_keyup;
bool is_action_already_active(const u32 sdl_keycode);
};

View File

@ -7,20 +7,109 @@ MouseDevice::MouseDevice(std::shared_ptr<game_settings::InputSettings> 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<PadData> 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<PadData> 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<PadData> 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<PadData> 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<PadData> 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<PadData> data,
std::optional<InputBindAssignmentMeta>& bind_assignment,
bool ignore_inputs) {
std::optional<InputBindAssignmentMeta>& 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()) {

View File

@ -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<void(std::shared_ptr<PadData>, InputBinding bind)> revert_action;
};
class MouseDevice : public InputDevice {
public:
struct MouseButtonStatus {
@ -16,11 +25,12 @@ class MouseDevice : public InputDevice {
MouseDevice(std::shared_ptr<game_settings::InputSettings> settings);
~MouseDevice() {}
void poll_state(std::shared_ptr<PadData> data);
void clear_actions(std::shared_ptr<PadData> data);
void process_event(const SDL_Event& event,
const CommandBindingGroups& commands,
std::shared_ptr<PadData> data,
std::optional<InputBindAssignmentMeta>& bind_assignment,
bool ignore_inputs = false) override;
std::optional<InputBindAssignmentMeta>& bind_assignment) override;
void close_device() override{
// there is nothing to close
};
@ -31,15 +41,20 @@ class MouseDevice : public InputDevice {
std::pair<int, int> 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<ActiveMouseAction> 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);
};

View File

@ -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

View File

@ -8,6 +8,7 @@
#include <variant>
#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<InputManager> input_manager) {
m_input_manager = input_manager;
}
private:
SDL_Window* m_window;
game_settings::DisplaySettings m_display_settings;
std::optional<std::shared_ptr<InputManager>> m_input_manager;
std::mutex event_queue_mtx;
std::queue<EEDisplayEvent> ee_event_queue;

View File

@ -193,28 +193,11 @@ std::vector<InputBindingInfo> InputBindingGroups::lookup_button_binds(PadData::B
return entry;
}
void InputBindingGroups::assign_analog_bind(u32 sdl_idx,
InputBindAssignmentMeta& bind_meta,
const std::optional<InputModifiers> 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<u32, std::vector<InputBinding>>& 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<std::pair<InputBinding, bool>> InputBindingGroups::find_button_bind_from_sdl_idx(
u32 sdl_idx,
const std::optional<InputModifiers> 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<InputBinding, bool> 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<InputBinding, bool> result = {bind, true};
return result;
}
}
}
}
return {};
}
void InputBindingGroups::assign_analog_bind(u32 sdl_idx,
InputBindAssignmentMeta& bind_meta,
const std::optional<InputModifiers> 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<std::pair<InputBinding, bool>> InputBindingGroups::find_analog_bind_from_sdl_idx(
u32 sdl_idx,
const std::optional<InputModifiers> 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<InputBinding, bool> result = {bind, false};
return result;
}
}
}
}
return {};
}
void InputBindingGroups::assign_button_bind(u32 sdl_idx,
InputBindAssignmentMeta& bind_meta,
const bool analog_button,
const std::optional<InputModifiers> 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;
}

View File

@ -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<BindCacheKey, std::vector<InputBindingInfo>, hash_name> m_analog_lookup;
std::unordered_map<BindCacheKey, std::vector<InputBindingInfo>, 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<u32, std::vector<InputBinding>>& bind_map);
std::optional<std::pair<InputBinding, bool>> find_button_bind_from_sdl_idx(
u32 sdl_idx,
const std::optional<InputModifiers> modifiers);
std::optional<std::pair<InputBinding, bool>> find_analog_bind_from_sdl_idx(
u32 sdl_idx,
const std::optional<InputModifiers> 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 };

View File

@ -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<std::mutex> lock(m_event_queue_mtx);
// Fully process any events from the EE

View File

@ -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<InputBindAssignmentMeta> 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);