diff --git a/src/mods/borderless.cpp b/src/mods/borderless.cpp index b303ce5..3603a8e 100644 --- a/src/mods/borderless.cpp +++ b/src/mods/borderless.cpp @@ -33,6 +33,7 @@ #include "game/engine/input.h" #include "game/render/dx_driver7.h" +#include "mods/hud.h" #include "mods/widescreen.h" #include "re/config.h" #include "game/addresses.h" @@ -209,6 +210,7 @@ void __cdecl GetMouseClientPos(HWND window, POINT* position) { GetMouseClientPos_original(window, position); if (window && g_viewport.active && window == g_viewport.window) { *position = ToGame(*position); + position->x -= mods::hud::CursorOffsetX(); } } diff --git a/src/mods/hud.cpp b/src/mods/hud.cpp index 7574eca..a9812b4 100644 --- a/src/mods/hud.cpp +++ b/src/mods/hud.cpp @@ -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(game::addr::g_mouseCursor); - if (dx == 0 || g_cursorShifted || !cursor) { + if (dx == 0 || g_cursorDx != 0 || !cursor) { return; } - x_ = reinterpret_cast(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(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(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((*static_cast(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(static_cast(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(static_cast(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(window); + while (auto* parent = *reinterpret_cast(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 + + diff --git a/src/mods/hud.h b/src/mods/hud.h index 20b87d0..65a1eec 100644 --- a/src/mods/hud.h +++ b/src/mods/hud.h @@ -5,4 +5,8 @@ namespace mods::hud { // Installs the Direct3D device hooks the HUD layout needs (once per 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