KBM Input Bug Fixes / Added Binds

Fixed input issues where some inputs would not bind when pressing (side mouse buttons, some symbols, etc). Also, fixed up code formatting in altered files (removed C-style casts and replaced with C++ <static_casts>, added a few macros and one member functions).

Update: Fixed file locations.
This commit is contained in:
nickci2002 2025-06-12 20:33:52 -04:00 committed by nickci2002
parent 226058d2e9
commit be6b25dbe9
4 changed files with 2295 additions and 0 deletions

718
input_handler.cpp Normal file
View File

@ -0,0 +1,718 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "input_handler.h"
#include <fstream>
#include <iostream>
#include <list>
#include <map>
#include <ranges>
#include <sstream>
#include <string>
#include <string_view>
#include <typeinfo>
#include <unordered_map>
#include <vector>
#include "SDL3/SDL_events.h"
#include "SDL3/SDL_timer.h"
#include "common/config.h"
#include "common/elf_info.h"
#include "common/io_file.h"
#include "common/path_util.h"
#include "input/controller.h"
#include "input/input_mouse.h"
namespace Input {
/*
Project structure:
n to m connection between inputs and outputs
Keyup and keydown events update a dynamic list* of u32 'flags' (what is currently in the list is
'pressed') On every event, after flag updates, we check for every input binding -> controller output
pair if all their flags are 'on' If not, disable; if so, enable them. For axes, we gather their data
into a struct cumulatively from all inputs, then after we checked all of those, we update them all
at once. Wheel inputs generate a timer that doesn't turn off their outputs automatically, but push a
userevent to do so.
What structs are needed?
InputBinding(key1, key2, key3)
ControllerOutput(button, axis) - we only need a const array of these, and one of the attr-s is
always 0 BindingConnection(inputBinding (member), controllerOutput (ref to the array element))
Things to always test before pushing like a dumbass:
Button outputs
Axis outputs
Input hierarchy
Multi key inputs
Mouse to joystick
Key toggle
Joystick halfmode
Don't be an idiot and test only the changed part expecting everything else to not be broken
*/
bool leftjoystick_halfmode = false, rightjoystick_halfmode = false;
std::pair<int, int> leftjoystick_deadzone, rightjoystick_deadzone, lefttrigger_deadzone,
righttrigger_deadzone;
std::list<std::pair<InputEvent, bool>> pressed_keys;
std::list<InputID> toggled_keys;
static std::vector<BindingConnection> connections;
auto output_array = std::array{
// Important: these have to be the first, or else they will update in the wrong order
ControllerOutput(LEFTJOYSTICK_HALFMODE),
ControllerOutput(RIGHTJOYSTICK_HALFMODE),
ControllerOutput(KEY_TOGGLE),
// Button mappings
ControllerOutput(SDL_GAMEPAD_BUTTON_NORTH), // Triangle
ControllerOutput(SDL_GAMEPAD_BUTTON_EAST), // Circle
ControllerOutput(SDL_GAMEPAD_BUTTON_SOUTH), // Cross
ControllerOutput(SDL_GAMEPAD_BUTTON_WEST), // Square
ControllerOutput(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER), // L1
ControllerOutput(SDL_GAMEPAD_BUTTON_LEFT_STICK), // L3
ControllerOutput(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER), // R1
ControllerOutput(SDL_GAMEPAD_BUTTON_RIGHT_STICK), // R3
ControllerOutput(SDL_GAMEPAD_BUTTON_START), // Options
ControllerOutput(SDL_GAMEPAD_BUTTON_TOUCHPAD), // TouchPad
ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_UP), // Up
ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_DOWN), // Down
ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_LEFT), // Left
ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_RIGHT), // Right
// Axis mappings
// ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTX, false),
// ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTY, false),
// ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTX, false),
// ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTY, false),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTX),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTY),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTX),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTY),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFT_TRIGGER),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_INVALID),
};
void ControllerOutput::LinkJoystickAxes() {
// for (int i = 17; i < 23; i += 2) {
// delete output_array[i].new_param;
// output_array[i].new_param = output_array[i + 1].new_param;
// }
}
static OrbisPadButtonDataOffset SDLGamepadToOrbisButton(u8 button) {
using OPBDO = OrbisPadButtonDataOffset;
switch (button) {
case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
return OPBDO::Down;
case SDL_GAMEPAD_BUTTON_DPAD_UP:
return OPBDO::Up;
case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
return OPBDO::Left;
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
return OPBDO::Right;
case SDL_GAMEPAD_BUTTON_SOUTH:
return OPBDO::Cross;
case SDL_GAMEPAD_BUTTON_NORTH:
return OPBDO::Triangle;
case SDL_GAMEPAD_BUTTON_WEST:
return OPBDO::Square;
case SDL_GAMEPAD_BUTTON_EAST:
return OPBDO::Circle;
case SDL_GAMEPAD_BUTTON_START:
return OPBDO::Options;
case SDL_GAMEPAD_BUTTON_TOUCHPAD:
return OPBDO::TouchPad;
case SDL_GAMEPAD_BUTTON_BACK:
return OPBDO::TouchPad;
case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
return OPBDO::L1;
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
return OPBDO::R1;
case SDL_GAMEPAD_BUTTON_LEFT_STICK:
return OPBDO::L3;
case SDL_GAMEPAD_BUTTON_RIGHT_STICK:
return OPBDO::R3;
default:
return OPBDO::None;
}
}
Axis GetAxisFromSDLAxis(u8 sdl_axis) {
switch (sdl_axis) {
case SDL_GAMEPAD_AXIS_LEFTX:
return Axis::LeftX;
case SDL_GAMEPAD_AXIS_LEFTY:
return Axis::LeftY;
case SDL_GAMEPAD_AXIS_RIGHTX:
return Axis::RightX;
case SDL_GAMEPAD_AXIS_RIGHTY:
return Axis::RightY;
case SDL_GAMEPAD_AXIS_LEFT_TRIGGER:
return Axis::TriggerLeft;
case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER:
return Axis::TriggerRight;
default:
return Axis::AxisMax;
}
}
// syntax: 'name, name,name' or 'name,name' or 'name'
InputBinding GetBindingFromString(std::string& line) {
std::array<InputID, 3> keys = {InputID(), InputID(), InputID()};
// Check and process tokens
for (const auto token : std::views::split(line, ',')) { // Split by comma
const std::string t(token.begin(), token.end());
InputID input;
if (string_to_keyboard_key_map.find(t) != string_to_keyboard_key_map.end()) {
input = InputID(InputType::KeyboardMouse, string_to_keyboard_key_map.at(t));
} else if (string_to_axis_map.find(t) != string_to_axis_map.end()) {
input = InputID(InputType::Axis, string_to_axis_map.at(t).axis);
} else if (string_to_cbutton_map.find(t) != string_to_cbutton_map.end()) {
input = InputID(InputType::Controller, string_to_cbutton_map.at(t));
} else {
// Invalid token found; return default binding
LOG_DEBUG(Input, "Invalid token found: {}", t);
return InputBinding();
}
// Assign to the first available slot
for (auto& key : keys) {
if (!key.IsValid()) {
key = input;
break;
}
}
}
LOG_DEBUG(Input, "Parsed line: {}", InputBinding(keys[0], keys[1], keys[2]).ToString());
return InputBinding(keys[0], keys[1], keys[2]);
}
void ParseInputConfig(const std::string game_id = "") {
std::string config_type = Config::GetUseUnifiedInputConfig() ? "default" : game_id;
const auto config_file = Config::GetFoolproofKbmConfigFile(config_type);
// we reset these here so in case the user fucks up or doesn't include some of these,
// we can fall back to default
connections.clear();
float mouse_deadzone_offset = 0.5;
float mouse_speed = 1;
float mouse_speed_offset = 0.125;
leftjoystick_deadzone = {1, 127};
rightjoystick_deadzone = {1, 127};
lefttrigger_deadzone = {1, 127};
righttrigger_deadzone = {1, 127};
Config::SetOverrideControllerColor(false);
Config::SetControllerCustomColor(0, 0, 255);
int lineCount = 0;
std::ifstream file(config_file);
std::string line = "";
while (std::getline(file, line)) {
lineCount++;
// Strip the ; and whitespace
line.erase(std::remove_if(line.begin(), line.end(),
[](unsigned char c) { return std::isspace(c); }),
line.end());
if (line.empty()) {
continue;
}
// Truncate lines starting at #
std::size_t comment_pos = line.find('#');
if (comment_pos != std::string::npos) {
line = line.substr(0, comment_pos);
}
// Remove trailing semicolon
if (!line.empty() && line[line.length() - 1] == ';') {
line = line.substr(0, line.length() - 1);
}
if (line.empty()) {
continue;
}
// Split the line by '='
std::size_t equal_pos = line.find('=');
if (equal_pos == std::string::npos) {
LOG_WARNING(Input, "Invalid format at line: {}, data: \"{}\", skipping line.",
lineCount, line);
continue;
}
std::string output_string = line.substr(0, equal_pos);
std::string input_string = line.substr(equal_pos + 1);
std::size_t comma_pos = input_string.find(',');
auto parseInt = [](const std::string& s) -> std::optional<int> {
try {
return std::stoi(s);
} catch (...) {
return std::nullopt;
}
};
if (output_string == "mouse_to_joystick") {
if (input_string == "left") {
SetMouseToJoystick(1);
} else if (input_string == "right") {
SetMouseToJoystick(2);
} else {
LOG_WARNING(Input, "Invalid argument for mouse-to-joystick binding");
SetMouseToJoystick(0);
}
continue;
} else if (output_string == "key_toggle") {
if (comma_pos != std::string::npos) {
// handle key-to-key toggling (separate list?)
InputBinding toggle_keys = GetBindingFromString(input_string);
if (toggle_keys.KeyCount() != 2) {
LOG_WARNING(Input,
"Syntax error: Please provide exactly 2 keys: "
"first is the toggler, the second is the key to toggle: {}",
line);
continue;
}
ControllerOutput* toggle_out =
&*std::ranges::find(output_array, ControllerOutput(KEY_TOGGLE));
BindingConnection toggle_connection = BindingConnection(
InputBinding(toggle_keys.keys[0]), toggle_out, 0, toggle_keys.keys[1]);
connections.insert(connections.end(), toggle_connection);
continue;
}
LOG_WARNING(Input, "Invalid format at line: {}, data: \"{}\", skipping line.",
lineCount, line);
continue;
} else if (output_string == "mouse_movement_params") {
std::stringstream ss(input_string);
char comma; // To hold the comma separators between the floats
ss >> mouse_deadzone_offset >> comma >> mouse_speed >> comma >> mouse_speed_offset;
// Check for invalid input (in case there's an unexpected format)
if (ss.fail()) {
LOG_WARNING(Input, "Failed to parse mouse movement parameters from line: {}", line);
continue;
}
SetMouseParams(mouse_deadzone_offset, mouse_speed, mouse_speed_offset);
continue;
} else if (output_string == "analog_deadzone") {
std::stringstream ss(input_string);
std::string device, inner_deadzone_str, outer_deadzone_str;
if (!std::getline(ss, device, ',') || !std::getline(ss, inner_deadzone_str, ',') ||
!std::getline(ss, outer_deadzone_str)) {
LOG_WARNING(Input, "Malformed deadzone config at line {}: \"{}\"", lineCount, line);
continue;
}
auto inner_deadzone = parseInt(inner_deadzone_str);
auto outer_deadzone = parseInt(outer_deadzone_str);
if (!inner_deadzone || !outer_deadzone) {
LOG_WARNING(Input, "Invalid deadzone values at line {}: \"{}\"", lineCount, line);
continue;
}
std::pair<int, int> deadzone = {*inner_deadzone, *outer_deadzone};
static std::unordered_map<std::string, std::pair<int, int>&> deadzone_map = {
{"leftjoystick", leftjoystick_deadzone},
{"rightjoystick", rightjoystick_deadzone},
{"l2", lefttrigger_deadzone},
{"r2", righttrigger_deadzone},
};
if (auto it = deadzone_map.find(device); it != deadzone_map.end()) {
it->second = deadzone;
LOG_DEBUG(Input, "Parsed deadzone: {} {} {}", device, inner_deadzone_str,
outer_deadzone_str);
} else {
LOG_WARNING(Input, "Invalid axis name at line {}: \"{}\", skipping line.",
lineCount, line);
}
continue;
} else if (output_string == "override_controller_color") {
std::stringstream ss(input_string);
std::string enable, r_s, g_s, b_s;
std::optional<int> r, g, b;
if (!std::getline(ss, enable, ',') || !std::getline(ss, r_s, ',') ||
!std::getline(ss, g_s, ',') || !std::getline(ss, b_s)) {
LOG_WARNING(Input, "Malformed controller color config at line {}: \"{}\"",
lineCount, line);
continue;
}
r = parseInt(r_s);
g = parseInt(g_s);
b = parseInt(b_s);
if (!r || !g || !b) {
LOG_WARNING(Input, "Invalid RGB values at line {}: \"{}\", skipping line.",
lineCount, line);
continue;
}
Config::SetOverrideControllerColor(enable == "true");
Config::SetControllerCustomColor(*r, *g, *b);
LOG_DEBUG(Input, "Parsed color settings: {} {} {} {}",
enable == "true" ? "override" : "no override", *r, *b, *g);
continue;
}
// normal cases
InputBinding binding = GetBindingFromString(input_string);
BindingConnection connection(InputID(), nullptr);
auto button_it = string_to_cbutton_map.find(output_string);
auto axis_it = string_to_axis_map.find(output_string);
if (binding.IsEmpty()) {
LOG_WARNING(Input, "Invalid format at line: {}, data: \"{}\", skipping line.",
lineCount, line);
continue;
}
if (button_it != string_to_cbutton_map.end()) {
connection = BindingConnection(
binding, &*std::ranges::find(output_array, ControllerOutput(button_it->second)));
connections.insert(connections.end(), connection);
} else if (axis_it != string_to_axis_map.end()) {
int value_to_set = binding.keys[2].type == InputType::Axis ? 0 : axis_it->second.value;
connection = BindingConnection(
binding,
&*std::ranges::find(output_array, ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID,
axis_it->second.axis,
axis_it->second.value >= 0)),
value_to_set);
connections.insert(connections.end(), connection);
} else {
LOG_WARNING(Input, "Invalid format at line: {}, data: \"{}\", skipping line.",
lineCount, line);
continue;
}
LOG_DEBUG(Input, "Succesfully parsed line {}", lineCount);
}
file.close();
std::sort(connections.begin(), connections.end());
for (auto& c : connections) {
LOG_DEBUG(Input, "Binding: {} : {}", c.output->ToString(), c.binding.ToString());
}
LOG_DEBUG(Input, "Done parsing the input config!");
}
u32 GetMouseWheelEvent(const SDL_Event& event) {
if (event.type != SDL_EVENT_MOUSE_WHEEL && event.type != SDL_EVENT_MOUSE_WHEEL_OFF) {
LOG_WARNING(Input, "Something went wrong with wheel input parsing!");
return UINT32_MAX;
}
if (event.wheel.y > 0) {
return SDL_MOUSE_WHEEL_UP;
} else if (event.wheel.y < 0) {
return SDL_MOUSE_WHEEL_DOWN;
} else if (event.wheel.x > 0) {
return SDL_MOUSE_WHEEL_RIGHT;
} else if (event.wheel.x < 0) {
return SDL_MOUSE_WHEEL_LEFT;
}
return UINT32_MAX;
}
InputEvent InputBinding::GetInputEventFromSDLEvent(const SDL_Event& e) {
switch (e.type) {
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:
return InputEvent(InputType::KeyboardMouse, e.key.key, e.key.down, 0);
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
return InputEvent(InputType::KeyboardMouse, static_cast<u32>(e.button.button),
e.button.down, 0);
case SDL_EVENT_MOUSE_WHEEL:
case SDL_EVENT_MOUSE_WHEEL_OFF:
return InputEvent(InputType::KeyboardMouse, GetMouseWheelEvent(e),
e.type == SDL_EVENT_MOUSE_WHEEL, 0);
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
case SDL_EVENT_GAMEPAD_BUTTON_UP:
return InputEvent(InputType::Controller, static_cast<u32>(e.gbutton.button),
e.gbutton.down, 0);
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
return InputEvent(InputType::Axis, static_cast<u32>(e.gaxis.axis),
true, e.gaxis.value / 256);
default:
return InputEvent();
}
}
GameController* ControllerOutput::controller = nullptr;
void ControllerOutput::SetControllerOutputController(GameController* c) {
ControllerOutput::controller = c;
}
void ToggleKeyInList(InputID input) {
if (input.type == InputType::Axis) {
LOG_ERROR(Input, "Toggling analog inputs is not supported!");
return;
}
auto it = std::find(toggled_keys.begin(), toggled_keys.end(), input);
if (it == toggled_keys.end()) {
toggled_keys.insert(toggled_keys.end(), input);
LOG_DEBUG(Input, "Added {} to toggled keys", input.ToString());
} else {
toggled_keys.erase(it);
LOG_DEBUG(Input, "Removed {} from toggled keys", input.ToString());
}
}
void ControllerOutput::ResetUpdate() {
state_changed = false;
new_button_state = false;
*new_param = 0; // bruh
}
void ControllerOutput::AddUpdate(InputEvent event) {
if (button == KEY_TOGGLE) {
if (event.active) {
ToggleKeyInList(event.input);
}
} else if (button != SDL_GAMEPAD_BUTTON_INVALID) {
if (event.input.type == InputType::Axis) {
bool temp = event.axis_value * (positive_axis ? 1 : -1) > 0x40;
new_button_state |= event.active && event.axis_value * (positive_axis ? 1 : -1) > 0x40;
if (temp) {
LOG_DEBUG(Input, "Toggled a button from an axis");
}
} else {
new_button_state |= event.active;
}
} else if (axis != SDL_GAMEPAD_AXIS_INVALID) {
*new_param = (event.active ? event.axis_value : 0) + *new_param;
}
}
void ControllerOutput::FinalizeUpdate() {
state_changed = old_button_state != new_button_state || old_param != *new_param;
if (!state_changed) {
return;
}
old_button_state = new_button_state;
old_param = *new_param;
float touchpad_x = 0;
if (button != SDL_GAMEPAD_BUTTON_INVALID) {
switch (button) {
case SDL_GAMEPAD_BUTTON_TOUCHPAD:
touchpad_x = Config::getBackButtonBehavior() == "left" ? 0.25f
: Config::getBackButtonBehavior() == "right" ? 0.75f
: 0.5f;
controller->SetTouchpadState(0, new_button_state, touchpad_x, 0.5f);
controller->CheckButton(0, SDLGamepadToOrbisButton(button), new_button_state);
break;
case LEFTJOYSTICK_HALFMODE:
leftjoystick_halfmode = new_button_state;
break;
case RIGHTJOYSTICK_HALFMODE:
rightjoystick_halfmode = new_button_state;
break;
// KEY_TOGGLE isn't handled here anymore, as this function doesn't have the necessary data
// to do it, and it would be inconvenient to force it here, when AddUpdate does the job just
// fine, and a toggle doesn't have to checked against every input that's bound to it, it's
// enough that one is pressed
default: // is a normal key (hopefully)
controller->CheckButton(0, SDLGamepadToOrbisButton(button), new_button_state);
break;
}
} else if (axis != SDL_GAMEPAD_AXIS_INVALID && positive_axis) {
// avoid double-updating axes, but don't skip directional button bindings
auto ApplyDeadzone = [](s16* value, std::pair<int, int> deadzone) {
if (std::abs(*value) <= deadzone.first || deadzone.first == deadzone.second) {
*value = 0;
} else {
*value = (*value >= 0 ? 1 : -1) *
std::clamp((int)((128.0 * (std::abs(*value) - deadzone.first)) /
(float)(deadzone.second - deadzone.first)),
0, 128);
}
};
float multiplier = 1.0;
Axis c_axis = GetAxisFromSDLAxis(axis);
switch (c_axis) {
case Axis::LeftX:
case Axis::LeftY:
ApplyDeadzone(new_param, leftjoystick_deadzone);
multiplier = leftjoystick_halfmode ? 0.5 : 1.0;
break;
case Axis::RightX:
case Axis::RightY:
ApplyDeadzone(new_param, rightjoystick_deadzone);
multiplier = rightjoystick_halfmode ? 0.5 : 1.0;
break;
case Axis::TriggerLeft:
ApplyDeadzone(new_param, lefttrigger_deadzone);
controller->Axis(0, c_axis, GetAxis(0x0, 0x7f, *new_param));
controller->CheckButton(0, OrbisPadButtonDataOffset::L2, *new_param > 0x20);
return;
case Axis::TriggerRight:
ApplyDeadzone(new_param, righttrigger_deadzone);
controller->Axis(0, c_axis, GetAxis(0x0, 0x7f, *new_param));
controller->CheckButton(0, OrbisPadButtonDataOffset::R2, *new_param > 0x20);
return;
default:
break;
}
controller->Axis(0, c_axis, GetAxis(-0x80, 0x7f, *new_param * multiplier));
}
}
// Updates the list of pressed keys with the given input.
// Returns whether the list was updated or not.
bool UpdatePressedKeys(InputEvent event) {
// Skip invalid inputs
InputID input = event.input;
if (input.sdl_id == UINT32_MAX) {
return false;
}
if (input.type == InputType::Axis) {
// analog input, it gets added when it first sends an event,
// and from there, it only changes the parameter
auto it = std::lower_bound(pressed_keys.begin(), pressed_keys.end(), input,
[](const std::pair<InputEvent, bool>& e, InputID i) {
return std::tie(e.first.input.type, e.first.input.sdl_id) <
std::tie(i.type, i.sdl_id);
});
if (it == pressed_keys.end() || it->first.input != input) {
pressed_keys.insert(it, {event, false});
LOG_DEBUG(Input, "Added axis {} to the input list", event.input.sdl_id);
} else {
// noise filter
if (std::abs(it->first.axis_value - event.axis_value) <= 1) {
return false;
}
it->first.axis_value = event.axis_value;
}
return true;
} else if (event.active) {
// Find the correct position for insertion to maintain order
auto it = std::lower_bound(pressed_keys.begin(), pressed_keys.end(), input,
[](const std::pair<InputEvent, bool>& e, InputID i) {
return std::tie(e.first.input.type, e.first.input.sdl_id) <
std::tie(i.type, i.sdl_id);
});
// Insert only if 'value' is not already in the list
if (it == pressed_keys.end() || it->first.input != input) {
pressed_keys.insert(it, {event, false});
return true;
}
} else {
// Remove 'value' from the list if it's not pressed
auto it = std::find_if(
pressed_keys.begin(), pressed_keys.end(),
[input](const std::pair<InputEvent, bool>& e) { return e.first.input == input; });
if (it != pressed_keys.end()) {
pressed_keys.erase(it);
return true;
}
}
LOG_DEBUG(Input, "No change was made!");
return false;
}
// Check if the binding's all keys are currently active.
// It also extracts the analog inputs' parameters, and updates the input hierarchy flags.
InputEvent BindingConnection::ProcessBinding() {
// the last key is always set (if the connection isn't empty),
// and the analog inputs are always the last one due to how they are sorted,
// so this signifies whether or not the input is analog
InputEvent event = InputEvent(binding.keys[0]);
if (pressed_keys.empty()) {
return event;
}
if (event.input.type != InputType::Axis) {
// for button inputs
event.axis_value = axis_param;
}
// it's a bit scuffed, but if the output is a toggle, then we put the key here
if (output->button == KEY_TOGGLE) {
event.input = toggle;
}
// Extract keys from InputBinding and ignore unused or toggled keys
std::list<InputID> input_keys = {binding.keys[0], binding.keys[1], binding.keys[2]};
input_keys.remove(InputID());
for (auto key = input_keys.begin(); key != input_keys.end();) {
if (std::find(toggled_keys.begin(), toggled_keys.end(), *key) != toggled_keys.end()) {
key = input_keys.erase(key); // Use the returned iterator
} else {
++key; // Increment only if no erase happened
}
}
if (input_keys.empty()) {
LOG_DEBUG(Input, "No actual inputs to check, returning true");
event.active = true;
return event;
}
// Iterator for pressed_keys, starting from the beginning
auto pressed_it = pressed_keys.begin();
// Store pointers to flags in pressed_keys that need to be set if all keys are active
std::list<bool*> flags_to_set;
// Check if all keys in input_keys are active
for (InputID key : input_keys) {
bool key_found = false;
while (pressed_it != pressed_keys.end()) {
if (pressed_it->first.input == key && (pressed_it->second == false)) {
key_found = true;
if (output->positive_axis) {
flags_to_set.push_back(&pressed_it->second);
}
if (pressed_it->first.input.type == InputType::Axis) {
event.axis_value = pressed_it->first.axis_value;
}
++pressed_it;
break;
}
++pressed_it;
}
if (!key_found) {
return event;
}
}
for (bool* flag : flags_to_set) {
*flag = true;
}
if (binding.keys[0].type != InputType::Axis) { // the axes spam inputs, making this unreadable
LOG_DEBUG(Input, "Input found: {}", binding.ToString());
}
event.active = true;
return event; // All keys are active
}
void ActivateOutputsFromInputs() {
// Reset values and flags
for (auto& it : pressed_keys) {
it.second = false;
}
for (auto& it : output_array) {
it.ResetUpdate();
}
// Iterate over all inputs, and update their respecive outputs accordingly
for (auto& it : connections) {
it.output->AddUpdate(it.ProcessBinding());
}
// Update all outputs
for (auto& it : output_array) {
it.FinalizeUpdate();
}
}
} // namespace Input

439
input_handler.h Normal file
View File

@ -0,0 +1,439 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <map>
#include <string>
#include <unordered_set>
#include "SDL3/SDL_events.h"
#include "SDL3/SDL_timer.h"
#include "common/logging/log.h"
#include "common/types.h"
#include "core/libraries/pad/pad.h"
#include "fmt/format.h"
#include "input/controller.h"
// +1 and +2 is taken
#define SDL_MOUSE_WHEEL_UP SDL_EVENT_MOUSE_WHEEL + 3
#define SDL_MOUSE_WHEEL_DOWN SDL_EVENT_MOUSE_WHEEL + 4
#define SDL_MOUSE_WHEEL_LEFT SDL_EVENT_MOUSE_WHEEL + 5
#define SDL_MOUSE_WHEEL_RIGHT SDL_EVENT_MOUSE_WHEEL + 7
// idk who already used what where so I just chose a big number
#define SDL_EVENT_MOUSE_WHEEL_OFF SDL_EVENT_USER + 10
#define LEFTJOYSTICK_HALFMODE 0x00010000
#define RIGHTJOYSTICK_HALFMODE 0x00020000
#define BACK_BUTTON 0x00040000
#define KEY_TOGGLE 0x00200000
namespace Input {
using Input::Axis;
using Libraries::Pad::OrbisPadButtonDataOffset;
struct AxisMapping {
u32 axis;
s16 value;
AxisMapping(SDL_GamepadAxis a, s16 v) : axis(a), value(v) {}
};
enum class InputType { Axis, KeyboardMouse, Controller, Count };
const std::array<std::string, 4> input_type_names = {"Axis", "KBM", "Controller", "Unknown"};
class InputID {
public:
InputType type;
u32 sdl_id;
InputID(InputType d = InputType::Count, u32 i = UINT32_MAX) : type(d), sdl_id(i) {}
bool operator==(const InputID& o) const {
return type == o.type && sdl_id == o.sdl_id;
}
bool operator!=(const InputID& o) const {
return type != o.type || sdl_id != o.sdl_id;
}
bool operator<=(const InputID& o) const {
return type <= o.type && sdl_id <= o.sdl_id;
}
bool IsValid() const {
return *this != InputID();
}
std::string ToString() {
return fmt::format("({}: {:x})", input_type_names[static_cast<u8>(type)], sdl_id);
}
};
class InputEvent {
public:
InputID input;
bool active;
s8 axis_value;
InputEvent(InputID i = InputID(), bool a = false, s8 v = 0)
: input(i), active(a), axis_value(v) {}
InputEvent(InputType d, u32 i, bool a = false, s8 v = 0)
: input(d, i), active(a), axis_value(v) {}
};
// i strongly suggest you collapse these maps
const std::map<std::string, u32> string_to_cbutton_map = {
{"triangle", SDL_GAMEPAD_BUTTON_NORTH},
{"circle", SDL_GAMEPAD_BUTTON_EAST},
{"cross", SDL_GAMEPAD_BUTTON_SOUTH},
{"square", SDL_GAMEPAD_BUTTON_WEST},
{"l1", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
{"r1", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
{"l3", SDL_GAMEPAD_BUTTON_LEFT_STICK},
{"r3", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
{"pad_up", SDL_GAMEPAD_BUTTON_DPAD_UP},
{"pad_down", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
{"pad_left", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
{"pad_right", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
{"options", SDL_GAMEPAD_BUTTON_START},
// these are outputs only (touchpad can only be bound to itself)
{"touchpad", SDL_GAMEPAD_BUTTON_TOUCHPAD},
{"leftjoystick_halfmode", LEFTJOYSTICK_HALFMODE},
{"rightjoystick_halfmode", RIGHTJOYSTICK_HALFMODE},
// this is only for input
{"back", SDL_GAMEPAD_BUTTON_BACK},
};
const std::map<std::string, AxisMapping> string_to_axis_map = {
{"axis_left_x_plus", {SDL_GAMEPAD_AXIS_LEFTX, 127}},
{"axis_left_x_minus", {SDL_GAMEPAD_AXIS_LEFTX, -127}},
{"axis_left_y_plus", {SDL_GAMEPAD_AXIS_LEFTY, 127}},
{"axis_left_y_minus", {SDL_GAMEPAD_AXIS_LEFTY, -127}},
{"axis_right_x_plus", {SDL_GAMEPAD_AXIS_RIGHTX, 127}},
{"axis_right_x_minus", {SDL_GAMEPAD_AXIS_RIGHTX, -127}},
{"axis_right_y_plus", {SDL_GAMEPAD_AXIS_RIGHTY, 127}},
{"axis_right_y_minus", {SDL_GAMEPAD_AXIS_RIGHTY, -127}},
{"l2", {SDL_GAMEPAD_AXIS_LEFT_TRIGGER, 127}},
{"r2", {SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, 127}},
// should only use these to bind analog inputs to analog outputs
{"axis_left_x", {SDL_GAMEPAD_AXIS_LEFTX, 127}},
{"axis_left_y", {SDL_GAMEPAD_AXIS_LEFTY, 127}},
{"axis_right_x", {SDL_GAMEPAD_AXIS_RIGHTX, 127}},
{"axis_right_y", {SDL_GAMEPAD_AXIS_RIGHTY, 127}},
};
const std::map<std::string, u32> string_to_keyboard_key_map = {
// alphanumeric
{"a", SDLK_A},
{"b", SDLK_B},
{"c", SDLK_C},
{"d", SDLK_D},
{"e", SDLK_E},
{"f", SDLK_F},
{"g", SDLK_G},
{"h", SDLK_H},
{"i", SDLK_I},
{"j", SDLK_J},
{"k", SDLK_K},
{"l", SDLK_L},
{"m", SDLK_M},
{"n", SDLK_N},
{"o", SDLK_O},
{"p", SDLK_P},
{"q", SDLK_Q},
{"r", SDLK_R},
{"s", SDLK_S},
{"t", SDLK_T},
{"u", SDLK_U},
{"v", SDLK_V},
{"w", SDLK_W},
{"x", SDLK_X},
{"y", SDLK_Y},
{"z", SDLK_Z},
{"0", SDLK_0},
{"1", SDLK_1},
{"2", SDLK_2},
{"3", SDLK_3},
{"4", SDLK_4},
{"5", SDLK_5},
{"6", SDLK_6},
{"7", SDLK_7},
{"8", SDLK_8},
{"9", SDLK_9},
// symbols
{"`", SDLK_GRAVE},
{"~", SDLK_TILDE},
{"!", SDLK_EXCLAIM},
{"@", SDLK_AT},
{"#", SDLK_HASH},
{"$", SDLK_DOLLAR},
{"%", SDLK_PERCENT},
{"^", SDLK_CARET},
{"&", SDLK_AMPERSAND},
{"*", SDLK_ASTERISK},
{"(", SDLK_LEFTPAREN},
{")", SDLK_RIGHTPAREN},
{"-", SDLK_MINUS},
{"_", SDLK_UNDERSCORE},
{"=", SDLK_EQUALS},
{"+", SDLK_PLUS},
{"[", SDLK_LEFTBRACKET},
{"]", SDLK_RIGHTBRACKET},
{"{", SDLK_LEFTBRACE},
{"}", SDLK_RIGHTBRACE},
{"\\", SDLK_BACKSLASH},
{"|", SDLK_PIPE},
{";", SDLK_SEMICOLON},
{":", SDLK_COLON},
{"'", SDLK_APOSTROPHE},
{"\"", SDLK_DBLAPOSTROPHE},
{",", SDLK_COMMA},
{"<", SDLK_LESS},
{".", SDLK_PERIOD},
{">", SDLK_GREATER},
{"/", SDLK_SLASH},
{"?", SDLK_QUESTION},
// special keys
{"printscreen", SDLK_PRINTSCREEN},
{"scrolllock", SDLK_SCROLLLOCK},
{"pausebreak", SDLK_PAUSE},
{"backspace", SDLK_BACKSPACE},
{"delete", SDLK_DELETE},
{"insert", SDLK_INSERT},
{"home", SDLK_HOME},
{"end", SDLK_END},
{"pgup", SDLK_PAGEUP},
{"pgdown", SDLK_PAGEDOWN},
{"tab", SDLK_TAB},
{"capslock", SDLK_CAPSLOCK},
{"enter", SDLK_RETURN},
{"lshift", SDLK_LSHIFT},
{"rshift", SDLK_RSHIFT},
{"lctrl", SDLK_LCTRL},
{"rctrl", SDLK_RCTRL},
{"lalt", SDLK_LALT},
{"ralt", SDLK_RALT},
{"lmeta", SDLK_LGUI},
{"rmeta", SDLK_RGUI},
{"lwin", SDLK_LGUI},
{"rwin", SDLK_RGUI},
{"space", SDLK_SPACE},
{"up", SDLK_UP},
{"down", SDLK_DOWN},
{"left", SDLK_LEFT},
{"right", SDLK_RIGHT},
// keypad
{"kp0", SDLK_KP_0},
{"kp1", SDLK_KP_1},
{"kp2", SDLK_KP_2},
{"kp3", SDLK_KP_3},
{"kp4", SDLK_KP_4},
{"kp5", SDLK_KP_5},
{"kp6", SDLK_KP_6},
{"kp7", SDLK_KP_7},
{"kp8", SDLK_KP_8},
{"kp9", SDLK_KP_9},
{"kp .", SDLK_KP_PERIOD},
{"kp ,", SDLK_KP_COMMA},
{"kp /", SDLK_KP_DIVIDE},
{"kp *", SDLK_KP_MULTIPLY},
{"kp -", SDLK_KP_MINUS},
{"kp +", SDLK_KP_PLUS},
{"kp =", SDLK_KP_EQUALS},
{"kp enter", SDLK_KP_ENTER},
// mouse
{"leftbutton", SDL_BUTTON_LEFT},
{"rightbutton", SDL_BUTTON_RIGHT},
{"middlebutton", SDL_BUTTON_MIDDLE},
{"sidebuttonback", SDL_BUTTON_X1},
{"sidebuttonforward", SDL_BUTTON_X2},
{"mousewheelup", SDL_MOUSE_WHEEL_UP},
{"mousewheeldown", SDL_MOUSE_WHEEL_DOWN},
{"mousewheelleft", SDL_MOUSE_WHEEL_LEFT},
{"mousewheelright", SDL_MOUSE_WHEEL_RIGHT},
};
void ParseInputConfig(const std::string game_id);
class InputBinding {
public:
InputID keys[3];
InputBinding(InputID k1 = InputID(), InputID k2 = InputID(), InputID k3 = InputID()) {
// we format the keys so comparing them will be very fast, because we will only have to
// compare 3 sorted elements, where the only possible duplicate item is 0
// duplicate entries get changed to one original, one null
if (k1 == k2 && k1 != InputID()) {
k2 = InputID();
}
if (k1 == k3 && k1 != InputID()) {
k3 = InputID();
}
if (k3 == k2 && k2 != InputID()) {
k2 = InputID();
}
// this sorts them
if (k1 <= k2 && k1 <= k3) {
keys[0] = k1;
if (k2 <= k3) {
keys[1] = k2;
keys[2] = k3;
} else {
keys[1] = k3;
keys[2] = k2;
}
} else if (k2 <= k1 && k2 <= k3) {
keys[0] = k2;
if (k1 <= k3) {
keys[1] = k1;
keys[2] = k3;
} else {
keys[1] = k3;
keys[2] = k1;
}
} else {
keys[0] = k3;
if (k1 <= k2) {
keys[1] = k1;
keys[2] = k2;
} else {
keys[1] = k2;
keys[3] = k1;
}
}
}
// copy ctor
InputBinding(const InputBinding& o) {
keys[0] = o.keys[0];
keys[1] = o.keys[1];
keys[2] = o.keys[2];
}
inline bool operator==(const InputBinding& o) {
// InputID() signifies an unused slot
return (keys[0] == o.keys[0] || keys[0] == InputID() || o.keys[0] == InputID()) &&
(keys[1] == o.keys[1] || keys[1] == InputID() || o.keys[1] == InputID()) &&
(keys[2] == o.keys[2] || keys[2] == InputID() || o.keys[2] == InputID());
// it is already very fast,
// but reverse order makes it check the actual keys first instead of possible 0-s,
// potenially skipping the later expressions of the three-way AND
}
inline int KeyCount() const {
return (keys[0].IsValid() ? 1 : 0) + (keys[1].IsValid() ? 1 : 0) +
(keys[2].IsValid() ? 1 : 0);
}
// Sorts by the amount of non zero keys - left side is 'bigger' here
bool operator<(const InputBinding& other) const {
return KeyCount() > other.KeyCount();
}
inline bool IsEmpty() {
return !(keys[0].IsValid() || keys[1].IsValid() || keys[2].IsValid());
}
std::string ToString() { // todo add device type
switch (KeyCount()) {
case 1:
return fmt::format("({})", keys[0].ToString());
case 2:
return fmt::format("({}, {})", keys[0].ToString(), keys[1].ToString());
case 3:
return fmt::format("({}, {}, {})", keys[0].ToString(), keys[1].ToString(),
keys[2].ToString());
default:
return "Empty";
}
}
// returns an InputEvent based on the event type (keyboard, mouse buttons/wheel, or controller)
static InputEvent GetInputEventFromSDLEvent(const SDL_Event& e);
};
class ControllerOutput {
static GameController* controller;
public:
static void SetControllerOutputController(GameController* c);
static void LinkJoystickAxes();
u32 button;
u32 axis;
// these are only used as s8,
// but I added some padding to avoid overflow if it's activated by multiple inputs
// axis_plus and axis_minus pairs share a common new_param, the other outputs have their own
s16 old_param;
s16* new_param;
bool old_button_state, new_button_state, state_changed, positive_axis;
ControllerOutput(const u32 b, u32 a = SDL_GAMEPAD_AXIS_INVALID, bool p = true) {
button = b;
axis = a;
new_param = new s16(0);
old_param = 0;
positive_axis = p;
}
ControllerOutput(const ControllerOutput& o) : button(o.button), axis(o.axis) {
new_param = new s16(*o.new_param);
}
~ControllerOutput() {
delete new_param;
}
inline bool operator==(const ControllerOutput& o) const { // fucking consts everywhere
return button == o.button && axis == o.axis;
}
inline bool operator!=(const ControllerOutput& o) const {
return button != o.button || axis != o.axis;
}
std::string ToString() const {
return fmt::format("({}, {}, {})", (s32)button, (int)axis, old_param);
}
inline bool IsButton() const {
return axis == SDL_GAMEPAD_AXIS_INVALID && button != SDL_GAMEPAD_BUTTON_INVALID;
}
inline bool IsAxis() const {
return axis != SDL_GAMEPAD_AXIS_INVALID && button == SDL_GAMEPAD_BUTTON_INVALID;
}
void ResetUpdate();
void AddUpdate(InputEvent event);
void FinalizeUpdate();
};
class BindingConnection {
public:
InputBinding binding;
ControllerOutput* output;
u32 axis_param;
InputID toggle;
BindingConnection(InputBinding b, ControllerOutput* out, u32 param = 0, InputID t = InputID()) {
binding = b;
axis_param = param;
output = out;
toggle = t;
}
bool operator<(const BindingConnection& other) const {
// a button is a higher priority than an axis, as buttons can influence axes
// (e.g. joystick_halfmode)
if (output->IsButton() &&
(other.output->IsAxis() && (other.output->axis != SDL_GAMEPAD_AXIS_LEFT_TRIGGER &&
other.output->axis != SDL_GAMEPAD_AXIS_RIGHT_TRIGGER))) {
return true;
}
if (binding < other.binding) {
return true;
}
return false;
}
InputEvent ProcessBinding();
};
// Updates the list of pressed keys with the given input.
// Returns whether the list was updated or not.
bool UpdatePressedKeys(InputEvent event);
void ActivateOutputsFromInputs();
} // namespace Input

1067
kbm_gui.cpp Normal file

File diff suppressed because it is too large Load Diff

71
kbm_gui.h Normal file
View File

@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QDialog>
#include "game_info.h"
// macros > declaring constants
// also, we were only using one counterpart
#ifdef _WIN32
#define LCTRL_KEY 29
#define LALT_KEY 56
#define LSHIFT_KEY 42
#else
#define LCTRL_KEY 37
#define LALT_KEY 64
#define LSHIFT_KEY 50
#endif
namespace Ui {
class KBMSettings;
}
class KBMSettings : public QDialog {
Q_OBJECT
public:
explicit KBMSettings(std::shared_ptr<GameInfoClass> game_info_get, QWidget* parent = nullptr);
~KBMSettings();
private Q_SLOTS:
void SaveKBMConfig(bool CloseOnSave);
void SetDefault();
void CheckMapping(QPushButton*& button);
void StartTimer(QPushButton*& button);
void onHelpClicked();
private:
std::unique_ptr<Ui::KBMSettings> ui;
std::shared_ptr<GameInfoClass> m_game_info;
bool eventFilter(QObject* obj, QEvent* event) override;
QString GetModifiedButton(Qt::KeyboardModifiers modifier,
std::string m_button, std::string n_button);
void ButtonConnects();
void SetUIValuestoMappings(std::string config_id);
void GetGameTitle();
void DisableMappingButtons();
void EnableMappingButtons();
void SetMapping(QString input);
QSet<QString> pressedKeys;
bool EnableMapping = false;
bool MappingCompleted = false;
bool HelpWindowOpen = false;
QString mapping;
QString modifier;
int MappingTimer;
QTimer* timer;
QPushButton* MappingButton;
QList<QPushButton*> ButtonsList;
std::string config_id;
const std::vector<std::string> ControllerInputs = {
"cross", "circle", "square", "triangle", "l1",
"r1", "l2", "r2", "l3",
"r3", "options", "pad_up",
"pad_down",
"pad_left", "pad_right", "axis_left_x", "axis_left_y", "axis_right_x",
"axis_right_y", "back"};
};