mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-11-05 13:20:11 +00:00
Merge pull request #14313 from oneandonlydean/carry-7499
feat(libScePad): read per-title input profiles beside the shared input file
This commit is contained in:
5 files changed
+126
-35
No files matched your search
@@ -2,7 +2,10 @@
|
||||
#define CORE_LIBS_PRX_LIBSCEPAD_INPUTMAPPING_HPP
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
#include "SDL_scancode.h"
|
||||
#include "SDL_mouse.h"
|
||||
@@ -15,6 +18,7 @@ inline constexpr int WheelPressDurationMs = 80;
|
||||
inline constexpr double DefaultMouseSensitivity = 1.0;
|
||||
inline constexpr double MaxMouseSensitivity = 10.0;
|
||||
inline constexpr int MaxStickDeadzonePercent = 90;
|
||||
inline constexpr std::size_t MaxTitleIdLength = 32;
|
||||
|
||||
struct InputSettings {
|
||||
int leftStickDeadzonePercent = 0;
|
||||
@@ -83,7 +87,8 @@ inline constexpr std::array InputMapping{
|
||||
InputBinding{SDL_SCANCODE_UNKNOWN, MouseButton::None, InputControl::Button, PadButton::Down, -1}
|
||||
};
|
||||
|
||||
InputConfiguration LoadInputMapping();
|
||||
InputConfiguration LoadInputMapping(std::string_view titleId = {});
|
||||
std::string InputProfileName(std::string_view titleId);
|
||||
std::array<std::uint8_t, 2> StickWithDeadzone(std::int16_t x, std::int16_t y, int deadzonePercent);
|
||||
|
||||
}
|
||||
|
||||
@@ -288,37 +288,10 @@ std::filesystem::path defaultConfigPath() {
|
||||
return "anyps5-input.ini";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
std::array<std::uint8_t, 2> Pad::StickWithDeadzone(std::int16_t x, std::int16_t y, int deadzonePercent) {
|
||||
if (deadzonePercent < 0 || deadzonePercent > MaxStickDeadzonePercent) throw std::invalid_argument("Pad: stick deadzone out of range");
|
||||
if (deadzonePercent == 0) return {stickByte(x), stickByte(y)};
|
||||
const double normalizedX = std::max(x / 32767.0, -1.0);
|
||||
const double normalizedY = std::max(y / 32767.0, -1.0);
|
||||
const double magnitude = std::hypot(normalizedX, normalizedY);
|
||||
const double deadzone = deadzonePercent / 100.0;
|
||||
if (magnitude <= deadzone) return {stickByte(0), stickByte(0)};
|
||||
const double scale = (magnitude - deadzone) / (1.0 - deadzone) / magnitude;
|
||||
const auto axis = [scale](double value) {
|
||||
return stickByte(static_cast<std::int32_t>(std::lround(std::clamp(value * scale, -1.0, 1.0) * 32767.0)));
|
||||
};
|
||||
return {axis(normalizedX), axis(normalizedY)};
|
||||
}
|
||||
|
||||
Pad::InputConfiguration Pad::LoadInputMapping() {
|
||||
InputConfiguration configuration{{InputMapping.begin(), InputMapping.end()}, {}};
|
||||
auto& bindings = configuration.bindings;
|
||||
const auto configured = configuredPath();
|
||||
const bool explicitPath = !configured.empty();
|
||||
const std::filesystem::path path = explicitPath ? configured : defaultConfigPath();
|
||||
void loadInputFile(Pad::InputConfiguration& configuration, const std::filesystem::path& path) {
|
||||
std::ifstream file(path);
|
||||
if (!file) {
|
||||
if (explicitPath || std::filesystem::exists(path)) {
|
||||
throw std::runtime_error("Pad: cannot read input mapping '" + pathToUtf8(path) + "'");
|
||||
}
|
||||
return configuration;
|
||||
}
|
||||
|
||||
if (!file) throw std::runtime_error("Pad: cannot read input mapping '" + pathToUtf8(path) + "'");
|
||||
auto& bindings = configuration.bindings;
|
||||
std::unordered_set<std::string> overriddenActions;
|
||||
std::array<bool, settingNames.size()> configuredSettings{};
|
||||
std::string line;
|
||||
@@ -353,9 +326,9 @@ Pad::InputConfiguration Pad::LoadInputMapping() {
|
||||
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); });
|
||||
std::erase_if(bindings, [action](const Pad::InputBinding& existing) { return matchesAction(existing, *action); });
|
||||
}
|
||||
const bool duplicate = std::any_of(bindings.begin(), bindings.end(), [&binding](const InputBinding& existing) {
|
||||
const bool duplicate = std::any_of(bindings.begin(), bindings.end(), [&binding](const Pad::InputBinding& existing) {
|
||||
return existing.key == binding.key && existing.mouseButton == binding.mouseButton &&
|
||||
existing.control == binding.control && existing.button == binding.button &&
|
||||
existing.wheelDirection == binding.wheelDirection;
|
||||
@@ -366,6 +339,42 @@ Pad::InputConfiguration Pad::LoadInputMapping() {
|
||||
}
|
||||
}
|
||||
if (file.bad()) throw std::runtime_error("Pad: cannot read input mapping '" + pathToUtf8(path) + "'");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
std::array<std::uint8_t, 2> Pad::StickWithDeadzone(std::int16_t x, std::int16_t y, int deadzonePercent) {
|
||||
if (deadzonePercent < 0 || deadzonePercent > MaxStickDeadzonePercent) throw std::invalid_argument("Pad: stick deadzone out of range");
|
||||
if (deadzonePercent == 0) return {stickByte(x), stickByte(y)};
|
||||
const double normalizedX = std::max(x / 32767.0, -1.0);
|
||||
const double normalizedY = std::max(y / 32767.0, -1.0);
|
||||
const double magnitude = std::hypot(normalizedX, normalizedY);
|
||||
const double deadzone = deadzonePercent / 100.0;
|
||||
if (magnitude <= deadzone) return {stickByte(0), stickByte(0)};
|
||||
const double scale = (magnitude - deadzone) / (1.0 - deadzone) / magnitude;
|
||||
const auto axis = [scale](double value) {
|
||||
return stickByte(static_cast<std::int32_t>(std::lround(std::clamp(value * scale, -1.0, 1.0) * 32767.0)));
|
||||
};
|
||||
return {axis(normalizedX), axis(normalizedY)};
|
||||
}
|
||||
|
||||
std::string Pad::InputProfileName(std::string_view titleId) {
|
||||
const bool valid = !titleId.empty() && titleId.size() <= MaxTitleIdLength && std::all_of(titleId.begin(), titleId.end(), [](char character) {
|
||||
return std::isalnum(static_cast<unsigned char>(character)) != 0 || character == '_' || character == '-';
|
||||
});
|
||||
return valid ? "anyps5-input." + std::string(titleId) + ".ini" : std::string();
|
||||
}
|
||||
|
||||
Pad::InputConfiguration Pad::LoadInputMapping(std::string_view titleId) {
|
||||
InputConfiguration configuration{{InputMapping.begin(), InputMapping.end()}, {}};
|
||||
const auto configured = configuredPath();
|
||||
const bool explicitPath = !configured.empty();
|
||||
const std::filesystem::path path = explicitPath ? configured : defaultConfigPath();
|
||||
if (explicitPath || std::filesystem::exists(path)) loadInputFile(configuration, path);
|
||||
const auto profile = InputProfileName(titleId);
|
||||
if (!profile.empty()) {
|
||||
const auto profilePath = path.parent_path() / profile;
|
||||
if (std::filesystem::exists(profilePath)) loadInputFile(configuration, profilePath);
|
||||
}
|
||||
return configuration;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
@@ -25,8 +25,19 @@
|
||||
#include "prx/libSceIme/include/ModalDialog.hpp"
|
||||
#include "prx/libSceVideoOut/include/HostVolumeInput.hpp"
|
||||
#include "prx/libScePad/include/HostVolumeSettings.hpp"
|
||||
#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp"
|
||||
|
||||
PadInput::PadInput() : PadInput(Pad::LoadInputMapping()) {
|
||||
namespace {
|
||||
|
||||
std::string appTitleId() {
|
||||
if (!std::filesystem::exists(ResolvePath_nid_no_patch("/app0/sce_sys/param.json")) && !std::filesystem::exists(ResolvePath_nid_no_patch("/app0/sce_sys/param.sfo"))) return {};
|
||||
const auto titleId = GetAppTitleId_nid_postfix();
|
||||
return std::string(titleId.value, std::find(titleId.value, titleId.value + sizeof(titleId.value), '\0'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PadInput::PadInput() : PadInput(Pad::LoadInputMapping(appTitleId())) {
|
||||
HostVolumeConfigureFromEnvironment_nid_no_patch();
|
||||
}
|
||||
|
||||
|
||||
@@ -415,6 +415,59 @@ void triggerRamp() {
|
||||
require(rejects("TriggerRampMs = 10\ntriggerrampms = 20\n"), "a trigger ramp given twice is rejected");
|
||||
}
|
||||
|
||||
void writeProfile(const std::string& titleId, const std::string& text) {
|
||||
std::ofstream file(root / Pad::InputProfileName(titleId), std::ios::binary | std::ios::trunc);
|
||||
file << text;
|
||||
require(file.good(), "write the input profile");
|
||||
}
|
||||
|
||||
bool crossOnKey(const Pad::InputConfiguration& configuration, SDL_Scancode key) {
|
||||
std::size_t crossBindings = 0;
|
||||
bool onKey = false;
|
||||
for (const auto& binding : configuration.bindings) {
|
||||
if (binding.control != Pad::InputControl::Button || binding.button != Pad::PadButton::Cross || binding.key == SDL_SCANCODE_UNKNOWN) continue;
|
||||
++crossBindings;
|
||||
onKey = onKey || binding.key == key;
|
||||
}
|
||||
return crossBindings == 1 && onKey;
|
||||
}
|
||||
|
||||
void profileNames() {
|
||||
require(Pad::InputProfileName("PPSA00001") == "anyps5-input.PPSA00001.ini", "a title ID names its profile");
|
||||
require(Pad::InputProfileName("CUSA_01-x") == "anyps5-input.CUSA_01-x.ini", "underscores and dashes are allowed");
|
||||
require(Pad::InputProfileName("").empty(), "an empty title ID has no profile");
|
||||
require(Pad::InputProfileName("../PPSA00001").empty(), "a title ID with a path separator has no profile");
|
||||
require(Pad::InputProfileName("PPSA00001.ini").empty(), "a title ID with a dot has no profile");
|
||||
require(Pad::InputProfileName(std::string(Pad::MaxTitleIdLength + 1, 'A')).empty(), "a title ID over the length limit has no profile");
|
||||
}
|
||||
|
||||
void profileOverridesShared() {
|
||||
useConfig("MouseSensitivity = 2\nCross = KEY:F\n");
|
||||
writeProfile("PPSA00001", "MouseSensitivity = 4\nMouseCurve = 2\nCross = KEY:G\n");
|
||||
const auto profiled = Pad::LoadInputMapping("PPSA00001");
|
||||
require(profiled.settings.mouseSensitivity == 4.0 && profiled.settings.mouseCurve == 2.0, "a profile overrides and adds settings");
|
||||
require(crossOnKey(profiled, SDL_SCANCODE_G), "a profile replaces the bindings of an action it lists");
|
||||
const auto other = Pad::LoadInputMapping("PPSA00002");
|
||||
require(other.settings.mouseSensitivity == 2.0 && other.settings.mouseCurve == 1.0 && crossOnKey(other, SDL_SCANCODE_F), "another title keeps the shared file");
|
||||
const auto none = Pad::LoadInputMapping();
|
||||
require(none.settings.mouseSensitivity == 2.0 && crossOnKey(none, SDL_SCANCODE_F), "no title ID keeps the shared file");
|
||||
writeProfile("PPSA00001", "MouseCurve = 3\n");
|
||||
const auto partial = Pad::LoadInputMapping("PPSA00001");
|
||||
require(partial.settings.mouseSensitivity == 2.0 && partial.settings.mouseCurve == 3.0 && crossOnKey(partial, SDL_SCANCODE_F), "a profile keeps what it does not list");
|
||||
writeProfile("PPSA00001", "MouseSensitivity = 4\n");
|
||||
require(Pad::LoadInputMapping("PPSA00001").settings.mouseSensitivity == 4.0, "a profile may set a setting the shared file also sets");
|
||||
writeProfile("PPSA00001", "MouseCurve = 9\n");
|
||||
std::string message;
|
||||
try {
|
||||
Pad::LoadInputMapping("PPSA00001");
|
||||
} catch (const std::runtime_error& error) {
|
||||
message = error.what();
|
||||
}
|
||||
require(message.find("anyps5-input.PPSA00001.ini:1") != std::string::npos, "an invalid profile names its file and line");
|
||||
require(Pad::LoadInputMapping("PPSA00002").settings.mouseCurve == 1.0, "an invalid profile does not affect other titles");
|
||||
std::filesystem::remove(root / Pad::InputProfileName("PPSA00001"));
|
||||
}
|
||||
|
||||
std::size_t toggleMouseBindings(const Pad::InputConfiguration& configuration, SDL_Scancode key, Pad::MouseButton button) {
|
||||
std::size_t count = 0;
|
||||
for (const auto& binding : configuration.bindings) {
|
||||
@@ -486,6 +539,8 @@ int main() try {
|
||||
stickModifier();
|
||||
keyboardRamp();
|
||||
triggerRamp();
|
||||
profileNames();
|
||||
profileOverridesShared();
|
||||
toggleMouseDefaults();
|
||||
nonAsciiPaths();
|
||||
deadzoneOffMatchesRawSticks();
|
||||
|
||||
@@ -131,6 +131,17 @@ With `TriggerRampMs` above 0, a key or mouse button bound to `L2` or `R2` presse
|
||||
|
||||
While the `Shake` input is held (X by default), the controller reports its acceleration and angular velocity swinging back and forth every 50 ms, for titles that ask the player to shake the controller. A controller's own motion sensors are used otherwise.
|
||||
|
||||
## Per-game profiles
|
||||
|
||||
A file named `anyps5-input.<TITLEID>.ini` beside `anyps5-input.ini` (or beside the file `ANYPS5_INPUT_CONFIG` names) is read after it when the running title has that ID, for example `anyps5-input.PPSA00001.ini`. The profile has the same format and overrides only what it lists: an action it names replaces the bindings from the shared file, a setting it names replaces the shared value, and everything else stays. Other titles don't read it. A title ID may contain letters, digits, `_` and `-`. An invalid line in a profile reports the profile's file and line number.
|
||||
|
||||
For example, a profile that gives one title a lower mouse sensitivity and Cross on F:
|
||||
|
||||
```ini
|
||||
MouseSensitivity = 0.6
|
||||
Cross = KEY:F
|
||||
```
|
||||
|
||||
## Mouse as an analog stick
|
||||
|
||||
`ToggleMouse` (the middle mouse button or F7 by default; a mouse without a middle button can use F7) captures the mouse and drives the analog stick selected by `MouseStickTarget` (`Right` by default) from how fast the mouse moves. Capture uses raw relative input, so the pointer speed and acceleration set in the operating system don't apply. Press it again to release the mouse. Capture also ends, and every held key is let go, when the window loses focus.
|
||||
|
||||
Reference in new issue
Block a user