Files
ReSkate/Extension/UI/Overlay/overlay.cpp
T
Fourth StudioandClaude Opus 5.5 6f41a16f1b Item grids: favorites, search and filters
In the skater's item grids (Apparel, Accessories, Board, Gestures) F favorites
the highlighted item, - (or Ctrl+F) opens a search box that filters as you
type, and X steps a filter (All, Favorites, Mods, Game items). Favorites sit
right after the worn item, carry the game's star where the tile shows its
rarity icon, and are kept in ReSkate.favorites.json beside Skate.exe.

The grids stay the game's own: their tile rows are reordered and filtered
through the native model functions, as the music screen's Mods shelf does.
Nothing is hooked for it and no file of the game or of a mod is changed.
See contrib/native-item-browser.md for how it works and what is not tried yet.

This is the Item Favorites mod done natively, so it no longer needs a DLL
loaded through a custom script.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:23:33 +02:00

437 lines
20 KiB
C++

#include "Engine/Core/Log/logging.h"
#include "overlay_internal.h"
#include "input_capture.h"
#include "cursor.h"
#include "park_previews.h"
#include "chat_emotes.h"
using namespace dingosdk::overlay::detail;
namespace dingosdk::overlay::detail {
State& state() { static State* value = new State; return *value; }
thread_local bool inside_present = false;
bool install(Hook& hook, void* target, void* detour) {
if (hook.target) return hook.target == target;
if (!target) return false;
void* trampoline = nullptr;
const auto created = dingosdk::hook_prepare(target, detour, &trampoline);
if (created != dingosdk::HookOk) {
dingosdk::logging::printf(dingosdk::logging::Level::error, dingosdk::logging::Channel::graphics, "Hook creation failed: target=%p status=%ld (%s)", target,
static_cast<long>(created), dingosdk::hook_status_string(created));
return false;
}
hook.target = target;
hook.original = trampoline;
const auto enabled = dingosdk::hook_enable(target);
if (enabled != dingosdk::HookOk) {
const auto failure = dingosdk::hook_last_failure();
dingosdk::logging::printf(dingosdk::logging::Level::error, dingosdk::logging::Channel::hooks,
"Hook activation failed: target=%p status=%ld (%s), operation=%s thread=%lu", target,
static_cast<long>(enabled), dingosdk::hook_status_string(enabled), failure.operation, failure.thread_id);
dingosdk::hook_remove(target);
hook = {};
return false;
}
return true;
}
bool is_input(UINT message) {
return (message >= WM_MOUSEFIRST && message <= WM_MOUSELAST)
|| (message >= WM_KEYFIRST && message <= WM_KEYLAST)
|| message == WM_INPUT;
}
bool interactive_visible(const State& s) {
return s.visible.load() || s.console_visible.load() || s.editor_visible.load() || s.chat_visible.load() ||
s.hub_typing.load() || s.item_search_visible.load();
}
void restore_input(bool hide_menu) {
auto& s = state();
if (hide_menu) {
s.visible.store(false);
s.console_visible.store(false);
s.chat_visible.store(false);
s.hub_typing.store(false);
s.item_search_visible.store(false);
s.editor_visible.store(false);
s.editor_flight.store(false);
}
// Also release ownership during swapchain recreation and shutdown.
s.input_attached.store(false);
sync_menu_cursor(true);
request_window_cursor_sync(s.window.load());
// Keep the pinned forwarding link until HWND destruction, including while
// detached. The posted release must still reach its owning window thread;
// removing the WndProc here can otherwise strand mouse capture after resize.
}
void request_stop_locked() {
auto& s = state();
if (s.editor_visible.load() && s.callbacks.queue_debug) {
std::array<char, 256> result{};
s.callbacks.queue_debug(s.callbacks.user, {dingosdk::overlay::DebugAction::set_park_editor, false}, result.data(), result.size());
}
s.stop.store(true);
if (s.stop_event) SetEvent(s.stop_event);
restore_input(true);
}
void request_stop() {
auto& s = state();
std::lock_guard lock(s.render_mutex);
request_stop_locked();
}
HRESULT STDMETHODCALLTYPE present(IDXGISwapChain* chain, UINT interval, UINT flags) {
PresentGuard guard;
if (guard.outermost) guarded_render(chain, flags);
const HRESULT result = original<PresentFn>(state().present)(chain, interval, flags);
if (result == DXGI_ERROR_DEVICE_REMOVED || result == DXGI_ERROR_DEVICE_RESET) {
ComPtr<ID3D12Device> device;
chain->GetDevice(IID_PPV_ARGS(&device));
record_device_failure(device.Get(), result, "Present");
request_stop();
}
return result;
}
HRESULT STDMETHODCALLTYPE present1(IDXGISwapChain1* chain, UINT interval, UINT flags,
const DXGI_PRESENT_PARAMETERS* parameters) {
PresentGuard guard;
if (guard.outermost) guarded_render(chain, flags);
const HRESULT result = original<Present1Fn>(state().present1)(chain, interval, flags, parameters);
if (result == DXGI_ERROR_DEVICE_REMOVED || result == DXGI_ERROR_DEVICE_RESET) {
ComPtr<ID3D12Device> device;
chain->GetDevice(IID_PPV_ARGS(&device));
record_device_failure(device.Get(), result, "Present1");
request_stop();
}
return result;
}
bool before_resize(IDXGISwapChain* chain) {
auto& s = state();
if (!s.swapchain) return true;
const auto candidate = object_identity(chain);
if (!candidate || candidate.Get() != s.swapchain_identity) return true;
if (!completed(s.next_fence, 500)) {
// Do not reset an allocator or release a buffer still used by the GPU.
// The caller can retry resizing without changing menu visibility.
dingosdk::logging::write(dingosdk::logging::Level::warning, dingosdk::logging::Channel::graphics, "Resize postponed: overlay GPU work has not completed.");
return false;
}
s.show_on_ready = s.visible.load();
destroy_graphics();
return true;
}
HRESULT STDMETHODCALLTYPE resize(IDXGISwapChain* chain, UINT count, UINT width, UINT height,
DXGI_FORMAT format, UINT flags) {
std::lock_guard lock(state().render_mutex);
if (!before_resize(chain)) return DXGI_ERROR_WAS_STILL_DRAWING;
return original<ResizeFn>(state().resize)(chain, count, width, height, format, flags);
}
HRESULT STDMETHODCALLTYPE resize1(IDXGISwapChain3* chain, UINT count, UINT width, UINT height,
DXGI_FORMAT format, UINT flags, const UINT* masks, IUnknown* const* queues) {
std::lock_guard lock(state().render_mutex);
const auto candidate = object_identity(chain);
if (!candidate || !find_binding(candidate.Get())) {
return original<Resize1Fn>(state().resize1)(
chain, count, width, height, format, flags, masks, queues);
}
const bool selected = candidate.Get() == state().swapchain_identity;
if (selected && !before_resize(chain)) return DXGI_ERROR_WAS_STILL_DRAWING;
// ResizeBuffers1 may change the presentation queue. A heterogeneous queue
// list cannot be rendered with a single queue. BufferCount == 0 retains the
// current count, so its queue array still describes every existing buffer.
ComPtr<ID3D12CommandQueue> replacement_queue;
bool unsupported_queues = false;
UINT queue_count = count;
if (queues && queue_count == 0) {
DXGI_SWAP_CHAIN_DESC description{};
if (FAILED(chain->GetDesc(&description))) unsupported_queues = true;
else queue_count = description.BufferCount;
}
if (queues && !unsupported_queues) {
if (queue_count == 0 || queue_count > DXGI_MAX_SWAP_CHAIN_BUFFERS || !queues[0]
|| FAILED(queues[0]->QueryInterface(IID_PPV_ARGS(&replacement_queue)))
|| replacement_queue->GetDesc().Type != D3D12_COMMAND_LIST_TYPE_DIRECT) {
unsupported_queues = true;
replacement_queue.Reset();
} else {
const auto first_identity = object_identity(replacement_queue.Get());
if (!first_identity) unsupported_queues = true;
for (UINT i = 1; i < queue_count && !unsupported_queues; ++i) {
ComPtr<ID3D12CommandQueue> entry;
const auto entry_identity = queues[i] ? object_identity(queues[i]) : nullptr;
if (!entry_identity || entry_identity.Get() != first_identity.Get()
|| FAILED(queues[i]->QueryInterface(IID_PPV_ARGS(&entry)))
|| entry->GetDesc().Type != D3D12_COMMAND_LIST_TYPE_DIRECT)
unsupported_queues = true;
}
if (unsupported_queues) replacement_queue.Reset();
}
}
const auto result = original<Resize1Fn>(state().resize1)(
chain, count, width, height, format, flags, masks, queues);
// DXGI owns the queue transition. Commit the candidate only when the real
// resize accepts it; a failed resize keeps the previous presentation queue.
if (SUCCEEDED(result) && replacement_queue) {
if (selected) state().queue = replacement_queue;
if (auto* binding = find_binding(candidate.Get()))
binding->queue = replacement_queue;
dingosdk::logging::printf(dingosdk::logging::Level::debug, dingosdk::logging::Channel::graphics, "ResizeBuffers1 accepted presentation queue change: chain=%p queue=%p buffers=%u",
chain, replacement_queue.Get(), queue_count);
} else if (SUCCEEDED(result) && unsupported_queues) {
dingosdk::logging::printf(dingosdk::logging::Level::warning, dingosdk::logging::Channel::graphics, "ResizeBuffers1 accepted an unsupported presentation queue list; stopping overlay drawing");
request_stop_locked();
}
return result;
}
HRESULT STDMETHODCALLTYPE create(IDXGIFactory* factory, IUnknown* device,
DXGI_SWAP_CHAIN_DESC* description, IDXGISwapChain** output) {
DXGI_SWAP_CHAIN_DESC windowed{};
if (state().force_windowed && description && !description->Windowed) {
windowed = *description;
windowed.Windowed = TRUE;
description = &windowed;
dingosdk::logging::printf(dingosdk::logging::Level::debug, dingosdk::logging::Channel::graphics, "Windowed startup: changed initial swapchain fullscreen request to windowed");
}
const HRESULT result = original<CreateFn>(state().create)(factory, device, description, output);
if (SUCCEEDED(result) && output && *output && device) {
ComPtr<ID3D12CommandQueue> queue;
if (SUCCEEDED(device->QueryInterface(IID_PPV_ARGS(&queue)))) DingoSDKOverlayBindDx12(*output, queue.Get());
}
return result;
}
HRESULT STDMETHODCALLTYPE create_hwnd(IDXGIFactory2* factory, IUnknown* device, HWND window,
const DXGI_SWAP_CHAIN_DESC1* description, const DXGI_SWAP_CHAIN_FULLSCREEN_DESC* fullscreen,
IDXGIOutput* restrict_output, IDXGISwapChain1** output) {
DXGI_SWAP_CHAIN_FULLSCREEN_DESC windowed{};
if (state().force_windowed && fullscreen && !fullscreen->Windowed) {
windowed = *fullscreen;
windowed.Windowed = TRUE;
fullscreen = &windowed;
dingosdk::logging::printf(dingosdk::logging::Level::debug, dingosdk::logging::Channel::graphics, "Windowed startup: changed initial HWND swapchain fullscreen request to windowed");
}
const HRESULT result = original<CreateHwndFn>(state().create_hwnd)(factory, device, window,
description, fullscreen, restrict_output, output);
if (SUCCEEDED(result) && output && *output && device) {
ComPtr<ID3D12CommandQueue> queue;
if (SUCCEEDED(device->QueryInterface(IID_PPV_ARGS(&queue)))) DingoSDKOverlayBindDx12(*output, queue.Get());
}
return result;
}
HRESULT STDMETHODCALLTYPE set_fullscreen(IDXGISwapChain* chain, BOOL fullscreen, IDXGIOutput* output) {
if (state().force_windowed && fullscreen) {
dingosdk::logging::printf(dingosdk::logging::Level::debug, dingosdk::logging::Channel::graphics, "Windowed startup: redirected SetFullscreenState(TRUE) to windowed");
fullscreen = FALSE;
output = nullptr;
}
return original<FullscreenFn>(state().fullscreen)(chain, fullscreen, output);
}
HRESULT WINAPI factory(REFIID iid, void** output) {
const HRESULT result = original<FactoryFn>(state().factory)(iid, output);
if (FAILED(result) || !output || !*output || state().stop.load()) return result;
ComPtr<IDXGIFactory> base;
auto* object = static_cast<IUnknown*>(*output);
if (FAILED(object->QueryInterface(IID_PPV_ARGS(&base)))) return result;
std::lock_guard lock(state().hook_mutex);
auto** table = *reinterpret_cast<void***>(base.Get());
const bool basic = install(state().create, table[10], reinterpret_cast<void*>(create));
ComPtr<IDXGIFactory2> extended;
bool hwnd = true;
if (SUCCEEDED(base.As(&extended))) {
table = *reinterpret_cast<void***>(extended.Get());
hwnd = install(state().create_hwnd, table[15], reinterpret_cast<void*>(create_hwnd));
}
if (!basic || !hwnd) dingosdk::logging::write(dingosdk::logging::Level::warning, dingosdk::logging::Channel::graphics, "DXGI factory differs from the hooked implementation; unsupported path ignored.");
return result;
}
bool install_factory_hook_locked() {
auto& s = state();
if (s.factory.target) return true;
constexpr const wchar_t* providers[] = {L"sl.interposer.dll", L"dxgi.dll"};
for (const auto* provider : providers) {
const HMODULE module = GetModuleHandleW(provider);
if (!module) continue;
const auto address = reinterpret_cast<void*>(GetProcAddress(module, "CreateDXGIFactory1"));
if (address && install(s.factory, address, reinterpret_cast<void*>(factory))) return true;
}
return false;
}
void CALLBACK dll_notification(ULONG reason, const void*, void*) {
// LDR_DLL_NOTIFICATION_REASON_LOADED is 1. Do only the loader-lock-safe
// event signal here; the worker performs module inspection and hook setup.
if (reason == 1 && state().provider_event) SetEvent(state().provider_event);
}
DWORD WINAPI factory_bootstrap(void*) noexcept {
auto& s = state();
while (!s.stop.load()) {
{
std::lock_guard lock(s.hook_mutex);
if (s.stop.load()) return 0;
if (install_factory_hook_locked()) {
dingosdk::logging::write(dingosdk::logging::Level::info, dingosdk::logging::Channel::graphics, "DXGI provider hooked; waiting for a DX12 swapchain.");
return 0;
}
}
const HANDLE events[] = {s.stop_event, s.provider_event};
const DWORD count = s.provider_event ? 2 : 1;
if (WaitForMultipleObjects(count, events, FALSE, 25) == WAIT_OBJECT_0) return 0;
}
return 0;
}
bool start_overlay(const dingosdk::overlay::CallbacksV3* callbacks) {
auto& s = state();
std::lock_guard lock(s.hook_mutex);
if (s.started || s.stop.load()) return false;
dingosdk::overlay::initialize_graphics_diagnostics();
s.force_windowed = dingosdk::overlay::graphics_option_enabled(L"RESKATE_FORCE_WINDOWED");
{
wchar_t game[32768]{};
const auto length = GetModuleFileNameW(nullptr, game, 32768);
if (length && length < 32768)
{
dingosdk::overlay::start_park_previews(std::filesystem::path(std::wstring(game, length)).parent_path());
dingosdk::overlay::start_chat_emotes();
}
}
HMODULE pinned = nullptr;
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN,
reinterpret_cast<LPCWSTR>(&start_overlay), &pinned)) return false;
const auto status = dingosdk::hook_initialize();
if (status != dingosdk::HookOk && status != dingosdk::HookAlreadyInitialized) return false;
const auto user32 = GetModuleHandleW(L"user32.dll");
if (!user32 || !install(s.clip_cursor, reinterpret_cast<void*>(GetProcAddress(user32, "ClipCursor")),
reinterpret_cast<void*>(clip_cursor)) ||
!install(s.set_cursor_pos, reinterpret_cast<void*>(GetProcAddress(user32, "SetCursorPos")),
reinterpret_cast<void*>(set_cursor_pos)) || !install_input_capture()) return false;
if (!s.stop_event) s.stop_event = CreateEventW(nullptr, TRUE, FALSE, nullptr);
if (!s.stop_event) return false;
if (callbacks) s.callbacks = *callbacks;
s.started = true;
if (install_factory_hook_locked()) {
dingosdk::logging::write(dingosdk::logging::Level::info, dingosdk::logging::Channel::graphics, "Waiting for the game's DX12 swapchain creation.");
return true;
}
if (!s.provider_event) s.provider_event = CreateEventW(nullptr, FALSE, FALSE, nullptr);
if (s.provider_event && !s.dll_notification_cookie) {
const HMODULE ntdll = GetModuleHandleW(L"ntdll.dll");
const auto register_notification = ntdll
? reinterpret_cast<RegisterDllNotificationFn>(
GetProcAddress(ntdll, "LdrRegisterDllNotification"))
: nullptr;
if (register_notification)
register_notification(0, dll_notification, nullptr, &s.dll_notification_cookie);
}
const HANDLE thread = CreateThread(nullptr, 0, factory_bootstrap, nullptr, 0, nullptr);
if (!thread) {
s.started = false;
s.callbacks = {};
return false;
}
CloseHandle(thread);
dingosdk::logging::write(dingosdk::logging::Level::info, dingosdk::logging::Channel::graphics, "DXGI provider not loaded yet; deferred hook bootstrap started.");
return true;
}
}
extern "C" bool DingoSDKOverlayStart(const dingosdk::overlay::Callbacks* callbacks) {
dingosdk::overlay::CallbacksV3 upgraded;
if (callbacks) {
upgraded.user = callbacks->user;
upgraded.read_model = callbacks->read_model;
upgraded.queue_load = callbacks->queue_load;
upgraded.queue_debug = callbacks->queue_debug;
}
return start_overlay(&upgraded);
}
extern "C" bool DingoSDKOverlayStartV2(const dingosdk::overlay::CallbacksV2* callbacks) {
dingosdk::overlay::CallbacksV3 upgraded;
if (callbacks) {
upgraded.user = callbacks->user;
upgraded.read_model = callbacks->read_model;
upgraded.queue_load = callbacks->queue_load;
upgraded.queue_debug = callbacks->queue_debug;
upgraded.queue_offline_feature = callbacks->queue_offline_feature;
}
return start_overlay(&upgraded);
}
extern "C" bool DingoSDKOverlayStartV3(const dingosdk::overlay::CallbacksV3* callbacks) {
return start_overlay(callbacks);
}
extern "C" bool DingoSDKOverlayBindDx12(IDXGISwapChain* chain, ID3D12CommandQueue* queue) {
if (!chain || !queue || queue->GetDesc().Type != D3D12_COMMAND_LIST_TYPE_DIRECT) return false;
auto& s = state();
std::lock_guard render_lock(s.render_mutex);
std::lock_guard hook_lock(s.hook_mutex);
if (!s.started || s.stop.load()) return false;
DXGI_SWAP_CHAIN_DESC description{};
if (FAILED(chain->GetDesc(&description)) || !description.OutputWindow
|| description.BufferCount < 2 || description.BufferCount > 8) return false;
DWORD process = 0;
GetWindowThreadProcessId(description.OutputWindow, &process);
if (process != GetCurrentProcessId()) return false;
ComPtr<IDXGISwapChain3> extended;
if (FAILED(chain->QueryInterface(IID_PPV_ARGS(&extended)))) return false;
const auto identity = object_identity(chain);
if (!identity) return false;
auto** base = *reinterpret_cast<void***>(chain);
auto** table = *reinterpret_cast<void***>(extended.Get());
// COM method indices are the Windows SDK's IDXGISwapChain[1/3] ABI.
if (!install(s.present, base[8], reinterpret_cast<void*>(present))
|| (s.force_windowed && !install(s.fullscreen, base[10], reinterpret_cast<void*>(set_fullscreen)))
|| !install(s.resize, base[13], reinterpret_cast<void*>(resize))
|| !install(s.present1, table[22], reinterpret_cast<void*>(present1))
|| !install(s.resize1, table[39], reinterpret_cast<void*>(resize1))) {
dingosdk::logging::write(dingosdk::logging::Level::warning, dingosdk::logging::Channel::graphics, "Swapchain hook setup incomplete; candidate ignored.");
return false;
}
if (auto* existing = find_binding(identity.Get())) {
existing->queue = queue;
existing->window = description.OutputWindow;
if (s.swapchain_identity == identity.Get()) s.queue = queue;
return true;
}
if (s.bindings.size() >= maximum_bindings) {
const auto evict = std::find_if(s.bindings.begin(), s.bindings.end(),
[&](const Binding& candidate) { return candidate.identity != s.swapchain_identity; });
if (evict == s.bindings.end()) return false;
s.bindings.erase(evict);
}
s.bindings.push_back({identity.Get(), queue, description.OutputWindow});
dingosdk::logging::write(dingosdk::logging::Level::info, dingosdk::logging::Channel::graphics, "Registered DX12 swapchain and its creation queue.");
return true;
}
extern "C" void DingoSDKOverlayRequestStop() {
request_stop();
}
bool dingosdk::overlay::keyboard_shortcuts_allowed() noexcept {
auto &s = state();
return s.started.load() && !s.stop.load() && !interactive_visible(s);
}
extern "C" void DingoSDKOverlayGetStatus(dingosdk::overlay::Status* output) {
if (!output) return;
auto& s = state();
std::lock_guard lock(s.render_mutex);
*output = {s.started.load(), s.swapchain != nullptr, s.ready, s.visible.load(),
s.stop.load(), s.failed.load(), s.rendered_frames};
}