feat(pad): hold the sticks from the controller command file
This commit is contained in:
parent
34a16b7f08
commit
e0eef6c57f
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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{});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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__":
|
||||
|
|
|
|||
Loading…
Reference in New Issue