diff --git a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp index d974db9e75..334984d1f7 100644 --- a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp @@ -214,13 +214,32 @@ std::uint8_t stickByte(std::int32_t axis) { return static_cast(((axis + 32768) * 255 + 32767) / 65535); } +std::filesystem::path pathFromUtf8(std::string_view text) { + return std::filesystem::path(std::u8string(reinterpret_cast(text.data()), text.size())); +} + +std::string pathToUtf8(const std::filesystem::path& path) { + const auto text = path.u8string(); + return std::string(reinterpret_cast(text.data()), text.size()); +} + +std::filesystem::path configuredPath() { +#ifdef _WIN32 + const wchar_t* configured = _wgetenv(L"ANYPS5_INPUT_CONFIG"); + return configured != nullptr ? std::filesystem::path(configured) : std::filesystem::path{}; +#else + const char* configured = std::getenv("ANYPS5_INPUT_CONFIG"); + return configured != nullptr ? pathFromUtf8(configured) : std::filesystem::path{}; +#endif +} + [[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); + throw std::runtime_error("Pad: invalid input mapping " + pathToUtf8(path) + ":" + std::to_string(line) + ": " + reason); } std::filesystem::path defaultConfigPath() { std::unique_ptr basePath(SDL_GetBasePath(), SDL_free); - if (basePath) return std::filesystem::path(basePath.get()) / "anyps5-input.ini"; + if (basePath) return pathFromUtf8(basePath.get()) / "anyps5-input.ini"; return "anyps5-input.ini"; } @@ -244,13 +263,13 @@ std::array Pad::StickWithDeadzone(std::int16_t x, std::int16_t Pad::InputConfiguration Pad::LoadInputMapping() { InputConfiguration configuration{{InputMapping.begin(), InputMapping.end()}, {}}; auto& bindings = configuration.bindings; - const char* configuredPath = std::getenv("ANYPS5_INPUT_CONFIG"); - const bool explicitPath = configuredPath != nullptr && configuredPath[0] != '\0'; - const std::filesystem::path path = explicitPath ? configuredPath : defaultConfigPath(); + const auto configured = configuredPath(); + const bool explicitPath = !configured.empty(); + const std::filesystem::path path = explicitPath ? configured : defaultConfigPath(); std::ifstream file(path); if (!file) { if (explicitPath || std::filesystem::exists(path)) { - throw std::runtime_error("Pad: cannot read input mapping '" + path.string() + "'"); + throw std::runtime_error("Pad: cannot read input mapping '" + pathToUtf8(path) + "'"); } return configuration; } @@ -301,7 +320,7 @@ Pad::InputConfiguration Pad::LoadInputMapping() { invalidLine(path, lineNumber, error.what()); } } - if (file.bad()) throw std::runtime_error("Pad: cannot read input mapping '" + path.string() + "'"); + if (file.bad()) throw std::runtime_error("Pad: cannot read input mapping '" + pathToUtf8(path) + "'"); return configuration; } diff --git a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp index daa2dad4d6..1fe0f4b30b 100644 --- a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp @@ -105,6 +105,48 @@ void mouseLookReject() { require(rejects("MouseDecayMs = 20\nmousedecayms = 30\n"), "a mouse setting given twice is rejected"); } +void setConfigPath(const std::filesystem::path& path) { +#ifdef _WIN32 + require(_wputenv_s(L"ANYPS5_INPUT_CONFIG", path.wstring().c_str()) == 0, "set ANYPS5_INPUT_CONFIG"); +#else + require(::setenv("ANYPS5_INPUT_CONFIG", path.string().c_str(), 1) == 0, "set ANYPS5_INPUT_CONFIG"); +#endif +} + +void nonAsciiPaths() { + const std::u8string name = u8"anyps5-input-\u00fc-\u65e5\u672c-" + std::u8string(root.filename().u8string()); + const auto directory = root / std::filesystem::path(name); + std::filesystem::create_directories(directory); + const auto file = directory / "anyps5-input.ini"; + { + std::ofstream stream(file, std::ios::binary | std::ios::trunc); + stream << "MouseSensitivity = 3\n"; + require(stream.good(), "write the input mapping in a non-ASCII directory"); + } + setConfigPath(file); + require(Pad::LoadInputMapping().settings.mouseSensitivity == 3.0, "ANYPS5_INPUT_CONFIG may name a path with non-ASCII characters"); + { + std::ofstream stream(file, std::ios::binary | std::ios::trunc); + stream << "MouseSensitivity = fast\n"; + } + std::string message; + try { + Pad::LoadInputMapping(); + } catch (const std::runtime_error& error) { + message = error.what(); + } + const auto utf8Name = std::string(reinterpret_cast(name.data()), name.size()); + require(message.find(utf8Name) != std::string::npos, "an error names the path in UTF-8"); + setConfigPath(directory / "missing.ini"); + message.clear(); + try { + Pad::LoadInputMapping(); + } catch (const std::runtime_error& error) { + message = error.what(); + } + require(message.find(utf8Name) != std::string::npos, "a missing file names the path in UTF-8"); +} + void deadzoneOffMatchesRawSticks() { for (const std::int16_t axis : {std::int16_t{-32768}, std::int16_t{-20000}, std::int16_t{-1}, std::int16_t{0}, std::int16_t{1}, std::int16_t{1234}, std::int16_t{32767}}) { const auto stick = Pad::StickWithDeadzone(axis, axis, 0); @@ -141,6 +183,7 @@ int main() try { mouseLookDefault(); mouseLookParse(); mouseLookReject(); + nonAsciiPaths(); deadzoneOffMatchesRawSticks(); deadzoneShapesSticks(); std::filesystem::remove_all(root); diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index d61e36d70a..54f26dc6ed 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -1,6 +1,6 @@ # Controller, keyboard and mouse input -AnyPS5 accepts SDL-mapped game controllers as well as its built-in keyboard and mouse bindings. Xbox-style controllers and PlayStation controllers recognized by SDL use the standard PS button layout; sticks and analog triggers are passed through, and controllers can be connected or disconnected while the game is running. The first recognized controller is used. AnyPS5 uses its built-in keyboard and mouse bindings when no configuration file is present. To change selected keyboard and mouse bindings, create `anyps5-input.ini` beside the generated game executable. Set `ANYPS5_INPUT_CONFIG` to use a file at another path. +AnyPS5 accepts SDL-mapped game controllers as well as its built-in keyboard and mouse bindings. Xbox-style controllers and PlayStation controllers recognized by SDL use the standard PS button layout; sticks and analog triggers are passed through, and controllers can be connected or disconnected while the game is running. The first recognized controller is used. AnyPS5 uses its built-in keyboard and mouse bindings when no configuration file is present. To change selected keyboard and mouse bindings, create `anyps5-input.ini` beside the generated game executable. Set `ANYPS5_INPUT_CONFIG` to use a file at another path. Both the game directory and that path may contain any characters, such as `ü` or `日本`. For an Xbox-layout controller, A/B/X/Y map to Cross/Circle/Square/Triangle, LB/RB map to L1/R1, and LT/RT map to analog L2/R2. Start maps to Options. D-pad and stick-click buttons are supported. On a PlayStation controller, the touchpad click button is forwarded; finger touch coordinates are not implemented.