fix(pad): expire a held injected button when its scripted press ends

A scripted button that answered a dialog stayed masked from the title
until a title read sampled it released. When the title did not read
during the release, a later scripted press of the same button was
masked as well. The mask now keeps only the buttons the command
timeline held without a break since the last read, so a press that
ends, or a new command, clears it whether or not it was sampled.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
JJenkxandClaude Opus 5.5 committed 2026-10-30 20:57:09 -04:00
1 parent c2a766ec22
commit 2e2f6b5eef
3 files changed
+76 -3

No files matched your search

@@ -60,6 +60,24 @@ public:
std::uint32_t Buttons(std::uint64_t now) { return Poll(now).buttons; }
std::uint32_t HeldThrough(std::uint64_t since, std::uint64_t now) const {
if (events.empty() || since < started || now < since) return 0;
const auto from = since - started;
const auto to = now - started;
auto event = std::upper_bound(events.begin(), events.end(), from,
[](std::uint64_t time, const Event& candidate) { return time < candidate.start; });
if (event == events.begin()) return 0;
--event;
std::uint32_t held = ~0u;
for (std::uint64_t covered = from; event != events.end() && event->start <= covered; ++event) {
if (covered >= event->end) return 0;
held &= event->input.buttons;
covered = event->end;
if (to < covered) return held;
}
return 0;
}
private:
struct Event {
std::uint64_t start = 0;
+10 -3
View File
@@ -19,6 +19,7 @@ namespace {
constexpr std::uint32_t kInterceptedButton = 0x80000000;
std::uint64_t injectedTimestamp = 0;
std::uint32_t injectedHeldThroughCapture = 0;
std::uint64_t injectedHeldAt = 0;
struct QueuedInput {
PadInputState input;
@@ -244,7 +245,7 @@ PadData BuildData(PadSlot& slot, const PadInputState& state, std::uint64_t chang
return data;
}
Pad::InputCommandFile::Input InjectedInput(std::uint64_t now) {
Pad::InputCommandFile& InjectedCommands() {
static Pad::InputCommandFile commands([] {
#ifdef _WIN32
const auto* path = _wgetenv(L"APS5_PAD_INPUT_FILE");
@@ -253,8 +254,12 @@ Pad::InputCommandFile::Input InjectedInput(std::uint64_t now) {
#endif
return path ? std::filesystem::path(path) : std::filesystem::path{};
}());
return commands;
}
Pad::InputCommandFile::Input InjectedInput(std::uint64_t now) {
static Pad::InputCommandFile::Input lastInjected;
const auto injected = commands.Poll(now);
const auto injected = InjectedCommands().Poll(now);
if (injected != lastInjected) {
lastInjected = injected;
slots[0].timestamp = now;
@@ -266,7 +271,8 @@ Pad::InputCommandFile::Input InjectedInput(std::uint64_t now) {
Pad::InputCommandFile::Input InjectedTitleInput(std::uint64_t now) {
auto injected = InjectedInput(now);
if (slots[0].state.intercepted) injectedHeldThroughCapture |= injected.buttons;
else injectedHeldThroughCapture &= injected.buttons;
else injectedHeldThroughCapture &= InjectedCommands().HeldThrough(injectedHeldAt, now);
injectedHeldAt = now;
injected.buttons &= ~injectedHeldThroughCapture;
return injected;
}
@@ -519,6 +525,7 @@ extern "C" void PadApplyInjectedInput_nid_postfix(PadInputState* state) {
std::lock_guard lock(stateMutex);
const auto injected = InjectedInput(sceKernelGetProcessTime());
injectedHeldThroughCapture |= injected.buttons;
injectedHeldAt = sceKernelGetProcessTime();
state->buttons |= injected.buttons;
if (injected.hasSticks) state->sticks = injected.sticks;
}
@@ -7,7 +7,9 @@
#include "prx/libScePad/include/PadState.hpp"
#include "prx/libSceIme/include/ImeInput.hpp"
#include "prx/libSceIme/include/ModalDialog.hpp"
#include <array>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
@@ -157,6 +159,50 @@ void InjectedAnswer(PadInput& padInput, const DisplayWindow& window, const std::
ModalDialogSetPresenter_nid_postfix(false);
}
std::uint32_t TitleButtons(bool useRead) {
if (!useRead) return Pad::ReadState(0).buttons;
std::array<PadData, 8> data{};
const int count = Pad::Read(data.data(), static_cast<int>(data.size()), 0);
Require(count > 0);
return data[static_cast<std::size_t>(count - 1)].buttons;
}
void InjectedPressAfterUnreadRelease(PadInput& padInput, const DisplayWindow& window, const std::filesystem::path& commandPath, std::uint64_t sequence, bool useRead) {
constexpr std::uint32_t Cross = 0x4000;
constexpr std::uint32_t Intercepted = 0x80000000;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
padInput.Update(window);
Require(TitleButtons(useRead) == 0);
ModalDialogSetPresenter_nid_postfix(true);
ModalDialogRequest request;
request.message = u"Create new data?";
request.buttons = {u"OK"};
Require(ModalDialogOpen_nid_postfix(request));
padInput.Update(window);
Require(TitleButtons(useRead) == Intercepted);
std::ofstream(commandPath) << "APS5_PAD_TIMELINE 1 " << sequence << " 2\n0 400 16384\n1000 400 16384\n";
const auto written = std::chrono::steady_clock::now();
std::size_t chosen = 9;
ModalDialogState state = ModalDialogState::Showing;
for (int frame = 0; frame < 30 && state == ModalDialogState::Showing; ++frame) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
padInput.Update(window);
state = ModalDialogPoll_nid_postfix(chosen);
}
Require(state == ModalDialogState::Answered && chosen == 0);
padInput.Update(window);
Require(TitleButtons(useRead) == 0);
Require(std::chrono::steady_clock::now() - written < std::chrono::milliseconds(350));
std::this_thread::sleep_until(written + std::chrono::milliseconds(1150));
padInput.Update(window);
Require(TitleButtons(useRead) == Cross);
std::this_thread::sleep_until(written + std::chrono::milliseconds(1700));
padInput.Update(window);
Require(TitleButtons(useRead) == 0);
ModalDialogReset_nid_postfix();
ModalDialogSetPresenter_nid_postfix(false);
}
}
int main() {
@@ -255,6 +301,8 @@ int main() {
Require(!padInput.SlotOccupied(0));
Require(!padInput.SlotOccupied(1));
InjectedAnswer(padInput, window, commandPath);
InjectedPressAfterUnreadRelease(padInput, window, commandPath, 3, false);
InjectedPressAfterUnreadRelease(padInput, window, commandPath, 4, true);
Require(SDL_JoystickDetachVirtual(dev1) == 0);
Require(SDL_JoystickDetachVirtual(dev0) == 0);