HUD: centre the taskbar, move the portrait/health panel to the right edge

New mods/hud: while a HUD window renders, screen-space vertices passed to
D3D7 DrawPrimitive/DrawIndexedPrimitive are shifted by dx; while it handles
an event, the UI event point (category 5, +0x48) is shifted by -dx. Applied
to cUI_Taskbar2 (centred) and cUI_Mercenary (portrait + health, right edge).
3D belt items line up without help (their projection is screen-centred).
re::AttachRuntimeHook hooks functions only known at runtime (COM methods).

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 03:37:05 +02:00
1 parent f73c4154ad
commit ed3163d312
8 files changed
+259 -1

No files matched your search

+213
View File
@@ -0,0 +1,213 @@
// HUD layout for widened views.
//
// The UI is laid out for 1024x768 and drawn into the top-left of the widened
// render target. Selected windows are moved as a whole:
// - cUI_Taskbar2 (orbs, belt, skills at the bottom): centred horizontally
// - cUI_Mercenary (party panel: portrait and health at the top): right edge
//
// 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 point of UI events (category 5,
// point at +0x48) 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:
// [hud]
// Enabled=1
#include <cstdint>
#include <cstring>
#include <vector>
#include <windows.h>
#include "game/render/dx_driver7.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/input.
thread_local int g_offsetX = 0;
enum class Anchor { Center, Right };
int OffsetFor(Anchor anchor) {
const int width = mods::widescreen::RenderWidth();
if (width <= kLayoutWidth || mods::widescreen::IsClassicFrame() || !re::ConfigInt("hud", "Enabled", 1)) {
return 0;
}
return anchor == Anchor::Center ? (width - kLayoutWidth) / 2 : width - kLayoutWidth;
}
// RAII: shift drawing by dx for the scope.
class DrawOffset {
public:
explicit DrawOffset(int dx) : previous_(g_offsetX) { 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) {
using CategoryFn = int(__fastcall*)(void* self, void* edx);
auto category = reinterpret_cast<CategoryFn>((*static_cast<void***>(event))[1]);
return category(event, nullptr) == 5;
}
// RAII: shift the event point by -dx for the scope.
class EventOffset {
public:
EventOffset(void* event, int dx) {
if (dx != 0 && IsUiPointEvent(event)) {
x_ = reinterpret_cast<int32_t*>(static_cast<uint8_t*>(event) + 0x48);
dx_ = dx;
*x_ -= dx_;
}
}
~EventOffset() {
if (x_) {
*x_ += dx_;
}
}
private:
int32_t* x_ = nullptr;
int dx_ = 0;
};
// --- 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;
}
// Returns vertices shifted by g_offsetX, or the input when nothing to do.
void* ShiftVertices(DWORD fvf, void* vertices, DWORD count) {
if (g_offsetX == 0 || (fvf & kFvfPositionMask) != kFvfXyzRhw || !vertices || count == 0) {
return vertices;
}
thread_local std::vector<uint8_t> buffer;
const size_t stride = VertexSize(fvf);
buffer.assign(static_cast<uint8_t*>(vertices), static_cast<uint8_t*>(vertices) + stride * count);
for (DWORD i = 0; i < count; ++i) {
auto* x = reinterpret_cast<float*>(buffer.data() + stride * i);
*x += static_cast<float>(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;
HRESULT __stdcall DrawPrimitiveShifted(void* device, DWORD type, DWORD fvf, void* vertices, DWORD count,
DWORD flags) {
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) {
return g_drawIndexedPrimitive(device, type, fvf, ShiftVertices(fvf, vertices, count), count, indices,
indexCount, flags);
}
} // namespace
void OnDevice(void* device) {
static void* hooked = nullptr;
if (!device || device == hooked) {
return;
}
hooked = device;
void** vtable = *static_cast<void***>(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<DrawPrimitiveFn>(vtable[25]);
re::AttachRuntimeHook("D3D7 DrawPrimitive", reinterpret_cast<void**>(&g_drawPrimitive),
reinterpret_cast<void*>(&DrawPrimitiveShifted));
g_drawIndexedPrimitive = reinterpret_cast<DrawIndexedPrimitiveFn>(vtable[26]);
re::AttachRuntimeHook("D3D7 DrawIndexedPrimitive", reinterpret_cast<void**>(&g_drawIndexedPrimitive),
reinterpret_cast<void*>(&DrawIndexedPrimitiveShifted));
}
}
} // namespace mods::hud
// --- HUD windows ---------------------------------------------------------------
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);
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);
}
} // namespace game
+8
View File
@@ -0,0 +1,8 @@
#pragma once
namespace mods::hud {
// Installs the Direct3D device hooks the HUD layout needs (once per device).
void OnDevice(void* device);
} // namespace mods::hud
+7
View File
@@ -23,6 +23,8 @@
#include "mods/widescreen.h"
#include "mods/hud.h"
#include <algorithm>
#include <atomic>
#include <bit>
@@ -284,6 +286,10 @@ void ApplyRenderSize(game::DxDisplayMode& mode, HWND window) {
re::Log("widescreen: render size %dx%d, ortho x +-%.1f y +-%.1f", width, height, g_orthoRight, g_orthoTop);
}
int RenderWidth() {
return g_renderWidth;
}
bool IsClassicFrame() {
if (g_renderWidth == 0) {
return false;
@@ -317,6 +323,7 @@ void __fastcall DxDriver7_LoadingScreen(DxDriver7* self, void* edx, uint32_t pro
// viewport (the borderless mod then presents just that area, scaled), and the
// world the full render size.
int __fastcall DxDriver7_BeginScene(DxDriver7* self, void* edx) {
mods::hud::OnDevice(self->device);
if (g_renderWidth != 0 && self->sceneDepth == 0 && self->device) {
const bool classic = IsClassicFrame();
Viewport7 viewport = {0, 0, classic ? 1024u : static_cast<DWORD>(g_renderWidth),
+3
View File
@@ -15,4 +15,7 @@ void ApplyRenderSize(game::DxDisplayMode& mode, HWND window);
// menu, loading screen); the borderless mod then presents only that area.
bool IsClassicFrame();
// Applied render width (0 while the original 1024x768 is in use).
int RenderWidth();
} // namespace mods::widescreen
+18
View File
@@ -59,4 +59,22 @@ int InstallHooks() {
return failed;
}
bool AttachRuntimeHook(const char* name, void** original, void* replacement) {
const void* target = *original;
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
LONG error = DetourAttach(original, replacement);
if (error == NO_ERROR) {
error = DetourTransactionCommit();
} else {
DetourTransactionAbort();
}
if (error != NO_ERROR) {
Log("hook %-24s %p FAILED (error %ld)", name, target, error);
return false;
}
Log("hook %-24s %p attached at runtime", name, target);
return true;
}
} // namespace re
+5
View File
@@ -31,6 +31,11 @@ public:
// number of hooks that failed to attach.
int InstallHooks();
// Hooks a function only known at runtime (e.g. a COM method read from a
// vtable). `*original` must hold the function's address; it receives the
// trampoline. Returns false (and logs) on failure.
bool AttachRuntimeHook(const char* name, void** original, void* replacement);
} // namespace re
// Hooks a Windows API function for the whole process (game and DLLs alike).