HUD: centre the taskbar, move the portrait/health panel to the right edge
New mods/hud: while a HUD window renders, screen-space vertices passed to D3D7 DrawPrimitive/DrawIndexedPrimitive are shifted by dx; while it handles an event, the UI event point (category 5, +0x48) is shifted by -dx. Applied to cUI_Taskbar2 (centred) and cUI_Mercenary (portrait + health, right edge). 3D belt items line up without help (their projection is screen-centred). re::AttachRuntimeHook hooks functions only known at runtime (COM methods). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
f73c4154ad
commit
ed3163d312
8 files changed
+259
-1
No files matched your search
@@ -0,0 +1,213 @@
|
||||
// HUD layout for widened views.
|
||||
//
|
||||
// The UI is laid out for 1024x768 and drawn into the top-left of the widened
|
||||
// render target. Selected windows are moved as a whole:
|
||||
// - cUI_Taskbar2 (orbs, belt, skills at the bottom): centred horizontally
|
||||
// - cUI_Mercenary (party panel: portrait and health at the top): right edge
|
||||
//
|
||||
// A window is moved by transforming what it draws and what it receives, so
|
||||
// positions it computes itself (e.g. the potion arc) follow as well:
|
||||
// - while its render method runs, every screen-space vertex (XYZRHW) passed
|
||||
// to DrawPrimitive / DrawIndexedPrimitive is shifted by dx (3D items such
|
||||
// as belt contents already line up: their projection is screen-centred);
|
||||
// - while its event handler runs, the point of UI events (category 5,
|
||||
// point at +0x48) is shifted by -dx. Children are handled inside both.
|
||||
// The world never sees these offsets: they are only active inside the hooks.
|
||||
//
|
||||
// sacred_re.ini:
|
||||
// [hud]
|
||||
// Enabled=1
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "game/render/dx_driver7.h"
|
||||
#include "mods/widescreen.h"
|
||||
#include "re/config.h"
|
||||
#include "re/hook.h"
|
||||
#include "re/log.h"
|
||||
|
||||
namespace mods::hud {
|
||||
namespace {
|
||||
|
||||
constexpr int kLayoutWidth = 1024;
|
||||
|
||||
// Horizontal offset applied to the current thread's UI drawing/input.
|
||||
thread_local int g_offsetX = 0;
|
||||
|
||||
enum class Anchor { Center, Right };
|
||||
|
||||
int OffsetFor(Anchor anchor) {
|
||||
const int width = mods::widescreen::RenderWidth();
|
||||
if (width <= kLayoutWidth || mods::widescreen::IsClassicFrame() || !re::ConfigInt("hud", "Enabled", 1)) {
|
||||
return 0;
|
||||
}
|
||||
return anchor == Anchor::Center ? (width - kLayoutWidth) / 2 : width - kLayoutWidth;
|
||||
}
|
||||
|
||||
// RAII: shift drawing by dx for the scope.
|
||||
class DrawOffset {
|
||||
public:
|
||||
explicit DrawOffset(int dx) : previous_(g_offsetX) { g_offsetX = dx; }
|
||||
~DrawOffset() { g_offsetX = previous_; }
|
||||
|
||||
private:
|
||||
int previous_;
|
||||
};
|
||||
|
||||
// UI events: vf04 returns the category, 5 = UI event with its point at +0x48.
|
||||
bool IsUiPointEvent(void* event) {
|
||||
using CategoryFn = int(__fastcall*)(void* self, void* edx);
|
||||
auto category = reinterpret_cast<CategoryFn>((*static_cast<void***>(event))[1]);
|
||||
return category(event, nullptr) == 5;
|
||||
}
|
||||
|
||||
// RAII: shift the event point by -dx for the scope.
|
||||
class EventOffset {
|
||||
public:
|
||||
EventOffset(void* event, int dx) {
|
||||
if (dx != 0 && IsUiPointEvent(event)) {
|
||||
x_ = reinterpret_cast<int32_t*>(static_cast<uint8_t*>(event) + 0x48);
|
||||
dx_ = dx;
|
||||
*x_ -= dx_;
|
||||
}
|
||||
}
|
||||
~EventOffset() {
|
||||
if (x_) {
|
||||
*x_ += dx_;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
int32_t* x_ = nullptr;
|
||||
int dx_ = 0;
|
||||
};
|
||||
|
||||
// --- Direct3D 7 device hooks -------------------------------------------------
|
||||
|
||||
constexpr DWORD kFvfPositionMask = 0x00e;
|
||||
constexpr DWORD kFvfXyzRhw = 0x004;
|
||||
|
||||
size_t VertexSize(DWORD fvf) {
|
||||
size_t size = 0;
|
||||
switch (fvf & kFvfPositionMask) {
|
||||
case 0x002: size = 12; break; // XYZ
|
||||
case 0x004: size = 16; break; // XYZRHW
|
||||
case 0x006: size = 16; break; // XYZB1
|
||||
case 0x008: size = 20; break; // XYZB2
|
||||
case 0x00a: size = 24; break; // XYZB3
|
||||
case 0x00c: size = 28; break; // XYZB4
|
||||
case 0x00e: size = 32; break; // XYZB5
|
||||
}
|
||||
if (fvf & 0x010) size += 12; // normal
|
||||
if (fvf & 0x040) size += 4; // diffuse
|
||||
if (fvf & 0x080) size += 4; // specular
|
||||
const DWORD textures = (fvf >> 8) & 0xf;
|
||||
for (DWORD i = 0; i < textures; ++i) {
|
||||
static const size_t kCoordinates[] = {2, 3, 4, 1};
|
||||
size += 4 * kCoordinates[(fvf >> (16 + 2 * i)) & 3];
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
// Returns vertices shifted by g_offsetX, or the input when nothing to do.
|
||||
void* ShiftVertices(DWORD fvf, void* vertices, DWORD count) {
|
||||
if (g_offsetX == 0 || (fvf & kFvfPositionMask) != kFvfXyzRhw || !vertices || count == 0) {
|
||||
return vertices;
|
||||
}
|
||||
thread_local std::vector<uint8_t> buffer;
|
||||
const size_t stride = VertexSize(fvf);
|
||||
buffer.assign(static_cast<uint8_t*>(vertices), static_cast<uint8_t*>(vertices) + stride * count);
|
||||
for (DWORD i = 0; i < count; ++i) {
|
||||
auto* x = reinterpret_cast<float*>(buffer.data() + stride * i);
|
||||
*x += static_cast<float>(g_offsetX);
|
||||
}
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
using DrawPrimitiveFn = HRESULT(__stdcall*)(void* device, DWORD type, DWORD fvf, void* vertices, DWORD count,
|
||||
DWORD flags);
|
||||
using DrawIndexedPrimitiveFn = HRESULT(__stdcall*)(void* device, DWORD type, DWORD fvf, void* vertices,
|
||||
DWORD count, WORD* indices, DWORD indexCount, DWORD flags);
|
||||
DrawPrimitiveFn g_drawPrimitive = nullptr;
|
||||
DrawIndexedPrimitiveFn g_drawIndexedPrimitive = nullptr;
|
||||
|
||||
HRESULT __stdcall DrawPrimitiveShifted(void* device, DWORD type, DWORD fvf, void* vertices, DWORD count,
|
||||
DWORD flags) {
|
||||
return g_drawPrimitive(device, type, fvf, ShiftVertices(fvf, vertices, count), count, flags);
|
||||
}
|
||||
|
||||
HRESULT __stdcall DrawIndexedPrimitiveShifted(void* device, DWORD type, DWORD fvf, void* vertices, DWORD count,
|
||||
WORD* indices, DWORD indexCount, DWORD flags) {
|
||||
return g_drawIndexedPrimitive(device, type, fvf, ShiftVertices(fvf, vertices, count), count, indices,
|
||||
indexCount, flags);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void OnDevice(void* device) {
|
||||
static void* hooked = nullptr;
|
||||
if (!device || device == hooked) {
|
||||
return;
|
||||
}
|
||||
hooked = device;
|
||||
void** vtable = *static_cast<void***>(device);
|
||||
// IDirect3DDevice7: DrawPrimitive 25, DrawIndexedPrimitive 26. (3D items in
|
||||
// the HUD need no help: their ortho projection is centred on the screen,
|
||||
// which lines up with a centred taskbar.)
|
||||
if (!g_drawPrimitive) {
|
||||
g_drawPrimitive = reinterpret_cast<DrawPrimitiveFn>(vtable[25]);
|
||||
re::AttachRuntimeHook("D3D7 DrawPrimitive", reinterpret_cast<void**>(&g_drawPrimitive),
|
||||
reinterpret_cast<void*>(&DrawPrimitiveShifted));
|
||||
g_drawIndexedPrimitive = reinterpret_cast<DrawIndexedPrimitiveFn>(vtable[26]);
|
||||
re::AttachRuntimeHook("D3D7 DrawIndexedPrimitive", reinterpret_cast<void**>(&g_drawIndexedPrimitive),
|
||||
reinterpret_cast<void*>(&DrawIndexedPrimitiveShifted));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mods::hud
|
||||
|
||||
// --- HUD windows ---------------------------------------------------------------
|
||||
|
||||
namespace game {
|
||||
|
||||
using mods::hud::Anchor;
|
||||
|
||||
// cUI_Taskbar2 (vtable 0x00894ac0): vf10 events, vf14 render.
|
||||
int __fastcall UiTaskbar_HandleEvent(void* self, void* edx, void* event);
|
||||
void __fastcall UiTaskbar_Render(void* self, void* edx, void* device);
|
||||
// Portrait/health panel (cUI_Mercenary).
|
||||
int __fastcall UiPortrait_HandleEvent(void* self, void* edx, void* event);
|
||||
void __fastcall UiPortrait_Render(void* self, void* edx, void* device);
|
||||
|
||||
RE_HOOK(0x006e1970, UiTaskbar_HandleEvent);
|
||||
RE_HOOK(0x006e3ac0, UiTaskbar_Render);
|
||||
// The portrait with health at the top is drawn by cUI_Mercenary (party panel,
|
||||
// vtable 0x008948cc), not cUI_NetPortrait.
|
||||
RE_HOOK(0x006d69f0, UiPortrait_HandleEvent);
|
||||
RE_HOOK(0x006d6e70, UiPortrait_Render);
|
||||
|
||||
int __fastcall UiTaskbar_HandleEvent(void* self, void* edx, void* event) {
|
||||
mods::hud::EventOffset offset(event, mods::hud::OffsetFor(Anchor::Center));
|
||||
return UiTaskbar_HandleEvent_original(self, edx, event);
|
||||
}
|
||||
|
||||
void __fastcall UiTaskbar_Render(void* self, void* edx, void* device) {
|
||||
mods::hud::DrawOffset offset(mods::hud::OffsetFor(Anchor::Center));
|
||||
UiTaskbar_Render_original(self, edx, device);
|
||||
}
|
||||
|
||||
int __fastcall UiPortrait_HandleEvent(void* self, void* edx, void* event) {
|
||||
mods::hud::EventOffset offset(event, mods::hud::OffsetFor(Anchor::Right));
|
||||
return UiPortrait_HandleEvent_original(self, edx, event);
|
||||
}
|
||||
|
||||
void __fastcall UiPortrait_Render(void* self, void* edx, void* device) {
|
||||
mods::hud::DrawOffset offset(mods::hud::OffsetFor(Anchor::Right));
|
||||
UiPortrait_Render_original(self, edx, device);
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
Reference in new issue
Block a user