HUD: inventory drag & drop, UI hit tests, centred overlays

- Mouse move events carry previous and current x (+4, +0xc); button events
  x at +8. All are shifted, and restored to the saved values.
- The cursor object's previous x (+0xc) is shifted too: picking up an item
  writes the cursor position into the event (0x6559a0).
- GetMouseClientPos returns the shifted position inside a moved window.
- Window hit tests (vf1c 0x731d40, taskbar 0x6dfb10) called outside a window
  scope, e.g. 'is UI under the cursor' for world picking (0x75a370), use the
  offset the top-level window was last drawn with.
- The item carried by the cursor (0x6555e0) and the UI manager's centred
  status line / message box (0x75bbf0, 0x75c200) draw with the centre offset.

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:55:37 +02:00
1 parent 2760b35829
commit d5e3da549a
3 files changed
+175 -35

No files matched your search

+2
View File
@@ -33,6 +33,7 @@
#include "game/engine/input.h" #include "game/engine/input.h"
#include "game/render/dx_driver7.h" #include "game/render/dx_driver7.h"
#include "mods/hud.h"
#include "mods/widescreen.h" #include "mods/widescreen.h"
#include "re/config.h" #include "re/config.h"
#include "game/addresses.h" #include "game/addresses.h"
@@ -209,6 +210,7 @@ void __cdecl GetMouseClientPos(HWND window, POINT* position) {
GetMouseClientPos_original(window, position); GetMouseClientPos_original(window, position);
if (window && g_viewport.active && window == g_viewport.window) { if (window && g_viewport.active && window == g_viewport.window) {
*position = ToGame(*position); *position = ToGame(*position);
position->x -= mods::hud::CursorOffsetX();
} }
} }
+169 -35
View File
@@ -54,37 +54,49 @@ int OffsetFor(Anchor anchor) {
return anchor == Anchor::Center ? (width - kLayoutWidth) / 2 : width - kLayoutWidth; return anchor == Anchor::Center ? (width - kLayoutWidth) / 2 : width - kLayoutWidth;
} }
// Whether the current thread has already shifted the mouse cursor position. // Offset by which the current thread has shifted the mouse cursor (0: none).
thread_local bool g_cursorShifted = false; thread_local int g_cursorDx = 0;
// RAII: shift the mouse cursor object's x by -dx for the scope (outermost // 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 // window only). Windows poll it for hover (highlights, tooltips) while they
// render and handle events. The window thread may store a new position // render and handle events: +4 is the current x, +0xc the one before the last
// meanwhile; that one is kept instead of restoring. // update (read by 0x6559a0, e.g. when an inventory item is picked up). The
// window thread may store a new position meanwhile; that one is kept instead
// of restoring.
class CursorOffset { class CursorOffset {
public: public:
explicit CursorOffset(int dx) { explicit CursorOffset(int dx) {
auto* cursor = *reinterpret_cast<uint8_t**>(game::addr::g_mouseCursor); auto* cursor = *reinterpret_cast<uint8_t**>(game::addr::g_mouseCursor);
if (dx == 0 || g_cursorShifted || !cursor) { if (dx == 0 || g_cursorDx != 0 || !cursor) {
return; return;
} }
x_ = reinterpret_cast<volatile int32_t*>(cursor + 4); fields_[0].Shift(cursor + 0x4, dx);
original_ = *x_; fields_[1].Shift(cursor + 0xc, dx);
shifted_ = original_ - dx; g_cursorDx = dx;
*x_ = shifted_;
g_cursorShifted = true;
} }
~CursorOffset() { ~CursorOffset() {
if (x_) { if (g_cursorDx != 0 && fields_[0].x) {
InterlockedCompareExchange(reinterpret_cast<volatile LONG*>(x_), original_, shifted_); fields_[0].Restore();
g_cursorShifted = false; fields_[1].Restore();
g_cursorDx = 0;
} }
} }
private: private:
volatile int32_t* x_ = nullptr; struct Field {
int32_t original_ = 0; volatile LONG* x = nullptr;
int32_t shifted_ = 0; LONG original = 0;
LONG shifted = 0;
void Shift(uint8_t* address, int dx) {
x = reinterpret_cast<volatile LONG*>(address);
original = *x;
shifted = original - dx;
*x = shifted;
}
void Restore() { InterlockedCompareExchange(x, original, shifted); }
};
Field fields_[2];
}; };
// Whether the current thread is inside a hooked window's render method. // Whether the current thread is inside a hooked window's render method.
@@ -121,12 +133,51 @@ void TrackHover(void* window, int dx) {
} }
} }
// Offsets top-level windows were last drawn with, for calls made outside their
// scope (hit tests by the UI manager). Most recent first.
struct ControlOffset {
const void* control;
int dx;
};
constexpr int kControlOffsetCount = 64;
ControlOffset g_controlOffsets[kControlOffsetCount] = {};
SRWLOCK g_controlOffsetsLock = SRWLOCK_INIT;
void RecordOffset(const void* control, int dx) {
AcquireSRWLockExclusive(&g_controlOffsetsLock);
int i = 0;
while (i < kControlOffsetCount - 1 && g_controlOffsets[i].control != control) {
++i;
}
for (; i > 0; --i) {
g_controlOffsets[i] = g_controlOffsets[i - 1];
}
g_controlOffsets[0] = {control, dx};
ReleaseSRWLockExclusive(&g_controlOffsetsLock);
}
int OffsetOf(const void* control) {
AcquireSRWLockShared(&g_controlOffsetsLock);
int dx = 0;
for (const ControlOffset& entry : g_controlOffsets) {
if (entry.control == control) {
dx = entry.dx;
break;
}
}
ReleaseSRWLockShared(&g_controlOffsetsLock);
return dx;
}
// RAII: shift drawing by dx for the scope. Windows nest (children, shared base // RAII: shift drawing by dx for the scope. Windows nest (children, shared base
// methods), so only the outermost window decides. // methods), so only the outermost window decides.
class DrawOffset { class DrawOffset {
public: public:
DrawOffset(void* window, int dx) : outermost_(!g_drawScoped), cursor_(outermost_ ? dx : 0) { DrawOffset(void* window, int dx) : outermost_(!g_drawScoped), cursor_(outermost_ ? dx : 0) {
if (outermost_) { if (outermost_) {
if (window) {
RecordOffset(window, dx);
}
TrackHover(window, dx); TrackHover(window, dx);
g_drawScoped = true; g_drawScoped = true;
g_offsetX = dx; g_offsetX = dx;
@@ -144,42 +195,57 @@ private:
CursorOffset cursor_; CursorOffset cursor_;
}; };
// Mouse events (cEvent_mouseMove / _mouseButton_down / _mouseButton_up): vf04 // Mouse events, by vf04 category (positions in render pixels):
// returns 1 / 2 / 3, the cursor position in render pixels is at +8 / +0xc. // 1 cEvent_mouseMove: previous x, y at +4 / +8, current at +0xc / +0x10
// Returns the x field, or nullptr for other events (keys, UI commands, ...). // 2 cEvent_mouseButton_down,
int32_t* MouseEventX(void* event) { // 3 cEvent_mouseButton_up: x, y at +8 / +0xc
// Stores the offsets of the x fields; returns their count (0: no mouse event).
int MouseEventXOffsets(void* event, int (&offsets)[2]) {
using CategoryFn = int(__fastcall*)(void* self, void* edx); using CategoryFn = int(__fastcall*)(void* self, void* edx);
auto category = reinterpret_cast<CategoryFn>((*static_cast<void***>(event))[1]); auto category = reinterpret_cast<CategoryFn>((*static_cast<void***>(event))[1]);
const int value = category(event, nullptr); switch (category(event, nullptr)) {
if (value < 1 || value > 3) { case 1:
return nullptr; offsets[0] = 0x4;
offsets[1] = 0xc;
return 2;
case 2:
case 3:
offsets[0] = 0x8;
return 1;
default:
return 0;
} }
return reinterpret_cast<int32_t*>(static_cast<uint8_t*>(event) + 8);
} }
// RAII: shift the mouse position by -dx for the scope (outermost window only). // RAII: shift the mouse position by -dx for the scope (outermost window only).
class EventOffset { class EventOffset {
public: public:
EventOffset(void* event, int dx) : cursor_(dx) { EventOffset(void* event, int dx) : cursor_(dx) {
if (dx != 0 && !g_eventShifted) { if (dx == 0 || g_eventShifted) {
x_ = MouseEventX(event); return;
if (x_) {
dx_ = dx;
*x_ -= dx_;
g_eventShifted = true;
}
} }
int offsets[2];
count_ = MouseEventXOffsets(event, offsets);
for (int i = 0; i < count_; ++i) {
x_[i] = reinterpret_cast<int32_t*>(static_cast<uint8_t*>(event) + offsets[i]);
original_[i] = *x_[i];
*x_[i] = original_[i] - dx;
}
g_eventShifted = count_ > 0;
} }
~EventOffset() { ~EventOffset() {
if (x_) { if (count_ > 0) {
*x_ += dx_; for (int i = 0; i < count_; ++i) {
*x_[i] = original_[i];
}
g_eventShifted = false; g_eventShifted = false;
} }
} }
private: private:
int32_t* x_ = nullptr; int count_ = 0;
int dx_ = 0; int32_t* x_[2] = {};
int32_t original_[2] = {};
CursorOffset cursor_; CursorOffset cursor_;
}; };
@@ -245,6 +311,10 @@ HRESULT __stdcall DrawIndexedPrimitiveShifted(void* device, DWORD type, DWORD fv
} // namespace } // namespace
int CursorOffsetX() {
return g_cursorDx;
}
void OnDevice(void* device) { void OnDevice(void* device) {
static void* hooked = nullptr; static void* hooked = nullptr;
if (!device || device == hooked) { if (!device || device == hooked) {
@@ -375,3 +445,67 @@ int __fastcall UiTooltip_Render(void* self, void* edx, void* device) {
} }
} // namespace game } // namespace game
namespace mods::hud {
// Window hit test (vf1c: is the point inside, positions relative to the parent
// at +0x50). The UI manager asks it with plain render coordinates, e.g. whether
// the world under the cursor is covered by UI (0x75a370); translate those by
// the offset the top-level window was drawn with. Calls from inside a moved
// window's scope already use its layout.
int HitTestX(void* window, int x) {
if (g_drawScoped || g_eventShifted || g_cursorDx != 0) {
return x;
}
auto* root = static_cast<uint8_t*>(window);
while (auto* parent = *reinterpret_cast<uint8_t**>(root + 0x50)) {
root = parent;
}
return x - OffsetOf(root);
}
} // namespace mods::hud
namespace game {
int __fastcall UiWindow_HitTest(void* self, void* edx, int x, int y);
RE_HOOK(0x00731d40, UiWindow_HitTest);
int __fastcall UiWindow_HitTest(void* self, void* edx, int x, int y) {
return UiWindow_HitTest_original(self, edx, mods::hud::HitTestX(self, x), y);
}
int __fastcall UiTaskbar_HitTest(void* self, void* edx, int x, int y);
RE_HOOK(0x006dfb10, UiTaskbar_HitTest);
int __fastcall UiTaskbar_HitTest(void* self, void* edx, int x, int y) {
return UiTaskbar_HitTest_original(self, edx, mods::hud::HitTestX(self, x), y);
}
// The item carried by the mouse cursor (0x6555e0, drawn by the render loop
// after the UI) is placed like UI items: relative to x = 512 of the layout.
// Items are only picked up from centred windows.
int __fastcall MouseCursor_DrawItem(void* self, void* edx, int a, int b);
RE_HOOK(0x006555e0, MouseCursor_DrawItem);
int __fastcall MouseCursor_DrawItem(void* self, void* edx, int a, int b) {
mods::hud::DrawOffset offset(nullptr, mods::hud::OffsetFor(Anchor::Center));
return MouseCursor_DrawItem_original(self, edx, a, b);
}
// UI manager overlays laid out around x = 512 (0x75bbf0 status/progress line,
// 0x75c200 message box above the taskbar).
int __fastcall UiManager_DrawStatusLine(void* self, void* edx, int a, int b);
RE_HOOK(0x0075bbf0, UiManager_DrawStatusLine);
int __fastcall UiManager_DrawStatusLine(void* self, void* edx, int a, int b) {
mods::hud::DrawOffset offset(nullptr, mods::hud::OffsetFor(Anchor::Center));
return UiManager_DrawStatusLine_original(self, edx, a, b);
}
int __fastcall UiManager_DrawMessage(void* self, void* edx, int a, int b, int c);
RE_HOOK(0x0075c200, UiManager_DrawMessage);
int __fastcall UiManager_DrawMessage(void* self, void* edx, int a, int b, int c) {
mods::hud::DrawOffset offset(nullptr, mods::hud::OffsetFor(Anchor::Center));
return UiManager_DrawMessage_original(self, edx, a, b, c);
}
} // namespace game
+4
View File
@@ -5,4 +5,8 @@ namespace mods::hud {
// Installs the Direct3D device hooks the HUD layout needs (once per device). // Installs the Direct3D device hooks the HUD layout needs (once per device).
void OnDevice(void* device); void OnDevice(void* device);
// Horizontal offset of the moved window whose render or event method runs on
// the current thread (0 outside): polled mouse positions are reduced by it.
int CursorOffsetX();
} // namespace mods::hud } // namespace mods::hud