Files
sacred_k/src/mods/hud.cpp
T
khallaandClaude Opus 5.5 5bccb0734e Characters: one file per character with a world per difficulty
- CharacterNN.sacredK holds the shared hero and one world per difficulty,
  plus an autosave file; the game works in save\characters\_work
- Load window: one row per character, a difficulty dialog on Laden
  (worlds, newer autosaves, "(neu)" starts a new game with the shared
  hero), Umbenennen, delete moves the character to characters\deleted
- The world keeps its own portals and hero position when the shared hero
  is put on
- K and the Esc menu's "Speichern" save directly (quicksave path)
- One-time migration of the old save rows and exported heroes, after a
  backup
- Startup no longer hangs in the game's foreground loop (0x810af0)
- Text overrides apply to our own text ids too; shared cUI_Button2
  helpers in game/ui/button.h

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 01:41:42 +02:00

844 lines
35 KiB
C++

// 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 <algorithm>
#include <cstdint>
#include <cstring>
#include <vector>
#include <windows.h>
#include "game/addresses.h"
#include "game/render/dx_driver7.h"
#include "game/ui/events.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).
enum class Anchor { Center, Right, Screen };
int OffsetFor(Anchor anchor) {
static const bool enabled = re::ConfigInt("hud", "Enabled", 1) != 0;
const int width = mods::widescreen::RenderWidth();
if (!enabled || width <= kLayoutWidth || mods::widescreen::IsClassicFrame() ||
mods::widescreen::MenuIsShowing() || 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<CRITICAL_SECTION*>(cursor + kCursorLockOffset);
}
int32_t& CursorField(uint8_t* cursor, size_t offset) {
return *reinterpret_cast<int32_t*>(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<uint8_t**>(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<uint8_t**>(game::addr::g_mouseCursor);
if (dx == 0 || !window || !cursor) {
return;
}
const auto* w = static_cast<const uint8_t*>(window);
if ((w[0x10] & 1) == 0) {
return; // hidden windows render too (and some cover the whole frame)
}
const int x = *reinterpret_cast<const int32_t*>(w + 0x24) + dx;
const int y = *reinterpret_cast<const int32_t*>(w + 0x28);
const int width = *reinterpret_cast<const uint16_t*>(w + 0x2c);
const int height = *reinterpret_cast<const uint16_t*>(w + 0x2e);
const int cx = *reinterpret_cast<const int32_t*>(cursor + 4);
const int cy = *reinterpret_cast<const int32_t*>(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_;
};
// 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<int32_t*>(static_cast<uint8_t*>(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;
}
// 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<uint8_t> buffer;
const size_t stride = VertexSize(fvf);
buffer.assign(static_cast<uint8_t*>(vertices), static_cast<uint8_t*>(vertices) + stride * count);
float minX = 1e9f, maxX = -1e9f, minY = 1e9f, maxY = -1e9f;
for (DWORD i = 0; i < count; ++i) {
const auto* position = reinterpret_cast<const float*>(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 = minX <= 0.5f && maxX >= kLayoutWidth - 0.5f && minY <= 0.5f && maxY >= 767.5f;
const float scale = static_cast<float>(mods::widescreen::RenderWidth()) / kLayoutWidth;
for (DWORD i = 0; i < count; ++i) {
auto* x = reinterpret_cast<float*>(buffer.data() + stride * i);
*x = fullLayout ? *x * scale : *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);
}
// 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<int>(viewport->width) >= mods::widescreen::RenderWidth())) {
return g_setViewport(device, viewport);
}
Viewport7 shifted = *viewport;
shifted.x = static_cast<DWORD>(static_cast<int>(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<Rect7> 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)
constexpr DWORD kClearTarget = 1; // D3DCLEAR_TARGET
// RAII for full-screen windows (character export, save games) that fill the
// 1024 frame with a picture: the render target left and right of the frame is
// cleared black, 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).
// Clear clips to the viewport, so the sides are cleared before it is set.
class FrameScope {
public:
FrameScope(void* window, void* device, int dx) {
const auto* w = static_cast<const uint8_t*>(window);
if (dx == 0 || g_drawScoped || !device || !g_setViewport || !g_clear || (w[kControlFlags] & 1) == 0) {
return;
}
auto getViewport = reinterpret_cast<GetViewportFn>((*static_cast<void***>(device))[kVtableGetViewport]);
if (FAILED(getViewport(device, &previous_))) {
return;
}
const LONG height = static_cast<LONG>(previous_.height);
Rect7 sides[] = {{0, 0, dx, height},
{dx + kLayoutWidth, 0, mods::widescreen::RenderWidth(), height}};
g_clear(device, 2, sides, kClearTarget, 0, 1.0f, 0);
Viewport7 frame = {static_cast<DWORD>(dx), 0, kLayoutWidth, 768, previous_.minZ, previous_.maxZ};
device_ = device;
g_setViewport(device, &frame);
}
~FrameScope() {
if (device_) {
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<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));
// Clear 10, SetViewport 13.
g_clear = reinterpret_cast<ClearFn>(vtable[10]);
re::AttachRuntimeHook("D3D7 Clear", reinterpret_cast<void**>(&g_clear), reinterpret_cast<void*>(&ClearShifted));
g_setViewport = reinterpret_cast<SetViewportFn>(vtable[13]);
re::AttachRuntimeHook("D3D7 SetViewport", reinterpret_cast<void**>(&g_setViewport),
reinterpret_cast<void*>(&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<const uint8_t*>(t.tooltip);
if ((p[kControlFlags] & 1) == 0) {
continue;
}
const int width = *reinterpret_cast<const uint16_t*>(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<uint8_t*>(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<uint8_t*>(window);
while (auto* parent = *reinterpret_cast<uint8_t**>(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);
}
} // 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(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