Files
ReSkate/Extension/UI/Overlay/overlay_input.cpp
T
AyeZeeBB 89a97caed7 Plugins: an item grid's own list and marks, items worn on top, keys kept from the game, the menu's category icons
General APIs from pull request 269 (the player's own item tabs and outfit layers), whose tabs,
panels and prompts are now a plugin's (contrib/plugins/item-tabs, kept on this machine):
- reskate.menu.grid_show(keys): a plugin's list in the item grid that has the focus, drawn from
  every grid of the screen; reskate.menu.grid_mark(keys): the game's check on tiles. menu.grid()
  also says whether a list shows, whether it is empty, and where the grid's own prompts end.
- reskate.outfit.on_top(items) and on_top_state(): items worn on top of the outfit in the slots it
  leaves empty, the item given the slot's category, a slot tried and left if the game reports a
  failed load (ReSkate.layer_slots.txt remembers which load where); paid store items refused.
  Part of the shared outfit service, with reskate.outfit.set.
- reskate.input.game_keys(keys): presses the game does not see, by any way it reads the keyboard;
  "Delete" is a key name.
- d:game_image draws the customization menu's 17 category icons ("cas_tops", ...).
dingosdk_plugin_tests skips the test of a contrib plugin that is not on this machine.
2026-10-21 19:18:01 -05:00

664 lines
35 KiB
C++

#include "Engine/Core/Platform/launcher_support.h"
#include "Engine/Core/Log/logging.h"
#include "Extension/Boot/exit_watch.h"
#include "overlay_internal.h"
#include "input_capture.h"
#include "playstation_input.h"
#include "cursor.h"
#include "Extension/Throwdowns/throwdown_kit.h"
#include "Engine/Game/Input/owned_menu_input.h"
using namespace dingosdk::overlay::detail;
namespace dingosdk::overlay::detail {
bool console_toggle_key(UINT message, WPARAM wp, LPARAM lp) {
const bool key_message = message == WM_KEYDOWN || message == WM_SYSKEYDOWN
|| message == WM_KEYUP || message == WM_SYSKEYUP;
const auto console = dingosdk::launcher::overlay_keys().console;
// The default also matches by position (scan code 0x29, left of 1), so
// layouts that put another character there still open the console.
if (console != dingosdk::launcher::default_console_key) return key_message && wp == console;
const auto scan_code = (static_cast<ULONG_PTR>(lp) >> 16) & 0xffu;
return key_message && (wp == VK_OEM_3 || scan_code == 0x29u);
}
bool console_character(UINT message) {
return message == WM_CHAR || message == WM_SYSCHAR
|| message == WM_DEADCHAR || message == WM_SYSDEADCHAR;
}
// Reads one WM_INPUT packet without consuming it (the game or DefWindowProc
// still receives the message). True when it was a mouse packet and was noted.
bool note_raw_mouse(HRAWINPUT handle, bool overlay_owns_pointer) {
RAWINPUT raw{};
UINT size = sizeof(raw);
{
OverlayInputAccess access;
if (GetRawInputData(handle, RID_INPUT, &raw, &size, sizeof(RAWINPUTHEADER)) == UINT(-1)) return false;
}
if (raw.header.dwType != RIM_TYPEMOUSE) return false;
note_raw_mouse_packet(raw.data.mouse, overlay_owns_pointer);
return true;
}
// Focus changes of the game window, with the window and program on the other side: the
// game (and Steam Input's controller layout) follow them, and a stray one can leave the
// controller unanswered until the player tabs out and back.
void log_focus_change(HWND window, UINT message, WPARAM wp, LPARAM lp) {
static std::atomic<unsigned> logged{};
if (logged.fetch_add(1) >= 400) return;
const auto describe = [](HWND other) {
if (!other) return std::string("no window");
wchar_t name[128]{};
GetClassNameW(other, name, 128);
DWORD process{};
GetWindowThreadProcessId(other, &process);
std::string text;
for (const wchar_t* c = name; *c && text.size() < 100; ++c) text.push_back(*c < 128 ? static_cast<char>(*c) : '?');
return text + (process == GetCurrentProcessId() ? " (this game)" : " (process " + std::to_string(process) + ")");
};
std::string text;
if (message == WM_ACTIVATEAPP)
text = wp ? "game became the active application" : "game stopped being the active application (thread " +
std::to_string(static_cast<DWORD>(lp)) + " took over)";
else if (message == WM_ACTIVATE)
text = LOWORD(wp) == WA_INACTIVE ? "game window deactivated for " + describe(reinterpret_cast<HWND>(lp))
: "game window activated from " + describe(reinterpret_cast<HWND>(lp));
else if (message == WM_KILLFOCUS)
text = "keyboard focus moved to " + describe(reinterpret_cast<HWND>(wp));
else if (message == WM_SETFOCUS)
text = "keyboard focus returned from " + describe(reinterpret_cast<HWND>(wp));
else
return;
(void)window;
dingosdk::logging::printf(dingosdk::logging::Level::info, dingosdk::logging::Channel::input, "Focus: %s.", text.c_str());
}
void release_game_buttons(HWND window, WNDPROC previous) {
auto& s = state();
if (!previous) return;
for (unsigned key = 0; key < s.game_keys.size(); ++key) if (s.game_keys[key]) {
s.game_keys[key] = false;
const LPARAM bits = 1 | (static_cast<LPARAM>(MapVirtualKeyW(key, MAPVK_VK_TO_VSC)) << 16) |
static_cast<LPARAM>((1ull << 30) | (1ull << 31));
CallWindowProcW(previous, window, WM_KEYUP, key, bits);
}
constexpr std::array releases{WM_LBUTTONUP, WM_RBUTTONUP, WM_MBUTTONUP, WM_XBUTTONUP, WM_XBUTTONUP};
for (unsigned i = 0; i < releases.size(); ++i) if (s.game_mouse_buttons & (1u << i))
CallWindowProcW(previous, window, releases[i], i >= 3 ? MAKEWPARAM(0, i == 3 ? XBUTTON1 : XBUTTON2) : 0, 0);
s.game_mouse_buttons = 0;
}
LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
struct NativeInputScope {
unsigned saved{overlay_input_access};
NativeInputScope() { overlay_input_access = 0; }
~NativeInputScope() { overlay_input_access = saved; }
} native_input;
auto& s = state();
if (message == WM_MOUSEWHEEL) s.look_wheel_message.fetch_add(GET_WHEEL_DELTA_WPARAM(wp));
WNDPROC previous{};
{
std::lock_guard lock(s.window_hook_mutex);
if (const auto found = s.window_hooks.find(window); found != s.window_hooks.end()) {
previous = found->second.previous;
if (message == WM_NCDESTROY) found->second.installed = false;
}
}
// Mouse movement and raw input arrive up to thousands of times a second, and
// whether the overlay owns the pointer costs window-manager calls (the
// foreground check). Nothing this procedure does with those messages changes
// it, so it is worked out once for them; keys, focus and everything else still
// ask at each step.
const bool pointer_message = window == s.window.load() && (message == WM_INPUT || message == WM_MOUSEMOVE ||
message == WM_SETCURSOR || message == WM_NCHITTEST);
const bool pointer_owned = pointer_message && owns_menu_cursor(s);
const auto owns_pointer = [&] { return pointer_message ? pointer_owned : owns_menu_cursor(s); };
if (window == s.window.load()) sync_raw_mouse_registration(owns_pointer());
if (message && message == cursor_sync_message()) {
window_cursor_input(window, message, wp);
return 0;
}
// An overlay above us may keep this forwarding link after the selected
// window changes. Its original procedure belongs to that HWND.
if (window != s.window.load())
return previous ? CallWindowProcW(previous, window, message, wp, lp)
: DefWindowProcW(window, message, wp, lp);
// The player closing the game: it is ended if it does not finish by itself.
dingosdk::exit_watch::note_window_message(window, message, wp);
struct ReleaseOnOpen {
HWND window; WNDPROC previous; bool was_visible;
~ReleaseOnOpen() {
if (!was_visible && interactive_visible(state())) release_game_buttons(window, previous);
}
} release_on_open{window, previous, interactive_visible(s)};
// The raw packet noted below stays noted while the game handles this message,
// so its own GetRawInputData read of the same packet is not counted again.
struct NotedRawInput {
HRAWINPUT saved{noted_raw_input};
~NotedRawInput() { noted_raw_input = saved; }
} noted_raw_input_scope;
if (message == WM_ACTIVATEAPP || message == WM_ACTIVATE || message == WM_KILLFOCUS || message == WM_SETFOCUS)
log_focus_change(window, message, wp, lp);
if ((message >= WM_KEYFIRST && message <= WM_KEYLAST && message != WM_CHAR) ||
(message >= WM_MOUSEFIRST && message <= WM_MOUSELAST))
note_input_message(message, wp);
if (!s.stop.load() && s.input_attached.load()) {
if (message == WM_INPUT && note_raw_mouse(reinterpret_cast<HRAWINPUT>(lp), pointer_owned))
noted_raw_input = reinterpret_cast<HRAWINPUT>(lp);
if (message == WM_KILLFOCUS) s.raw_mouse_buttons.store(0);
if (pointer_message) window_cursor_input(window, message, wp, pointer_owned);
else window_cursor_input(window, message, wp);
const bool key = message == WM_KEYDOWN || message == WM_SYSKEYDOWN;
const bool release = message == WM_KEYUP || message == WM_SYSKEYUP;
if((key || release) && challenge_menu_key(static_cast<int>(wp)))return 0;
if (key && wp < 256 && key_hidden(static_cast<unsigned>(wp))) return 0;
if (console_character(message) && s.console_character_pending.exchange(false)) return 0;
if (console_character(message) && s.chat_character_pending.exchange(false)) return 0;
if (console_character(message) && s.item_search_character_pending.exchange(false)) return 0;
// The skater item grids (item_browser_overlay.cpp): while one has the focus and nothing of ours
// is open, F favorites the highlighted item, X steps the filter and "-" (or Ctrl+F) opens the
// search box, which then takes the keys until Enter or Esc. (The game uses Q, E, T and G here:
// G opens its feedback card.)
if (s.item_search_visible.load() || s.item_search_escape_pending.load()) {
if (wp == VK_ESCAPE && (key || release) && !s.console_visible.load() && !s.visible.load()) {
if (key && (static_cast<ULONG_PTR>(lp) & (1ull << 30)) == 0) {
s.item_search_escape_pending.store(true);
s.item_search_visible.store(false);
item_browser_search_cleared();
sync_menu_cursor();
window_cursor_input(window, message, wp);
std::lock_guard lock(s.input_mutex);
s.input.clear();
} else if (release) s.item_search_escape_pending.store(false);
return 0;
}
} else if (key && (static_cast<ULONG_PTR>(lp) & (1ull << 30)) == 0 && !interactive_visible(s) &&
!(GetKeyState(VK_MENU) & 0x8000) && item_browser_open()) {
const bool control = (GetKeyState(VK_CONTROL) & 0x8000) != 0;
const bool minus = wp == VK_SUBTRACT || (MapVirtualKeyW(static_cast<UINT>(wp), MAPVK_VK_TO_CHAR) == L'-' &&
!(GetKeyState(VK_SHIFT) & 0x8000));
if ((minus && !control) || (wp == 'F' && control)) {
s.item_search_character_pending.store(true);
s.item_search_visible.store(true);
s.item_search_focus_requested.store(true);
sync_menu_cursor();
window_cursor_input(window, message, wp);
std::lock_guard lock(s.input_mutex);
s.input.clear();
return 0;
}
if (!control && wp == 'F') item_browser_key(ItemBrowserKey::favorite);
if (!control && wp == 'X') item_browser_key(ItemBrowserKey::filter);
}
// T opens the chat, the key the game itself reserves for it
// (Processor_Keyboard_Gameplay_Chat), while in a session and nothing
// else of ours is open. Its character is swallowed like the console's.
// "/" opens it too, already holding the "/" so the command list shows at once: the key
// that types "/" on this keyboard layout (unshifted), or the numpad's.
const bool slash = key && (wp == VK_DIVIDE ||
(MapVirtualKeyW(static_cast<UINT>(wp), MAPVK_VK_TO_CHAR) == L'/' &&
!(GetKeyState(VK_SHIFT) & 0x8000)));
if (key && (wp == 'T' || slash) && (static_cast<ULONG_PTR>(lp) & (1ull << 30)) == 0 &&
s.chat_available.load() && !interactive_visible(s) &&
!(GetKeyState(VK_CONTROL) & 0x8000) && !(GetKeyState(VK_MENU) & 0x8000)) {
s.chat_character_pending.store(true);
s.chat_command_requested.store(slash);
s.chat_visible.store(true);
s.chat_focus_requested.store(true);
sync_menu_cursor();
window_cursor_input(window, message, wp);
std::lock_guard lock(s.input_mutex);
s.input.clear();
return 0;
}
if ((s.chat_visible.load() || s.chat_escape_pending.load()) && !s.console_visible.load() &&
!s.visible.load() && wp == VK_ESCAPE && (key || release)) {
if (key && (static_cast<ULONG_PTR>(lp) & (1ull << 30)) == 0) {
s.chat_escape_pending.store(true);
s.chat_visible.store(false);
sync_menu_cursor();
window_cursor_input(window, message, wp);
std::lock_guard lock(s.input_mutex);
s.input.clear();
} else if (release) s.chat_escape_pending.store(false);
return 0;
}
if (console_toggle_key(message, wp, lp)) {
if (key) {
// TranslateMessage creates the character before the key message
// reaches this procedure. Consume that separate packet as well.
s.console_character_pending.store(true);
if ((static_cast<ULONG_PTR>(lp) & (1ull << 30)) == 0) {
const bool opened = !s.console_visible.load();
s.console_visible.store(opened);
if (opened) s.console_focus_requested.store(true);
sync_menu_cursor();
window_cursor_input(window, message, wp);
std::lock_guard lock(s.input_mutex);
s.input.clear();
}
} else if (release) {
s.console_character_pending.store(false);
}
return 0;
}
if ((s.console_visible.load() || s.console_escape_pending.load()) &&
wp == VK_ESCAPE && (key || release)) {
if (key && (static_cast<ULONG_PTR>(lp) & (1ull << 30)) == 0) {
s.console_escape_pending.store(true);
s.console_visible.store(false);
sync_menu_cursor();
window_cursor_input(window, message, wp);
std::lock_guard lock(s.input_mutex);
s.input.clear();
} else if (release) s.console_escape_pending.store(false);
return 0;
}
if (wp == dingosdk::launcher::overlay_keys().menu && (key || release)) {
if (key && (static_cast<ULONG_PTR>(lp) & (1ull << 30)) == 0) {
if (s.editor_visible.load()) s.editor_exit_requested.store(true);
else s.visible.store(!s.visible.load());
sync_menu_cursor();
window_cursor_input(window, message, wp);
std::lock_guard lock(s.input_mutex);
s.input.clear();
}
return 0;
}
if (message == WM_NCDESTROY) s.selected_window_destroyed.store(true);
if (message == WM_KILLFOCUS || message == WM_SETFOCUS || message == WM_NCDESTROY) sync_menu_cursor();
// ImGui's Win32 backend ignores raw input, so WM_INPUT is not queued.
// (The pause menu's server browser scrolls with the wheel, which only arrives this way.)
if ((interactive_visible(s) || message == WM_KILLFOCUS || message == WM_SETFOCUS ||
(message == WM_MOUSEWHEEL && s.hub_pointer.load())) && message != WM_INPUT) {
std::lock_guard lock(s.input_mutex);
if (message == WM_MOUSEMOVE && !s.input.empty() && s.input.back().message == WM_MOUSEMOVE &&
s.input.back().window == window) {
// Moves in a row: ImGui applies them all in the same frame and keeps
// the last, so only the newest is kept.
s.input.back() = {window, message, wp, lp};
} else if (s.input.size() < 512) s.input.push_back({window, message, wp, lp});
else {
// Mouse movement can exceed the bounded queue while a frame is
// delayed. Drop the oldest packet without changing menu state.
s.input.pop_front();
s.input.push_back({window, message, wp, lp});
}
}
const bool countdown = game::owned_menu_countdown_capture(
dingosdk::throwdowns::kit::match().input_lock,challenge_panel_owns_input());
const bool freecam_capture = s.freecam_controller_active.load(std::memory_order_relaxed) ||
s.prompt_input_active.load(std::memory_order_relaxed);
if (countdown || freecam_capture) release_game_buttons(window, previous);
const bool capture = owns_pointer() || (countdown && wp != VK_ESCAPE && !console_toggle_key(message, wp, lp));
if ((capture || freecam_capture) && is_input(message)) {
// Foreground raw-input packets still need DefWindowProc cleanup.
return message == WM_INPUT ? DefWindowProcW(window, message, wp, lp) : 0;
}
if (capture && message == WM_SETCURSOR && LOWORD(lp) == HTCLIENT) {
OverlayInputAccess access;
SetCursor(nullptr); // The software cursor is independent of the game's ShowCursor counter.
return TRUE;
}
if ((key || release) && wp < s.game_keys.size()) s.game_keys[wp] = key;
constexpr std::array<UINT, 4> down{WM_LBUTTONDOWN, WM_RBUTTONDOWN, WM_MBUTTONDOWN, WM_XBUTTONDOWN};
constexpr std::array<UINT, 4> up{WM_LBUTTONUP, WM_RBUTTONUP, WM_MBUTTONUP, WM_XBUTTONUP};
for (unsigned i = 0; i < down.size(); ++i) {
const auto bit = 1u << (i == 3 && GET_XBUTTON_WPARAM(wp) == XBUTTON2 ? 4 : i);
if (message == down[i]) s.game_mouse_buttons |= bit;
if (message == up[i]) s.game_mouse_buttons &= ~bit;
}
if (message == WM_KILLFOCUS) { s.game_keys.fill(false); s.game_mouse_buttons = 0; }
}
return previous ? CallWindowProcW(previous, window, message, wp, lp)
: DefWindowProcW(window, message, wp, lp);
}
}
namespace {
struct ControllerSample {
XINPUT_GAMEPAD pad{};
unsigned device{};
dingosdk::ControllerStyle style{};
};
using GetState = DWORD (WINAPI*)(DWORD, XINPUT_STATE*);
GetState xinput_get_state() {
// Optional OS API, resolved outside DllMain. Keep the module for process
// lifetime; a missing controller/runtime must not prevent keyboard flight.
static const auto get_state = []() -> GetState {
for (const auto name : {L"xinput1_4.dll", L"xinput1_3.dll", L"xinput9_1_0.dll"}) {
const auto module = LoadLibraryExW(name, nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32);
if (!module) continue;
const auto entry = GetProcAddress(module, "XInputGetState");
if (entry) {
GetState fn{};
static_assert(sizeof(fn) == sizeof(entry));
std::memcpy(&fn, &entry, sizeof(fn));
return fn;
}
FreeLibrary(module);
}
return nullptr;
}();
return get_state;
}
// The XInput slots with a pad in them, shared by every reader. Polling an empty
// slot is slow (it looks for a device), so readers poll only these, and a
// background thread retries the empty ones once a second while pads are read:
// the client tick, which reads bound combos, never waits for that.
struct XInputSlots {
std::atomic<unsigned> connected{};
std::atomic<ULONGLONG> next_probe{};
std::atomic<HANDLE> wake{}; // set when a probe is due; the probe thread waits on it
std::mutex start; // starting the thread (tried again at the next probe if it fails)
};
XInputSlots& xinput_slots() { static auto* value = new XInputSlots; return *value; } // process lifetime
DWORD WINAPI probe_xinput_slots(void* parameter) noexcept {
auto& slots = *static_cast<XInputSlots*>(parameter);
const auto get_state = xinput_get_state();
for (;;) {
WaitForSingleObject(slots.wake.load(), INFINITE);
keep_xinput_capture_first();
OverlayInputAccess access;
for (DWORD slot = 0; get_state && slot < XUSER_MAX_COUNT; ++slot) {
const unsigned bit = 1u << slot;
if (slots.connected.load() & bit) continue;
XINPUT_STATE sample{};
if (get_state(slot, &sample) == ERROR_SUCCESS) slots.connected.fetch_or(bit);
}
}
}
// Wakes the probe thread (starting it the first time) at most once a second.
void request_xinput_probe(XInputSlots& slots) {
const auto now = GetTickCount64();
auto due = slots.next_probe.load();
if (now < due || !slots.next_probe.compare_exchange_strong(due, now + 1000)) return;
if (!slots.wake.load()) {
std::lock_guard lock(slots.start);
if (!slots.wake.load()) {
if (const auto wake = CreateEventW(nullptr, FALSE, FALSE, nullptr)) {
slots.wake.store(wake);
if (const auto thread = CreateThread(nullptr, 0, probe_xinput_slots, &slots, 0, nullptr))
CloseHandle(thread);
else {
slots.wake.store(nullptr);
CloseHandle(wake);
}
}
}
}
if (const auto wake = slots.wake.load()) SetEvent(wake);
}
ControllerSample read_controller_sample() {
OverlayInputAccess access; // Binding capture needs the real controller while the menu is open.
const auto get_state = xinput_get_state();
// Virtual pads (DS4Windows, Steam, ViGEm leftovers) can take any slot, and
// an idle one may sit ahead of the pad in use, so merge every connected
// slot. Empty slots are retried in the background (xinput_slots above).
auto& slots = xinput_slots();
if (get_state) request_xinput_probe(slots);
const auto connected_slots = slots.connected.load();
unsigned connected = 0;
ControllerSample result;
for (DWORD slot = 0; get_state && slot < XUSER_MAX_COUNT; ++slot) {
const unsigned bit = 1u << slot;
if (!(connected_slots & bit)) continue;
XINPUT_STATE sample{};
if (get_state(slot, &sample) != ERROR_SUCCESS) { slots.connected.fetch_and(~bit); continue; }
connected |= bit;
const auto& pad = sample.Gamepad;
result.pad.wButtons |= pad.wButtons;
result.pad.bLeftTrigger = std::max(result.pad.bLeftTrigger, pad.bLeftTrigger);
result.pad.bRightTrigger = std::max(result.pad.bRightTrigger, pad.bRightTrigger);
const auto magnitude = [](SHORT x, SHORT y) { return std::abs(int(x)) + std::abs(int(y)); };
if (magnitude(pad.sThumbLX, pad.sThumbLY) > magnitude(result.pad.sThumbLX, result.pad.sThumbLY)) {
result.pad.sThumbLX = pad.sThumbLX; result.pad.sThumbLY = pad.sThumbLY;
}
if (magnitude(pad.sThumbRX, pad.sThumbRY) > magnitude(result.pad.sThumbRX, result.pad.sThumbRY)) {
result.pad.sThumbRX = pad.sThumbRX; result.pad.sThumbRY = pad.sThumbRY;
}
}
// A PlayStation pad the tool left visible still names the buttons, even
// while its virtual XInput twin supplies them.
const auto playstation = read_playstation_pads();
result.style = playstation.style;
if (connected) {
// XInput wins whenever a pad is there: reading both would double a
// DS4Windows pad and mix its remapped buttons with the physical ones.
result.device = connected;
return result;
}
if (!playstation.available) return {};
const auto& pad = playstation.pad;
result.pad.wButtons = pad.buttons;
result.pad.bLeftTrigger = pad.left_trigger; result.pad.bRightTrigger = pad.right_trigger;
result.pad.sThumbLX = pad.left_x; result.pad.sThumbLY = pad.left_y;
result.pad.sThumbRX = pad.right_x; result.pad.sThumbRY = pad.right_y;
result.device = 0x100u + (playstation.generation & 0xffffffu);
return result;
}
dingosdk::overlay::FlightInput read_player_flight_controller() {
const auto sample = read_controller_sample();
if (!sample.device) return {};
const auto& pad = sample.pad;
auto input = dingosdk::controller_flight_input(pad.sThumbLX, pad.sThumbLY, pad.bLeftTrigger, pad.bRightTrigger,
(pad.wButtons & XINPUT_GAMEPAD_LEFT_THUMB) != 0);
const float rx = static_cast<float>(pad.sThumbRX), ry = static_cast<float>(pad.sThumbRY);
const float magnitude = std::sqrt(rx * rx + ry * ry);
constexpr float deadzone = 8689.0f;
if (magnitude > deadzone) {
const float scale = std::clamp((magnitude - deadzone) / (32767.0f - deadzone), 0.0f, 1.0f) / magnitude;
input.look_x = rx * scale * 25.0f;
input.look_y = ry * scale * -25.0f;
}
return input;
}
}
namespace dingosdk::overlay {
void begin_input_access() noexcept { ++detail::overlay_input_access; }
void end_input_access() noexcept { --detail::overlay_input_access; }
Gamepad read_gamepad() {
const auto sample = read_controller_sample();
Gamepad out;
if (!sample.device) return out;
const auto axis = [](std::int16_t v) { return std::clamp(static_cast<float>(v) / 32767.0f, -1.0f, 1.0f); };
out.valid = true;
out.playstation = sample.device >= 0x100u;
out.left_x = axis(sample.pad.sThumbLX); out.left_y = axis(sample.pad.sThumbLY);
out.right_x = axis(sample.pad.sThumbRX); out.right_y = axis(sample.pad.sThumbRY);
out.left_trigger = static_cast<float>(sample.pad.bLeftTrigger) / 255.0f;
out.right_trigger = static_cast<float>(sample.pad.bRightTrigger) / 255.0f;
out.buttons = sample.pad.wButtons;
return out;
}
} // namespace dingosdk::overlay
extern "C" void DingoSDKOverlaySetFreecamInputCapture(bool active) {
const bool previous = state().freecam_controller_active.exchange(active, std::memory_order_relaxed);
if (previous != active)
dingosdk::logging::printf(dingosdk::logging::Level::info, dingosdk::logging::Channel::input,
"Freecam controller input capture %s.", active ? "enabled" : "disabled");
}
extern "C" void DingoSDKOverlaySetPromptInputCapture(bool active) {
const bool before = state().prompt_input_active.exchange(active, std::memory_order_relaxed);
if (before == active) return;
// For the log: which of the ways input can be kept from the game the game actually read
// its controller by while it was held. All zero means it reads some other way, and the
// hold did not reach it.
if (active) {
keep_xinput_capture_first();
(void)take_prompt_reads();
dingosdk::logging::write(dingosdk::logging::Level::info, dingosdk::logging::Channel::input, "Card input hold on.");
} else {
const auto reads = take_prompt_reads();
// And which controller libraries the game has loaded at all.
std::string loaded;
for (const auto* name : {L"xinput1_4.dll", L"xinput1_3.dll", L"xinput9_1_0.dll", L"gameinput.dll", L"gameinputredist.dll",
L"windows.gaming.input.dll", L"steamclient64.dll", L"dinput8.dll"})
if (GetModuleHandleW(name)) {
if (!loaded.empty()) loaded += ", ";
for (const wchar_t* at = name; *at; ++at) loaded += static_cast<char>(*at);
}
dingosdk::logging::printf(dingosdk::logging::Level::info, dingosdk::logging::Channel::input,
"Card input hold off. While held the game read: XInput %u, DirectInput %u, raw input %u time(s). Controller libraries loaded: %s. XInput leads to: %s.",
reads.xinput, reads.direct_input, reads.raw, loaded.empty() ? "none" : loaded.c_str(), xinput_entry_owners().c_str());
}
}
extern "C" unsigned DingoSDKOverlayReadPromptKeys() {
struct PreserveError { DWORD value = GetLastError(); ~PreserveError() { SetLastError(value); } } preserve_error;
const auto& s = state();
const HWND window = s.window.load();
// (Not while a menu, the console or chat has the keyboard; the card itself having the pointer is no reason.)
const bool typing = s.visible.load() || s.console_visible.load() || s.editor_visible.load() || s.chat_visible.load() || s.hub_typing.load();
if (!window || s.stop.load() || s.failed.load() || typing || !game_window_foreground(window)) return 0;
unsigned keys{};
constexpr int wanted[]{VK_LEFT, VK_RIGHT, VK_RETURN, VK_ESCAPE};
if (std::array<std::uint64_t, 4> held{}; sampled_keys(window, held)) {
for (unsigned bit = 0; bit < 4; ++bit)
if (held[wanted[bit] / 64] >> (wanted[bit] % 64) & 1) keys |= 1u << bit;
return keys;
}
OverlayInputAccess access;
for (unsigned bit = 0; bit < 4; ++bit)
if (GetAsyncKeyState(wanted[bit]) & 0x8000) keys |= 1u << bit;
return keys;
}
extern "C" void DingoSDKOverlayReadControllerInput(dingosdk::ControllerInput* output, bool allow_menu) {
if (!output) return;
struct PreserveError { DWORD value = GetLastError(); ~PreserveError() { SetLastError(value); } } preserve_error;
*output = {};
const auto& s = state();
const HWND window = s.window.load();
if (!window || s.stop.load() || s.failed.load() || (!allow_menu && interactive_visible(s)) ||
!game_window_foreground(window)) return;
// Read through the overlay's bypass: freecam capture hides keys from the game. (As sampled
// off this thread when there is a recent sample, cursor.cpp.)
if (std::array<std::uint64_t, 4> held{}; sampled_keys(window, held)) {
for (unsigned key = 0; key < 256; ++key)
if (dingosdk::bindable_keyboard_key(key) && (held[key / 64] >> (key % 64) & 1))
output->keys[key / 64] |= std::uint64_t{1} << (key % 64);
} else {
OverlayInputAccess access;
for (unsigned key = 0; key < 256; ++key) {
if (dingosdk::bindable_keyboard_key(key) && (GetAsyncKeyState(key) & 0x8000)) {
output->keys[key / 64] |= std::uint64_t{1} << (key % 64);
}
}
}
const auto sample = read_controller_sample();
output->style = sample.style;
if (!sample.device) return;
const auto& pad = sample.pad;
output->available = true;
output->device = sample.device;
output->buttons = pad.wButtons & dingosdk::controller_button_mask;
if (pad.bLeftTrigger > XINPUT_GAMEPAD_TRIGGER_THRESHOLD) output->buttons |= 0x10000;
if (pad.bRightTrigger > XINPUT_GAMEPAD_TRIGGER_THRESHOLD) output->buttons |= 0x20000;
}
// Raw mouse packets arrived recently enough to trust the tracked buttons.
static bool raw_mouse_live(const State& s) {
const auto time = s.raw_mouse_time.load();
return time && GetTickCount64() < time + 10000;
}
namespace dingosdk::overlay {
bool read_mouse_look(MouseLook& output) noexcept {
struct PreserveError { DWORD value = GetLastError(); ~PreserveError() { SetLastError(value); } } preserve_error;
output = {};
auto& s = state();
// Taken whatever happens next, so nothing piles up while a menu is open.
const long x = s.look_mouse_x.exchange(0), y = s.look_mouse_y.exchange(0);
const long wheel_raw = s.look_wheel_raw.exchange(0), wheel_message = s.look_wheel_message.exchange(0);
const HWND window = s.window.load();
if (!window || s.stop.load() || s.failed.load() || interactive_visible(s) || !game_window_foreground(window)) return false;
output.x = x;
output.y = y;
// The same notch can arrive both ways: the raw count where raw packets come in, else the message's.
output.wheel = raw_mouse_live(s) && wheel_raw != 0 ? wheel_raw : wheel_message;
return true;
}
} // namespace dingosdk::overlay
extern "C" void DingoSDKOverlayReadFlightInput(dingosdk::overlay::FlightInput* output, bool flight_active, bool player_flight) {
if (!output) return;
struct PreserveError { DWORD value = GetLastError(); ~PreserveError() { SetLastError(value); } } preserve_error;
*output = {};
// Only this client thread owns the mouse baseline; ImGui is never accessed.
thread_local POINT previous{};
thread_local bool looking = false;
const auto& s = state();
const HWND window = s.window.load();
const bool editor_flight = s.editor_visible.load() && s.editor_flight.load() && !s.console_visible.load() &&
GetTickCount64() < s.editor_input_time.load() + 150;
if (!flight_active || !window || s.stop.load() || s.failed.load() || (interactive_visible(s) && !editor_flight) ||
!game_window_foreground(window)) {
looking = false;
return;
}
OverlayInputAccess editor_access;
// (The keys as sampled off this thread when there is a sample, cursor.cpp: this runs every
// client frame while flying.)
std::array<std::uint64_t, 4> sampled{};
const bool have_sample = sampled_keys(window, sampled);
const auto held = [&](int key) {
return have_sample ? ((sampled[key / 64] >> (key % 64)) & 1) != 0 : (GetAsyncKeyState(key) & 0x8000) != 0;
};
// Physical right button: the logical one unless the buttons are swapped.
const bool swapped = GetSystemMetrics(SM_SWAPBUTTON) != 0;
const bool raw = raw_mouse_live(s);
const bool raw_look_button = raw && (s.raw_mouse_buttons.load() & (swapped ? 1u : 2u)) != 0;
const bool look_button = held(VK_RBUTTON) || raw_look_button;
if (s.editor_visible.load() && !look_button) { looking = false; return; }
output->active = true;
output->right = static_cast<float>(held('D')) - static_cast<float>(held('A'));
output->up = static_cast<float>(held('E')) - static_cast<float>(held('Q'));
output->forward = static_cast<float>(held('W')) - static_cast<float>(held('S'));
output->boost = held(VK_SHIFT);
if (player_flight) {
const auto controller = read_player_flight_controller();
output->right = std::clamp(output->right + controller.right, -1.0f, 1.0f);
output->forward = std::clamp(output->forward + controller.forward, -1.0f, 1.0f);
output->up = std::clamp(output->up + controller.up, -1.0f, 1.0f);
output->boost = output->boost || controller.boost;
output->look_x = controller.look_x;
output->look_y = controller.look_y;
looking = false;
return; // Native camera owns look input; do not sample or recenter the cursor.
}
POINT current{};
const long raw_x = state().raw_mouse_x.exchange(0), raw_y = state().raw_mouse_y.exchange(0);
if (look_button && GetCursorPos(&current)) {
if (looking) {
// Raw deltas when the game window receives raw input: they keep
// working whatever Skate does with the Windows cursor.
output->look_x = static_cast<float>(raw ? raw_x : current.x - previous.x);
output->look_y = static_cast<float>(raw ? raw_y : current.y - previous.y);
}
previous = current;
looking = true;
RECT bounds{};
if (GetClientRect(window, &bounds)) {
POINT center{(bounds.left + bounds.right) / 2, (bounds.top + bounds.bottom) / 2};
if (ClientToScreen(window, &center) && SetCursorPos(center.x, center.y)) previous = center;
}
} else looking = false;
}
// For native tools that run beside the game (a controller translator, say): whether ReSkate's menu,
// a free camera on the pad or a prompt has the input now, so they hold their own input back while it
// does. 1 or 0. Versioned: a later meaning gets a new name. Any thread; the caller's last error is kept.
extern "C" __declspec(dllexport) unsigned long WINAPI ReSkateOverlayOwnsInputV1() {
const auto error = GetLastError();
const auto& s = state();
const bool owns = owns_menu_cursor(s) || s.freecam_controller_active.load(std::memory_order_relaxed) ||
s.prompt_input_active.load(std::memory_order_relaxed);
SetLastError(error);
return owns ? 1ul : 0ul;
}