- 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>
844 lines
35 KiB
C++
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
|
|
|
|
|