From 2853f826275cb2696ade37ea43a7befa0f6f0af0 Mon Sep 17 00:00:00 2001 From: Torhan Bartel Date: Wed, 14 Oct 2026 04:11:49 +0200 Subject: [PATCH] HUD: hover and tooltips for moved windows Windows poll the mouse cursor object (0xcdbadc, x +4) for hover; it is shifted inside a moved window's render/event scope like the events. Tooltips are separate windows placed by the hovered window in its 1024 layout, so cUI_Tooltip renders with the offset of the moved window under the cursor. Adds Move-To to the test input helpers. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/game/addresses.h | 1 + src/mods/hud.cpp | 99 ++++++++++++++++++++++++++++++++++++++++---- tools/game-input.ps1 | 10 +++++ 3 files changed, 103 insertions(+), 7 deletions(-) diff --git a/src/game/addresses.h b/src/game/addresses.h index e6effa7..aab92bd 100644 --- a/src/game/addresses.h +++ b/src/game/addresses.h @@ -26,6 +26,7 @@ constexpr uint32_t WorldToScreen = 0x00623c40; // --- globals ----------------------------------------------------------------- constexpr uint32_t g_dxDriver = 0x00cda99c; // DxDriver7* constexpr uint32_t g_appFlags = 0x0182cdb8; // uint32_t, 0x20000 = inactive +constexpr uint32_t g_mouseCursor = 0x00cdbadc; // mouse cursor object*: x +4, y +8 (render pixels) // --- read-only constants ----------------------------------------------------- constexpr uint32_t kScreenWidthF = 0x0088e6f4; // float 1024.0 diff --git a/src/mods/hud.cpp b/src/mods/hud.cpp index a3c5b2c..17055aa 100644 --- a/src/mods/hud.cpp +++ b/src/mods/hud.cpp @@ -25,6 +25,7 @@ #include +#include "game/addresses.h" #include "game/render/dx_driver7.h" #include "mods/widescreen.h" #include "re/config.h" @@ -53,19 +54,94 @@ int OffsetFor(Anchor anchor) { return anchor == Anchor::Center ? (width - kLayoutWidth) / 2 : width - kLayoutWidth; } +// Whether the current thread has already shifted the mouse cursor position. +thread_local bool g_cursorShifted = false; + +// RAII: shift the mouse cursor object's x by -dx for the scope (outermost +// window only). Windows poll it for hover (highlights, tooltips) while they +// render and handle events. The window thread may store a new position +// meanwhile; that one is kept instead of restoring. +class CursorOffset { +public: + explicit CursorOffset(int dx) { + auto* cursor = *reinterpret_cast(game::addr::g_mouseCursor); + if (dx == 0 || g_cursorShifted || !cursor) { + return; + } + x_ = reinterpret_cast(cursor + 4); + original_ = *x_; + shifted_ = original_ - dx; + *x_ = shifted_; + g_cursorShifted = true; + } + ~CursorOffset() { + if (x_) { + InterlockedCompareExchange(reinterpret_cast(x_), original_, shifted_); + g_cursorShifted = false; + } + } + +private: + volatile int32_t* x_ = nullptr; + int32_t original_ = 0; + int32_t shifted_ = 0; +}; + +// Whether the current thread is inside a hooked window's render method. +thread_local bool g_drawScoped = false; + +// Offset of the moved window under the cursor, noted while the windows render +// (expires when no moved window is hovered any more). Tooltips are separate +// windows positioned by the hovered window in its own layout, so they take its +// offset. +volatile LONG g_hoverOffset = 0; +volatile DWORD g_hoverTime = 0; +constexpr DWORD kHoverExpiryMs = 150; + +int HoverOffset() { + return GetTickCount() - g_hoverTime < kHoverExpiryMs ? g_hoverOffset : 0; +} + +// Notes the window's offset if the cursor is over it (screen position). +void TrackHover(void* window, int dx) { + auto* cursor = *reinterpret_cast(game::addr::g_mouseCursor); + if (dx == 0 || !window || !cursor) { + return; + } + const auto* w = static_cast(window); + const int x = *reinterpret_cast(w + 0x24) + dx; + const int y = *reinterpret_cast(w + 0x28); + const int width = *reinterpret_cast(w + 0x2c); + const int height = *reinterpret_cast(w + 0x2e); + const int cx = *reinterpret_cast(cursor + 4); + const int cy = *reinterpret_cast(cursor + 8); + if (cx >= x && cx < x + width && cy >= y && cy < y + height) { + g_hoverOffset = dx; + g_hoverTime = GetTickCount(); + } +} + // RAII: shift drawing by dx for the scope. Windows nest (children, shared base // methods), so only the outermost window decides. class DrawOffset { public: - explicit DrawOffset(int dx) : previous_(g_offsetX) { - if (g_offsetX == 0) { + DrawOffset(void* window, int dx) : outermost_(!g_drawScoped), cursor_(outermost_ ? dx : 0) { + if (outermost_) { + TrackHover(window, dx); + g_drawScoped = true; g_offsetX = dx; } } - ~DrawOffset() { g_offsetX = previous_; } + ~DrawOffset() { + if (outermost_) { + g_drawScoped = false; + g_offsetX = 0; + } + } private: - int previous_; + bool outermost_; + CursorOffset cursor_; }; // Mouse events (cEvent_mouseMove / _mouseButton_down / _mouseButton_up): vf04 @@ -84,7 +160,7 @@ int32_t* MouseEventX(void* event) { // RAII: shift the mouse position by -dx for the scope (outermost window only). class EventOffset { public: - EventOffset(void* event, int dx) { + EventOffset(void* event, int dx) : cursor_(dx) { if (dx != 0 && !g_eventShifted) { x_ = MouseEventX(event); if (x_) { @@ -104,6 +180,7 @@ public: private: int32_t* x_ = nullptr; int dx_ = 0; + CursorOffset cursor_; }; // --- Direct3D 7 device hooks ------------------------------------------------- @@ -209,7 +286,7 @@ void OnDevice(void* device) { int __fastcall name(void* self, void* edx, void* device); \ RE_HOOK(address, name); \ int __fastcall name(void* self, void* edx, void* device) { \ - mods::hud::DrawOffset offset(mods::hud::OffsetFor(anchor)); \ + mods::hud::DrawOffset offset(self, mods::hud::OffsetFor(anchor)); \ return name##_original(self, edx, device); \ } @@ -217,7 +294,7 @@ void OnDevice(void* device) { int __fastcall name(void* self, void* edx, void* device, int a, int b); \ RE_HOOK(address, name); \ int __fastcall name(void* self, void* edx, void* device, int a, int b) { \ - mods::hud::DrawOffset offset(mods::hud::OffsetFor(anchor)); \ + mods::hud::DrawOffset offset(self, mods::hud::OffsetFor(anchor)); \ return name##_original(self, edx, device, a, b); \ } @@ -289,4 +366,12 @@ HUD_RENDER(0x0071dd80, UiSavegame_Render, Anchor::Center) HUD_EVENT(0x00720c10, UiBusyDlg_HandleEvent, Anchor::Center) HUD_RENDER(0x007213a0, UiBusyDlg_Render, Anchor::Center) +// Tooltips: placed by the hovered window in its own layout (see TrackHover). +int __fastcall UiTooltip_Render(void* self, void* edx, void* device); +RE_HOOK(0x006e7300, UiTooltip_Render); +int __fastcall UiTooltip_Render(void* self, void* edx, void* device) { + mods::hud::DrawOffset offset(nullptr, mods::hud::HoverOffset()); + return UiTooltip_Render_original(self, edx, device); +} + } // namespace game diff --git a/tools/game-input.ps1 b/tools/game-input.ps1 index f6d3f3f..bf655ae 100644 --- a/tools/game-input.ps1 +++ b/tools/game-input.ps1 @@ -75,3 +75,13 @@ function Save-Screen([string]$Path, [int]$Width = 3440, [int]$Height = 1440) { $graphics.Dispose(); $bitmap.Dispose() $full } + +# Moves the cursor in small steps (the game only sees real mouse movement). +function Move-To([int]$X, [int]$Y, [int]$AfterMs = 800) { + [SacredRe.Input]::SetCursorPos($X - 20, $Y - 20) | Out-Null + for ($i = 1; $i -le 10; $i++) { + Start-Sleep -Milliseconds 20 + [SacredRe.Input]::SetCursorPos($X - 20 + 2 * $i, $Y - 20 + 2 * $i) | Out-Null + } + Start-Sleep -Milliseconds $AfterMs +}