feat(pad): accept timed controller pulses from a local command file
Add an optional input helper for test runs without desktop input injection. Preserve physical input and release injected buttons automatically. Validation: guest_pad tests and Python helper CLI smoke checks passed. Written with AI assistance.
This commit is contained in:
parent
1c75f4d228
commit
3a35841aad
|
|
@ -0,0 +1,59 @@
|
|||
#ifndef CORE_LIBS_PRX_LIBSCEPAD_INPUTCOMMANDFILE_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEPAD_INPUTCOMMANDFILE_HPP
|
||||
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace Pad {
|
||||
class InputCommandFile {
|
||||
public:
|
||||
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;
|
||||
if (now >= nextPoll) {
|
||||
nextPoll = now + 100000;
|
||||
const auto command = read();
|
||||
if (command.sequence > lastSequence) {
|
||||
lastSequence = command.sequence;
|
||||
buttons = command.buttons;
|
||||
until = now + command.durationMs * 1000ull;
|
||||
}
|
||||
}
|
||||
return now < until ? buttons : 0;
|
||||
}
|
||||
|
||||
private:
|
||||
struct Command {
|
||||
std::uint64_t sequence = 0;
|
||||
std::uint32_t buttons = 0;
|
||||
std::uint32_t durationMs = 0;
|
||||
};
|
||||
|
||||
Command read() const {
|
||||
std::ifstream file(path);
|
||||
char line[128]{};
|
||||
if (!file.getline(line, sizeof(line))) return {};
|
||||
const std::string text(line);
|
||||
if (text.find('-') != std::string::npos) return {};
|
||||
std::istringstream fields(text);
|
||||
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 {};
|
||||
return command;
|
||||
}
|
||||
|
||||
std::string path;
|
||||
std::uint64_t lastSequence = 0;
|
||||
std::uint64_t nextPoll = 0;
|
||||
std::uint64_t until = 0;
|
||||
std::uint32_t buttons = 0;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
#include "prx/libScePad/include/PadState.hpp"
|
||||
#include "prx/libScePad/include/Pad.hpp"
|
||||
#include "prx/libScePad/include/InputCommandFile.hpp"
|
||||
#include "prx/libkernel/Time/include/Time.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
|
|
@ -107,15 +109,15 @@ void Pad::Initialize() {
|
|||
}
|
||||
|
||||
namespace {
|
||||
PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uint64_t now) {
|
||||
PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uint64_t now, std::uint32_t injected) {
|
||||
PadData data{};
|
||||
data.buttons = state.buttons;
|
||||
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.analog_buttons_l2 = std::max<std::uint8_t>(state.analogButtonsL2, (state.buttons & 0x100) != 0 ? 255 : 0);
|
||||
data.analog_buttons_r2 = std::max<std::uint8_t>(state.analogButtonsR2, (state.buttons & 0x200) != 0 ? 255 : 0);
|
||||
data.analog_buttons_l2 = std::max<std::uint8_t>(state.analogButtonsL2, (data.buttons & 0x100) != 0 ? 255 : 0);
|
||||
data.analog_buttons_r2 = std::max<std::uint8_t>(state.analogButtonsR2, (data.buttons & 0x200) != 0 ? 255 : 0);
|
||||
|
||||
const bool live = state.hasMotion && output.motionEnabled;
|
||||
const std::array<float, 3> rest{0.0f, 9.80665f, 0.0f};
|
||||
|
|
@ -169,13 +171,26 @@ PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uin
|
|||
data.touch_data_touch_num = touchNum;
|
||||
return data;
|
||||
}
|
||||
|
||||
std::uint32_t InjectedButtons(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);
|
||||
if (injected != lastInjected) {
|
||||
lastInjected = injected;
|
||||
timestamp = now;
|
||||
}
|
||||
return injected;
|
||||
}
|
||||
}
|
||||
|
||||
PadData Pad::ReadState() {
|
||||
std::lock_guard lock(stateMutex);
|
||||
if (failure) std::rethrow_exception(failure);
|
||||
if (!initialized) throw std::runtime_error("Pad: read before initialization");
|
||||
return BuildData(state, timestamp, sceKernelGetProcessTime());
|
||||
const std::uint64_t now = sceKernelGetProcessTime();
|
||||
const std::uint32_t injected = InjectedButtons(now);
|
||||
return BuildData(state, timestamp, now, injected);
|
||||
}
|
||||
|
||||
int Pad::Read(PadData* data, int num) {
|
||||
|
|
@ -184,14 +199,15 @@ 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);
|
||||
if (queue.empty()) {
|
||||
data[0] = BuildData(state, timestamp, now);
|
||||
data[0] = BuildData(state, timestamp, now, injected);
|
||||
return 1;
|
||||
}
|
||||
int count = 0;
|
||||
while (count < num && !queue.empty()) {
|
||||
const QueuedInput& entry = queue.front();
|
||||
data[count++] = BuildData(entry.input, entry.timestamp, now);
|
||||
data[count++] = BuildData(entry.input, entry.timestamp, now, injected);
|
||||
queue.pop_front();
|
||||
}
|
||||
return count;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
#include "prx/libScePad/include/Pad.hpp"
|
||||
#include "prx/libScePad/include/PadInputTypes.hpp"
|
||||
#include "prx/libScePad/include/PadState.hpp"
|
||||
#include "prx/libScePad/include/InputCommandFile.hpp"
|
||||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <thread>
|
||||
|
||||
extern "C" {
|
||||
|
|
@ -25,6 +28,41 @@ int APS5_VABI scePadIsRemoteController(int, bool*);
|
|||
|
||||
static void Require(bool value) { if (!value) std::abort(); }
|
||||
|
||||
static void CheckInputCommands() {
|
||||
const auto path = std::filesystem::temp_directory_path() / ("anyps5-pad-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
struct Cleanup {
|
||||
std::filesystem::path path;
|
||||
~Cleanup() { std::error_code error; std::filesystem::remove(path, error); }
|
||||
} cleanup{path};
|
||||
const auto write = [&](const char* command) { std::ofstream(path) << command << '\n'; };
|
||||
write("1 16384 200");
|
||||
Pad::InputCommandFile commands(path.string().c_str());
|
||||
Require(commands.Buttons(1000) == 0);
|
||||
write("2 16384 200");
|
||||
Require(commands.Buttons(101000) == 0x4000);
|
||||
Require(commands.Buttons(300999) == 0x4000);
|
||||
Require(commands.Buttons(301000) == 0);
|
||||
Require(commands.Buttons(401000) == 0);
|
||||
write("3 8192 5001");
|
||||
Require(commands.Buttons(501000) == 0);
|
||||
write("-3 8192 300");
|
||||
Require(commands.Buttons(601000) == 0);
|
||||
write("3 1 300");
|
||||
Require(commands.Buttons(701000) == 0);
|
||||
write("3 8192 300 trailing");
|
||||
Require(commands.Buttons(801000) == 0);
|
||||
write("3 8192 300");
|
||||
Require(commands.Buttons(901000) == 0x2000);
|
||||
write("4 0 1");
|
||||
Require(commands.Buttons(1001000) == 0);
|
||||
write("2 16384 200");
|
||||
Require(commands.Buttons(1101000) == 0);
|
||||
std::filesystem::remove(path);
|
||||
Require(commands.Buttons(1201000) == 0);
|
||||
Pad::InputCommandFile disabled(nullptr);
|
||||
Require(disabled.Buttons(0) == 0);
|
||||
}
|
||||
|
||||
static float SettleOrientationW() {
|
||||
float w = 1.0f;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
|
|
@ -96,7 +134,43 @@ static void CheckRemoteController(int handle) {
|
|||
Require(!remote);
|
||||
}
|
||||
|
||||
static void CheckInjectedState(const std::filesystem::path& path) {
|
||||
PadInputState physical;
|
||||
physical.buttons = 0x2000;
|
||||
physical.analogButtonsR2 = 73;
|
||||
physical.sticks[0] = 42;
|
||||
PadPublishInput_nid_postfix(physical);
|
||||
const auto before = Pad::ReadState();
|
||||
std::ofstream(path) << "1 16640 300\n";
|
||||
PadData pressed{};
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
||||
do {
|
||||
pressed = Pad::ReadState();
|
||||
if ((pressed.buttons & 0x4000) != 0) break;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
} while (std::chrono::steady_clock::now() < deadline);
|
||||
Require(pressed.buttons == (0x2000 | 0x4000 | 0x100));
|
||||
Require(pressed.analog_buttons_l2 == 255 && pressed.analog_buttons_r2 == 73);
|
||||
Require(pressed.left_stick_x == 42 && pressed.timestamp > before.timestamp);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(310));
|
||||
const auto released = Pad::ReadState();
|
||||
Require(released.buttons == 0x2000 && released.analog_buttons_l2 == 0);
|
||||
Require(released.timestamp > pressed.timestamp);
|
||||
PadPublishInput_nid_postfix(PadInputState{});
|
||||
}
|
||||
|
||||
int main() {
|
||||
CheckInputCommands();
|
||||
const auto path = std::filesystem::temp_directory_path() / ("anyps5-pad-state-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
struct Cleanup {
|
||||
std::filesystem::path path;
|
||||
~Cleanup() { std::error_code error; std::filesystem::remove(path, error); }
|
||||
} cleanup{path};
|
||||
#ifdef _WIN32
|
||||
Require(_putenv_s("APS5_PAD_INPUT_FILE", path.string().c_str()) == 0);
|
||||
#else
|
||||
Require(setenv("APS5_PAD_INPUT_FILE", path.string().c_str(), 1) == 0);
|
||||
#endif
|
||||
constexpr int noHandle = static_cast<int>(0x80920008);
|
||||
constexpr int user = 0x10000000;
|
||||
|
||||
|
|
@ -118,6 +192,7 @@ int main() {
|
|||
CheckReadQueue(handle);
|
||||
CheckReadStateHandle(handle);
|
||||
CheckRemoteController(handle);
|
||||
CheckInjectedState(path);
|
||||
Require(scePadSetVibrationMode(handle, 1) == 0);
|
||||
Require(scePadSetVibrationMode(handle, 2) == 0);
|
||||
Require(scePadSetVibrationMode(handle, 3) == PAD_ERROR_INVALID_ARG);
|
||||
|
|
|
|||
|
|
@ -103,6 +103,19 @@ Windows PowerShell:
|
|||
.\app.exe
|
||||
```
|
||||
|
||||
### Controller input for test runs
|
||||
|
||||
Set `APS5_PAD_INPUT_FILE` to an absolute path before launching the application to enable controller pulses from a local command file. Once the application is reading controller input, send a button with the helper from the repository root:
|
||||
|
||||
```sh
|
||||
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
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### System fonts
|
||||
|
||||
Games that open the console's system font sets (`sceFontOpenFontSet`) need font files in an `anyps5-fonts/` directory beside the output executable; set `ANYPS5_SYSTEM_FONTS` to use another directory. Files dumped from the console are used under their own names (`SST-Roman.otf`, `SST-Bold.otf`, `SSTJpPro-Regular.otf`, ...). Without them, these openly licensed substitutes are used when present: `NotoSans-{Light,Regular,Medium,Bold}.ttf` and `NotoSans-{LightItalic,Italic,MediumItalic,BoldItalic}.ttf` (Latin and Vietnamese), `NotoSansMono-{Light,Regular,Medium,Bold}.ttf` (typewriter), `NotoSansThai-{Light,Regular,Medium,Bold}.ttf` (Thai) and `NotoSansCJK-{Light,Regular,Medium,Bold}.ttc` (Japanese and Chinese). Without either, opening a system font set fails and the game shows no text in those fonts.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
|
||||
BUTTONS = {
|
||||
"l3": 0x2, "r3": 0x4, "options": 0x8,
|
||||
"up": 0x10, "right": 0x20, "down": 0x40, "left": 0x80,
|
||||
"l2": 0x100, "r2": 0x200, "l1": 0x400, "r1": 0x800,
|
||||
"triangle": 0x1000, "circle": 0x2000, "cross": 0x4000,
|
||||
"square": 0x8000, "touchpad": 0x100000, "release": 0,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Queue a controller pulse for APS5_PAD_INPUT_FILE.")
|
||||
parser.add_argument("path", type=Path)
|
||||
parser.add_argument("button", choices=BUTTONS)
|
||||
parser.add_argument("--hold-ms", type=int, default=300)
|
||||
args = parser.parse_args()
|
||||
if not 1 <= args.hold_ms <= 5000:
|
||||
parser.error("--hold-ms must be between 1 and 5000")
|
||||
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")
|
||||
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}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue