From e0eef6c57fc221ce339bb0c5c07aebf19c59ce81 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Sun, 11 Oct 2026 23:27:28 +0200 Subject: [PATCH] feat(pad): hold the sticks from the controller command file --- .../libScePad/include/InputCommandFile.hpp | 47 +++++++++++++++---- core/libs/prx/libScePad/src/PadState.cpp | 25 +++++----- core/libs/tests/GuestPad.cpp | 46 +++++++++++++++++- docs/user/USAGE.md | 4 +- tools/pad_input.py | 12 ++++- 5 files changed, 109 insertions(+), 25 deletions(-) diff --git a/core/libs/prx/libScePad/include/InputCommandFile.hpp b/core/libs/prx/libScePad/include/InputCommandFile.hpp index 8915531f4..7433fd9f4 100644 --- a/core/libs/prx/libScePad/include/InputCommandFile.hpp +++ b/core/libs/prx/libScePad/include/InputCommandFile.hpp @@ -1,6 +1,8 @@ #ifndef CORE_LIBS_PRX_LIBSCEPAD_INPUTCOMMANDFILE_HPP #define CORE_LIBS_PRX_LIBSCEPAD_INPUTCOMMANDFILE_HPP +#include +#include #include #include #include @@ -9,28 +11,38 @@ namespace Pad { class InputCommandFile { public: + struct Input { + std::uint32_t buttons = 0; + bool hasSticks = false; + std::array sticks{}; + + bool operator==(const Input&) const = default; + }; + explicit InputCommandFile(const char* path) : path(path != nullptr ? path : "") { if (!this->path.empty()) lastSequence = read().sequence; } - std::uint32_t Buttons(std::uint64_t now) { - if (path.empty()) return 0; + Input Poll(std::uint64_t now) { + if (path.empty()) return {}; if (now >= nextPoll) { nextPoll = now + 100000; const auto command = read(); if (command.sequence > lastSequence) { lastSequence = command.sequence; - buttons = command.buttons; + input = command.input; until = now + command.durationMs * 1000ull; } } - return now < until ? buttons : 0; + return now < until ? input : Input{}; } + std::uint32_t Buttons(std::uint64_t now) { return Poll(now).buttons; } + private: struct Command { std::uint64_t sequence = 0; - std::uint32_t buttons = 0; + Input input; std::uint32_t durationMs = 0; }; @@ -41,10 +53,27 @@ private: const std::string text(line); if (text.find('-') != std::string::npos) return {}; std::istringstream fields(text); + std::string tokens[8]; + int count = 0; + while (count < 8 && fields >> tokens[count]) ++count; + if (count != 3 && count != 7) return {}; + std::uint64_t values[7]{}; + for (int i = 0; i < count; ++i) { + const char* first = tokens[i].data() + (tokens[i].front() == '+' ? 1 : 0); + const char* last = tokens[i].data() + tokens[i].size(); + const auto parsed = std::from_chars(first, last, values[i]); + if (first == last || parsed.ec != std::errc{} || parsed.ptr != last) return {}; + } + if (values[0] == 0 || (values[1] & ~0x10fffeull) != 0 || values[2] == 0 || values[2] > 5000) return {}; Command command; - std::string extra; - if (!(fields >> command.sequence >> command.buttons >> command.durationMs) || fields >> extra) return {}; - if (command.sequence == 0 || (command.buttons & ~0x10fffeu) != 0 || command.durationMs == 0 || command.durationMs > 5000) return {}; + command.sequence = values[0]; + command.input.buttons = static_cast(values[1]); + command.durationMs = static_cast(values[2]); + for (int i = 3; i < count; ++i) { + if (values[i] > 255) return {}; + command.input.sticks[i - 3] = static_cast(values[i]); + } + command.input.hasSticks = count == 7; return command; } @@ -52,7 +81,7 @@ private: std::uint64_t lastSequence = 0; std::uint64_t nextPoll = 0; std::uint64_t until = 0; - std::uint32_t buttons = 0; + Input input; }; } diff --git a/core/libs/prx/libScePad/src/PadState.cpp b/core/libs/prx/libScePad/src/PadState.cpp index ca01712ee..9d8adf4c3 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -109,15 +109,16 @@ void Pad::Initialize() { } namespace { -PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uint64_t now, std::uint32_t injected) { +PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uint64_t now, const Pad::InputCommandFile::Input& injected) { PadData data{}; - data.buttons = state.buttons | injected; - data.left_stick_x = state.sticks[0]; - data.left_stick_y = state.sticks[1]; - data.right_stick_x = state.sticks[2]; - data.right_stick_y = state.sticks[3]; + data.buttons = state.buttons | injected.buttons; + const auto& sticks = injected.hasSticks ? injected.sticks : state.sticks; + data.left_stick_x = sticks[0]; + data.left_stick_y = sticks[1]; + data.right_stick_x = sticks[2]; + data.right_stick_y = sticks[3]; const auto trigger = [&](std::uint8_t analog, std::uint32_t bit) -> std::uint8_t { - return (injected & bit) != 0 || (analog == 0 && (state.buttons & bit) != 0) ? 255 : analog; + return (injected.buttons & bit) != 0 || (analog == 0 && (state.buttons & bit) != 0) ? 255 : analog; }; data.analog_buttons_l2 = trigger(state.analogButtonsL2, 0x100); data.analog_buttons_r2 = trigger(state.analogButtonsR2, 0x200); @@ -175,10 +176,10 @@ PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uin return data; } -std::uint32_t InjectedButtons(std::uint64_t now) { +Pad::InputCommandFile::Input InjectedInput(std::uint64_t now) { static Pad::InputCommandFile commands(std::getenv("APS5_PAD_INPUT_FILE")); - static std::uint32_t lastInjected = 0; - const auto injected = commands.Buttons(now); + static Pad::InputCommandFile::Input lastInjected; + const auto injected = commands.Poll(now); if (injected != lastInjected) { lastInjected = injected; timestamp = now; @@ -192,7 +193,7 @@ PadData Pad::ReadState() { if (failure) std::rethrow_exception(failure); if (!initialized) throw std::runtime_error("Pad: read before initialization"); const std::uint64_t now = sceKernelGetProcessTime(); - const std::uint32_t injected = InjectedButtons(now); + const auto injected = InjectedInput(now); return BuildData(state, timestamp, now, injected); } @@ -202,7 +203,7 @@ int Pad::Read(PadData* data, int num) { if (failure) std::rethrow_exception(failure); if (!initialized) throw std::runtime_error("Pad: read before initialization"); const std::uint64_t now = sceKernelGetProcessTime(); - const std::uint32_t injected = InjectedButtons(now); + const auto injected = InjectedInput(now); if (queue.empty()) { data[0] = BuildData(state, timestamp, now, injected); return 1; diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index a74a4bab5..4eb1d8b0b 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -57,8 +57,34 @@ static void CheckInputCommands() { Require(commands.Buttons(1001000) == 0); write("2 16384 200"); Require(commands.Buttons(1101000) == 0); + write("5 0 300 0 128 255 64"); + const auto tilted = commands.Poll(1201000); + Require(tilted.buttons == 0 && tilted.hasSticks); + Require(tilted.sticks[0] == 0 && tilted.sticks[1] == 128 && tilted.sticks[2] == 255 && tilted.sticks[3] == 64); + Require(!commands.Poll(1501000).hasSticks); + write("6 16384 300"); + const auto pressed = commands.Poll(1601000); + Require(pressed.buttons == 0x4000 && !pressed.hasSticks); + const Pad::InputCommandFile::Input none; + write("7 16384 300 128 128"); + Require(commands.Poll(1901000) == none); + write("7 16384 300 128 128 128 256"); + Require(commands.Poll(2001000) == none); + write("7 16384 300 128 128 128 128 128"); + Require(commands.Poll(2101000) == none); + write("7 16384 300 128 128 128 up"); + Require(commands.Poll(2201000) == none); + write("7 16384 300 +"); + Require(commands.Poll(2301000) == none); + write("7 16384 300 4294967296"); + Require(commands.Poll(2401000) == none); + write("7 16384 300 128 128 128 128x"); + Require(commands.Poll(2501000) == none); + write("7 8192 300 1 2 3 4"); + const auto accepted = commands.Poll(2601000); + Require(accepted.buttons == 0x2000 && accepted.hasSticks && accepted.sticks[3] == 4); std::filesystem::remove(path); - Require(commands.Buttons(1201000) == 0); + Require(commands.Buttons(3001000) == 0); Pad::InputCommandFile disabled(nullptr); Require(disabled.Buttons(0) == 0); } @@ -173,6 +199,24 @@ static void CheckInjectedState(const std::filesystem::path& path) { const auto released = Pad::ReadState(); Require(released.buttons == 0x2000 && released.analog_buttons_l2 == 0); Require(released.timestamp > pressed.timestamp); + physical.sticks[3] = 17; + PadPublishInput_nid_postfix(physical); + const auto still = Pad::ReadState(); + std::ofstream(path) << "2 0 300 200 0 128 128\n"; + PadData moved{}; + const auto stickDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + do { + moved = Pad::ReadState(); + if (moved.left_stick_x == 200) break; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } while (std::chrono::steady_clock::now() < stickDeadline); + Require(moved.left_stick_x == 200 && moved.left_stick_y == 0); + Require(moved.right_stick_x == 128 && moved.right_stick_y == 128); + Require(moved.buttons == 0x2000 && moved.timestamp > still.timestamp); + std::this_thread::sleep_for(std::chrono::milliseconds(310)); + const auto centred = Pad::ReadState(); + Require(centred.left_stick_x == 42 && centred.right_stick_y == 17); + Require(centred.timestamp > moved.timestamp); PadPublishInput_nid_postfix(PadInputState{}); } diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 3ff968d17..b9914eff7 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -115,9 +115,11 @@ APS5_PAD_INPUT_FILE=/tmp/anyps5-pad ./app.elf python3 tools/pad_input.py /tmp/anyps5-pad cross python3 tools/pad_input.py /tmp/anyps5-pad down --hold-ms 500 python3 tools/pad_input.py /tmp/anyps5-pad release +python3 tools/pad_input.py /tmp/anyps5-pad release --left-stick 128 0 --hold-ms 2000 +python3 tools/pad_input.py /tmp/anyps5-pad cross --left-stick 255 128 --right-stick 0 128 ``` -Run the helper in a second terminal. Commands are polled at most every 100 ms during controller reads and add to physical controller input. A press lasts 300 ms by default, starting when the application reads it, and releases automatically; `--hold-ms` accepts 1–5000 ms. Wait for the application to observe a release before sending another press. The file holds only the latest command; rapid commands can replace one another. Commands left over when the application first reads the controller are ignored. Without the environment variable, file input is disabled. +Run the helper in a second terminal. Commands are polled at most every 100 ms during controller reads, and their buttons add to physical controller input. A press lasts 300 ms by default, starting when the application reads it, and releases automatically; `--hold-ms` accepts 1–5000 ms. `--left-stick X Y` and `--right-stick X Y` hold the sticks for the same time: each axis is 0-255, 128 is the centre, X grows to the right and Y grows downward. While such a command is active both sticks come from it instead of the physical controller, and a stick that was not named stays centred. Wait for the application to observe a release before sending another press. The file holds only the latest command; rapid commands can replace one another. Commands left over when the application first reads the controller are ignored. Without the environment variable, file input is disabled. ### System fonts diff --git a/tools/pad_input.py b/tools/pad_input.py index 8e1bed63d..e68f3185b 100644 --- a/tools/pad_input.py +++ b/tools/pad_input.py @@ -19,19 +19,27 @@ def main(): parser.add_argument("path", type=Path) parser.add_argument("button", choices=BUTTONS) parser.add_argument("--hold-ms", type=int, default=300) + parser.add_argument("--left-stick", type=int, nargs=2, metavar=("X", "Y")) + parser.add_argument("--right-stick", type=int, nargs=2, metavar=("X", "Y")) args = parser.parse_args() if not 1 <= args.hold_ms <= 5000: parser.error("--hold-ms must be between 1 and 5000") + sticks = "" + if args.left_stick is not None or args.right_stick is not None: + axes = (args.left_stick or [128, 128]) + (args.right_stick or [128, 128]) + if not all(0 <= axis <= 255 for axis in axes): + parser.error("stick axes must be between 0 and 255") + sticks = "".join(f" {axis}" for axis in axes) temporary = None try: with tempfile.NamedTemporaryFile(mode="w", dir=args.path.parent, delete=False) as output: temporary = Path(output.name) - output.write(f"{time.monotonic_ns()} {BUTTONS[args.button]} {args.hold_ms}\n") + output.write(f"{time.monotonic_ns()} {BUTTONS[args.button]} {args.hold_ms}{sticks}\n") os.replace(temporary, args.path) finally: if temporary is not None: temporary.unlink(missing_ok=True) - print(f"Queued {args.button} for {args.hold_ms} ms in {args.path}") + print(f"Queued {args.button}{' with sticks' + sticks if sticks else ''} for {args.hold_ms} ms in {args.path}") if __name__ == "__main__":