From 72aa7b87c00cbf29c9fe382c1dfabc1132518430 Mon Sep 17 00:00:00 2001 From: Belcross <139927374+Belcross-Dev@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:04:38 +0100 Subject: [PATCH] Grind exits: work with DualShock 4 and DualSense pads read natively skate. reads a DualShock 4 or DualSense without Steam Input through its own HID reports, not XInput, and the gamepad filter only ran on XInput reads. With such a pad grind exit triggers never saw a trigger: no side was picked, and the grab and lean guards did nothing. The filter now also runs on the game's own native PlayStation reads that the capture already follows (the ones it neutralises while a menu holds the input): the report is parsed into XInput's layout, filtered, and the changed triggers (with their L2/R2 digital bits) and left stick X are written back in the report's own layout, the Bluetooth CRC refreshed. The CRC code is shared with the neutral report's. --- Extension/UI/Overlay/input_capture.cpp | 89 ++++++++++++++++++++------ 1 file changed, 70 insertions(+), 19 deletions(-) diff --git a/Extension/UI/Overlay/input_capture.cpp b/Extension/UI/Overlay/input_capture.cpp index 79c687a8..6a19a268 100644 --- a/Extension/UI/Overlay/input_capture.cpp +++ b/Extension/UI/Overlay/input_capture.cpp @@ -77,6 +77,24 @@ BOOLEAN __stdcall captured_hid_attributes(HANDLE file, PHIDD_ATTRIBUTES attribut SetLastError(error); return result; } +std::atomic& gamepad_filter() { + static std::atomic value{}; + return value; +} +// Full Bluetooth reports have a CRC over the 0xA1 input prefix and payload. +// A stale checksum makes the native reader discard a changed report. +void refresh_bluetooth_crc(std::uint8_t* bytes, DWORD size) noexcept { + if (size != 78 || (bytes[0] != 0x31 && bytes[0] != 0x11)) return; + std::uint32_t crc = 0xffffffffu; + const auto feed = [&](std::uint8_t byte) { + crc ^= byte; + for (unsigned bit = 0; bit < 8; ++bit) crc = (crc >> 1) ^ (0xedb88320u & (0u - (crc & 1u))); + }; + feed(0xa1); + for (DWORD i = 0; i < size - 4; ++i) feed(bytes[i]); + crc = ~crc; + std::memcpy(bytes + size - 4, &crc, sizeof(crc)); +} void neutral_hid(const InputCaptureHooks::HidRead& read, DWORD size) { if (!block_polled_input() || !read.buffer || size > read.capacity) return; auto* bytes = static_cast(read.buffer); @@ -100,25 +118,62 @@ void neutral_hid(const InputCaptureHooks::HidRead& read, DWORD size) { data[4] = 8; data[5] = 0; data[6] &= 0xfc; // retain DS4 sequence bits data[7] = data[8] = 0; } - // Full Bluetooth reports have a CRC over the 0xA1 input prefix and payload. - // A stale checksum makes the native reader discard the neutral report. - if (size == 78 && (bytes[0] == 0x31 || bytes[0] == 0x11)) { - std::uint32_t crc = 0xffffffffu; - const auto feed = [&](std::uint8_t byte) { - crc ^= byte; - for (unsigned bit = 0; bit < 8; ++bit) crc = (crc >> 1) ^ (0xedb88320u & (0u - (crc & 1u))); - }; - feed(0xa1); - for (DWORD i = 0; i < size - 4; ++i) feed(bytes[i]); - crc = ~crc; - std::memcpy(bytes + size - 4, &crc, sizeof(crc)); - } + refresh_bluetooth_crc(bytes, size); static std::atomic reported{}; if (!reported.exchange(true)) logging::printf(logging::Level::info, logging::Channel::input, "Native PlayStation HID report neutralized; overlay retains its independent input stream. (%s over %s, report 0x%02x of %lu bytes, " "the game's buffer %lu.)", read.kind == PlayStationPad::dualsense ? "DualSense" : "DualShock 4", bluetooth ? "Bluetooth" : "USB", bytes[0], size, read.capacity); } +// The gamepad filter (DingoSDKOverlaySetGamepadFilter) on the game's own native PlayStation +// reads, as filter_pad applies it to XInput: a DualShock 4 or DualSense read without Steam Input +// never goes through XInput. The report is parsed into XInput's layout, filtered, and what the +// filter changed (triggers, left stick X) is written back in the report's own layout. +inline constexpr unsigned hid_filter_slot = 3; // the filter's pad slot for native PlayStation pads +void filter_hid(const InputCaptureHooks::HidRead& read, DWORD size) noexcept { + const auto filter = gamepad_filter().load(std::memory_order_acquire); + if (!filter || !read.buffer || !size || size > read.capacity) return; + auto* bytes = static_cast(read.buffer); + const bool bluetooth = playstation_bluetooth(size); + const auto parsed = parse_playstation_report(read.kind, bluetooth, bytes, size); + if (!parsed) return; + const dingosdk::overlay::GamepadValues before{parsed->buttons, parsed->left_trigger, parsed->right_trigger, + parsed->left_x, parsed->left_y, parsed->right_x, parsed->right_y}; + auto pad = before; + filter(hid_filter_slot, pad); + if (pad.left_trigger == before.left_trigger && pad.right_trigger == before.right_trigger && pad.left_x == before.left_x) return; + // Where parse_playstation_report found them: sticks (left X first), the shoulder byte (L2 0x04, + // R2 0x08 digital bits) and the two analog triggers. + std::uint8_t *sticks{}, *shoulder{}, *left{}, *right{}; + const auto id = bytes[0]; + if (read.kind == PlayStationPad::dualsense && ((id == 0x01 && !bluetooth) || id == 0x31)) { + auto* data = bytes + (id == 0x31 ? 2 : 1); + sticks = data; left = data + 4; right = data + 5; shoulder = data + 8; + } else if (id == 0x01) { + sticks = bytes + 1; shoulder = bytes + 6; left = bytes + 8; right = bytes + 9; + } else if (read.kind == PlayStationPad::dualshock4 && id == 0x11) { + sticks = bytes + 3; shoulder = bytes + 8; left = bytes + 10; right = bytes + 11; + } else { + return; + } + if (pad.left_trigger != before.left_trigger) { + *left = pad.left_trigger; + if (!pad.left_trigger) *shoulder = static_cast(*shoulder & ~0x04u); + } + if (pad.right_trigger != before.right_trigger) { + *right = pad.right_trigger; + if (!pad.right_trigger) *shoulder = static_cast(*shoulder & ~0x08u); + } + if (pad.left_x != before.left_x) + sticks[0] = static_cast(std::clamp(static_cast(pad.left_x) / 258 + 128, 0, 255)); + refresh_bluetooth_crc(bytes, size); +} +// A finished native PlayStation read of the game's: neutral while a menu holds the input, else +// through the gamepad filter. +void handle_hid(const InputCaptureHooks::HidRead& read, DWORD size) noexcept { + if (block_polled_input()) neutral_hid(read, size); + else filter_hid(read, size); +} void complete_hid(OVERLAPPED* request, DWORD bytes, bool success, bool finished) { auto& h = input_hooks(); InputCaptureHooks::HidRead read{}; @@ -129,7 +184,7 @@ void complete_hid(OVERLAPPED* request, DWORD bytes, bool success, bool finished) read = found->second; if (finished) h.hid_reads.erase(found); } - if (success) neutral_hid(read, bytes); + if (success) handle_hid(read, bytes); } BOOL WINAPI captured_read_file(HANDLE file, LPVOID buffer, DWORD size, LPDWORD bytes, LPOVERLAPPED request) { auto& h = input_hooks(); @@ -163,7 +218,7 @@ BOOL WINAPI captured_read_file(HANDLE file, LPVOID buffer, DWORD size, LPDWORD b const auto result = original(h.read_file)(file, buffer, size, bytes, request); const auto error = GetLastError(); if (read.kind != PlayStationPad::none) { - if (result) neutral_hid(read, bytes ? *bytes : request ? static_cast(request->InternalHigh) : 0); + if (result) handle_hid(read, bytes ? *bytes : request ? static_cast(request->InternalHigh) : 0); if (!result && error != ERROR_IO_PENDING && request) complete_hid(request, 0, false, true); } SetLastError(error); @@ -266,10 +321,6 @@ std::string xinput_entry_owners() { return text.empty() ? "none" : text; } -std::atomic& gamepad_filter() { - static std::atomic value{}; - return value; -} // The game's own read only: not ReSkate's (overlay_input_access) and not an inner layer of a // read already being answered (Steam's hook going on to XInput runs inside OverlayInputAccess). void filter_pad(bool outer, DWORD user, XINPUT_STATE* output) noexcept {