#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" 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(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 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(*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(lp)) + " took over)"; else if (message == WM_ACTIVATE) text = LOWORD(wp) == WA_INACTIVE ? "game window deactivated for " + describe(reinterpret_cast(lp)) : "game window activated from " + describe(reinterpret_cast(lp)); else if (message == WM_KILLFOCUS) text = "keyboard focus moved to " + describe(reinterpret_cast(wp)); else if (message == WM_SETFOCUS) text = "keyboard focus returned from " + describe(reinterpret_cast(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(MapVirtualKeyW(key, MAPVK_VK_TO_VSC)) << 16) | static_cast((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(); 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(lp), pointer_owned)) noted_raw_input = reinterpret_cast(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 (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(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(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(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(wp), MAPVK_VK_TO_CHAR) == L'/' && !(GetKeyState(VK_SHIFT) & 0x8000))); if (key && (wp == 'T' || slash) && (static_cast(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(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(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(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(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 capture = owns_pointer(); const bool freecam_capture = s.freecam_controller_active.load(std::memory_order_relaxed) || s.prompt_input_active.load(std::memory_order_relaxed); if (freecam_capture) release_game_buttons(window, previous); 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 down{WM_LBUTTONDOWN, WM_RBUTTONDOWN, WM_MBUTTONDOWN, WM_XBUTTONDOWN}; constexpr std::array 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 connected{}; std::atomic next_probe{}; std::atomic 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(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(pad.sThumbRX), ry = static_cast(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(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(sample.pad.bLeftTrigger) / 255.0f; out.right_trigger = static_cast(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(*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 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 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; } 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 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(held('D')) - static_cast(held('A')); output->up = static_cast(held('E')) - static_cast(held('Q')); output->forward = static_cast(held('W')) - static_cast(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(¤t)) { 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(raw ? raw_x : current.x - previous.x); output->look_y = static_cast(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, ¢er) && SetCursorPos(center.x, center.y)) previous = center; } } else looking = false; }