- Megamap / TAB map zoomed view: heroes' markers and the other symbols (integer 200h centre immediates) follow the view's centre. - Map reveal: the hero's reveal stamp scaled 10x (mods/mapreveal.cpp, docs/map-reveal.md); creatures on the zoomed map shown within 10x the sight range. - Load / save progress screen drawn on the rock background across the full width, the picture blended over it (mods/loadingscreen.cpp); the classic 1024x768 frame is gone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
986 lines
40 KiB
C++
986 lines
40 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 <atomic>
|
|
#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/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<int> 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<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_;
|
|
};
|
|
|
|
// 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<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;
|
|
}
|
|
|
|
// 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<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 = !g_canvasScope && 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;
|
|
|
|
// 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<void*> 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<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)
|
|
|
|
// 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<const uint8_t*>(window);
|
|
if (dx == 0 || g_drawScoped || !device || !g_setViewport || (w[kControlFlags] & 1) == 0) {
|
|
return;
|
|
}
|
|
auto getViewport = reinterpret_cast<GetViewportFn>((*static_cast<void***>(device))[kVtableGetViewport]);
|
|
if (FAILED(getViewport(device, &previous_))) {
|
|
return;
|
|
}
|
|
mods::screen::DrawBackground(device);
|
|
Viewport7 frame = {static_cast<DWORD>(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<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));
|
|
// SetTexture 35.
|
|
g_setTexture = reinterpret_cast<SetTextureFn>(vtable[35]);
|
|
re::AttachRuntimeHook("D3D7 SetTexture", reinterpret_cast<void**>(&g_setTexture),
|
|
reinterpret_cast<void*>(&SetTextureTracked));
|
|
// 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);
|
|
}
|
|
|
|
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<GetViewportFn>((*static_cast<void***>(device))[kVtableGetViewport]) : nullptr;
|
|
if (dx == 0 || g_drawScoped || !g_setViewport || !getViewport || FAILED(getViewport(device, &previous))) {
|
|
return body(context);
|
|
}
|
|
Viewport7 frame = {static_cast<DWORD>(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
|
|
|
|
|