Phase 1a: focus spoofing + hook observability

Two problems surfaced in testing: (1) no way to tell whether the injected
hook was actually the input source, and (2) the final design needs the tool
window focused for Steam RPT capture, which would pause/silence games that
react to focus loss. Both are addressed here.

- Focus spoofing (hook/focus_spoof): find the game's main window, subclass it
  to rewrite/swallow WM_ACTIVATE/ACTIVATEAPP/NCACTIVATE/KILLFOCUS, and inline-
  hook GetForegroundWindow/GetActiveWindow/GetFocus to always report the game
  as active. The game keeps running and polling while unfocused.
- Status back-channel (protocol v2): the DLL reports attached/focus-spoof
  flags, game pid/hwnd, a heartbeat, and a cumulative XInputGetState counter.
  The host overlay turns the counter into a live poll rate, so "is the hook
  working" is directly observable.
- Synthetic test-input toggle in the host: forwards a known automated pattern
  (stick circle + periodic A) to prove forwarding independent of the physical
  pad.
- hook_selftest extended to assert the status channel; passes.

Documented the windowed/borderless requirement and the new observable test
flow in the README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 00:13:06 +02:00
parent e370c8dcc5
commit df4325d21b
14 changed files with 447 additions and 34 deletions

View File

@@ -3,6 +3,7 @@
// forwarded pad state the host publishes each frame.
#pragma once
#include <atomic>
#include <cstdint>
#include <windows.h>
@@ -54,6 +55,45 @@ public:
return read_pads(*block_, out, count);
}
// --- Status back-channel (hook -> host diagnostics) --------------------
// Record that the game queried a controller slot; the host turns the
// cumulative count into a poll rate to prove the hook is live.
void note_query(std::uint32_t user_index)
{
if (block_ != nullptr)
{
block_->status.xinput_queries.fetch_add(1, std::memory_order_relaxed);
block_->status.last_user_index = user_index;
}
}
void mark_attached()
{
if (block_ != nullptr)
{
block_->status.game_pid = GetCurrentProcessId();
block_->status.attached = 1;
}
}
void mark_focus_spoof(bool active, std::uint64_t game_hwnd)
{
if (block_ != nullptr)
{
block_->status.focus_spoof = active ? 1u : 0u;
block_->status.game_hwnd = game_hwnd;
}
}
void heartbeat()
{
if (block_ != nullptr)
{
block_->status.heartbeat.fetch_add(1, std::memory_order_relaxed);
}
}
private:
SharedMemory shm_;
SharedBlock* block_ = nullptr;