Phase 1a: per-slot poll viz + input-path diagnostics
Surfaced by a game (Life is Strange: Before the Storm) that ignores controller input when it lacks true OS focus even though it still polls XInput. To find the focus-gated detection path, instrument the hook. Protocol v3 status back-channel now reports: - per-slot XInputGetState and XInputGetCapabilities counters (replacing the single aggregate), so the overlay shows exactly which slots the game polls and how fast; - focus-API call counts (GetForegroundWindow/GetActiveWindow/GetFocus) to see whether the game consults the APIs we spoof; - input-path diagnostics: whether the process registered Raw Input for a gamepad usage and whether it set RIDEV_INPUTSINK (background delivery), and whether a DirectInput dll is loaded. Host overlay gains a per-slot poll table and an "Input path" section. The DLL refreshes input diagnostics each worker tick via GetRegisteredRawInputDevices. hook_selftest updated for per-slot counters; passes. This is diagnostic-only: once a real run shows which path LiS uses, the targeted focus fix (e.g. forcing RIDEV_INPUTSINK or DI background coop) follows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ namespace coop
|
|||||||
|
|
||||||
// Bump whenever the layout of SharedBlock or CoopPadState changes. The hook
|
// Bump whenever the layout of SharedBlock or CoopPadState changes. The hook
|
||||||
// refuses to attach to a host with a mismatched version.
|
// refuses to attach to a host with a mismatched version.
|
||||||
inline constexpr std::uint32_t kProtocolVersion = 2;
|
inline constexpr std::uint32_t kProtocolVersion = 3;
|
||||||
|
|
||||||
// 'COOP' little-endian, used to sanity-check the mapping before trusting it.
|
// 'COOP' little-endian, used to sanity-check the mapping before trusting it.
|
||||||
inline constexpr std::uint32_t kProtocolMagic = 0x504F4F43u;
|
inline constexpr std::uint32_t kProtocolMagic = 0x504F4F43u;
|
||||||
@@ -42,19 +42,37 @@ struct CoopPadState
|
|||||||
|
|
||||||
static_assert(sizeof(CoopPadState) == 20, "CoopPadState layout must stay stable across both modules");
|
static_assert(sizeof(CoopPadState) == 20, "CoopPadState layout must stay stable across both modules");
|
||||||
|
|
||||||
|
// Indices into HookStatus::focus_query_calls.
|
||||||
|
enum FocusApi : std::uint32_t
|
||||||
|
{
|
||||||
|
FocusApi_Foreground = 0, // GetForegroundWindow
|
||||||
|
FocusApi_Active = 1, // GetActiveWindow
|
||||||
|
FocusApi_Focus = 2, // GetFocus
|
||||||
|
FocusApi_Count = 3,
|
||||||
|
};
|
||||||
|
|
||||||
// Hook -> host back-channel. The injected DLL is the sole writer; the host reads
|
// Hook -> host back-channel. The injected DLL is the sole writer; the host reads
|
||||||
// it for the diagnostics overlay (is the hook attached? is the game actually
|
// it for the diagnostics overlay: is the hook attached, which slots is the game
|
||||||
// polling it? did focus spoofing take?). Diagnostics only, so the few non-atomic
|
// polling, does it use the focus APIs, and does it read input through a
|
||||||
|
// focus-gated path (Raw Input / DirectInput)? Diagnostics only, so the non-atomic
|
||||||
// fields tolerate benign cross-process races.
|
// fields tolerate benign cross-process races.
|
||||||
struct HookStatus
|
struct HookStatus
|
||||||
{
|
{
|
||||||
std::atomic<std::uint32_t> heartbeat; // DLL bumps this ~4x/sec while alive
|
std::atomic<std::uint32_t> heartbeat; // DLL bumps ~4x/sec while alive
|
||||||
std::atomic<std::uint64_t> xinput_queries; // cumulative XInputGetState/Ex calls served
|
std::atomic<std::uint64_t> get_state_calls[kMaxPads]; // XInputGetState/Ex per slot
|
||||||
|
std::atomic<std::uint64_t> get_caps_calls[kMaxPads]; // XInputGetCapabilities per slot
|
||||||
|
std::atomic<std::uint64_t> focus_query_calls[FocusApi_Count]; // focus API calls, see FocusApi
|
||||||
|
|
||||||
std::uint32_t attached; // 1 once XInput hooks are installed
|
std::uint32_t attached; // 1 once XInput hooks are installed
|
||||||
std::uint32_t focus_spoof; // 1 once focus spoofing is active
|
std::uint32_t focus_spoof; // 1 once focus spoofing is active
|
||||||
std::uint32_t game_pid; // the DLL's own pid (sanity check)
|
std::uint32_t game_pid; // the DLL's own pid (sanity check)
|
||||||
std::uint32_t last_user_index; // last slot the game queried
|
|
||||||
std::uint64_t game_hwnd; // window the DLL subclassed (0 if none yet)
|
std::uint64_t game_hwnd; // window the DLL subclassed (0 if none yet)
|
||||||
|
|
||||||
|
// Input-path diagnostics: which focus-gated mechanism (if any) the game uses.
|
||||||
|
std::uint32_t raw_input_registered; // process has any Raw Input registration
|
||||||
|
std::uint32_t raw_input_gamepad; // ... for a joystick/gamepad usage page
|
||||||
|
std::uint32_t raw_input_gamepad_sink; // ... and that usage has RIDEV_INPUTSINK (bg delivery)
|
||||||
|
std::uint32_t dinput_loaded; // dinput8.dll is present in the process
|
||||||
};
|
};
|
||||||
|
|
||||||
// Top-level shared block. The host is the sole writer of pad state; the hook is
|
// Top-level shared block. The host is the sole writer of pad state; the hook is
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ DWORD WINAPI worker_thread(LPVOID)
|
|||||||
{
|
{
|
||||||
focus_installed = coop::hook::install_focus_spoof(g_ipc);
|
focus_installed = coop::hook::install_focus_spoof(g_ipc);
|
||||||
}
|
}
|
||||||
|
coop::hook::update_input_diagnostics(g_ipc); // refreshes each tick; registrations can change
|
||||||
g_ipc.heartbeat();
|
g_ipc.heartbeat();
|
||||||
Sleep(250);
|
Sleep(250);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ HWND g_game_hwnd = nullptr;
|
|||||||
WNDPROC g_orig_proc = nullptr;
|
WNDPROC g_orig_proc = nullptr;
|
||||||
bool g_unicode = true;
|
bool g_unicode = true;
|
||||||
std::vector<safetyhook::InlineHook> g_focus_hooks;
|
std::vector<safetyhook::InlineHook> g_focus_hooks;
|
||||||
|
IpcClient* g_focus_ipc = nullptr;
|
||||||
|
|
||||||
struct EnumContext
|
struct EnumContext
|
||||||
{
|
{
|
||||||
@@ -85,16 +86,28 @@ LRESULT CALLBACK subclass_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam
|
|||||||
|
|
||||||
HWND WINAPI hk_GetForegroundWindow()
|
HWND WINAPI hk_GetForegroundWindow()
|
||||||
{
|
{
|
||||||
|
if (g_focus_ipc != nullptr)
|
||||||
|
{
|
||||||
|
g_focus_ipc->note_focus_query(FocusApi_Foreground);
|
||||||
|
}
|
||||||
return g_game_hwnd;
|
return g_game_hwnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
HWND WINAPI hk_GetActiveWindow()
|
HWND WINAPI hk_GetActiveWindow()
|
||||||
{
|
{
|
||||||
|
if (g_focus_ipc != nullptr)
|
||||||
|
{
|
||||||
|
g_focus_ipc->note_focus_query(FocusApi_Active);
|
||||||
|
}
|
||||||
return g_game_hwnd;
|
return g_game_hwnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
HWND WINAPI hk_GetFocus()
|
HWND WINAPI hk_GetFocus()
|
||||||
{
|
{
|
||||||
|
if (g_focus_ipc != nullptr)
|
||||||
|
{
|
||||||
|
g_focus_ipc->note_focus_query(FocusApi_Focus);
|
||||||
|
}
|
||||||
return g_game_hwnd;
|
return g_game_hwnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +123,7 @@ void hook_export(HMODULE module, const char* name, void* detour)
|
|||||||
|
|
||||||
bool install_focus_spoof(IpcClient& ipc)
|
bool install_focus_spoof(IpcClient& ipc)
|
||||||
{
|
{
|
||||||
|
g_focus_ipc = &ipc;
|
||||||
if (g_game_hwnd != nullptr)
|
if (g_game_hwnd != nullptr)
|
||||||
{
|
{
|
||||||
return true; // already active
|
return true; // already active
|
||||||
@@ -142,6 +156,40 @@ bool install_focus_spoof(IpcClient& ipc)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void update_input_diagnostics(IpcClient& ipc)
|
||||||
|
{
|
||||||
|
// Inspect how the game currently reads input, to find a focus-gated path that
|
||||||
|
// would explain a controller working only when the game has true focus.
|
||||||
|
bool raw_registered = false;
|
||||||
|
bool raw_gamepad = false;
|
||||||
|
bool raw_gamepad_sink = false;
|
||||||
|
|
||||||
|
UINT count = 0;
|
||||||
|
if (GetRegisteredRawInputDevices(nullptr, &count, sizeof(RAWINPUTDEVICE)) == 0 && count > 0)
|
||||||
|
{
|
||||||
|
std::vector<RAWINPUTDEVICE> devices(count);
|
||||||
|
const UINT got = GetRegisteredRawInputDevices(devices.data(), &count, sizeof(RAWINPUTDEVICE));
|
||||||
|
if (got != static_cast<UINT>(-1))
|
||||||
|
{
|
||||||
|
raw_registered = got > 0;
|
||||||
|
for (UINT i = 0; i < got; ++i)
|
||||||
|
{
|
||||||
|
// Generic Desktop (0x01) joystick (0x04) / gamepad (0x05).
|
||||||
|
const bool is_pad =
|
||||||
|
devices[i].usUsagePage == 0x01 && (devices[i].usUsage == 0x04 || devices[i].usUsage == 0x05);
|
||||||
|
if (is_pad)
|
||||||
|
{
|
||||||
|
raw_gamepad = true;
|
||||||
|
raw_gamepad_sink = (devices[i].dwFlags & RIDEV_INPUTSINK) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool dinput = GetModuleHandleW(L"dinput8.dll") != nullptr || GetModuleHandleW(L"dinput.dll") != nullptr;
|
||||||
|
ipc.set_input_diagnostics(raw_registered, raw_gamepad, raw_gamepad_sink, dinput);
|
||||||
|
}
|
||||||
|
|
||||||
void remove_focus_spoof()
|
void remove_focus_spoof()
|
||||||
{
|
{
|
||||||
if (g_game_hwnd != nullptr && g_orig_proc != nullptr)
|
if (g_game_hwnd != nullptr && g_orig_proc != nullptr)
|
||||||
@@ -158,6 +206,7 @@ void remove_focus_spoof()
|
|||||||
g_focus_hooks.clear();
|
g_focus_hooks.clear();
|
||||||
g_game_hwnd = nullptr;
|
g_game_hwnd = nullptr;
|
||||||
g_orig_proc = nullptr;
|
g_orig_proc = nullptr;
|
||||||
|
g_focus_ipc = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace coop::hook
|
} // namespace coop::hook
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ namespace coop::hook
|
|||||||
// status through `ipc`.
|
// status through `ipc`.
|
||||||
bool install_focus_spoof(IpcClient& ipc);
|
bool install_focus_spoof(IpcClient& ipc);
|
||||||
|
|
||||||
|
// Reports how the game currently reads input (Raw Input gamepad usage + sink
|
||||||
|
// flag, DirectInput presence) so the host can identify a focus-gated path. Cheap
|
||||||
|
// to call each tick.
|
||||||
|
void update_input_diagnostics(IpcClient& ipc);
|
||||||
|
|
||||||
// Restores the original window procedure and removes the focus API hooks.
|
// Restores the original window procedure and removes the focus API hooks.
|
||||||
void remove_focus_spoof();
|
void remove_focus_spoof();
|
||||||
|
|
||||||
|
|||||||
@@ -57,14 +57,28 @@ public:
|
|||||||
|
|
||||||
// --- Status back-channel (hook -> host diagnostics) --------------------
|
// --- Status back-channel (hook -> host diagnostics) --------------------
|
||||||
|
|
||||||
// Record that the game queried a controller slot; the host turns the
|
// Record that the game queried a controller slot via XInputGetState/Ex.
|
||||||
// cumulative count into a poll rate to prove the hook is live.
|
void note_state_query(std::uint32_t user_index)
|
||||||
void note_query(std::uint32_t user_index)
|
|
||||||
{
|
{
|
||||||
if (block_ != nullptr)
|
if (block_ != nullptr && user_index < kMaxPads)
|
||||||
{
|
{
|
||||||
block_->status.xinput_queries.fetch_add(1, std::memory_order_relaxed);
|
block_->status.get_state_calls[user_index].fetch_add(1, std::memory_order_relaxed);
|
||||||
block_->status.last_user_index = user_index;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void note_caps_query(std::uint32_t user_index)
|
||||||
|
{
|
||||||
|
if (block_ != nullptr && user_index < kMaxPads)
|
||||||
|
{
|
||||||
|
block_->status.get_caps_calls[user_index].fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void note_focus_query(FocusApi which)
|
||||||
|
{
|
||||||
|
if (block_ != nullptr && which < FocusApi_Count)
|
||||||
|
{
|
||||||
|
block_->status.focus_query_calls[which].fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +100,17 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void set_input_diagnostics(bool raw_registered, bool raw_gamepad, bool raw_gamepad_sink, bool dinput)
|
||||||
|
{
|
||||||
|
if (block_ != nullptr)
|
||||||
|
{
|
||||||
|
block_->status.raw_input_registered = raw_registered ? 1u : 0u;
|
||||||
|
block_->status.raw_input_gamepad = raw_gamepad ? 1u : 0u;
|
||||||
|
block_->status.raw_input_gamepad_sink = raw_gamepad_sink ? 1u : 0u;
|
||||||
|
block_->status.dinput_loaded = dinput ? 1u : 0u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void heartbeat()
|
void heartbeat()
|
||||||
{
|
{
|
||||||
if (block_ != nullptr)
|
if (block_ != nullptr)
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ DWORD query_state(DWORD user_index, XINPUT_STATE* state, bool keep_guide)
|
|||||||
}
|
}
|
||||||
if (g_ipc != nullptr)
|
if (g_ipc != nullptr)
|
||||||
{
|
{
|
||||||
g_ipc->note_query(user_index); // proves to the host the game is polling us
|
g_ipc->note_state_query(user_index); // proves to the host the game is polling us
|
||||||
}
|
}
|
||||||
refresh_cache();
|
refresh_cache();
|
||||||
const CoopPadState& pad = g_cache[user_index];
|
const CoopPadState& pad = g_cache[user_index];
|
||||||
@@ -100,6 +100,10 @@ DWORD WINAPI hk_XInputGetCapabilities(DWORD user_index, DWORD /*flags*/, XINPUT_
|
|||||||
{
|
{
|
||||||
return ERROR_DEVICE_NOT_CONNECTED;
|
return ERROR_DEVICE_NOT_CONNECTED;
|
||||||
}
|
}
|
||||||
|
if (g_ipc != nullptr)
|
||||||
|
{
|
||||||
|
g_ipc->note_caps_query(user_index);
|
||||||
|
}
|
||||||
refresh_cache();
|
refresh_cache();
|
||||||
if (!g_cache[user_index].connected)
|
if (!g_cache[user_index].connected)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -148,15 +148,18 @@ void InjectionPanel::draw_hook_status()
|
|||||||
|
|
||||||
const HookStatusView status = server_.hook_status();
|
const HookStatusView status = server_.hook_status();
|
||||||
|
|
||||||
// Convert the cumulative query counter into a rate every half second.
|
// Convert the cumulative per-slot counters into rates every half second.
|
||||||
const double now = ImGui::GetTime();
|
const double now = ImGui::GetTime();
|
||||||
if (now - last_sample_time_ >= 0.5)
|
if (now - last_sample_time_ >= 0.5)
|
||||||
{
|
{
|
||||||
const double dt = now - last_sample_time_;
|
const double dt = now - last_sample_time_;
|
||||||
|
for (int i = 0; i < static_cast<int>(kMaxPads); ++i)
|
||||||
|
{
|
||||||
const unsigned long long delta =
|
const unsigned long long delta =
|
||||||
status.xinput_queries >= last_query_count_ ? status.xinput_queries - last_query_count_ : 0;
|
status.get_state[i] >= last_state_count_[i] ? status.get_state[i] - last_state_count_[i] : 0;
|
||||||
query_rate_ = dt > 0.0 ? static_cast<double>(delta) / dt : 0.0;
|
state_rate_[i] = dt > 0.0 ? static_cast<double>(delta) / dt : 0.0;
|
||||||
last_query_count_ = status.xinput_queries;
|
last_state_count_[i] = status.get_state[i];
|
||||||
|
}
|
||||||
last_sample_time_ = now;
|
last_sample_time_ = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,14 +172,65 @@ void InjectionPanel::draw_hook_status()
|
|||||||
|
|
||||||
ImGui::TextColored(kGreen, "Attached (game pid %u, hwnd 0x%llX)", status.game_pid,
|
ImGui::TextColored(kGreen, "Attached (game pid %u, hwnd 0x%llX)", status.game_pid,
|
||||||
static_cast<unsigned long long>(status.game_hwnd));
|
static_cast<unsigned long long>(status.game_hwnd));
|
||||||
ImGui::Text("XInput polled: %.0f/s (last slot %u)", query_rate_, status.last_user_index);
|
|
||||||
if (query_rate_ > 0.0)
|
|
||||||
{
|
|
||||||
ImGui::SameLine();
|
|
||||||
ImGui::TextColored(kGreen, " <- game is reading our input");
|
|
||||||
}
|
|
||||||
ImGui::TextColored(status.focus_spoof ? kGreen : kGrey, "Focus spoof: %s",
|
ImGui::TextColored(status.focus_spoof ? kGreen : kGrey, "Focus spoof: %s",
|
||||||
status.focus_spoof ? "active" : "inactive");
|
status.focus_spoof ? "active" : "inactive");
|
||||||
|
|
||||||
|
// Per-slot XInput polling: shows exactly which slots the game reads and how
|
||||||
|
// fast -- the requested visualization.
|
||||||
|
if (ImGui::BeginTable("slots", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp))
|
||||||
|
{
|
||||||
|
ImGui::TableSetupColumn("Slot");
|
||||||
|
ImGui::TableSetupColumn("GetState/s");
|
||||||
|
ImGui::TableSetupColumn("GetState total");
|
||||||
|
ImGui::TableSetupColumn("GetCaps total");
|
||||||
|
ImGui::TableHeadersRow();
|
||||||
|
for (int i = 0; i < static_cast<int>(kMaxPads); ++i)
|
||||||
|
{
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::Text("%d", i);
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
if (state_rate_[i] > 0.0)
|
||||||
|
{
|
||||||
|
ImGui::TextColored(kGreen, "%.0f", state_rate_[i]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui::TextDisabled("0");
|
||||||
|
}
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::Text("%llu", static_cast<unsigned long long>(status.get_state[i]));
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::Text("%llu", static_cast<unsigned long long>(status.get_caps[i]));
|
||||||
|
}
|
||||||
|
ImGui::EndTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus-API usage: tells us whether the game even consults these (which we
|
||||||
|
// spoof) when deciding it lost focus.
|
||||||
|
ImGui::Text("Focus API calls FG:%llu Active:%llu Focus:%llu",
|
||||||
|
static_cast<unsigned long long>(status.focus_calls[coop::FocusApi_Foreground]),
|
||||||
|
static_cast<unsigned long long>(status.focus_calls[coop::FocusApi_Active]),
|
||||||
|
static_cast<unsigned long long>(status.focus_calls[coop::FocusApi_Focus]));
|
||||||
|
|
||||||
|
// Input-path diagnostics: a focus-gated detection path would explain a game
|
||||||
|
// that only accepts the controller when it has true focus.
|
||||||
|
ImGui::SeparatorText("Input path");
|
||||||
|
if (status.raw_input_gamepad)
|
||||||
|
{
|
||||||
|
ImGui::TextColored(status.raw_input_gamepad_sink ? kGreen : kRed, "Raw Input gamepad: yes (INPUTSINK %s)",
|
||||||
|
status.raw_input_gamepad_sink ? "set -> bg ok" : "MISSING -> focus-gated!");
|
||||||
|
}
|
||||||
|
else if (status.raw_input_registered)
|
||||||
|
{
|
||||||
|
ImGui::TextColored(kGrey, "Raw Input: registered, but not for a gamepad usage");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui::TextColored(kGrey, "Raw Input: not registered");
|
||||||
|
}
|
||||||
|
ImGui::TextColored(status.dinput_loaded ? kRed : kGrey, "DirectInput dll loaded: %s",
|
||||||
|
status.dinput_loaded ? "yes (could be foreground-gated)" : "no");
|
||||||
}
|
}
|
||||||
|
|
||||||
void InjectionPanel::draw()
|
void InjectionPanel::draw()
|
||||||
|
|||||||
@@ -41,10 +41,10 @@ private:
|
|||||||
|
|
||||||
bool test_input_ = false;
|
bool test_input_ = false;
|
||||||
|
|
||||||
// Sampled to turn the hook's cumulative query counter into a poll rate.
|
// Sampled to turn the hook's cumulative per-slot counters into poll rates.
|
||||||
unsigned long long last_query_count_ = 0;
|
unsigned long long last_state_count_[kMaxPads] = {};
|
||||||
|
double state_rate_[kMaxPads] = {};
|
||||||
double last_sample_time_ = 0.0;
|
double last_sample_time_ = 0.0;
|
||||||
double query_rate_ = 0.0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace coop
|
} // namespace coop
|
||||||
|
|||||||
@@ -51,10 +51,21 @@ HookStatusView IpcServer::hook_status() const
|
|||||||
view.attached = s.attached != 0;
|
view.attached = s.attached != 0;
|
||||||
view.focus_spoof = s.focus_spoof != 0;
|
view.focus_spoof = s.focus_spoof != 0;
|
||||||
view.heartbeat = s.heartbeat.load(std::memory_order_relaxed);
|
view.heartbeat = s.heartbeat.load(std::memory_order_relaxed);
|
||||||
view.xinput_queries = s.xinput_queries.load(std::memory_order_relaxed);
|
for (std::uint32_t i = 0; i < kMaxPads; ++i)
|
||||||
|
{
|
||||||
|
view.get_state[i] = s.get_state_calls[i].load(std::memory_order_relaxed);
|
||||||
|
view.get_caps[i] = s.get_caps_calls[i].load(std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
for (std::uint32_t i = 0; i < FocusApi_Count; ++i)
|
||||||
|
{
|
||||||
|
view.focus_calls[i] = s.focus_query_calls[i].load(std::memory_order_relaxed);
|
||||||
|
}
|
||||||
view.game_pid = s.game_pid;
|
view.game_pid = s.game_pid;
|
||||||
view.game_hwnd = s.game_hwnd;
|
view.game_hwnd = s.game_hwnd;
|
||||||
view.last_user_index = s.last_user_index;
|
view.raw_input_registered = s.raw_input_registered != 0;
|
||||||
|
view.raw_input_gamepad = s.raw_input_gamepad != 0;
|
||||||
|
view.raw_input_gamepad_sink = s.raw_input_gamepad_sink != 0;
|
||||||
|
view.dinput_loaded = s.dinput_loaded != 0;
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,15 @@ struct HookStatusView
|
|||||||
bool attached = false; // XInput hooks installed in the game
|
bool attached = false; // XInput hooks installed in the game
|
||||||
bool focus_spoof = false; // focus spoofing active
|
bool focus_spoof = false; // focus spoofing active
|
||||||
std::uint32_t heartbeat = 0; // DLL liveness counter
|
std::uint32_t heartbeat = 0; // DLL liveness counter
|
||||||
std::uint64_t xinput_queries = 0;
|
std::uint64_t get_state[kMaxPads] = {};
|
||||||
|
std::uint64_t get_caps[kMaxPads] = {};
|
||||||
|
std::uint64_t focus_calls[FocusApi_Count] = {};
|
||||||
std::uint32_t game_pid = 0;
|
std::uint32_t game_pid = 0;
|
||||||
std::uint64_t game_hwnd = 0;
|
std::uint64_t game_hwnd = 0;
|
||||||
std::uint32_t last_user_index = 0;
|
bool raw_input_registered = false;
|
||||||
|
bool raw_input_gamepad = false;
|
||||||
|
bool raw_input_gamepad_sink = false;
|
||||||
|
bool dinput_loaded = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
class IpcServer
|
class IpcServer
|
||||||
|
|||||||
@@ -79,7 +79,9 @@ int main()
|
|||||||
|
|
||||||
// Status back-channel: the host relies on these to prove the hook is live.
|
// Status back-channel: the host relies on these to prove the hook is live.
|
||||||
check(block->status.attached == 1, "status reports attached");
|
check(block->status.attached == 1, "status reports attached");
|
||||||
check(block->status.xinput_queries.load(std::memory_order_relaxed) >= 2, "status counts XInput queries");
|
check(block->status.get_state_calls[0].load(std::memory_order_relaxed) >= 1, "status counts slot 0 GetState");
|
||||||
|
check(block->status.get_state_calls[1].load(std::memory_order_relaxed) >= 1, "status counts slot 1 GetState");
|
||||||
|
check(block->status.get_caps_calls[0].load(std::memory_order_relaxed) >= 1, "status counts slot 0 GetCaps");
|
||||||
|
|
||||||
hook::remove_xinput_hooks();
|
hook::remove_xinput_hooks();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user