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) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-14 04:11:49 +02:00
1 parent a640f2fb17
commit 2853f82627
3 files changed
+103 -7

No files matched your search

+1
View File
@@ -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
+92 -7
View File
@@ -25,6 +25,7 @@
#include <windows.h>
#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<uint8_t**>(game::addr::g_mouseCursor);
if (dx == 0 || g_cursorShifted || !cursor) {
return;
}
x_ = reinterpret_cast<volatile int32_t*>(cursor + 4);
original_ = *x_;
shifted_ = original_ - dx;
*x_ = shifted_;
g_cursorShifted = true;
}
~CursorOffset() {
if (x_) {
InterlockedCompareExchange(reinterpret_cast<volatile LONG*>(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<uint8_t**>(game::addr::g_mouseCursor);
if (dx == 0 || !window || !cursor) {
return;
}
const auto* w = static_cast<const uint8_t*>(window);
const int x = *reinterpret_cast<const int32_t*>(w + 0x24) + dx;
const int y = *reinterpret_cast<const int32_t*>(w + 0x28);
const int width = *reinterpret_cast<const uint16_t*>(w + 0x2c);
const int height = *reinterpret_cast<const uint16_t*>(w + 0x2e);
const int cx = *reinterpret_cast<const int32_t*>(cursor + 4);
const int cy = *reinterpret_cast<const int32_t*>(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