// HUD layout for widened views. // // The UI is laid out for 1024x768 and drawn into the top-left of the widened // render target. Windows are moved as a whole: // - cUI_Mercenary (party panel: portrait and health at the top): right edge // - everything else (bottom bar, dialogs, inventory, maps, ...): centred // See docs/hud.md for the hooks and what happens outside a window's scope. // // A window is moved by transforming what it draws and what it receives, so // positions it computes itself (e.g. the potion arc) follow as well: // - while its render method runs, every screen-space vertex (XYZRHW) passed // to DrawPrimitive / DrawIndexedPrimitive is shifted by dx (3D items such // as belt contents already line up: their projection is screen-centred); // - while its event handler runs, the cursor position of mouse events // (move / button down / up) is shifted by -dx. Children are handled // inside both. // The world never sees these offsets: they are only active inside the hooks. // // sacredK.ini: // [hud] // Enabled=1 #include #include #include #include #include #include #include "game/addresses.h" #include "game/render/dx_driver7.h" #include "game/ui/events.h" #include "mods/hud.h" #include "mods/screen.h" #include "mods/widescreen.h" #include "re/config.h" #include "re/hook.h" #include "re/log.h" namespace mods::hud { namespace { constexpr int kLayoutWidth = 1024; // Horizontal offset applied to the current thread's UI drawing. thread_local int g_offsetX = 0; // Whether the UI event currently being handled has already been shifted. thread_local bool g_eventShifted = false; // Screen: laid out across the whole render width by its mod (drawn unshifted). // Canvas: a 16:9 screen (kCanvasWidth), centred. enum class Anchor { Center, Right, Screen, Canvas }; // Offset of the main menu's 1024 frame while it is drawn centred (set each // menu frame by RenderMenuFrame; 0 while a 16:9 screen draws itself). std::atomic g_menuFrameDx = 0; int OffsetFor(Anchor anchor) { static const bool enabled = re::ConfigInt("hud", "Enabled", 1) != 0; const int width = mods::widescreen::RenderWidth(); if (anchor == Anchor::Canvas) { return width > kCanvasWidth ? (width - kCanvasWidth) / 2 : 0; } // The main menu's frame shifts the windows in it; over a 16:9 screen // (no frame) dialogs are centred like in a game (user, 2026-10-21). const bool inMenuFrame = mods::widescreen::MenuIsShowing() && g_menuFrameDx != 0; if (!enabled || width <= kLayoutWidth || mods::widescreen::IsLoadingScreen() || inMenuFrame || anchor == Anchor::Screen) { return 0; } return anchor == Anchor::Center ? (width - kLayoutWidth) / 2 : width - kLayoutWidth; } // Offset by which the current thread sees the mouse cursor shifted (0: none). thread_local int g_cursorDx = 0; // The mouse cursor object (0x70 bytes, the only instance is returned by // MouseCursor_Instance 0x6550f0): x, y at +4 / +8, the position before the // last update at +0xc / +0x10 (read by 0x6559a0, e.g. when an inventory item // is picked up), a CRITICAL_SECTION at +0x24 guarding the carried item. constexpr size_t kCursorSize = 0x70; constexpr size_t kCursorLockOffset = 0x24; constexpr size_t kCursorLockEnd = kCursorLockOffset + sizeof(CRITICAL_SECTION); static_assert(kCursorLockEnd == 0x3c); // Per-thread copy of the cursor object, x shifted by -dx, that the accessor // returns while a moved window runs on this thread. The shared object is never // shifted: other threads (world picking, hold-to-move, the cursor sprite) read // it at any time. struct CursorShadow { alignas(8) uint8_t bytes[kCursorSize]; uint8_t snapshot[kCursorSize]; // bytes as handed out, to find what the window changed uint8_t* real = nullptr; bool lockInitialized = false; }; thread_local CursorShadow g_cursorShadow; CRITICAL_SECTION* CursorLock(uint8_t* cursor) { return reinterpret_cast(cursor + kCursorLockOffset); } int32_t& CursorField(uint8_t* cursor, size_t offset) { return *reinterpret_cast(cursor + offset); } // RAII: windows poll the cursor for hover (highlights, tooltips) while they // render and handle events; let them see it shifted by -dx for the scope // (outermost window only). Whatever the window stores into the cursor (carried // item, a new position) is written back to the shared object afterwards. class CursorOffset { public: explicit CursorOffset(int dx) { auto* cursor = *reinterpret_cast(game::addr::g_mouseCursor); if (dx == 0 || g_cursorDx != 0 || !cursor) { return; } CursorShadow& shadow = g_cursorShadow; if (!shadow.lockInitialized) { InitializeCriticalSection(CursorLock(shadow.bytes)); shadow.lockInitialized = true; } EnterCriticalSection(CursorLock(cursor)); memcpy(shadow.bytes, cursor, kCursorLockOffset); memcpy(shadow.bytes + kCursorLockEnd, cursor + kCursorLockEnd, kCursorSize - kCursorLockEnd); LeaveCriticalSection(CursorLock(cursor)); CursorField(shadow.bytes, 0x4) -= dx; CursorField(shadow.bytes, 0xc) -= dx; memcpy(shadow.snapshot, shadow.bytes, kCursorSize); shadow.real = cursor; g_cursorDx = dx; active_ = true; } ~CursorOffset() { if (!active_) { return; } CursorShadow& shadow = g_cursorShadow; // Only dwords the window changed, so concurrent updates of the others // (e.g. the position from the window thread) are kept. EnterCriticalSection(CursorLock(shadow.real)); for (size_t offset = 0; offset < kCursorSize; offset += 4) { if (offset >= kCursorLockOffset && offset < kCursorLockEnd) { continue; } int32_t value = CursorField(shadow.bytes, offset); if (value == CursorField(shadow.snapshot, offset)) { continue; } if (offset == 0x4 || offset == 0xc) { value += g_cursorDx; } CursorField(shadow.real, offset) = value; } LeaveCriticalSection(CursorLock(shadow.real)); shadow.real = nullptr; g_cursorDx = 0; } CursorOffset(const CursorOffset&) = delete; CursorOffset& operator=(const CursorOffset&) = delete; private: bool active_ = false; }; // 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(); // Tooltips seen so far: whether visible, and the offset chosen when they // became visible. struct TooltipState { void* tooltip; bool visible; bool dialog; int dx; }; constexpr int kTooltipCount = 64; TooltipState g_tooltips[kTooltipCount] = {}; constexpr size_t kControlFlags = 0x10; // bit 0: visible 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); if ((w[0x10] & 1) == 0) { return; // hidden windows render too (and some cover the whole frame) } 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(); } } // 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; } } ~DrawOffset() { if (outermost_) { g_drawScoped = false; g_offsetX = 0; } } private: bool outermost_; CursorOffset cursor_; }; // RAII: shift drawing only (the cursor is not seen shifted), for things // already in the shifted layout's coordinates (the main menu's cursor). class DrawShift { public: explicit DrawShift(int dx) : active_(!g_drawScoped && dx != 0) { if (active_) { g_drawScoped = true; g_offsetX = dx; } } ~DrawShift() { if (active_) { g_drawScoped = false; g_offsetX = 0; } } DrawShift(const DrawShift&) = delete; DrawShift& operator=(const DrawShift&) = delete; private: bool active_; }; // Stores the offsets of a mouse event's x fields (game/ui/events.h); returns // their count (0: no mouse event). int MouseEventXOffsets(void* event, int (&offsets)[2]) { switch (game::MouseEventKind(event)) { case game::MouseEvent::Move: offsets[0] = 0x4; offsets[1] = 0xc; return 2; case game::MouseEvent::ButtonDown: case game::MouseEvent::ButtonUp: offsets[0] = game::kMouseButtonX; return 1; default: return 0; } } // RAII: shift the mouse position by -dx for the scope (outermost window only; // one with dx 0, e.g. Anchor::Screen, still keeps its children from shifting). class EventOffset { public: EventOffset(void* event, int dx) : outermost_(!g_eventShifted), cursor_(outermost_ ? dx : 0) { if (!outermost_) { 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 (count_ > 0) { for (int i = 0; i < count_; ++i) { *x_[i] = original_[i]; } g_eventShifted = false; } } private: bool outermost_; int count_ = 0; int32_t* x_[2] = {}; int32_t original_[2] = {}; CursorOffset cursor_; }; // --- Direct3D 7 device hooks ------------------------------------------------- constexpr DWORD kFvfPositionMask = 0x00e; constexpr DWORD kFvfXyzRhw = 0x004; size_t VertexSize(DWORD fvf) { size_t size = 0; switch (fvf & kFvfPositionMask) { case 0x002: size = 12; break; // XYZ case 0x004: size = 16; break; // XYZRHW case 0x006: size = 16; break; // XYZB1 case 0x008: size = 20; break; // XYZB2 case 0x00a: size = 24; break; // XYZB3 case 0x00c: size = 28; break; // XYZB4 case 0x00e: size = 32; break; // XYZB5 } if (fvf & 0x010) size += 12; // normal if (fvf & 0x040) size += 4; // diffuse if (fvf & 0x080) size += 4; // specular const DWORD textures = (fvf >> 8) & 0xf; for (DWORD i = 0; i < textures; ++i) { static const size_t kCoordinates[] = {2, 3, 4, 1}; size += 4 * kCoordinates[(fvf >> (16 + 2 * i)) & 3]; } return size; } // A 16:9 screen draws (RenderCanvas): no 1024 layout to stretch. thread_local bool g_canvasScope = false; // Returns vertices shifted by g_offsetX, or the input when nothing to do. // Primitives covering the whole 1024x768 layout (e.g. the dimming behind // modal dialogs) are stretched across the render width instead. void* ShiftVertices(DWORD fvf, void* vertices, DWORD count) { if (g_offsetX == 0 || (fvf & kFvfPositionMask) != kFvfXyzRhw || !vertices || count == 0) { return vertices; } thread_local std::vector buffer; const size_t stride = VertexSize(fvf); buffer.assign(static_cast(vertices), static_cast(vertices) + stride * count); float minX = 1e9f, maxX = -1e9f, minY = 1e9f, maxY = -1e9f; for (DWORD i = 0; i < count; ++i) { const auto* position = reinterpret_cast(buffer.data() + stride * i); minX = std::min(minX, position[0]); maxX = std::max(maxX, position[0]); minY = std::min(minY, position[1]); maxY = std::max(maxY, position[1]); } const bool fullLayout = !g_canvasScope && minX <= 0.5f && maxX >= kLayoutWidth - 0.5f && minY <= 0.5f && maxY >= 767.5f; const float scale = static_cast(mods::widescreen::RenderWidth()) / kLayoutWidth; for (DWORD i = 0; i < count; ++i) { auto* x = reinterpret_cast(buffer.data() + stride * i); *x = fullLayout ? *x * scale : *x + static_cast(g_offsetX); } return buffer.data(); } using DrawPrimitiveFn = HRESULT(__stdcall*)(void* device, DWORD type, DWORD fvf, void* vertices, DWORD count, DWORD flags); using DrawIndexedPrimitiveFn = HRESULT(__stdcall*)(void* device, DWORD type, DWORD fvf, void* vertices, DWORD count, WORD* indices, DWORD indexCount, DWORD flags); DrawPrimitiveFn g_drawPrimitive = nullptr; DrawIndexedPrimitiveFn g_drawIndexedPrimitive = nullptr; // Textures not drawn (SetHiddenTextures): the surface bound to stage 0 is // tracked per thread (each thread draws with its own device calls). Only // inside a frame drawn over the menu background (g_hidingPictures: the main // menu's, the options' in a game): the list keeps surfaces the cache may // have freed since, and a game texture made at the same address was skipped // in the world view (user, 2026-10-21: "rendering glitches ingame"). constexpr int kHiddenTextureMax = 64; std::atomic g_hiddenTextures[kHiddenTextureMax] = {}; thread_local void* g_boundTexture = nullptr; thread_local int g_hidingPictures = 0; struct HidingPictures { HidingPictures() { ++g_hidingPictures; } ~HidingPictures() { --g_hidingPictures; } HidingPictures(const HidingPictures&) = delete; HidingPictures& operator=(const HidingPictures&) = delete; }; using SetTextureFn = HRESULT(__stdcall*)(void* device, DWORD stage, void* surface); SetTextureFn g_setTexture = nullptr; HRESULT __stdcall SetTextureTracked(void* device, DWORD stage, void* surface) { if (stage == 0) { g_boundTexture = surface; } return g_setTexture(device, stage, surface); } bool BoundTextureHidden() { if (!g_boundTexture || g_hidingPictures == 0) { return false; } for (const auto& hidden : g_hiddenTextures) { if (hidden.load(std::memory_order_relaxed) == g_boundTexture) { return true; } } return false; } HRESULT __stdcall DrawPrimitiveShifted(void* device, DWORD type, DWORD fvf, void* vertices, DWORD count, DWORD flags) { if (BoundTextureHidden()) { return S_OK; } return g_drawPrimitive(device, type, fvf, ShiftVertices(fvf, vertices, count), count, flags); } HRESULT __stdcall DrawIndexedPrimitiveShifted(void* device, DWORD type, DWORD fvf, void* vertices, DWORD count, WORD* indices, DWORD indexCount, DWORD flags) { if (BoundTextureHidden()) { return S_OK; } return g_drawIndexedPrimitive(device, type, fvf, ShiftVertices(fvf, vertices, count), count, indices, indexCount, flags); } // Viewports and cleared rectangles in screen space (the overview map, TAB, // sets a viewport at its frame, 162,120 700x525, and draws the map inside with // its own projection): moved like the vertices while a window draws. struct Viewport7 { DWORD x, y, width, height; float minZ, maxZ; }; struct Rect7 { LONG x1, y1, x2, y2; }; using SetViewportFn = HRESULT(__stdcall*)(void* device, Viewport7* viewport); using ClearFn = HRESULT(__stdcall*)(void* device, DWORD count, Rect7* rects, DWORD flags, DWORD color, float z, DWORD stencil); SetViewportFn g_setViewport = nullptr; ClearFn g_clear = nullptr; // A viewport across the whole render width is a reset, not one of the window's // own (e.g. the zoomed map view in the quest book restores it with 0x646510 // when done): shifted, it would reach past the render target, SetViewport // fails and the window's small viewport stays set, clipping the cursor. HRESULT __stdcall SetViewportShifted(void* device, Viewport7* viewport) { if (g_offsetX == 0 || !viewport || (viewport->x == 0 && static_cast(viewport->width) >= mods::widescreen::RenderWidth())) { return g_setViewport(device, viewport); } Viewport7 shifted = *viewport; shifted.x = static_cast(static_cast(shifted.x) + g_offsetX); return g_setViewport(device, &shifted); } HRESULT __stdcall ClearShifted(void* device, DWORD count, Rect7* rects, DWORD flags, DWORD color, float z, DWORD stencil) { if (g_offsetX == 0 || count == 0 || !rects) { return g_clear(device, count, rects, flags, color, z, stencil); } thread_local std::vector shifted; shifted.assign(rects, rects + count); for (Rect7& r : shifted) { r.x1 += g_offsetX; r.x2 += g_offsetX; } return g_clear(device, count, shifted.data(), flags, color, z, stencil); } using GetViewportFn = HRESULT(__stdcall*)(void* device, Viewport7* viewport); constexpr int kVtableGetViewport = 15; // IDirect3DDevice7 (13 SetViewport, 14 MultiplyTransform) // RAII for full-screen windows (options in a game) that fill the 1024 frame // with a picture: the menu screens' background (mods/screen.h) goes under // it, and the window draws with a viewport on the frame. The // export screen's 3D scene (heroes, background) uses a projection for a // 1024x768 viewport; across the widened one it was stretched 1.8x wide, away // from the slot labels and hit areas (measured on a screenshot, 2026-10-18). class FrameScope { public: FrameScope(void* window, void* device, int dx) { const auto* w = static_cast(window); if (dx == 0 || g_drawScoped || !device || !g_setViewport || (w[kControlFlags] & 1) == 0) { return; } auto getViewport = reinterpret_cast((*static_cast(device))[kVtableGetViewport]); if (FAILED(getViewport(device, &previous_))) { return; } mods::screen::DrawBackground(device); Viewport7 frame = {static_cast(dx), 0, kLayoutWidth, 768, previous_.minZ, previous_.maxZ}; device_ = device; g_setViewport(device, &frame); ++g_hidingPictures; } ~FrameScope() { if (device_) { --g_hidingPictures; g_setViewport(device_, &previous_); } } FrameScope(const FrameScope&) = delete; FrameScope& operator=(const FrameScope&) = delete; private: void* device_ = nullptr; Viewport7 previous_ = {}; }; } // namespace int CursorOffsetX() { return g_cursorDx; } int DrawOffsetX() { return g_offsetX; } void* CursorFor(void* cursor) { return g_cursorDx != 0 && cursor == g_cursorShadow.real ? g_cursorShadow.bytes : cursor; } void OnDevice(void* device) { static void* hooked = nullptr; if (!device || device == hooked) { return; } hooked = device; void** vtable = *static_cast(device); // IDirect3DDevice7: DrawPrimitive 25, DrawIndexedPrimitive 26. (3D items in // the HUD need no help: their ortho projection is centred on the screen, // which lines up with a centred taskbar.) if (!g_drawPrimitive) { g_drawPrimitive = reinterpret_cast(vtable[25]); re::AttachRuntimeHook("D3D7 DrawPrimitive", reinterpret_cast(&g_drawPrimitive), reinterpret_cast(&DrawPrimitiveShifted)); g_drawIndexedPrimitive = reinterpret_cast(vtable[26]); re::AttachRuntimeHook("D3D7 DrawIndexedPrimitive", reinterpret_cast(&g_drawIndexedPrimitive), reinterpret_cast(&DrawIndexedPrimitiveShifted)); // SetTexture 35. g_setTexture = reinterpret_cast(vtable[35]); re::AttachRuntimeHook("D3D7 SetTexture", reinterpret_cast(&g_setTexture), reinterpret_cast(&SetTextureTracked)); // Clear 10, SetViewport 13. g_clear = reinterpret_cast(vtable[10]); re::AttachRuntimeHook("D3D7 Clear", reinterpret_cast(&g_clear), reinterpret_cast(&ClearShifted)); g_setViewport = reinterpret_cast(vtable[13]); re::AttachRuntimeHook("D3D7 SetViewport", reinterpret_cast(&g_setViewport), reinterpret_cast(&SetViewportShifted)); } } // Dialogs among the tooltips: visible and at least kDialogMinWidth wide (the // quest dialog: 752; hover tooltips and speech bubbles are narrow). The // hover rule above cannot tell: a full-screen window drawn while hidden counts // as hovered whenever the cursor is inside the 1024 frame. constexpr int kDialogMinWidth = 400; int VisibleDialogTooltipsImpl(void** out, int max) { int count = 0; for (const TooltipState& t : g_tooltips) { if (!t.tooltip) { continue; } const auto* p = static_cast(t.tooltip); if ((p[kControlFlags] & 1) == 0) { continue; } const int width = *reinterpret_cast(p + 0x2c); if (width >= kDialogMinWidth && count < max) { out[count++] = t.tooltip; } } return count; } // A tooltip is destroyed: its entry goes, so its memory is never taken for a // tooltip again (Esc closed a "visible dialog" that was a freed one: crash). void ForgetTooltip(void* tooltip) { for (TooltipState& t : g_tooltips) { if (t.tooltip == tooltip) { t = {}; } } } int KnownTooltipOffset(void* tooltip) { for (const TooltipState& t : g_tooltips) { if (t.tooltip == tooltip) { return t.dx; } } return OffsetFor(Anchor::Center); } int TooltipOffset(void* tooltip) { TooltipState* state = nullptr; for (TooltipState& t : g_tooltips) { if (t.tooltip == tooltip) { state = &t; break; } if (!state && !t.tooltip) { state = &t; } } if (!state) { return HoverOffset(); } const bool visible = (static_cast(tooltip)[kControlFlags] & 1) != 0; if (state->tooltip != tooltip || (visible && !state->visible)) { const int hover = HoverOffset(); state->tooltip = tooltip; state->dialog = hover == 0; state->dx = hover != 0 ? hover : OffsetFor(Anchor::Center); } state->visible = visible; return state->dx; } } // namespace mods::hud // --- HUD windows --------------------------------------------------------------- // // Each hook wraps one window method; nested windows and shared base methods are // handled by "outermost wins" in DrawOffset/EventOffset. Addresses are the // vf10 (event, ret 4), vf14 (render, ret 4) and vf44 (render, ret 0Ch) entries // of the cUI_* vtables (see tools/rtti.mjs). #define HUD_EVENT(address, name, anchor) \ int __fastcall name(void* self, void* edx, void* event); \ RE_HOOK(address, name); \ int __fastcall name(void* self, void* edx, void* event) { \ mods::hud::EventOffset offset(event, mods::hud::OffsetFor(anchor)); \ return name##_original(self, edx, event); \ } #define HUD_RENDER(address, name, anchor) \ 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(self, mods::hud::OffsetFor(anchor)); \ return name##_original(self, edx, device); \ } #define HUD_RENDER3(address, name, anchor) \ 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(self, mods::hud::OffsetFor(anchor)); \ return name##_original(self, edx, device, a, b); \ } // Centred full-screen windows: black beside the frame, viewport on the frame. #define HUD_RENDER_FRAMED(address, name) \ int __fastcall name(void* self, void* edx, void* device); \ RE_HOOK(address, name); \ int __fastcall name(void* self, void* edx, void* device) { \ const int dx = mods::hud::OffsetFor(mods::hud::Anchor::Center); \ mods::hud::FrameScope frame(self, device, dx); \ mods::hud::DrawOffset offset(self, dx); \ return name##_original(self, edx, device); \ } #define HUD_RENDER3_FRAMED(address, name) \ 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) { \ const int dx = mods::hud::OffsetFor(mods::hud::Anchor::Center); \ mods::hud::FrameScope frame(self, device, dx); \ mods::hud::DrawOffset offset(self, dx); \ return name##_original(self, edx, device, a, b); \ } namespace game { using mods::hud::Anchor; // Bottom bar: orbs, belt, skills. HUD_EVENT(0x006e1970, UiTaskbar_HandleEvent, Anchor::Center) HUD_RENDER(0x006e3ac0, UiTaskbar_Render, Anchor::Center) // Portrait and health at the top: cUI_Mercenary (party panel), not // cUI_NetPortrait. HUD_EVENT(0x006d69f0, UiPortrait_HandleEvent, Anchor::Right) HUD_RENDER(0x006d6e70, UiPortrait_Render, Anchor::Right) // Everything else keeps its 1024x768 layout, centred: dialogs, inventory, // character, maps, traders, ESC menu, ... HUD_EVENT(0x00726880, UiWindow_HandleEvent, Anchor::Center) // cUI_Window2 (shared) HUD_RENDER(0x00727040, UiWindow_Render, Anchor::Center) // cUI_Window2 (shared) HUD_EVENT(0x006a4140, UiBlacksmith_HandleEvent, Anchor::Center) HUD_RENDER3(0x006a3160, UiBlacksmith_Render, Anchor::Center) // cUI_StatisticsChar (0x6a5dd0 / 0x6a9be0): hooked by mods/respec.cpp. HUD_EVENT(0x006acc70, UiChest_HandleEvent, Anchor::Center) HUD_RENDER(0x006ac8b0, UiChest_Render, Anchor::Center) HUD_EVENT(0x006adc70, UiConsole_HandleEvent, Anchor::Center) HUD_RENDER(0x006adb40, UiConsole_Render, Anchor::Center) HUD_EVENT(0x006aea20, UiDiary_HandleEvent, Anchor::Center) HUD_RENDER3(0x006aeba0, UiDiary_Render, Anchor::Center) HUD_EVENT(0x006b3620, UiBook_HandleEvent, Anchor::Center) HUD_EVENT(0x006b6330, UiEquipment_HandleEvent, Anchor::Center) HUD_RENDER(0x006b73a0, UiEquipment_Render, Anchor::Center) HUD_EVENT(0x006bb720, UiEscMenu_HandleEvent, Anchor::Center) HUD_RENDER(0x006bb030, UiEscMenu_Render, Anchor::Center) HUD_EVENT(0x006be0d0, UiHorse_HandleEvent, Anchor::Center) HUD_RENDER3(0x006bd180, UiHorse_Render, Anchor::Center) HUD_EVENT(0x006c5f40, UiInventory_HandleEvent, Anchor::Center) HUD_RENDER(0x006c6140, UiInventory_Render, Anchor::Center) HUD_EVENT(0x006c70e0, UiOverviewMap_HandleEvent, Anchor::Center) HUD_RENDER(0x006c69d0, UiOverviewMap_Render, Anchor::Center) HUD_EVENT(0x006c7ec0, UiMegamap_HandleEvent, Anchor::Screen) HUD_RENDER(0x006c9ce0, UiMegamap_Render, Anchor::Screen) // laid out by mods/megamap.cpp HUD_EVENT(0x006d1080, UiMaster_HandleEvent, Anchor::Center) HUD_RENDER(0x006d0f00, UiMaster_Render, Anchor::Center) HUD_EVENT(0x006d2450, UiMasterCombo_HandleEvent, Anchor::Center) HUD_RENDER(0x006d2b70, UiMasterCombo_Render, Anchor::Center) // cUI_MasterLotto (rune master): event and render in mods/runetrade.cpp. HUD_EVENT(0x006d5e20, UiMasterPortal_HandleEvent, Anchor::Center) HUD_RENDER(0x006d5e00, UiMasterPortal_Render, Anchor::Center) HUD_EVENT(0x006dab10, UiMerchandise_HandleEvent, Anchor::Center) HUD_RENDER(0x006da7f0, UiMerchandise_Render, Anchor::Center) HUD_EVENT(0x006dbcd0, UiMerchant_HandleEvent, Anchor::Center) HUD_RENDER3(0x006db840, UiMerchant_Render, Anchor::Center) HUD_EVENT(0x006dd720, UiPurchase_HandleEvent, Anchor::Center) HUD_RENDER3(0x006dcc80, UiPurchase_Render, Anchor::Center) HUD_EVENT(0x006e8860, UiCube_HandleEvent, Anchor::Center) HUD_RENDER(0x006e8870, UiCube_Render, Anchor::Center) HUD_EVENT(0x006e9ff0, UiTrade_HandleEvent, Anchor::Center) HUD_RENDER(0x006ea750, UiTrade_Render, Anchor::Center) HUD_EVENT(0x006f8960, UiNetworkInfo_HandleEvent, Anchor::Center) HUD_RENDER(0x006f73f0, UiNetworkInfo_Render, Anchor::Center) HUD_EVENT(0x0070e860, UiCharacter_HandleEvent, Anchor::Center) HUD_RENDER3_FRAMED(0x0070f160, UiCharacter_Render) // export screen in-game // cUI_Options (0x716b30 / 0x716640): hooked by mods/options.cpp. // cUI_Savegame (0x71d130 / 0x71dd80): hooked by mods/characters.cpp. HUD_EVENT(0x00720c10, UiBusyDlg_HandleEvent, Anchor::Center) HUD_RENDER(0x007213a0, UiBusyDlg_Render, Anchor::Center) // Controls that can be top-level windows of their own, e.g. the quest dialog // (buttons and texts placed directly in the UI manager). Inside one of the // windows above they are already in its scope (outermost wins); on their own // they were drawn and clicked unshifted (or shifted by whichever window was // active), which made the dialog jump and its buttons miss. HUD_EVENT(0x0072adf0, UiButton_HandleEvent, Anchor::Center) HUD_RENDER(0x0072af90, UiButton_Render, Anchor::Center) HUD_EVENT(0x00753c70, UiStaticText_HandleEvent, Anchor::Center) HUD_RENDER(0x00753a20, UiStaticText_Render, Anchor::Center) HUD_EVENT(0x00754610, UiStaticTextFx_HandleEvent, Anchor::Center) HUD_RENDER(0x007541e0, UiStaticTextFx_Render, Anchor::Center) HUD_EVENT(0x00753360, UiStatic_HandleEvent, Anchor::Center) HUD_RENDER(0x007530a0, UiStatic_Render, Anchor::Center) HUD_RENDER(0x007228b0, UiTextLines_Render, Anchor::Center) // Tooltips (cUI_Tooltip, a pool at cUI_Manager +0x124, one panel each). A // tooltip is either a hover tooltip placed by the window under the cursor in // its own layout, or a dialog of its own, e.g. a quest dialog (laid out in the // 1024 frame, measured: rect 136,306 752x157). When a tooltip becomes visible // it keeps an offset until it is hidden: the hovered window's (TrackHover) if // a moved window is under the cursor, otherwise the centre (a dialog). Taking // the hover offset on every frame made dialogs jump to the 1024 frame when // the cursor left the UI, and without a recorded offset their hit tests used // none, so clicks only worked at the unshifted position. // Its event handler (vf10 0x6e7120) tests the click itself before it calls // the shared window handler: it needs the same offset as its drawing. int __fastcall UiTooltip_HandleEvent(void* self, void* edx, void* event); RE_HOOK(0x006e7120, UiTooltip_HandleEvent); int __fastcall UiTooltip_HandleEvent(void* self, void* edx, void* event) { mods::hud::EventOffset offset(event, mods::hud::KnownTooltipOffset(self)); return UiTooltip_HandleEvent_original(self, edx, event); } 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(self, mods::hud::TooltipOffset(self)); return UiTooltip_Render_original(self, edx, device); } // cUI_Tooltip::~cUI_Tooltip (0x6e6560, plain ret; from the deleting destructor vf00). void __fastcall UiTooltip_Destruct(void* self, void* edx); RE_HOOK(0x006e6560, UiTooltip_Destruct); void __fastcall UiTooltip_Destruct(void* self, void* edx) { mods::hud::ForgetTooltip(self); UiTooltip_Destruct_original(self, edx); } } // 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); } int VisibleDialogTooltips(void** out, int max) { return VisibleDialogTooltipsImpl(out, max); } int RenderCentred(void* window, int (*body)(void* context), void* context) { DrawOffset offset(window, OffsetFor(Anchor::Center)); return body(context); } int RenderFramed(void* window, void* device, int (*body)(void* context), void* context) { const int dx = OffsetFor(Anchor::Center); FrameScope frame(window, device, dx); DrawOffset offset(window, dx); return body(context); } int HandleCentred(void* event, int (*body)(void* context), void* context) { EventOffset offset(event, OffsetFor(Anchor::Center)); return body(context); } void SetHiddenTextures(void* const* surfaces, int count) { for (int i = 0; i < kHiddenTextureMax; ++i) { g_hiddenTextures[i].store(i < count ? surfaces[i] : nullptr, std::memory_order_relaxed); } } int MenuFrameOffsetX() { return mods::widescreen::MenuIsShowing() ? g_menuFrameDx.load() : 0; } int RenderMenuFrame(void* device, bool framed, int (*body)(void* context), void* context) { const int width = mods::widescreen::RenderWidth(); const int dx = framed && width > kLayoutWidth ? (width - kLayoutWidth) / 2 : 0; g_menuFrameDx = dx; HidingPictures hiding; Viewport7 previous; auto getViewport = device ? reinterpret_cast((*static_cast(device))[kVtableGetViewport]) : nullptr; if (dx == 0 || g_drawScoped || !g_setViewport || !getViewport || FAILED(getViewport(device, &previous))) { return body(context); } Viewport7 frame = {static_cast(dx), 0, kLayoutWidth, 768, previous.minZ, previous.maxZ}; g_setViewport(device, &frame); g_drawScoped = true; // the windows inside draw in the frame's coordinates g_offsetX = dx; const int result = body(context); g_offsetX = 0; g_drawScoped = false; g_setViewport(device, &previous); return result; } int CanvasOffsetX() { return OffsetFor(Anchor::Canvas); } int RenderCanvas(void* window, int (*body)(void* context), void* context) { DrawOffset offset(window, OffsetFor(Anchor::Canvas)); const bool outer = !g_canvasScope; g_canvasScope = true; const int result = body(context); g_canvasScope = !outer; return result; } int HandleCanvas(void* event, int (*body)(void* context), void* context) { EventOffset offset(event, OffsetFor(Anchor::Canvas)); return body(context); } } // 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 cursor and the item it carries (0x6555e0, drawn by the render loop // after the UI) are placed like UI items: relative to x = 512 of the layout. // Items are only picked up from centred windows. In the main menu the cursor // is in the centred frame's coordinates (MenuFrameOffsetX). 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) { if (mods::widescreen::MenuIsShowing()) { mods::hud::DrawShift shift(mods::hud::MenuFrameOffsetX()); return MouseCursor_DrawItem_original(self, edx, a, b); } mods::hud::DrawOffset offset(nullptr, mods::hud::OffsetFor(Anchor::Center)); return MouseCursor_DrawItem_original(mods::hud::CursorFor(self), edx, a, b); } // Every use of the mouse cursor object goes through this accessor (it creates // the object on first use); inside a moved window, hand out the shifted copy. void* __cdecl MouseCursor_Instance(); RE_HOOK(0x006550f0, MouseCursor_Instance); void* __cdecl MouseCursor_Instance() { return mods::hud::CursorFor(MouseCursor_Instance_original()); } // 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