From a640f2fb17812614f406b6ed1b78b01076c9ee3e Mon Sep 17 00:00:00 2001 From: Torhan Bartel Date: Wed, 14 Oct 2026 04:01:35 +0200 Subject: [PATCH] HUD: centre all UI windows, shift mouse events (not UI commands) Generic cUI_Window2 hooks centre dialogs, inventory, maps, traders and the ESC menu. Event shifting now targets the cursor position of mouse events (move/down/up, x at +8) instead of category 5 UI events, whose +0x48 is not a point for commands such as opening the ESC menu. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/mods/hud.cpp | 171 +++++++++++++++++++++++++++++----------- src/mods/widescreen.cpp | 10 +++ src/mods/widescreen.h | 4 + 3 files changed, 139 insertions(+), 46 deletions(-) diff --git a/src/mods/hud.cpp b/src/mods/hud.cpp index 4a43ee6..a3c5b2c 100644 --- a/src/mods/hud.cpp +++ b/src/mods/hud.cpp @@ -10,8 +10,9 @@ // - 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 point of UI events (category 5, -// point at +0x48) is shifted by -dx. Children are handled inside both. +// - 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. // // sacred_re.ini: @@ -35,49 +36,68 @@ namespace { constexpr int kLayoutWidth = 1024; -// Horizontal offset applied to the current thread's UI drawing/input. +// 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; enum class Anchor { Center, Right }; int OffsetFor(Anchor anchor) { + static const bool enabled = re::ConfigInt("hud", "Enabled", 1) != 0; const int width = mods::widescreen::RenderWidth(); - if (width <= kLayoutWidth || mods::widescreen::IsClassicFrame() || !re::ConfigInt("hud", "Enabled", 1)) { + if (!enabled || width <= kLayoutWidth || mods::widescreen::IsClassicFrame() || + mods::widescreen::MenuIsShowing()) { return 0; } return anchor == Anchor::Center ? (width - kLayoutWidth) / 2 : width - kLayoutWidth; } -// RAII: shift drawing by dx for the scope. +// 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) { g_offsetX = dx; } + explicit DrawOffset(int dx) : previous_(g_offsetX) { + if (g_offsetX == 0) { + g_offsetX = dx; + } + } ~DrawOffset() { g_offsetX = previous_; } private: int previous_; }; -// UI events: vf04 returns the category, 5 = UI event with its point at +0x48. -bool IsUiPointEvent(void* event) { +// 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) { using CategoryFn = int(__fastcall*)(void* self, void* edx); auto category = reinterpret_cast((*static_cast(event))[1]); - return category(event, nullptr) == 5; + const int value = category(event, nullptr); + if (value < 1 || value > 3) { + return nullptr; + } + return reinterpret_cast(static_cast(event) + 8); } -// RAII: shift the event point by -dx for the scope. +// RAII: shift the mouse position by -dx for the scope (outermost window only). class EventOffset { public: EventOffset(void* event, int dx) { - if (dx != 0 && IsUiPointEvent(event)) { - x_ = reinterpret_cast(static_cast(event) + 0x48); - dx_ = dx; - *x_ -= dx_; + if (dx != 0 && !g_eventShifted) { + x_ = MouseEventX(event); + if (x_) { + dx_ = dx; + *x_ -= dx_; + g_eventShifted = true; + } } } ~EventOffset() { if (x_) { *x_ += dx_; + g_eventShifted = false; } } @@ -171,43 +191,102 @@ void OnDevice(void* device) { } // 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(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(mods::hud::OffsetFor(anchor)); \ + return name##_original(self, edx, device, a, b); \ + } namespace game { using mods::hud::Anchor; -// cUI_Taskbar2 (vtable 0x00894ac0): vf10 events, vf14 render. -int __fastcall UiTaskbar_HandleEvent(void* self, void* edx, void* event); -void __fastcall UiTaskbar_Render(void* self, void* edx, void* device); -// Portrait/health panel (cUI_Mercenary). -int __fastcall UiPortrait_HandleEvent(void* self, void* edx, void* event); -void __fastcall UiPortrait_Render(void* self, void* edx, void* device); +// 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) -RE_HOOK(0x006e1970, UiTaskbar_HandleEvent); -RE_HOOK(0x006e3ac0, UiTaskbar_Render); -// The portrait with health at the top is drawn by cUI_Mercenary (party panel, -// vtable 0x008948cc), not cUI_NetPortrait. -RE_HOOK(0x006d69f0, UiPortrait_HandleEvent); -RE_HOOK(0x006d6e70, UiPortrait_Render); - -int __fastcall UiTaskbar_HandleEvent(void* self, void* edx, void* event) { - mods::hud::EventOffset offset(event, mods::hud::OffsetFor(Anchor::Center)); - return UiTaskbar_HandleEvent_original(self, edx, event); -} - -void __fastcall UiTaskbar_Render(void* self, void* edx, void* device) { - mods::hud::DrawOffset offset(mods::hud::OffsetFor(Anchor::Center)); - UiTaskbar_Render_original(self, edx, device); -} - -int __fastcall UiPortrait_HandleEvent(void* self, void* edx, void* event) { - mods::hud::EventOffset offset(event, mods::hud::OffsetFor(Anchor::Right)); - return UiPortrait_HandleEvent_original(self, edx, event); -} - -void __fastcall UiPortrait_Render(void* self, void* edx, void* device) { - mods::hud::DrawOffset offset(mods::hud::OffsetFor(Anchor::Right)); - UiPortrait_Render_original(self, edx, device); -} +// 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) +HUD_EVENT(0x006a5dd0, UiStatisticsChar_HandleEvent, Anchor::Center) +HUD_RENDER(0x006a9be0, UiStatisticsChar_Render, Anchor::Center) +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::Center) +HUD_RENDER(0x006c9ce0, UiMegamap_Render, Anchor::Center) +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) +HUD_EVENT(0x006d54e0, UiMasterLotto_HandleEvent, Anchor::Center) +HUD_RENDER(0x006d4720, UiMasterLotto_Render, Anchor::Center) +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(0x0070f160, UiCharacter_Render, Anchor::Center) +HUD_EVENT(0x00716b30, UiOptions_HandleEvent, Anchor::Center) +HUD_RENDER3(0x00716640, UiOptions_Render, Anchor::Center) +HUD_EVENT(0x0071d130, UiSavegame_HandleEvent, Anchor::Center) +HUD_RENDER(0x0071dd80, UiSavegame_Render, Anchor::Center) +HUD_EVENT(0x00720c10, UiBusyDlg_HandleEvent, Anchor::Center) +HUD_RENDER(0x007213a0, UiBusyDlg_Render, Anchor::Center) } // namespace game diff --git a/src/mods/widescreen.cpp b/src/mods/widescreen.cpp index 7861920..926d216 100644 --- a/src/mods/widescreen.cpp +++ b/src/mods/widescreen.cpp @@ -65,6 +65,9 @@ int g_renderHeight = 0; // Frames from the main menu thread and the loading screen are laid out for // 1024x768 ("classic"); everything else is the widened in-game view. std::atomic g_menuThread = 0; +// Whether the most recent scene (on any thread) was a classic one: the main +// menu is showing. UI input is processed outside the menu thread. +std::atomic g_lastSceneClassic = false; thread_local int g_loadingScreenDepth = 0; // D3DVIEWPORT7 (d3d.h is not in current Windows SDKs). @@ -290,6 +293,10 @@ int RenderWidth() { return g_renderWidth; } +bool MenuIsShowing() { + return g_lastSceneClassic; +} + bool IsClassicFrame() { if (g_renderWidth == 0) { return false; @@ -324,6 +331,9 @@ void __fastcall DxDriver7_LoadingScreen(DxDriver7* self, void* edx, uint32_t pro // world the full render size. int __fastcall DxDriver7_BeginScene(DxDriver7* self, void* edx) { mods::hud::OnDevice(self->device); + if (self->sceneDepth == 0) { + g_lastSceneClassic = IsClassicFrame(); + } if (g_renderWidth != 0 && self->sceneDepth == 0 && self->device) { const bool classic = IsClassicFrame(); Viewport7 viewport = {0, 0, classic ? 1024u : static_cast(g_renderWidth), diff --git a/src/mods/widescreen.h b/src/mods/widescreen.h index 563783c..07763fa 100644 --- a/src/mods/widescreen.h +++ b/src/mods/widescreen.h @@ -18,4 +18,8 @@ bool IsClassicFrame(); // Applied render width (0 while the original 1024x768 is in use). int RenderWidth(); +// True while the main menu / loading screen (classic layout) is on screen, +// regardless of the calling thread. +bool MenuIsShowing(); + } // namespace mods::widescreen