From c13f0eee673bb80ccb687aefa688b6df349d83d5 Mon Sep 17 00:00:00 2001 From: MartinCajiao Date: Sun, 27 Sep 2026 19:54:19 -0500 Subject: [PATCH] feat(input): allow configurable keyboard and mouse bindings --- README.md | 4 + .../prx/libScePad/include/InputMapping.hpp | 5 +- core/libs/prx/libSceVideoOut/CMakeLists.txt | 1 + .../prx/libSceVideoOut/include/PadInput.hpp | 7 +- .../prx/libSceVideoOut/src/InputMapping.cpp | 172 ++++++++++++++++++ core/libs/prx/libSceVideoOut/src/PadInput.cpp | 33 ++-- .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 2 +- docs/INPUT_MAPPING.md | 28 +++ docs/TechnicalDebt.md | 1 - 9 files changed, 233 insertions(+), 20 deletions(-) create mode 100644 core/libs/prx/libSceVideoOut/src/InputMapping.cpp create mode 100644 docs/INPUT_MAPPING.md diff --git a/README.md b/README.md index c22ffe6ca..d54a4320b 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,10 @@ The relinker uses only the C++20 standard library and should build with any conf The project targets maximum compiler portability. Support for additional compilers will be addressed after the first successful game launch. +## Input mapping + +Keyboard and mouse controls can be configured with an `anyps5-input.ini` file. See [input mapping](docs/INPUT_MAPPING.md) for the format and behavior. + ## Disclaimer This project is intended for interoperability, research, preservation, and compatibility purposes. It does not include, distribute, or require copyrighted software, firmware, cryptographic keys, or proprietary libraries. Users are responsible for ensuring that any binaries used with this project are obtained and used in accordance with applicable laws and their respective license terms. diff --git a/core/libs/prx/libScePad/include/InputMapping.hpp b/core/libs/prx/libScePad/include/InputMapping.hpp index 67c310a19..ea3513278 100644 --- a/core/libs/prx/libScePad/include/InputMapping.hpp +++ b/core/libs/prx/libScePad/include/InputMapping.hpp @@ -3,6 +3,7 @@ #include #include +#include #include "SDL_scancode.h" #include "SDL_mouse.h" @@ -50,6 +51,8 @@ inline constexpr std::array InputMapping{ InputBinding{SDL_SCANCODE_UNKNOWN, MouseButton::None, InputControl::Button, PadButton::Down, -1} }; +std::vector LoadInputMapping(); + } -#endif \ No newline at end of file +#endif diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 452f68c24..e41cf34bd 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -6,6 +6,7 @@ add_library(libSceVideoOut SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/VideoOutDriver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/DisplayWindow.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/PadInput.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/InputMapping.cpp ) target_include_directories(libSceVideoOut PRIVATE diff --git a/core/libs/prx/libSceVideoOut/include/PadInput.hpp b/core/libs/prx/libSceVideoOut/include/PadInput.hpp index e86efd382..99928b941 100644 --- a/core/libs/prx/libSceVideoOut/include/PadInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/PadInput.hpp @@ -5,19 +5,22 @@ #include "prx/libScePad/include/InputMapping.hpp" #include #include +#include class DisplayWindow; class PadInput { public: + PadInput(); void HandleEvent(const SDL_Event& event, DisplayWindow& window); void Update(); private: void publish(); void setMouseMode(bool enabled); - std::array pressed{}; - std::array wheelReleaseTimes{}; + std::vector bindings; + std::vector pressed; + std::vector wheelReleaseTimes; std::array mouseStick{128, 128}; std::chrono::steady_clock::time_point nextMousePoll{}; bool mouseEnabled = false; diff --git a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp new file mode 100644 index 000000000..3b925fc91 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp @@ -0,0 +1,172 @@ +#include "prx/libScePad/include/InputMapping.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "SDL_keyboard.h" + +namespace { + +struct InputAction { + std::string_view name; + Pad::InputControl control; + Pad::PadButton button; +}; + +constexpr auto actions = std::array{ + InputAction{"Cross", Pad::InputControl::Button, Pad::PadButton::Cross}, + InputAction{"Circle", Pad::InputControl::Button, Pad::PadButton::Circle}, + InputAction{"Triangle", Pad::InputControl::Button, Pad::PadButton::Triangle}, + InputAction{"Square", Pad::InputControl::Button, Pad::PadButton::Square}, + InputAction{"L1", Pad::InputControl::Button, Pad::PadButton::L1}, + InputAction{"R1", Pad::InputControl::Button, Pad::PadButton::R1}, + InputAction{"L2", Pad::InputControl::Button, Pad::PadButton::L2}, + InputAction{"R2", Pad::InputControl::Button, Pad::PadButton::R2}, + InputAction{"L3", Pad::InputControl::Button, Pad::PadButton::L3}, + InputAction{"R3", Pad::InputControl::Button, Pad::PadButton::R3}, + InputAction{"Options", Pad::InputControl::Button, Pad::PadButton::Options}, + InputAction{"Up", Pad::InputControl::Button, Pad::PadButton::Up}, + InputAction{"Right", Pad::InputControl::Button, Pad::PadButton::Right}, + InputAction{"Down", Pad::InputControl::Button, Pad::PadButton::Down}, + InputAction{"Left", Pad::InputControl::Button, Pad::PadButton::Left}, + InputAction{"LeftStickLeft", Pad::InputControl::LeftStickLeft, Pad::PadButton::None}, + InputAction{"LeftStickRight", Pad::InputControl::LeftStickRight, Pad::PadButton::None}, + InputAction{"LeftStickUp", Pad::InputControl::LeftStickUp, Pad::PadButton::None}, + InputAction{"LeftStickDown", Pad::InputControl::LeftStickDown, Pad::PadButton::None}, + InputAction{"RightStickLeft", Pad::InputControl::RightStickLeft, Pad::PadButton::None}, + InputAction{"RightStickRight", Pad::InputControl::RightStickRight, Pad::PadButton::None}, + InputAction{"RightStickUp", Pad::InputControl::RightStickUp, Pad::PadButton::None}, + InputAction{"RightStickDown", Pad::InputControl::RightStickDown, Pad::PadButton::None}, + InputAction{"TouchLeft", Pad::InputControl::TouchLeft, Pad::PadButton::None}, + InputAction{"TouchRight", Pad::InputControl::TouchRight, Pad::PadButton::None}, + InputAction{"ToggleMouse", Pad::InputControl::ToggleMouse, Pad::PadButton::None}, + InputAction{"ToggleFullscreen", Pad::InputControl::ToggleFullscreen, Pad::PadButton::None} +}; + +std::string upper(std::string_view value) { + std::string result(value); + std::transform(result.begin(), result.end(), result.begin(), [](unsigned char character) { + return static_cast(std::toupper(character)); + }); + return result; +} + +std::string_view trim(std::string_view value) { + while (!value.empty() && std::isspace(static_cast(value.front())) != 0) value.remove_prefix(1); + while (!value.empty() && std::isspace(static_cast(value.back())) != 0) value.remove_suffix(1); + return value; +} + +const InputAction* findAction(std::string_view name) { + const auto normalized = upper(name); + for (const auto& action : actions) { + if (upper(action.name) == normalized) return &action; + } + return nullptr; +} + +bool matchesAction(const Pad::InputBinding& binding, const InputAction& action) { + if (action.control == Pad::InputControl::Button) { + return binding.control == Pad::InputControl::Button && binding.button == action.button; + } + return binding.control == action.control; +} + +Pad::MouseButton parseMouseButton(std::string_view name) { + const auto normalized = upper(name); + if (normalized == "LEFT") return Pad::MouseButton::Left; + if (normalized == "MIDDLE") return Pad::MouseButton::Middle; + if (normalized == "RIGHT") return Pad::MouseButton::Right; + if (normalized == "X1") return Pad::MouseButton::X1; + if (normalized == "X2") return Pad::MouseButton::X2; + throw std::runtime_error("unknown mouse button '" + std::string(name) + "'"); +} + +Pad::InputBinding parseBinding(const InputAction& action, std::string_view source) { + const auto separator = source.find(':'); + if (separator == std::string_view::npos) throw std::runtime_error("source must use TYPE:VALUE format"); + const auto type = upper(trim(source.substr(0, separator))); + const auto value = trim(source.substr(separator + 1)); + if (value.empty()) throw std::runtime_error("source value is empty"); + + if (type == "KEY") { + const std::string keyName(value); + const auto key = SDL_GetScancodeFromName(keyName.c_str()); + if (key == SDL_SCANCODE_UNKNOWN) throw std::runtime_error("unknown SDL key name '" + keyName + "'"); + return {key, Pad::MouseButton::None, action.control, action.button, 0}; + } + if (type == "MOUSE") { + if (action.control == Pad::InputControl::ToggleFullscreen) { + throw std::runtime_error("ToggleFullscreen can only use a keyboard key"); + } + return {SDL_SCANCODE_UNKNOWN, parseMouseButton(value), action.control, action.button, 0}; + } + if (type == "WHEEL") { + if (action.control != Pad::InputControl::Button) throw std::runtime_error("mouse wheel can only map to a pad button"); + const auto direction = upper(value); + if (direction == "UP") return {SDL_SCANCODE_UNKNOWN, Pad::MouseButton::None, action.control, action.button, 1}; + if (direction == "DOWN") return {SDL_SCANCODE_UNKNOWN, Pad::MouseButton::None, action.control, action.button, -1}; + throw std::runtime_error("mouse wheel direction must be UP or DOWN"); + } + throw std::runtime_error("source type must be KEY, MOUSE or WHEEL"); +} + +[[noreturn]] void invalidLine(const std::filesystem::path& path, std::size_t line, const std::string& reason) { + throw std::runtime_error("Pad: invalid input mapping " + path.string() + ":" + std::to_string(line) + ": " + reason); +} + +} + +std::vector Pad::LoadInputMapping() { + std::vector bindings(InputMapping.begin(), InputMapping.end()); + const char* configuredPath = std::getenv("ANYPS5_INPUT_CONFIG"); + const bool explicitPath = configuredPath != nullptr && configuredPath[0] != '\0'; + const std::filesystem::path path = explicitPath ? configuredPath : "anyps5-input.ini"; + std::ifstream file(path); + if (!file) { + if (explicitPath || std::filesystem::exists(path)) { + throw std::runtime_error("Pad: cannot read input mapping '" + path.string() + "'"); + } + return bindings; + } + + std::unordered_set overriddenActions; + std::string line; + std::size_t lineNumber = 0; + while (std::getline(file, line)) { + ++lineNumber; + const auto comment = line.find_first_of("#;"); + const std::string_view content = trim(std::string_view(line).substr(0, comment)); + if (content.empty()) continue; + const auto separator = content.find('='); + if (separator == std::string_view::npos) invalidLine(path, lineNumber, "expected Action = TYPE:VALUE"); + + const auto actionName = trim(content.substr(0, separator)); + const auto source = trim(content.substr(separator + 1)); + const auto* action = findAction(actionName); + if (action == nullptr) invalidLine(path, lineNumber, "unknown action '" + std::string(actionName) + "'"); + if (source.empty()) invalidLine(path, lineNumber, "source is empty"); + + try { + auto binding = parseBinding(*action, source); + const auto normalizedAction = upper(action->name); + if (overriddenActions.insert(normalizedAction).second) { + std::erase_if(bindings, [action](const InputBinding& existing) { return matchesAction(existing, *action); }); + } + bindings.push_back(binding); + } catch (const std::runtime_error& error) { + invalidLine(path, lineNumber, error.what()); + } + } + if (file.bad()) throw std::runtime_error("Pad: cannot read input mapping '" + path.string() + "'"); + + return bindings; +} diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 6631fb5a9..ff08df751 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -10,6 +10,9 @@ #include "prx/libScePad/include/PadState.hpp" #include "prx/libScePad/include/PadInputTypes.hpp" +PadInput::PadInput() + : bindings(Pad::LoadInputMapping()), pressed(bindings.size()), wheelReleaseTimes(bindings.size()) {} + void PadInput::setMouseMode(bool enabled) { if (SDL_SetRelativeMouseMode(enabled ? SDL_TRUE : SDL_FALSE) != 0) throw std::runtime_error(std::string("Pad: relative mouse mode failed: ") + SDL_GetError()); int deltaX = 0; @@ -22,8 +25,8 @@ void PadInput::setMouseMode(bool enabled) { void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) { if (event.type == SDL_WINDOWEVENT && (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST || event.window.event == SDL_WINDOWEVENT_CLOSE)) { - pressed.fill(false); - wheelReleaseTimes.fill({}); + std::fill(pressed.begin(), pressed.end(), false); + std::fill(wheelReleaseTimes.begin(), wheelReleaseTimes.end(), std::chrono::steady_clock::time_point{}); if (mouseEnabled) setMouseMode(false); publish(); return; @@ -37,8 +40,8 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) { if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) direction = -direction; if (direction == 0) return; const auto releaseTime = std::chrono::steady_clock::now() + std::chrono::milliseconds(Pad::WheelPressDurationMs); - for (std::size_t index = 0; index < Pad::InputMapping.size(); ++index) { - const auto& binding = Pad::InputMapping[index]; + for (std::size_t index = 0; index < bindings.size(); ++index) { + const auto& binding = bindings[index]; if (binding.wheelDirection == 0) continue; pressed[index] = binding.wheelDirection == direction; wheelReleaseTimes[index] = pressed[index] ? releaseTime : std::chrono::steady_clock::time_point{}; @@ -52,11 +55,11 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) { if (ignoreInput) return; if (keyboard && event.key.repeat != 0) return; const bool down = event.type == SDL_KEYDOWN || event.type == SDL_MOUSEBUTTONDOWN; - for (std::size_t index = 0; index < Pad::InputMapping.size(); ++index) { - const auto& binding = Pad::InputMapping[index]; - const bool keyMatches = binding.key != SDL_SCANCODE_UNKNOWN && binding.key == event.key.keysym.scancode; - const bool mouseMatches = binding.mouseButton != Pad::MouseButton::None && binding.mouseButton == static_cast(event.button.button); - const bool matches = keyboard ? keyMatches : mouseMatches; + for (std::size_t index = 0; index < bindings.size(); ++index) { + const auto& binding = bindings[index]; + const bool matches = keyboard + ? binding.key != SDL_SCANCODE_UNKNOWN && binding.key == event.key.keysym.scancode + : binding.mouseButton != Pad::MouseButton::None && binding.mouseButton == static_cast(event.button.button); if (!matches) continue; if (binding.control == Pad::InputControl::ToggleFullscreen) { @@ -71,8 +74,8 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) { void PadInput::Update() { const auto now = std::chrono::steady_clock::now(); bool released = false; - for (std::size_t index = 0; index < Pad::InputMapping.size(); ++index) { - if (Pad::InputMapping[index].wheelDirection == 0 || !pressed[index] || now < wheelReleaseTimes[index]) continue; + for (std::size_t index = 0; index < bindings.size(); ++index) { + if (bindings[index].wheelDirection == 0 || !pressed[index] || now < wheelReleaseTimes[index]) continue; pressed[index] = false; wheelReleaseTimes[index] = {}; released = true; @@ -80,8 +83,8 @@ void PadInput::Update() { if (released) publish(); if (!mouseEnabled) return; if (SDL_GetKeyboardFocus() == nullptr) { - pressed.fill(false); - wheelReleaseTimes.fill({}); + std::fill(pressed.begin(), pressed.end(), false); + std::fill(wheelReleaseTimes.begin(), wheelReleaseTimes.end(), std::chrono::steady_clock::time_point{}); setMouseMode(false); publish(); return; @@ -105,9 +108,9 @@ void PadInput::publish() { PadInputState state; std::array negative{}; std::array positive{}; - for (std::size_t index = 0; index < Pad::InputMapping.size(); ++index) { + for (std::size_t index = 0; index < bindings.size(); ++index) { if (!pressed[index]) continue; - const auto& binding = Pad::InputMapping[index]; + const auto& binding = bindings[index]; switch (binding.control) { case Pad::InputControl::Button: state.buttons |= static_cast(binding.button); break; case Pad::InputControl::LeftStickLeft: negative[0] = true; break; diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index a929e098b..f1ad46926 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -463,8 +463,8 @@ void VideoOutDriver::processFlip(FlipRequest& req) { void VideoOutDriver::presentLoop(std::stop_token token) { std::shared_ptr current; - PadInput padInput; try { + PadInput padInput; while (!token.stop_requested()) { { std::unique_lock lock(flipQueue->mutex); diff --git a/docs/INPUT_MAPPING.md b/docs/INPUT_MAPPING.md new file mode 100644 index 000000000..3904986f9 --- /dev/null +++ b/docs/INPUT_MAPPING.md @@ -0,0 +1,28 @@ +# Keyboard and mouse mapping + +AnyPS5 uses its built-in keyboard and mouse bindings when no configuration file is present. To change selected bindings, create `anyps5-input.ini` in the game's working directory. Set `ANYPS5_INPUT_CONFIG` to use a file at another path. + +Each non-empty line has the form `Action = Type:Value`. Action names are case-insensitive. A `#` or `;` starts a comment. The first line for an action replaces its built-in bindings; later lines for the same action add alternate inputs. Actions omitted from the file keep their built-in bindings. + +Supported input sources are: + +- `KEY:Return`, `KEY:Space`, or another key name accepted by SDL. +- `MOUSE:Left`, `MOUSE:Middle`, `MOUSE:Right`, `MOUSE:X1`, or `MOUSE:X2`. +- `WHEEL:Up` or `WHEEL:Down` for pad buttons. + +Supported actions are `Cross`, `Circle`, `Triangle`, `Square`, `L1`, `R1`, `L2`, `R2`, `L3`, `R3`, `Options`, `Up`, `Right`, `Down`, `Left`, `LeftStickLeft`, `LeftStickRight`, `LeftStickUp`, `LeftStickDown`, `RightStickLeft`, `RightStickRight`, `RightStickUp`, `RightStickDown`, `TouchLeft`, `TouchRight`, `ToggleMouse`, and `ToggleFullscreen`. + +For example, this changes Cross to F, adds IJKL alternatives for the left stick, uses the mouse buttons for Square and R2, and keeps all other built-in bindings: + +```ini +Cross = KEY:F +LeftStickLeft = KEY:J +LeftStickRight = KEY:L +LeftStickUp = KEY:I +LeftStickDown = KEY:K +Square = MOUSE:Left +R2 = MOUSE:Right +ToggleMouse = MOUSE:Middle +``` + +An invalid line reports the file and line number and stops input initialization. If the configured file does not exist or cannot be read, AnyPS5 reports an error. With no `anyps5-input.ini` and no `ANYPS5_INPUT_CONFIG`, the built-in mapping is used. diff --git a/docs/TechnicalDebt.md b/docs/TechnicalDebt.md index 6156464b1..87b34664b 100644 --- a/docs/TechnicalDebt.md +++ b/docs/TechnicalDebt.md @@ -30,7 +30,6 @@ Throughout the project, every function at every stage either **does exactly what ### Functional -- There's no way to specify keyboard and mouse input mapping when using a gamepad. The [default mapping](../core/libs/prx/libScePad/include/InputMapping.hpp) is always used. - [Shader recompilation](../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../core/shader/recompiler) was written completely independently from [libs/prx](../core/libs/prx). - The executable file that [relinker](../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens the console when launched, which is inconvenient for playability. - [Relinker](../core/relinker/elfpatcher/src) doesn't add an icon to the generated executable. This should be done without adding dependencies (only standard).