feat(pad): hold the sticks from the controller command file

This commit is contained in:
fma-rc 2026-10-11 23:27:28 +02:00
parent 34a16b7f08
commit e0eef6c57f
No known key found for this signature in database
5 changed files with 109 additions and 25 deletions

View File

@ -1,6 +1,8 @@
#ifndef CORE_LIBS_PRX_LIBSCEPAD_INPUTCOMMANDFILE_HPP
#define CORE_LIBS_PRX_LIBSCEPAD_INPUTCOMMANDFILE_HPP
#include <array>
#include <charconv>
#include <cstdint>
#include <fstream>
#include <sstream>
@ -9,28 +11,38 @@
namespace Pad {
class InputCommandFile {
public:
struct Input {
std::uint32_t buttons = 0;
bool hasSticks = false;
std::array<std::uint8_t, 4> 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<std::uint32_t>(values[1]);
command.durationMs = static_cast<std::uint32_t>(values[2]);
for (int i = 3; i < count; ++i) {
if (values[i] > 255) return {};
command.input.sticks[i - 3] = static_cast<std::uint8_t>(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;
};
}

View File

@ -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;

View File

@ -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{});
}

View File

@ -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

View File

@ -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__":