#pragma once #include #include #include #include #include #include #include "common/common_types.h" #include "common/log/log.h" #include "common/util/json_util.h" #include "SDL3/SDL.h" #define GET_PRESSURE_BUTTON_DATA(button_name) \ {button_data.at(ButtonIndex::button_name), \ pressure_data.at(PressureIndex::button_name##_PRESSURE)}; /// A simple abstraction around the PS2 controller data with some convenience functions for /// pulling specific data out if it's useful. struct PadData { enum AnalogIndex { LEFT_X = 0, LEFT_Y, RIGHT_X, RIGHT_Y = 3 }; enum ButtonIndex { SELECT = 0, L3, R3, START, DPAD_UP, DPAD_RIGHT, DPAD_DOWN, DPAD_LEFT, L2, R2, L1, R1, TRIANGLE, CIRCLE, CROSS, SQUARE = 15 }; enum PressureIndex { INVALID_PRESSURE = -1, DPAD_RIGHT_PRESSURE = 0, DPAD_LEFT_PRESSURE, DPAD_UP_PRESSURE, DPAD_DOWN_PRESSURE, TRIANGLE_PRESSURE, CIRCLE_PRESSURE, CROSS_PRESSURE, SQUARE_PRESSURE, L1_PRESSURE, R1_PRESSURE, L2_PRESSURE, R2_PRESSURE = 11 }; static const int ANALOG_NEUTRAL = 127; // NOTE - store analog values as larger signed integers and then clamp them to their 0-255 // u8 range. This is to make it easier to properly handle multiple non-analog sources attempting // to simulate analog sticks // // Imagine you have 100 keys bound to moving forward and back, and all those key up / key down // events are encountered asynchrously, only when their state changes // // There are a lot of strategies to handle this: // - you can delay applying the input by one frame, so you have a wholistic viewpoint. But this // delay is undesirable and doesn't work elegantly with multiple input sources // - you can have a bunch of complicated state overseeing it all // - or (with this approach) you can process the events asynchronously, adding and subtracting // from the aggregate amount and organically end up with the correct value // // For a tangible example, imagine you have `W` bound to move forward, as well as left click // - subtract `127` when `W` is pressed = 0 (127 is neutral) // - subtract `127` when `Mouse1` is pressed = -127 (clamp to 0 for the game) // - add `127` when `W` is released = 0 // - add `127` when `Mouse1` is released = 127 (back to neutral) // // Because keys that are pressed, must eventually be released -- this has a fairly strong // guarantee. About the only hole is if you decided to unplug your keyboard while holding a key, // which is something we can distinctly detect and handle with a decent compromise (reset the // inputs) std::array analog_data = {ANALOG_NEUTRAL, ANALOG_NEUTRAL, ANALOG_NEUTRAL, ANALOG_NEUTRAL}; std::pair analog_left() const { return {std::clamp(analog_data.at(AnalogIndex::LEFT_X), 0, 255), std::clamp(analog_data.at(AnalogIndex::LEFT_Y), 0, 255)}; } std::pair analog_right() const { return {std::clamp(analog_data.at(AnalogIndex::RIGHT_X), 0, 255), std::clamp(analog_data.at(AnalogIndex::RIGHT_Y), 0, 255)}; } std::array button_data = {false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false}; std::array pressure_data = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // Normal Buttons bool select() const { return button_data.at(static_cast(ButtonIndex::SELECT)); }; bool l3() const { return button_data.at(static_cast(ButtonIndex::L3)); }; bool r3() const { return button_data.at(static_cast(ButtonIndex::R3)); }; bool start() const { return button_data.at(static_cast(ButtonIndex::START)); }; // Pressure Buttons std::pair dpad_up() const { return GET_PRESSURE_BUTTON_DATA(DPAD_UP); }; std::pair dpad_right() const { return GET_PRESSURE_BUTTON_DATA(DPAD_RIGHT); }; std::pair dpad_down() const { return GET_PRESSURE_BUTTON_DATA(DPAD_DOWN); }; std::pair dpad_left() const { return GET_PRESSURE_BUTTON_DATA(DPAD_LEFT); }; std::pair l2() const { return GET_PRESSURE_BUTTON_DATA(L2); }; std::pair r2() const { return GET_PRESSURE_BUTTON_DATA(R2); }; std::pair l1() const { return GET_PRESSURE_BUTTON_DATA(L1); }; std::pair r1() const { return GET_PRESSURE_BUTTON_DATA(R1); }; std::pair triangle() const { return GET_PRESSURE_BUTTON_DATA(TRIANGLE); }; std::pair circle() const { return GET_PRESSURE_BUTTON_DATA(CIRCLE); }; std::pair cross() const { return GET_PRESSURE_BUTTON_DATA(CROSS); }; std::pair square() const { return GET_PRESSURE_BUTTON_DATA(SQUARE); }; PressureIndex button_index_to_pressure_index(ButtonIndex button_index) const { switch (button_index) { case ButtonIndex::DPAD_RIGHT: return PressureIndex::DPAD_RIGHT_PRESSURE; case ButtonIndex::DPAD_LEFT: return PressureIndex::DPAD_LEFT_PRESSURE; case ButtonIndex::DPAD_UP: return PressureIndex::DPAD_UP_PRESSURE; case ButtonIndex::DPAD_DOWN: return PressureIndex::DPAD_DOWN_PRESSURE; case ButtonIndex::TRIANGLE: return PressureIndex::TRIANGLE_PRESSURE; case ButtonIndex::CIRCLE: return PressureIndex::CIRCLE_PRESSURE; case ButtonIndex::CROSS: return PressureIndex::CROSS_PRESSURE; case ButtonIndex::SQUARE: return PressureIndex::SQUARE_PRESSURE; case ButtonIndex::L1: return PressureIndex::L1_PRESSURE; case ButtonIndex::R1: return PressureIndex::R1_PRESSURE; case ButtonIndex::L2: return PressureIndex::L2_PRESSURE; case ButtonIndex::R2: return PressureIndex::R2_PRESSURE; default: return PressureIndex::INVALID_PRESSURE; } } // Analog Simulation Tracking // There exists a flaw with the described analog tracking approach described above, and that has // to do with imprecise analog drift / fluctuating numbers. // // If you switch between a keyboard and a controller, sometimes the controllers analog stick can // oscillate between values triggering frivolous events. This impacts the usage of the // alternative input device such as the keyboard or mouse // // To solve this, we keep track of if the user is actively using the keyboard or mouse to // manipulate the analog values and if they are, we can ignore controller analog inputs. Once all // inputs are released the controller input can resume. private: int analog_sim_tracker = 0; public: bool analogs_being_simulated() { return analog_sim_tracker > 0; } void update_analog_sim_tracker(bool released) { if (released) { analog_sim_tracker--; } else { analog_sim_tracker++; } if (analog_sim_tracker < 0) { analog_sim_tracker = 0; } } void clear() { for (auto& x : button_data) { x = 0; } clear_analogs(); } void clear_analogs() { for (auto& x : analog_data) { x = ANALOG_NEUTRAL; } analog_sim_tracker = 0; } }; // https://wiki.libsdl.org/SDL3/SDL_Keymod struct InputModifiers { InputModifiers() = default; InputModifiers(const u16 sdl_mod_state); bool need_shift = false; bool need_ctrl = false; bool need_meta = false; // aka GUI / windows key 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); void from_json(const json& j, InputModifiers& obj); /// Contains all information needed when processing a host input (ie. from SDL) /// For example -- for a keyboard binding it informs us what modifiers need to be hit at the same /// time, etc /// /// All bindings _must_ provide the PS2 button/analog index they map to /// /// There is also a special case for binary inputs mapping to the analog sticks. In such a /// situation both keys will be modifying the same underlying value, but one will mutate the value /// to the minimum of the range and the other to the maximum. The key intended to map to the /// minimum should specify `true` for `minimum_in_range`. /// /// For example, pressing both W and S should result in Jak not moving. And then letting go of W /// should make him move towards the camera. struct InputBinding { InputBinding() = default; InputBinding(int index) : pad_data_index(index) {}; InputBinding(int index, const std::optional _modifiers) : pad_data_index(index) { if (_modifiers) { modifiers = _modifiers.value(); } }; InputBinding(int index, bool _minimum_in_range) : pad_data_index(index), minimum_in_range(_minimum_in_range) {}; InputBinding(int index, bool _minimum_in_range, const std::optional _modifiers) : pad_data_index(index), minimum_in_range(_minimum_in_range) { if (_modifiers) { modifiers = _modifiers.value(); } }; /// Corresponds to PadData::AnalogIndex or PadData::ButtonIndex int pad_data_index; /// If considered pressed, it will invert the value (ie, left/right on an analog stick) bool minimum_in_range = false; InputModifiers modifiers; }; void to_json(json& j, const InputBinding& obj); void from_json(const json& j, InputBinding& obj); enum InputDeviceType { CONTROLLER = 0, KEYBOARD = 1, MOUSE = 2 }; struct InputBindingInfo { s32 sdl_idx; u32 pad_idx; std::string host_name; bool analog_button; InputModifiers modifiers; InputBindingInfo() = default; InputBindingInfo(const InputBinding bind, const InputDeviceType device_type, const s32 sdl_code, const bool analog_button); }; // Contains all info related to the current binding we are waiting for // so the relevant device can successfully apply it struct InputBindAssignmentMeta { InputDeviceType device_type = InputDeviceType::CONTROLLER; int pad_idx; bool for_analog = false; bool for_analog_minimum = false; // For only some rebindings, we have to know what keys were originally bound to the // confirmation buttons this is because the user has to hit said key to initiate waiting for the // new assignment // // For most input sources this doesn't matter because we listen for the DOWN event, but in order // to allow modifiers as binds (ie. Shift for X) we have to listen to UP events as well (only for // the modifiers). This is also relevant for analog rebinds as the transition from fully pressed // to unpressed triggers another press. // // TLDR - we ignore the first UP event if it was bound to a confirmation key. Additionally, this // depends on the game as Jak 1 treats X or O as a confirm key... std::vector keyboard_confirmation_binds = {}; bool seen_keyboard_confirm_up = false; std::vector controller_confirmation_binds = {}; bool seen_controller_confirm_neutral = false; // Indicates the binding has been received, assigned, and we can proceed. bool assigned = false; }; struct InputBindingGroups { InputBindingGroups() = default; InputBindingGroups(const InputDeviceType _device_type, std::unordered_map> _analog_axii, std::unordered_map> _button_axii, std::unordered_map> _buttons) : device_type(_device_type), analog_axii(_analog_axii), button_axii(_button_axii), buttons(_buttons) {}; // TODO - make these private InputDeviceType device_type; std::unordered_map> analog_axii; std::unordered_map> button_axii; std::unordered_map> buttons; std::vector lookup_analog_binds(PadData::AnalogIndex idx, bool only_minimum_binds = false); std::vector lookup_button_binds(PadData::ButtonIndex idx); void assign_analog_bind(u32 sdl_idx, InputBindAssignmentMeta& bind_meta, const std::optional modifiers = {}); void assign_button_bind(u32 sdl_idx, InputBindAssignmentMeta& bind_meta, const bool analog_button = false, const std::optional modifiers = {}); void set_bindings(const InputBindingGroups& binds); private: typedef std::pair BindCacheKey; struct hash_name { size_t operator()(const BindCacheKey& key) const { return std::hash()(key.first) ^ std::hash()(key.second); } }; // These are caches for reverse-lookups (from the mapped bind instead of the host bind) // for reading inputs we keep things fast -- we start with an SDL host value and map it to the // required binds // // However there are some situations where we want to the reverse -- find out what binds // 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); void from_json(const json& j, InputBindingGroups& obj); /// https://wiki.libsdl.org/SDL3/SDL_GamepadButton extern const InputBindingGroups DEFAULT_CONTROLLER_BINDS; /// https://wiki.libsdl.org/SDL3/SDL_Keycode extern const InputBindingGroups DEFAULT_KEYBOARD_BINDS; /// https://wiki.libsdl.org/SDL3/SDL_MouseButtonEvent extern const InputBindingGroups DEFAULT_MOUSE_BINDS; /// A CommandBinding by contrast is a way to map some arbitrary runtime command to /// a user initiated input. /// /// These are not used to mutate the state of a PadData object and instead run /// 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 class Source { CONTROLLER, KEYBOARD, MOUSE }; u32 host_key; InputModifiers modifiers; // Three types of callbacks: one with SDL_Event, one without, and one with input modifiers std::function command = nullptr; std::function event_command = nullptr; CommandBinding(u32 _host_key, std::function _command) : host_key(_host_key), command(std::move(_command)) {} CommandBinding(u32 _host_key, std::function _command) : host_key(_host_key), event_command(std::move(_command)) {} CommandBinding(u32 _host_key, const InputModifiers& _modifiers, std::function _command) : host_key(_host_key), modifiers(_modifiers), event_command(std::move(_command)) {} }; struct CommandBindingGroups { std::unordered_map> controller_binds; std::unordered_map> keyboard_binds; std::unordered_map> mouse_binds; };