Files
ReSkate/Extension/UI/Overlay/input_capture.cpp
T
AyeZeeBBandClaude Opus 5.5 a0b4d4c0aa Overlay: the PlayStation pad reader no longer needs the rest of the overlay to link
The reader's input-access counter was defined in input_capture.cpp. Built on
its own, as the launcher's controller support (#63) builds it, the reader
then failed to link with an unresolved overlay_input_access. The counter is
now defined beside the reader; the overlay uses the same one as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:02:12 -05:00

747 lines
42 KiB
C++

#include "Engine/Core/Log/logging.h"
#include "overlay_internal.h"
#include "input_capture.h"
#include "held_input.h"
#include "Engine/Game/Input/playstation_report.h"
#include <hidsdi.h>
#include <atomic>
#include <format>
#include <array>
#include <cstring>
#include <string>
#include <mutex>
#include <set>
#include <span>
#include <tuple>
namespace dingosdk::overlay::detail {
// Window messages do not cover the game's independently polled input devices.
// Only the overlay's own Win32 backend and binding reader may bypass these gates.
thread_local HRAWINPUT noted_raw_input = nullptr;
bool block_polled_input() {
const auto error = GetLastError();
const bool capture = !overlay_input_access && (owns_menu_cursor(state()) || state().freecam_controller_active.load(std::memory_order_relaxed));
SetLastError(error);
return capture;
}
void note_input_record(const RAWINPUT& record) noexcept;
HeldInput& held_input() { static auto* value = new HeldInput; return *value; }
struct InputCaptureHooks {
Hook hid_attributes, read_file, overlapped, overlapped_ex, completion, completion_ex, close_handle;
struct HidRead { HANDLE file; void* buffer; DWORD capacity; PlayStationPad kind; };
std::mutex hid_mutex;
std::map<HANDLE, PlayStationPad> hid_devices;
std::map<OVERLAPPED*, HidRead> hid_reads;
Hook register_raw;
Hook async_key, key, keyboard, raw_data, raw_buffer, capture, release_capture, cursor, show_cursor, physical_cursor, cursor_position;
std::array<Hook, 5> xinput_state, xinput_extended, xinput_keystroke;
Hook direct_create;
std::array<Hook, 2> device_create;
// Mouse/keyboard and A/W devices may have distinct implementations. Slots
// describe callable addresses, not the CreateDevice interface index.
static constexpr std::size_t device_slots = 8;
std::array<Hook, device_slots> device_state, device_data, device_format;
struct Format { DWORD size{}; std::vector<DIOBJECTDATAFORMAT> objects; };
std::mutex formats_mutex;
std::map<void*, Format> formats;
};
InputCaptureHooks& input_hooks() { static auto* hooks = new InputCaptureHooks; return *hooks; }
BOOLEAN __stdcall captured_hid_attributes(HANDLE file, PHIDD_ATTRIBUTES attributes) {
const auto result = original<decltype(&HidD_GetAttributes)>(input_hooks().hid_attributes)(file, attributes);
const auto error = GetLastError();
if (result && attributes) {
const auto kind = playstation_pad(attributes->VendorID, attributes->ProductID);
if (kind != PlayStationPad::none) {
std::lock_guard lock(input_hooks().hid_mutex);
input_hooks().hid_devices[file] = kind;
}
}
SetLastError(error);
return result;
}
void neutral_hid(const InputCaptureHooks::HidRead& read, DWORD size) {
if (!block_polled_input() || !read.buffer || size > read.capacity) return;
auto* bytes = static_cast<std::uint8_t*>(read.buffer);
if (!parse_playstation_report(read.kind, playstation_bluetooth(read.capacity), bytes, size)) return;
const bool sense_full = read.kind == PlayStationPad::dualsense &&
(bytes[0] == 0x31 || (bytes[0] == 1 && !playstation_bluetooth(read.capacity)));
const unsigned offset = bytes[0] == 0x31 ? 2 : bytes[0] == 0x11 ? 3 : 1;
auto* data = bytes + offset;
std::fill_n(data, 4, std::uint8_t{128});
if (sense_full) {
data[4] = data[5] = 0; // triggers; retain report sequence at +6
data[7] = 8; data[8] = data[9] = 0;
} else {
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));
}
static std::atomic<bool> reported{};
if (!reported.exchange(true)) logging::write(logging::Level::info, logging::Channel::input,
"Native PlayStation HID report neutralized; overlay retains its independent input stream.");
}
void complete_hid(OVERLAPPED* request, DWORD bytes, bool success, bool finished) {
auto& h = input_hooks();
InputCaptureHooks::HidRead read{};
{
std::lock_guard lock(h.hid_mutex);
const auto found = h.hid_reads.find(request);
if (found == h.hid_reads.end()) return;
read = found->second;
if (finished) h.hid_reads.erase(found);
}
if (success) neutral_hid(read, bytes);
}
BOOL WINAPI captured_read_file(HANDLE file, LPVOID buffer, DWORD size, LPDWORD bytes, LPOVERLAPPED request) {
auto& h = input_hooks();
InputCaptureHooks::HidRead read{file, buffer, size, PlayStationPad::none};
if (!overlay_input_access) {
// Some readers inspect a temporary enumeration handle, then open a
// second handle for reports. Identify that handle as well.
bool known{};
{
std::lock_guard lock(h.hid_mutex);
known = h.hid_devices.contains(file);
}
if (!known && size >= 10 && size <= 512) {
const auto error = GetLastError();
HIDD_ATTRIBUTES attributes{sizeof(attributes)};
const auto okay = original<decltype(&HidD_GetAttributes)>(h.hid_attributes)(file, &attributes);
const auto kind = okay ? playstation_pad(attributes.VendorID, attributes.ProductID) : PlayStationPad::none;
{
std::lock_guard lock(h.hid_mutex);
h.hid_devices[file] = kind;
}
SetLastError(error);
}
std::lock_guard lock(h.hid_mutex);
const auto found = h.hid_devices.find(file);
if (found != h.hid_devices.end() && found->second != PlayStationPad::none) {
read.kind = found->second;
if (request) h.hid_reads[request] = read;
}
}
const auto result = original<decltype(&ReadFile)>(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<DWORD>(request->InternalHigh) : 0);
if (!result && error != ERROR_IO_PENDING && request) complete_hid(request, 0, false, true);
}
SetLastError(error);
return result;
}
BOOL WINAPI captured_overlapped(HANDLE file, LPOVERLAPPED request, LPDWORD bytes, BOOL wait) {
const auto result = original<decltype(&GetOverlappedResult)>(input_hooks().overlapped)(file, request, bytes, wait);
const auto error = GetLastError();
complete_hid(request, result && bytes ? *bytes : 0, result != FALSE, result || error != ERROR_IO_INCOMPLETE);
SetLastError(error); return result;
}
BOOL WINAPI captured_overlapped_ex(HANDLE file, LPOVERLAPPED request, LPDWORD bytes, DWORD timeout, BOOL alertable) {
const auto result = original<decltype(&GetOverlappedResultEx)>(input_hooks().overlapped_ex)(file, request, bytes, timeout, alertable);
const auto error = GetLastError();
complete_hid(request, result && bytes ? *bytes : 0, result != FALSE,
result || (error != ERROR_IO_INCOMPLETE && error != WAIT_TIMEOUT && error != WAIT_IO_COMPLETION));
SetLastError(error); return result;
}
BOOL WINAPI captured_completion(HANDLE port, LPDWORD bytes, PULONG_PTR key, LPOVERLAPPED* request, DWORD timeout) {
const auto result = original<decltype(&GetQueuedCompletionStatus)>(input_hooks().completion)(port, bytes, key, request, timeout);
const auto error = GetLastError();
if (request && *request) complete_hid(*request, result && bytes ? *bytes : 0, result != FALSE, true);
SetLastError(error); return result;
}
BOOL WINAPI captured_completion_ex(HANDLE port, LPOVERLAPPED_ENTRY entries, ULONG count, PULONG removed, DWORD timeout, BOOL alertable) {
const auto result = original<decltype(&GetQueuedCompletionStatusEx)>(input_hooks().completion_ex)(port, entries, count, removed, timeout, alertable);
const auto error = GetLastError();
if (result && entries && removed) for (ULONG i = 0; i < *removed; ++i)
complete_hid(entries[i].lpOverlapped, entries[i].dwNumberOfBytesTransferred,
static_cast<LONG>(entries[i].Internal) >= 0, true);
SetLastError(error); return result;
}
BOOL WINAPI captured_close_handle(HANDLE file) {
auto& h = input_hooks();
{
std::lock_guard lock(h.hid_mutex);
if (h.hid_devices.erase(file))
std::erase_if(h.hid_reads, [&](const auto& entry) { return entry.second.file == file; });
}
return original<decltype(&CloseHandle)>(h.close_handle)(file);
}
SHORT WINAPI captured_async_key(int key) {
const auto value = original<SHORT(WINAPI*)(int)>(input_hooks().async_key)(key);
return block_polled_input() ? 0 : value;
}
SHORT WINAPI captured_key(int key) {
const auto value = original<SHORT(WINAPI*)(int)>(input_hooks().key)(key);
return block_polled_input() ? 0 : value;
}
BOOL WINAPI captured_keyboard(PBYTE keys) {
const auto result = original<BOOL(WINAPI*)(PBYTE)>(input_hooks().keyboard)(keys);
if (result && keys && block_polled_input()) std::memset(keys, 0, 256);
return result;
}
UINT WINAPI captured_raw_data(HRAWINPUT input, UINT command, LPVOID data, PUINT size, UINT header_size) {
const auto result = original<UINT(WINAPI*)(HRAWINPUT, UINT, LPVOID, PUINT, UINT)>(
input_hooks().raw_data)(input, command, data, size, header_size);
if (result != UINT(-1) && data && command == RID_INPUT && !overlay_input_access && input != noted_raw_input &&
result >= offsetof(RAWINPUT, data) + sizeof(RAWMOUSE) &&
static_cast<RAWINPUT*>(data)->header.dwType == RIM_TYPEMOUSE)
note_raw_mouse_packet(static_cast<RAWINPUT*>(data)->data.mouse);
if (result != UINT(-1) && data && command == RID_INPUT && !overlay_input_access &&
result >= offsetof(RAWINPUT, data) + sizeof(RAWKEYBOARD))
note_input_record(*static_cast<RAWINPUT*>(data));
if (result != UINT(-1) && data && command == RID_INPUT && result >= sizeof(RAWINPUTHEADER)) {
auto* raw = static_cast<RAWINPUT*>(data);
const bool mouse = raw->header.dwType == RIM_TYPEMOUSE && result >= offsetof(RAWINPUT, data) + sizeof(RAWMOUSE);
const bool keyboard =
raw->header.dwType == RIM_TYPEKEYBOARD && result >= offsetof(RAWINPUT, data) + sizeof(RAWKEYBOARD);
if (!block_polled_input()) {
if ((mouse || keyboard) && !overlay_input_access) held_input().seen(*raw);
} else if ((mouse || keyboard) && held_input().release_only(*raw)) {
// A key or button the game holds still comes up; presses stay blocked.
} else if (mouse)
raw->data.mouse = {};
else if (keyboard) {
raw->data.keyboard = {};
raw->data.keyboard.VKey = 255; // documented discard packet; never synthesize a key press
raw->data.keyboard.Flags = RI_KEY_BREAK;
raw->data.keyboard.Message = WM_KEYUP;
} else if (raw->header.dwType == RIM_TYPEHID && result >= offsetof(RAWINPUT, data) + 8)
raw->data.hid.dwCount = 0;
}
return result;
}
// The latest keyboard and mouse input the game read, with the device it came from, for the
// log line when the game switches to mouse and keyboard: which input made it switch. Raw
// input injected by software (SendInput: Steam Input's desktop layout, macro tools) carries
// no device handle.
struct LastInput {
std::atomic<ULONGLONG> key_at{}, mouse_at{}, message_at{};
std::atomic<std::uint32_t> key{}; // virtual key | make code << 16
std::atomic<std::int32_t> mouse_x{}, mouse_y{};
std::atomic<std::uint32_t> mouse_buttons{};
std::atomic<HANDLE> key_device{}, mouse_device{};
std::atomic<std::uint32_t> message{}, message_key{}; // the game window's last input message
};
LastInput& last_input() { static auto* value = new LastInput; return *value; }
void note_input_record(const RAWINPUT& record) noexcept {
auto& last = last_input();
if (record.header.dwType == RIM_TYPEKEYBOARD) {
last.key.store(record.data.keyboard.VKey | (std::uint32_t{record.data.keyboard.MakeCode} << 16),
std::memory_order_relaxed);
last.key_device.store(record.header.hDevice, std::memory_order_relaxed);
last.key_at.store(GetTickCount64(), std::memory_order_relaxed);
} else if (record.header.dwType == RIM_TYPEMOUSE &&
(record.data.mouse.lLastX || record.data.mouse.lLastY || record.data.mouse.usButtonFlags)) {
last.mouse_x.store(record.data.mouse.lLastX, std::memory_order_relaxed);
last.mouse_y.store(record.data.mouse.lLastY, std::memory_order_relaxed);
last.mouse_buttons.store(record.data.mouse.usButtonFlags, std::memory_order_relaxed);
last.mouse_device.store(record.header.hDevice, std::memory_order_relaxed);
last.mouse_at.store(GetTickCount64(), std::memory_order_relaxed);
}
}
std::string input_device_name(HANDLE device) {
if (!device) return "no device (injected by software)";
std::array<wchar_t, 256> name{};
UINT size = static_cast<UINT>(name.size());
if (GetRawInputDeviceInfoW(device, RIDI_DEVICENAME, name.data(), &size) == UINT(-1)) return "an unknown device";
std::string text;
for (const wchar_t* c = name.data(); *c && text.size() < 120; ++c) text.push_back(*c < 128 ? static_cast<char>(*c) : '?');
return text;
}
void note_input_message(UINT message, WPARAM wp) noexcept {
auto& last = last_input();
last.message.store(message, std::memory_order_relaxed);
last.message_key.store(static_cast<std::uint32_t>(wp), std::memory_order_relaxed);
last.message_at.store(GetTickCount64(), std::memory_order_relaxed);
}
// What the game last read from the keyboard and mouse, within the 3 s before now.
std::string recent_input() {
auto& last = last_input();
const auto now = GetTickCount64();
std::string text;
const auto age = [&](ULONGLONG at) { return std::to_string(now - at) + " ms before"; };
if (const auto at = last.key_at.load(); at && now - at <= 3000) {
const auto key = last.key.load();
text += std::format("key 0x{:02x} (scan 0x{:02x}) from {}, {}", key & 0xffff, key >> 16,
input_device_name(last.key_device.load()), age(at));
}
if (const auto at = last.mouse_at.load(); at && now - at <= 3000) {
if (!text.empty()) text += "; ";
text += std::format("mouse move ({}, {}) buttons 0x{:x} from {}, {}", last.mouse_x.load(), last.mouse_y.load(),
last.mouse_buttons.load(), input_device_name(last.mouse_device.load()), age(at));
}
if (const auto at = last.message_at.load(); at && now - at <= 3000) {
if (!text.empty()) text += "; ";
text += std::format("window message 0x{:x} ({:#x}), {}", last.message.load(), last.message_key.load(), age(at));
}
return text.empty() ? "no keyboard or mouse input in the 3 s before" : text;
}
UINT WINAPI captured_raw_buffer(PRAWINPUT data, PUINT size, UINT header_size) {
// Calling the API drains queued input. While the overlay owns input the game gets only the
// releases of keys and buttons it holds (HeldInput), so nothing pressed in the menu or chat
// replays when it closes and nothing pressed before it stays stuck.
const auto result = original<UINT(WINAPI*)(PRAWINPUT, PUINT, UINT)>(
input_hooks().raw_buffer)(data, size, header_size);
const bool capture = result != UINT(-1) && data && block_polled_input();
if (result != UINT(-1) && data && header_size == sizeof(RAWINPUTHEADER)) {
// Skate drains its mouse here; read the packets on the way past.
// NEXTRAWINPUTBLOCK: records are 8-byte aligned on x64.
const auto align = [](std::uintptr_t at) { return (at + 7) & ~std::uintptr_t{7}; };
auto* record = data;
auto write = reinterpret_cast<std::uintptr_t>(data);
UINT kept{};
for (UINT i = 0; i < result && record; ++i) {
const auto bytes = record->header.dwSize;
const auto next = align(reinterpret_cast<std::uintptr_t>(record) + bytes);
if (record->header.dwType == RIM_TYPEMOUSE) note_raw_mouse_packet(record->data.mouse);
note_input_record(*record);
const bool mouse = record->header.dwType == RIM_TYPEMOUSE && bytes >= offsetof(RAWINPUT, data) + sizeof(RAWMOUSE);
const bool keyboard =
record->header.dwType == RIM_TYPEKEYBOARD && bytes >= offsetof(RAWINPUT, data) + sizeof(RAWKEYBOARD);
if (!capture) {
if ((mouse || keyboard) && !overlay_input_access) held_input().seen(*record);
} else if ((mouse || keyboard) && held_input().release_only(*record)) {
// Kept records move forward over the dropped ones, in the same layout.
if (write != reinterpret_cast<std::uintptr_t>(record))
std::memmove(reinterpret_cast<void*>(write), record, bytes);
write = align(write + bytes);
++kept;
}
record = reinterpret_cast<RAWINPUT*>(next);
}
if (capture) return kept;
}
return capture ? 0 : result;
}
// The game's latest mouse registration, and whether the overlay has replaced
// it with a cursor-moving one.
struct RawMouse {
std::mutex mutex;
RAWINPUTDEVICE game{};
bool known{}, overridden{};
};
RawMouse& raw_mouse() { static auto* value = new RawMouse; return *value; }
constexpr DWORD cursor_blocking_flags = RIDEV_NOLEGACY | RIDEV_CAPTUREMOUSE | RIDEV_NOHOTKEYS;
bool is_mouse(const RAWINPUTDEVICE& device) { return device.usUsagePage == 1 && device.usUsage == 2; }
// Logs how the game registers raw input (flags change with its input mode),
// once per distinct registration.
BOOL WINAPI captured_register_raw(PCRAWINPUTDEVICE devices, UINT count, UINT size) {
const auto register_raw = original<BOOL(WINAPI*)(PCRAWINPUTDEVICE, UINT, UINT)>(input_hooks().register_raw);
BOOL result{};
if (devices && count && size == sizeof(RAWINPUTDEVICE) && count <= 64) {
std::vector<RAWINPUTDEVICE> requested(devices, devices + count);
auto& mouse = raw_mouse();
std::lock_guard lock(mouse.mutex);
for (auto& device : requested) {
if (!is_mouse(device)) continue;
if (device.dwFlags & RIDEV_REMOVE) mouse.known = false;
else { mouse.game = device; mouse.known = true; }
// While the overlay owns the pointer the cursor must keep moving.
if (mouse.overridden && !(device.dwFlags & RIDEV_REMOVE)) device.dwFlags &= ~cursor_blocking_flags;
}
result = register_raw(requested.data(), count, size);
if (result)
for (const auto& device : requested)
if (is_mouse(device))
state().mouse_legacy_blocked.store(!(device.dwFlags & RIDEV_REMOVE) &&
(device.dwFlags & RIDEV_NOLEGACY) == RIDEV_NOLEGACY);
// Skate re-registers the mouse whenever it switches between controller and
// mouse/keyboard: the one trace of which input it thinks the player uses.
static DWORD last_mode = ~0ul;
for (const auto& device : devices ? std::span(devices, count) : std::span<const RAWINPUTDEVICE>{})
if (is_mouse(device) && device.dwFlags != last_mode) {
last_mode = device.dwFlags;
// Leaving controller mode: say what input came just before.
const auto cause = device.dwFlags & RIDEV_NOLEGACY ? std::string{} : "; last input: " + recent_input();
dingosdk::logging::printf(dingosdk::logging::Level::info, dingosdk::logging::Channel::input,
"Game input mode: mouse registered with flags 0x%lx (%s)%s%s.", device.dwFlags,
device.dwFlags & RIDEV_REMOVE ? "removed"
: (device.dwFlags & RIDEV_NOLEGACY) ? "controller: no Windows mouse messages" : "mouse and keyboard",
mouse.overridden ? "; the overlay owns the pointer" : "", cause.c_str());
}
} else {
result = register_raw(devices, count, size);
}
try {
static std::mutex mutex;
static std::set<std::tuple<USHORT, USHORT, DWORD>> seen;
for (UINT i = 0; devices && size == sizeof(RAWINPUTDEVICE) && i < count; ++i) {
const auto& device = devices[i];
{
std::lock_guard lock(mutex);
if (!seen.emplace(device.usUsagePage, device.usUsage, device.dwFlags).second) continue;
}
dingosdk::logging::printf(dingosdk::logging::Level::debug, dingosdk::logging::Channel::input,
"Game raw input registration: page=%u usage=%u flags=0x%lx window=%p result=%d",
device.usUsagePage, device.usUsage, device.dwFlags, device.hwndTarget, result ? 1 : 0);
}
} catch (...) {}
return result;
}
void apply_raw_mouse(RawMouse& mouse, bool overlay) {
auto device = mouse.game;
if (overlay) device.dwFlags &= ~cursor_blocking_flags;
const auto register_raw = original<BOOL(WINAPI*)(PCRAWINPUTDEVICE, UINT, UINT)>(input_hooks().register_raw);
OverlayInputAccess access;
const bool applied = register_raw ? register_raw(&device, 1, sizeof(device)) != FALSE
: RegisterRawInputDevices(&device, 1, sizeof(device)) != FALSE;
if (applied) state().mouse_legacy_blocked.store((device.dwFlags & RIDEV_NOLEGACY) == RIDEV_NOLEGACY);
dingosdk::logging::printf(dingosdk::logging::Level::info, dingosdk::logging::Channel::input,
"Raw mouse registration %s (flags=0x%lx, %s, Windows error %lu).",
overlay ? "released for the overlay" : "returned to the game", device.dwFlags, applied ? "ok" : "failed",
applied ? 0ul : GetLastError());
}
HWND WINAPI captured_capture(HWND window) {
if (block_polled_input() || (overlay_input_access &&
GetWindowThreadProcessId(window, nullptr) != GetCurrentThreadId())) return GetCapture();
return original<HWND(WINAPI*)(HWND)>(input_hooks().capture)(window);
}
BOOL WINAPI captured_release_capture() {
if (block_polled_input()) return TRUE;
return original<BOOL(WINAPI*)()>(input_hooks().release_capture)();
}
HCURSOR WINAPI captured_cursor(HCURSOR cursor) {
if (block_polled_input()) return GetCursor();
return original<HCURSOR(WINAPI*)(HCURSOR)>(input_hooks().cursor)(cursor);
}
int WINAPI captured_show_cursor(BOOL show) {
// ImGui draws the menu cursor. Do not let the game change the OS display
// counter while it is open; the counter resumes unchanged on close.
if (block_polled_input()) return show ? 0 : -1;
return original<int(WINAPI*)(BOOL)>(input_hooks().show_cursor)(show);
}
BOOL WINAPI captured_physical_cursor(int x, int y) {
if (block_polled_input()) return TRUE;
return original<BOOL(WINAPI*)(int, int)>(input_hooks().physical_cursor)(x, y);
}
BOOL WINAPI captured_cursor_position(LPPOINT output) {
thread_local POINT last{};
thread_local bool sampled = false;
if (output && block_polled_input() && sampled) { *output = last; return TRUE; }
const auto result = original<BOOL(WINAPI*)(LPPOINT)>(input_hooks().cursor_position)(output);
if (result && output && !overlay_input_access) { last = *output; sampled = true; }
return result;
}
template<std::size_t I, bool Extended = false>
DWORD WINAPI captured_xinput(DWORD user, XINPUT_STATE* output) {
const auto& hook = Extended ? input_hooks().xinput_extended[I] : input_hooks().xinput_state[I];
const bool capture = block_polled_input();
DWORD result;
{ // XInput exports can forward through another hooked XInput export.
OverlayInputAccess access;
result = original<DWORD(WINAPI*)(DWORD, XINPUT_STATE*)>(hook)(user, output);
}
if (result == ERROR_SUCCESS && output && capture) {
output->Gamepad = {};
// The game must notice a neutral state even if the physical packet did
// not change between opening the menu and its next controller poll.
output->dwPacketNumber ^= 0x80000000u;
}
return result;
}
template<std::size_t I>
DWORD WINAPI captured_keystroke(DWORD user, DWORD reserved, PXINPUT_KEYSTROKE output) {
const auto result = original<DWORD(WINAPI*)(DWORD, DWORD, PXINPUT_KEYSTROKE)>(
input_hooks().xinput_keystroke[I])(user, reserved, output);
if (result == ERROR_SUCCESS && output && block_polled_input()) { *output = {}; return ERROR_EMPTY; }
return result;
}
template<std::size_t I>
HRESULT STDMETHODCALLTYPE captured_device_format(void* device, const DIDATAFORMAT* format) {
const auto result = original<HRESULT(STDMETHODCALLTYPE*)(void*, const DIDATAFORMAT*)>(
input_hooks().device_format[I])(device, format);
if (SUCCEEDED(result) && format && format->dwSize == sizeof(DIDATAFORMAT) &&
format->dwObjSize == sizeof(DIOBJECTDATAFORMAT) && format->dwNumObjs <= 1024 &&
(!format->dwNumObjs || format->rgodf)) {
InputCaptureHooks::Format copy;
copy.size = format->dwDataSize;
for (DWORD i = 0; i < format->dwNumObjs; ++i) copy.objects.push_back(format->rgodf[i]);
std::lock_guard lock(input_hooks().formats_mutex);
input_hooks().formats[device] = std::move(copy);
}
return result;
}
template<std::size_t I>
HRESULT STDMETHODCALLTYPE captured_device_state(void* device, DWORD size, LPVOID data) {
const auto result = original<HRESULT(STDMETHODCALLTYPE*)(void*, DWORD, LPVOID)>(
input_hooks().device_state[I])(device, size, data);
if (SUCCEEDED(result) && data && block_polled_input()) {
std::memset(data, 0, size);
std::lock_guard lock(input_hooks().formats_mutex);
const auto found = input_hooks().formats.find(device);
if (found != input_hooks().formats.end() && found->second.size == size) {
for (const auto& object : found->second.objects) {
if (object.dwOfs > size || size - object.dwOfs < 4) continue;
DWORD neutral = 0;
if (object.dwType & DIDFT_POV) neutral = 0xffffffffu;
else if (object.dwType & DIDFT_ABSAXIS) {
neutral = 32767; // Standard DirectInput axis midpoint if range lookup fails.
DIPROPRANGE range{};
range.diph = {sizeof(range), sizeof(range.diph), object.dwOfs, DIPH_BYOFFSET};
auto** vtable = *reinterpret_cast<void***>(device);
using GetProperty = HRESULT(STDMETHODCALLTYPE*)(void*, REFGUID, LPDIPROPHEADER);
if (SUCCEEDED(reinterpret_cast<GetProperty>(vtable[5])(device, DIPROP_RANGE, &range.diph)))
neutral = static_cast<DWORD>((static_cast<std::int64_t>(range.lMin) + range.lMax) / 2);
} else continue;
std::memcpy(static_cast<std::byte*>(data) + object.dwOfs, &neutral, 4);
}
} else if (size == sizeof(DIJOYSTATE) || size == sizeof(DIJOYSTATE2)) {
// A zeroed DirectInput axis is full travel, not the center. Some
// controllers use the standard joystick layout without a format
// record we can inspect; keep those pads neutral while captured.
auto* bytes = static_cast<std::byte*>(data);
const DWORD center = 32767;
const DWORD released_pov = 0xffffffffu;
for (std::size_t offset = 0; offset < 8 * sizeof(DWORD); offset += sizeof(DWORD))
std::memcpy(bytes + offset, &center, sizeof(center));
for (std::size_t offset = 8 * sizeof(DWORD); offset < 12 * sizeof(DWORD); offset += sizeof(DWORD))
std::memcpy(bytes + offset, &released_pov, sizeof(released_pov));
}
}
return result;
}
template<std::size_t I>
HRESULT STDMETHODCALLTYPE captured_device_data(void* device, DWORD size, LPDIDEVICEOBJECTDATA data, LPDWORD count, DWORD flags) {
const bool capture = block_polled_input();
const auto result = original<HRESULT(STDMETHODCALLTYPE*)(void*, DWORD, LPDIDEVICEOBJECTDATA, LPDWORD, DWORD)>(
input_hooks().device_data[I])(device, size, data, count, capture ? flags & ~DIGDD_PEEK : flags);
if (SUCCEEDED(result) && capture && count) *count = 0;
return result;
}
bool install_device_capture(void* device) {
auto** table = *reinterpret_cast<void***>(device);
auto& hooks = input_hooks();
const auto states = []<std::size_t... I>(std::index_sequence<I...>) {
return std::array{reinterpret_cast<void*>(captured_device_state<I>)...};
}(std::make_index_sequence<InputCaptureHooks::device_slots>{});
const auto data = []<std::size_t... I>(std::index_sequence<I...>) {
return std::array{reinterpret_cast<void*>(captured_device_data<I>)...};
}(std::make_index_sequence<InputCaptureHooks::device_slots>{});
const auto formats = []<std::size_t... I>(std::index_sequence<I...>) {
return std::array{reinterpret_cast<void*>(captured_device_format<I>)...};
}(std::make_index_sequence<InputCaptureHooks::device_slots>{});
const auto add = [&](auto& family, void* target, const auto& detours) {
for (const auto& hook : family) if (hook.target == target) return true;
for (std::size_t slot = 0; slot < family.size(); ++slot)
if (!family[slot].target) return install(family[slot], target, detours[slot]);
return false;
};
// Keep installing independent methods if one fails. Report the exact gap.
const bool state_ready = add(hooks.device_state, table[9], states);
const bool data_ready = add(hooks.device_data, table[10], data);
const bool format_ready = add(hooks.device_format, table[11], formats);
if (!state_ready || !data_ready || !format_ready)
logging::log(logging::Level::warning, logging::Channel::input,
"DirectInput capture incomplete: state={}, buffered={}, format={}; mouse input may reach the game while the menu is open.",
state_ready, data_ready, format_ready);
std::lock_guard formats_lock(hooks.formats_mutex);
hooks.formats.erase(device); // COM addresses can be reused after Release.
return state_ready && data_ready && format_ready;
}
template<std::size_t I>
HRESULT STDMETHODCALLTYPE captured_device_create(void* owner, REFGUID guid, void** output, LPUNKNOWN outer) {
const auto result = original<HRESULT(STDMETHODCALLTYPE*)(void*, REFGUID, void**, LPUNKNOWN)>(
input_hooks().device_create[I])(owner, guid, output, outer);
if (SUCCEEDED(result) && output && *output) {
std::lock_guard lock(state().hook_mutex);
install_device_capture(*output);
}
return result;
}
bool prepare_standard_devices(HMODULE direct) {
using Create = HRESULT(WINAPI*)(HINSTANCE, DWORD, REFIID, void**, IUnknown*);
const auto create = reinterpret_cast<Create>(GetProcAddress(direct, "DirectInput8Create"));
if (!create) return false;
bool ready = true;
std::array<void*, 2> create_targets{};
std::size_t create_count{};
for (const auto& iid : {IID_IDirectInput8A, IID_IDirectInput8W}) {
ComPtr<IUnknown> owner;
if (FAILED(create(GetModuleHandleW(nullptr), DIRECTINPUT_VERSION, iid,
reinterpret_cast<void**>(owner.GetAddressOf()), nullptr))) { ready = false; continue; }
auto** table = *reinterpret_cast<void***>(owner.Get());
create_targets[create_count++] = table[3];
using CreateDevice = HRESULT(STDMETHODCALLTYPE*)(void*, REFGUID, void**, IUnknown*);
for (const auto& guid : {GUID_SysMouse, GUID_SysKeyboard}) {
ComPtr<IUnknown> device;
if (FAILED(reinterpret_cast<CreateDevice>(table[3])(owner.Get(), guid,
reinterpret_cast<void**>(device.GetAddressOf()), nullptr))) { ready = false; continue; }
// No Acquire or cooperative-level change: these temporary objects
// identify shared implementations before the game starts workers.
if (!install_device_capture(device.Get())) ready = false;
}
}
auto& family = input_hooks().device_create;
const std::array detours{reinterpret_cast<void*>(captured_device_create<0>),
reinterpret_cast<void*>(captured_device_create<1>)};
for (std::size_t i = 0; i < create_count; ++i) {
if (std::any_of(family.begin(), family.end(), [&](const auto& hook) { return hook.target == create_targets[i]; })) continue;
const auto slot = std::find_if(family.begin(), family.end(), [](const auto& hook) { return !hook.target; });
if (slot == family.end() || !install(*slot, create_targets[i], detours[slot - family.begin()])) ready = false;
}
return ready;
}
HRESULT WINAPI captured_direct_create(HINSTANCE instance, DWORD version, REFIID iid, LPVOID* output, LPUNKNOWN outer) {
const auto result = original<HRESULT(WINAPI*)(HINSTANCE, DWORD, REFIID, LPVOID*, LPUNKNOWN)>(
input_hooks().direct_create)(instance, version, iid, output, outer);
if (SUCCEEDED(result) && output && *output) {
auto** table = *reinterpret_cast<void***>(*output);
std::lock_guard lock(state().hook_mutex);
auto& hooks = input_hooks();
// A/W implementations sometimes share their callable address.
for (const auto& hook : hooks.device_create) if (hook.target == table[3]) return result;
if (!hooks.device_create[0].target)
install(hooks.device_create[0], table[3], reinterpret_cast<void*>(captured_device_create<0>));
else install(hooks.device_create[1], table[3], reinterpret_cast<void*>(captured_device_create<1>));
}
return result;
}
namespace {
// `owns_pointer` is asked only when the packet moves a cursor Windows is not moving.
template<class OwnsPointer> void note_packet(const RAWMOUSE& mouse, OwnsPointer&& owns_pointer) {
auto& s = state();
const bool relative = !(mouse.usFlags & MOUSE_MOVE_ABSOLUTE);
if (relative) {
s.raw_mouse_x.fetch_add(mouse.lLastX);
s.raw_mouse_y.fetch_add(mouse.lLastY);
}
auto buttons = s.raw_mouse_buttons.load();
const auto flags = mouse.usButtonFlags;
if (flags & RI_MOUSE_LEFT_BUTTON_DOWN) buttons |= 1u;
if (flags & RI_MOUSE_LEFT_BUTTON_UP) buttons &= ~1u;
if (flags & RI_MOUSE_RIGHT_BUTTON_DOWN) buttons |= 2u;
if (flags & RI_MOUSE_RIGHT_BUTTON_UP) buttons &= ~2u;
if (flags & RI_MOUSE_MIDDLE_BUTTON_DOWN) buttons |= 4u;
if (flags & RI_MOUSE_MIDDLE_BUTTON_UP) buttons &= ~4u;
s.raw_mouse_buttons.store(buttons);
s.raw_mouse_time.store(GetTickCount64());
// Windows is not moving the cursor (RIDEV_NOLEGACY): move it for the
// overlay so its pointer and the editor's hover keep working.
if (relative && (mouse.lLastX || mouse.lLastY) && s.mouse_legacy_blocked.load() && owns_pointer()) {
OverlayInputAccess access;
POINT cursor{};
if (GetCursorPos(&cursor)) SetCursorPos(cursor.x + mouse.lLastX, cursor.y + mouse.lLastY);
}
}
} // namespace
void note_raw_mouse_packet(const RAWMOUSE& mouse) {
note_packet(mouse, [] { return owns_menu_cursor(state()); });
}
void note_raw_mouse_packet(const RAWMOUSE& mouse, bool overlay_owns_pointer) {
note_packet(mouse, [overlay_owns_pointer] { return overlay_owns_pointer; });
}
void sync_raw_mouse_registration() {
sync_raw_mouse_registration(owns_menu_cursor(state()));
}
void sync_raw_mouse_registration(bool overlay_owns_pointer) {
try {
auto& mouse = raw_mouse();
std::lock_guard lock(mouse.mutex);
const bool overlay = overlay_owns_pointer;
if (overlay == mouse.overridden) return;
mouse.overridden = overlay;
// Only a registration that actually blocks the cursor needs replacing.
if (!mouse.known || !(mouse.game.dwFlags & cursor_blocking_flags)) return;
apply_raw_mouse(mouse, overlay);
} catch (...) {}
}
bool install_input_capture() {
auto& h = input_hooks();
const auto hid = LoadLibraryExW(L"hid.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32);
const auto kernel = GetModuleHandleW(L"kernel32.dll");
if (!hid || !install(h.hid_attributes, reinterpret_cast<void*>(GetProcAddress(hid, "HidD_GetAttributes")),
reinterpret_cast<void*>(captured_hid_attributes))) return false;
const auto kernel_hook = [&](Hook& hook, const char* name, void* detour) {
return install(hook, reinterpret_cast<void*>(GetProcAddress(kernel, name)), detour);
};
if (!kernel_hook(h.read_file, "ReadFile", reinterpret_cast<void*>(captured_read_file)) ||
!kernel_hook(h.overlapped, "GetOverlappedResult", reinterpret_cast<void*>(captured_overlapped)) ||
!kernel_hook(h.overlapped_ex, "GetOverlappedResultEx", reinterpret_cast<void*>(captured_overlapped_ex)) ||
!kernel_hook(h.completion, "GetQueuedCompletionStatus", reinterpret_cast<void*>(captured_completion)) ||
!kernel_hook(h.completion_ex, "GetQueuedCompletionStatusEx", reinterpret_cast<void*>(captured_completion_ex)) ||
!kernel_hook(h.close_handle, "CloseHandle", reinterpret_cast<void*>(captured_close_handle))) return false;
const auto user32 = GetModuleHandleW(L"user32.dll");
const auto add = [&](Hook& hook, const char* name, void* detour) {
const bool okay = install(hook, reinterpret_cast<void*>(GetProcAddress(user32, name)), detour);
if (!okay) dingosdk::logging::write(dingosdk::logging::Level::warning, dingosdk::logging::Channel::input, name);
return okay;
};
add(h.register_raw, "RegisterRawInputDevices", reinterpret_cast<void*>(captured_register_raw));
if (!add(h.async_key, "GetAsyncKeyState", reinterpret_cast<void*>(captured_async_key)) ||
!add(h.key, "GetKeyState", reinterpret_cast<void*>(captured_key)) ||
!add(h.keyboard, "GetKeyboardState", reinterpret_cast<void*>(captured_keyboard)) ||
!add(h.raw_data, "GetRawInputData", reinterpret_cast<void*>(captured_raw_data)) ||
!add(h.raw_buffer, "GetRawInputBuffer", reinterpret_cast<void*>(captured_raw_buffer)) ||
!add(h.capture, "SetCapture", reinterpret_cast<void*>(captured_capture)) ||
!add(h.release_capture, "ReleaseCapture", reinterpret_cast<void*>(captured_release_capture)) ||
!add(h.cursor, "SetCursor", reinterpret_cast<void*>(captured_cursor)) ||
!add(h.cursor_position, "GetCursorPos", reinterpret_cast<void*>(captured_cursor_position)) ||
!add(h.show_cursor, "ShowCursor", reinterpret_cast<void*>(captured_show_cursor))) return false;
// Some Windows builds alias both cursor-position exports to one function,
// which is already intercepted by the overlay's SetCursorPos hook.
const auto physical = GetProcAddress(user32, "SetPhysicalCursorPos");
if (physical && physical != GetProcAddress(user32, "SetCursorPos") &&
!add(h.physical_cursor, "SetPhysicalCursorPos", reinterpret_cast<void*>(captured_physical_cursor))) return false;
const std::array<void*, 5> states{reinterpret_cast<void*>(captured_xinput<0>), reinterpret_cast<void*>(captured_xinput<1>),
reinterpret_cast<void*>(captured_xinput<2>), reinterpret_cast<void*>(captured_xinput<3>), reinterpret_cast<void*>(captured_xinput<4>)};
const std::array<void*, 5> extended{reinterpret_cast<void*>(captured_xinput<0, true>), reinterpret_cast<void*>(captured_xinput<1, true>),
reinterpret_cast<void*>(captured_xinput<2, true>), reinterpret_cast<void*>(captured_xinput<3, true>), reinterpret_cast<void*>(captured_xinput<4, true>)};
const std::array<void*, 5> strokes{reinterpret_cast<void*>(captured_keystroke<0>), reinterpret_cast<void*>(captured_keystroke<1>),
reinterpret_cast<void*>(captured_keystroke<2>), reinterpret_cast<void*>(captured_keystroke<3>), reinterpret_cast<void*>(captured_keystroke<4>)};
const std::array names{L"xinput1_4.dll", L"xinput1_3.dll", L"xinput9_1_0.dll", L"xinput1_2.dll", L"xinput1_1.dll"};
std::vector<void*> seen;
for (std::size_t i = 0; i < names.size(); ++i) {
const auto module = LoadLibraryExW(names[i], nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32);
if (!module) continue;
const auto hook_export = [&](Hook& hook, const char* name, void* detour) {
const auto target = reinterpret_cast<void*>(GetProcAddress(module, name));
if (!target || std::find(seen.begin(), seen.end(), target) != seen.end()) return true;
if (!install(hook, target, detour)) { dingosdk::logging::printf(dingosdk::logging::Level::error, dingosdk::logging::Channel::input, "XInput input hook failed: module=%zu target=%p", i, target); return false; }
seen.push_back(target); return true;
};
if (!hook_export(h.xinput_state[i], "XInputGetState", states[i]) ||
!hook_export(h.xinput_extended[i], MAKEINTRESOURCEA(100), extended[i]) ||
!hook_export(h.xinput_keystroke[i], "XInputGetKeystroke", strokes[i])) return false;
}
const auto direct = LoadLibraryExW(L"dinput8.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32);
if (direct) {
if (prepare_standard_devices(direct))
logging::write(logging::Level::info, logging::Channel::input, "DirectInput mouse/keyboard capture prepared before game startup.");
else logging::write(logging::Level::warning, logging::Channel::input,
"DirectInput startup preparation incomplete; device creation will retry the missing hooks.");
if (!install(h.direct_create, reinterpret_cast<void*>(GetProcAddress(direct, "DirectInput8Create")),
reinterpret_cast<void*>(captured_direct_create))) return false;
}
dingosdk::logging::write(dingosdk::logging::Level::info, dingosdk::logging::Channel::input, "Menu input capture installed: keyboard, raw input, XInput, DirectInput, PlayStation HID and cursor ownership.");
return true;
}
}