mirror of
https://github.com/open-goal/jak-project
synced 2026-06-07 03:58:11 -04:00
653604080f
This should solve the issue where buttons are held down until you push them the same time. This would cause the cars in jak 3 to not move until you pushed square/circle and "released" the brake. Co-authored-by: water111 <awaterford1111445@gmail.com>
420 lines
16 KiB
C++
420 lines
16 KiB
C++
#pragma once
|
|
|
|
#include <array>
|
|
#include <functional>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
|
|
#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<int, 4> analog_data = {ANALOG_NEUTRAL, ANALOG_NEUTRAL, ANALOG_NEUTRAL, ANALOG_NEUTRAL};
|
|
|
|
std::pair<u8, u8> 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<u8, u8> 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<bool, 16> button_data = {false, false, false, false, false, false, false, false,
|
|
false, false, false, false, false, false, false, false};
|
|
std::array<u8, 12> 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<int>(ButtonIndex::SELECT)); };
|
|
bool l3() const { return button_data.at(static_cast<int>(ButtonIndex::L3)); };
|
|
bool r3() const { return button_data.at(static_cast<int>(ButtonIndex::R3)); };
|
|
bool start() const { return button_data.at(static_cast<int>(ButtonIndex::START)); };
|
|
|
|
// Pressure Buttons
|
|
std::pair<bool, u8> dpad_up() const { return GET_PRESSURE_BUTTON_DATA(DPAD_UP); };
|
|
std::pair<bool, u8> dpad_right() const { return GET_PRESSURE_BUTTON_DATA(DPAD_RIGHT); };
|
|
std::pair<bool, u8> dpad_down() const { return GET_PRESSURE_BUTTON_DATA(DPAD_DOWN); };
|
|
std::pair<bool, u8> dpad_left() const { return GET_PRESSURE_BUTTON_DATA(DPAD_LEFT); };
|
|
|
|
std::pair<bool, u8> l2() const { return GET_PRESSURE_BUTTON_DATA(L2); };
|
|
std::pair<bool, u8> r2() const { return GET_PRESSURE_BUTTON_DATA(R2); };
|
|
std::pair<bool, u8> l1() const { return GET_PRESSURE_BUTTON_DATA(L1); };
|
|
std::pair<bool, u8> r1() const { return GET_PRESSURE_BUTTON_DATA(R1); };
|
|
|
|
std::pair<bool, u8> triangle() const { return GET_PRESSURE_BUTTON_DATA(TRIANGLE); };
|
|
std::pair<bool, u8> circle() const { return GET_PRESSURE_BUTTON_DATA(CIRCLE); };
|
|
std::pair<bool, u8> cross() const { return GET_PRESSURE_BUTTON_DATA(CROSS); };
|
|
std::pair<bool, u8> 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<InputModifiers> _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<InputModifiers> _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<InputBindingInfo> keyboard_confirmation_binds = {};
|
|
bool seen_keyboard_confirm_up = false;
|
|
std::vector<InputBindingInfo> 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<u32, std::vector<InputBinding>> _analog_axii,
|
|
std::unordered_map<u32, std::vector<InputBinding>> _button_axii,
|
|
std::unordered_map<u32, std::vector<InputBinding>> _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<u32, std::vector<InputBinding>> analog_axii;
|
|
std::unordered_map<u32, std::vector<InputBinding>> button_axii;
|
|
std::unordered_map<u32, std::vector<InputBinding>> buttons;
|
|
|
|
std::vector<InputBindingInfo> lookup_analog_binds(PadData::AnalogIndex idx,
|
|
bool only_minimum_binds = false);
|
|
std::vector<InputBindingInfo> lookup_button_binds(PadData::ButtonIndex idx);
|
|
|
|
void assign_analog_bind(u32 sdl_idx,
|
|
InputBindAssignmentMeta& bind_meta,
|
|
const std::optional<InputModifiers> modifiers = {});
|
|
void assign_button_bind(u32 sdl_idx,
|
|
InputBindAssignmentMeta& bind_meta,
|
|
const bool analog_button = false,
|
|
const std::optional<InputModifiers> modifiers = {});
|
|
|
|
void set_bindings(const InputBindingGroups& binds);
|
|
|
|
private:
|
|
typedef std::pair<int, bool> BindCacheKey;
|
|
|
|
struct hash_name {
|
|
size_t operator()(const BindCacheKey& key) const {
|
|
return std::hash<int>()(key.first) ^ std::hash<bool>()(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<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);
|
|
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 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<void()> command = nullptr;
|
|
std::function<void(const SDL_Event&)> event_command = nullptr;
|
|
|
|
CommandBinding(u32 _host_key, std::function<void()> _command)
|
|
: host_key(_host_key), command(std::move(_command)) {}
|
|
|
|
CommandBinding(u32 _host_key, std::function<void(const SDL_Event&)> _command)
|
|
: host_key(_host_key), event_command(std::move(_command)) {}
|
|
|
|
CommandBinding(u32 _host_key,
|
|
const InputModifiers& _modifiers,
|
|
std::function<void(const SDL_Event&)> _command)
|
|
: host_key(_host_key), modifiers(_modifiers), event_command(std::move(_command)) {}
|
|
};
|
|
|
|
struct CommandBindingGroups {
|
|
std::unordered_map<u32, std::vector<CommandBinding>> controller_binds;
|
|
std::unordered_map<u32, std::vector<CommandBinding>> keyboard_binds;
|
|
std::unordered_map<u32, std::vector<CommandBinding>> mouse_binds;
|
|
};
|