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:
1 parent
2760b35829
commit
d5e3da549a
3 files changed
+175
-35
No files matched your search
+169
-35
@@ -54,37 +54,49 @@ 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;
|
||||
// Offset by which the current thread has shifted the mouse cursor (0: none).
|
||||
thread_local int g_cursorDx = 0;
|
||||
|
||||
// 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.
|
||||
// render and handle events: +4 is the current x, +0xc the one before the last
|
||||
// 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 {
|
||||
public:
|
||||
explicit CursorOffset(int dx) {
|
||||
auto* cursor = *reinterpret_cast<uint8_t**>(game::addr::g_mouseCursor);
|
||||
if (dx == 0 || g_cursorShifted || !cursor) {
|
||||
if (dx == 0 || g_cursorDx != 0 || !cursor) {
|
||||
return;
|
||||
}
|
||||
x_ = reinterpret_cast<volatile int32_t*>(cursor + 4);
|
||||
original_ = *x_;
|
||||
shifted_ = original_ - dx;
|
||||
*x_ = shifted_;
|
||||
g_cursorShifted = true;
|
||||
fields_[0].Shift(cursor + 0x4, dx);
|
||||
fields_[1].Shift(cursor + 0xc, dx);
|
||||
g_cursorDx = dx;
|
||||
}
|
||||
~CursorOffset() {
|
||||
if (x_) {
|
||||
InterlockedCompareExchange(reinterpret_cast<volatile LONG*>(x_), original_, shifted_);
|
||||
g_cursorShifted = false;
|
||||
if (g_cursorDx != 0 && fields_[0].x) {
|
||||
fields_[0].Restore();
|
||||
fields_[1].Restore();
|
||||
g_cursorDx = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
volatile int32_t* x_ = nullptr;
|
||||
int32_t original_ = 0;
|
||||
int32_t shifted_ = 0;
|
||||
struct Field {
|
||||
volatile LONG* x = nullptr;
|
||||
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.
|
||||
@@ -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
|
||||
// methods), so only the outermost window decides.
|
||||
class DrawOffset {
|
||||
public:
|
||||
DrawOffset(void* window, int dx) : outermost_(!g_drawScoped), cursor_(outermost_ ? dx : 0) {
|
||||
if (outermost_) {
|
||||
if (window) {
|
||||
RecordOffset(window, dx);
|
||||
}
|
||||
TrackHover(window, dx);
|
||||
g_drawScoped = true;
|
||||
g_offsetX = dx;
|
||||
@@ -144,42 +195,57 @@ private:
|
||||
CursorOffset cursor_;
|
||||
};
|
||||
|
||||
// Mouse events (cEvent_mouseMove / _mouseButton_down / _mouseButton_up): vf04
|
||||
// returns 1 / 2 / 3, the cursor position in render pixels is at +8 / +0xc.
|
||||
// Returns the x field, or nullptr for other events (keys, UI commands, ...).
|
||||
int32_t* MouseEventX(void* event) {
|
||||
// Mouse events, by vf04 category (positions in render pixels):
|
||||
// 1 cEvent_mouseMove: previous x, y at +4 / +8, current at +0xc / +0x10
|
||||
// 2 cEvent_mouseButton_down,
|
||||
// 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);
|
||||
auto category = reinterpret_cast<CategoryFn>((*static_cast<void***>(event))[1]);
|
||||
const int value = category(event, nullptr);
|
||||
if (value < 1 || value > 3) {
|
||||
return nullptr;
|
||||
switch (category(event, nullptr)) {
|
||||
case 1:
|
||||
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).
|
||||
class EventOffset {
|
||||
public:
|
||||
EventOffset(void* event, int dx) : cursor_(dx) {
|
||||
if (dx != 0 && !g_eventShifted) {
|
||||
x_ = MouseEventX(event);
|
||||
if (x_) {
|
||||
dx_ = dx;
|
||||
*x_ -= dx_;
|
||||
g_eventShifted = true;
|
||||
}
|
||||
if (dx == 0 || g_eventShifted) {
|
||||
return;
|
||||
}
|
||||
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() {
|
||||
if (x_) {
|
||||
*x_ += dx_;
|
||||
if (count_ > 0) {
|
||||
for (int i = 0; i < count_; ++i) {
|
||||
*x_[i] = original_[i];
|
||||
}
|
||||
g_eventShifted = false;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
int32_t* x_ = nullptr;
|
||||
int dx_ = 0;
|
||||
int count_ = 0;
|
||||
int32_t* x_[2] = {};
|
||||
int32_t original_[2] = {};
|
||||
CursorOffset cursor_;
|
||||
};
|
||||
|
||||
@@ -245,6 +311,10 @@ HRESULT __stdcall DrawIndexedPrimitiveShifted(void* device, DWORD type, DWORD fv
|
||||
|
||||
} // namespace
|
||||
|
||||
int CursorOffsetX() {
|
||||
return g_cursorDx;
|
||||
}
|
||||
|
||||
void OnDevice(void* device) {
|
||||
static void* hooked = nullptr;
|
||||
if (!device || device == hooked) {
|
||||
@@ -375,3 +445,67 @@ int __fastcall UiTooltip_Render(void* self, void* edx, void* device) {
|
||||
}
|
||||
|
||||
} // 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
|
||||
|
||||
|
||||
Reference in new issue
Block a user