mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-18 03:47:01 +02:00
The Multiplayer and Custom Stuff pages of the game's pause menu were built from the game's own menu widgets: a button, a text box and a line of text, which cannot make a list with columns. The game now keeps only the frame around each page (top bar, title, Back prompt) and ReSkate draws everything in it, tabs included. The native page is one empty body. Multiplayer - Server browser: a list with name, map and skaters columns that sort, search, filters (all, servers, lobbies, friends, hide full, my map), and a panel for the session pointed at. Pressing a row joins it; the row says JOIN while pointed at. Join by code is in the panel, and its own tab is gone. - Host lobby, as before. - Current session (only in one): who is here, leave, map, join code, party. - Voice chat (only in a session). - Session settings (only for a lobby's host or a server's admin). They are sent as explicit values, which a dedicated server needs. Custom Stuff - Official maps and Custom maps: pressing a map loads it. - World: traffic, pedestrians, time of day. - Parks: a card for each lot with its style, layout and load. - Player: noclip, no bail, Hall of Meat, board wear, the camera views, flight speed and the boosts. - Online: player and nametag distances, nametags, chat, chat bubbles, Discord status, and showing your tag and items. - Visuals: camera effects, challenges, shuffle playlists. - Binds: record or clear the button for each action. - The player card name is no longer on the page (cardname in the console still sets it). Input: the game keeps the mouse and controller while its menu is up, so the pages only watch them. They have their own focus, moved with the D-pad or arrow keys and pressed with A or Enter, with LT/RT or Z/C for tabs; lists remember the row the focus left. Typing in a text box or recording a bind takes the keyboard and controller for that long, as chat does. Nothing is taken while the game is not the window in front, and the overlay's own menu draws on top. Layout follows the game's menu at any window shape: scaled to fit whole, 192 units clear at the sides, centred past 4704 units wide. Sounds: the pages raise the game's own UI one-shot event for moving the focus, pressing and changing tab.
170 lines
7.3 KiB
C++
170 lines
7.3 KiB
C++
#include "overlay_internal.h"
|
|
#include "cursor.h"
|
|
#include "input_capture.h"
|
|
#include "Engine/Core/Log/logging.h"
|
|
|
|
namespace dingosdk::overlay::detail {
|
|
bool game_window_foreground(HWND window) {
|
|
if (!window || !IsWindow(window)) return false;
|
|
const auto root = GetAncestor(window, GA_ROOT);
|
|
return GetForegroundWindow() == (root ? root : window) && !IsIconic(root ? root : window);
|
|
}
|
|
|
|
bool owns_menu_cursor(const State& s) {
|
|
// WM_KILLFOCUS can refer to a child control. A cached flag must not let the
|
|
// game resume clipping/centering while its top-level window is still active.
|
|
return s.input_attached.load() && !s.stop.load() && !s.failed.load() &&
|
|
interactive_visible(s) && game_window_foreground(s.window.load());
|
|
}
|
|
|
|
UINT cursor_sync_message() {
|
|
static const auto message = RegisterWindowMessageW(L"ReSkate.Overlay.CursorOwnership");
|
|
return message;
|
|
}
|
|
|
|
void request_window_cursor_sync(HWND window) {
|
|
const auto message = cursor_sync_message();
|
|
if (window && message) PostMessageW(window, message, 0, 0);
|
|
}
|
|
|
|
void sync_menu_cursor(bool release) {
|
|
auto& s = state();
|
|
bool changed{};
|
|
const auto window = s.window.load();
|
|
{
|
|
std::lock_guard lock(s.cursor_mutex);
|
|
const auto clip = original<ClipCursorFn>(s.clip_cursor);
|
|
if (!clip) return;
|
|
const bool own = !release && owns_menu_cursor(s);
|
|
changed = own != s.cursor_released;
|
|
if (own) {
|
|
if (!s.cursor_released) s.cursor_clip_pending = GetClipCursor(&s.cursor_clip) != FALSE;
|
|
s.cursor_released = true;
|
|
clip(nullptr);
|
|
} else if (s.cursor_released) {
|
|
s.cursor_released = false;
|
|
// Do not restore a game's clip over another foreground application.
|
|
clip(!release && game_window_foreground(window) && s.cursor_clip_pending ? &s.cursor_clip : nullptr);
|
|
s.cursor_clip_pending = false;
|
|
}
|
|
}
|
|
if (changed) {
|
|
// GetCapture/ReleaseCapture only affect the calling thread's queue.
|
|
// The present thread cannot release the window thread's game capture.
|
|
request_window_cursor_sync(window);
|
|
logging::write(logging::Level::info, logging::Channel::input,
|
|
!release && owns_menu_cursor(s) ? "Input ownership: overlay." : "Input ownership: game or desktop.");
|
|
}
|
|
}
|
|
|
|
BOOL WINAPI clip_cursor(const RECT* rectangle) {
|
|
auto& s = state();
|
|
std::lock_guard lock(s.cursor_mutex);
|
|
const auto native = original<ClipCursorFn>(s.clip_cursor);
|
|
if (owns_menu_cursor(s)) {
|
|
// Remember the latest native request for closing the menu; never apply
|
|
// it while either overlay surface owns the pointer.
|
|
if (!s.cursor_released) {
|
|
s.cursor_released = true;
|
|
request_window_cursor_sync(s.window.load());
|
|
}
|
|
s.cursor_clip_pending = rectangle != nullptr;
|
|
if (rectangle) s.cursor_clip = *rectangle;
|
|
return native(nullptr);
|
|
}
|
|
s.cursor_released = false; s.cursor_clip_pending = false;
|
|
return native(rectangle);
|
|
}
|
|
|
|
BOOL WINAPI set_cursor_pos(int x, int y) {
|
|
if (block_polled_input()) return TRUE;
|
|
return original<SetCursorPosFn>(state().set_cursor_pos)(x, y);
|
|
}
|
|
|
|
void window_cursor_input(HWND window, UINT message, WPARAM wp) {
|
|
if (GetWindowThreadProcessId(window, nullptr) != GetCurrentThreadId()) return;
|
|
const auto& s = state();
|
|
window_cursor_input(window, message, wp, message != WM_NCDESTROY && window == s.window.load() && owns_menu_cursor(s));
|
|
}
|
|
|
|
void window_cursor_input(HWND window, UINT message, WPARAM wp, bool overlay_owns_pointer) {
|
|
auto& s = state();
|
|
if (GetWindowThreadProcessId(window, nullptr) != GetCurrentThreadId()) return;
|
|
// These fields belong to the HWND thread, including when Present posts the
|
|
// handoff after initial setup, a close button, or swapchain recreation.
|
|
thread_local HWND owner{};
|
|
thread_local unsigned buttons{};
|
|
const bool active = message != WM_NCDESTROY && window == s.window.load() && overlay_owns_pointer;
|
|
if (!active) {
|
|
if (owner == window) {
|
|
owner = nullptr; buttons = 0;
|
|
OverlayInputAccess access;
|
|
if (GetCapture() == window) ReleaseCapture();
|
|
}
|
|
return;
|
|
}
|
|
OverlayInputAccess access;
|
|
if (owner != window) {
|
|
const auto captured = GetCapture();
|
|
const auto previous = owner;
|
|
owner = window; buttons = 0;
|
|
if (captured && (captured == previous || captured == window || IsChild(window, captured))) ReleaseCapture();
|
|
}
|
|
const auto button = [&]() -> unsigned {
|
|
switch (message) {
|
|
case WM_LBUTTONDOWN: case WM_LBUTTONDBLCLK: case WM_LBUTTONUP: return 1;
|
|
case WM_RBUTTONDOWN: case WM_RBUTTONDBLCLK: case WM_RBUTTONUP: return 2;
|
|
case WM_MBUTTONDOWN: case WM_MBUTTONDBLCLK: case WM_MBUTTONUP: return 4;
|
|
case WM_XBUTTONDOWN: case WM_XBUTTONDBLCLK: case WM_XBUTTONUP: return HIWORD(wp) == XBUTTON2 ? 16 : 8;
|
|
default: return 0;
|
|
}
|
|
}();
|
|
const bool up = message == WM_LBUTTONUP || message == WM_RBUTTONUP ||
|
|
message == WM_MBUTTONUP || message == WM_XBUTTONUP;
|
|
if (button) {
|
|
if (up) buttons &= ~button;
|
|
else buttons |= button;
|
|
if (buttons && GetCapture() != window) SetCapture(window);
|
|
else if (!buttons && GetCapture() == window) ReleaseCapture();
|
|
}
|
|
if (message == WM_CANCELMODE || message == WM_KILLFOCUS) {
|
|
buttons = 0;
|
|
if (GetCapture() == window) ReleaseCapture();
|
|
}
|
|
}
|
|
|
|
void update_menu_pointer() {
|
|
auto& s = state();
|
|
auto& io = ImGui::GetIO();
|
|
const bool active = owns_menu_cursor(s);
|
|
// The pause menu's server browser watches the pointer while the game keeps it: the same
|
|
// reads, with the game's own cursor on screen and nothing taken from the game.
|
|
const bool watching = !active && s.hub_pointer.load() && s.input_attached && !s.stop && !s.failed &&
|
|
game_window_foreground(s.window);
|
|
io.AddFocusEvent(active || watching);
|
|
io.MouseDrawCursor = active;
|
|
if (!active && !watching) {
|
|
io.ClearInputKeys(); io.ClearInputMouse();
|
|
s.raw_mouse_buttons.store(0);
|
|
return;
|
|
}
|
|
if (watching) io.ClearInputKeys();
|
|
OverlayInputAccess access;
|
|
POINT point{};
|
|
// The Win32 backend only polls when it thinks WM_MOUSEMOVE tracking ended.
|
|
// Skate can stop legacy messages during gameplay without WM_MOUSELEAVE.
|
|
if (GetCursorPos(&point) && ScreenToClient(s.window.load(), &point))
|
|
io.AddMousePosEvent(static_cast<float>(point.x), static_cast<float>(point.y));
|
|
const bool swapped = GetSystemMetrics(SM_SWAPBUTTON) != 0;
|
|
const std::array keys{swapped ? VK_RBUTTON : VK_LBUTTON, swapped ? VK_LBUTTON : VK_RBUTTON,
|
|
VK_MBUTTON, VK_XBUTTON1, VK_XBUTTON2};
|
|
// Raw buttons fill in when Skate's controller mode leaves the legacy state stale.
|
|
const auto time = s.raw_mouse_time.load();
|
|
const auto raw = time && GetTickCount64() < time + 10000 ? s.raw_mouse_buttons.load() : 0u;
|
|
const std::array<unsigned, 3> raw_bits{swapped ? 2u : 1u, swapped ? 1u : 2u, 4u};
|
|
for (int button = 0; button < static_cast<int>(keys.size()); ++button)
|
|
io.AddMouseButtonEvent(button, (GetAsyncKeyState(keys[button]) & 0x8000) != 0 ||
|
|
(button < 3 && (raw & raw_bits[static_cast<std::size_t>(button)]) != 0));
|
|
}
|
|
}
|