feat(input): allow configurable keyboard and mouse bindings
This commit is contained in:
parent
7656458121
commit
c13f0eee67
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#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<InputBinding> LoadInputMapping();
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -5,19 +5,22 @@
|
|||
#include "prx/libScePad/include/InputMapping.hpp"
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
|
||||
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<bool, Pad::InputMapping.size()> pressed{};
|
||||
std::array<std::chrono::steady_clock::time_point, Pad::InputMapping.size()> wheelReleaseTimes{};
|
||||
std::vector<Pad::InputBinding> bindings;
|
||||
std::vector<bool> pressed;
|
||||
std::vector<std::chrono::steady_clock::time_point> wheelReleaseTimes;
|
||||
std::array<std::uint8_t, 2> mouseStick{128, 128};
|
||||
std::chrono::steady_clock::time_point nextMousePoll{};
|
||||
bool mouseEnabled = false;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,172 @@
|
|||
#include "prx/libScePad/include/InputMapping.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_set>
|
||||
|
||||
#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<char>(std::toupper(character));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string_view trim(std::string_view value) {
|
||||
while (!value.empty() && std::isspace(static_cast<unsigned char>(value.front())) != 0) value.remove_prefix(1);
|
||||
while (!value.empty() && std::isspace(static_cast<unsigned char>(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::InputBinding> Pad::LoadInputMapping() {
|
||||
std::vector<InputBinding> 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<std::string> 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;
|
||||
}
|
||||
|
|
@ -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<Pad::MouseButton>(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<Pad::MouseButton>(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<bool, 4> negative{};
|
||||
std::array<bool, 4> 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<std::uint32_t>(binding.button); break;
|
||||
case Pad::InputControl::LeftStickLeft: negative[0] = true; break;
|
||||
|
|
|
|||
|
|
@ -463,8 +463,8 @@ void VideoOutDriver::processFlip(FlipRequest& req) {
|
|||
|
||||
void VideoOutDriver::presentLoop(std::stop_token token) {
|
||||
std::shared_ptr<FlipRequest> current;
|
||||
PadInput padInput;
|
||||
try {
|
||||
PadInput padInput;
|
||||
while (!token.stop_requested()) {
|
||||
{
|
||||
std::unique_lock lock(flipQueue->mutex);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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).
|
||||
|
|
|
|||
Loading…
Reference in New Issue