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
@@ -32,6 +32,7 @@ add_library(sacred_re SHARED
|
|||||||
src/game/engine/cmdline.cpp
|
src/game/engine/cmdline.cpp
|
||||||
src/mods/borderless.cpp
|
src/mods/borderless.cpp
|
||||||
src/mods/widescreen.cpp
|
src/mods/widescreen.cpp
|
||||||
|
src/mods/hud.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(sacred_re PRIVATE src)
|
target_include_directories(sacred_re PRIVATE src)
|
||||||
target_compile_options(sacred_re PRIVATE ${SACRED_RE_CXX_FLAGS})
|
target_compile_options(sacred_re PRIVATE ${SACRED_RE_CXX_FLAGS})
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace mods::hud {
|
||||||
|
|
||||||
|
// Installs the Direct3D device hooks the HUD layout needs (once per device).
|
||||||
|
void OnDevice(void* device);
|
||||||
|
|
||||||
|
} // namespace mods::hud
|
||||||
@@ -23,6 +23,8 @@
|
|||||||
|
|
||||||
#include "mods/widescreen.h"
|
#include "mods/widescreen.h"
|
||||||
|
|
||||||
|
#include "mods/hud.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <bit>
|
#include <bit>
|
||||||
@@ -284,6 +286,10 @@ void ApplyRenderSize(game::DxDisplayMode& mode, HWND window) {
|
|||||||
re::Log("widescreen: render size %dx%d, ortho x +-%.1f y +-%.1f", width, height, g_orthoRight, g_orthoTop);
|
re::Log("widescreen: render size %dx%d, ortho x +-%.1f y +-%.1f", width, height, g_orthoRight, g_orthoTop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int RenderWidth() {
|
||||||
|
return g_renderWidth;
|
||||||
|
}
|
||||||
|
|
||||||
bool IsClassicFrame() {
|
bool IsClassicFrame() {
|
||||||
if (g_renderWidth == 0) {
|
if (g_renderWidth == 0) {
|
||||||
return false;
|
return false;
|
||||||
@@ -317,6 +323,7 @@ void __fastcall DxDriver7_LoadingScreen(DxDriver7* self, void* edx, uint32_t pro
|
|||||||
// viewport (the borderless mod then presents just that area, scaled), and the
|
// viewport (the borderless mod then presents just that area, scaled), and the
|
||||||
// world the full render size.
|
// world the full render size.
|
||||||
int __fastcall DxDriver7_BeginScene(DxDriver7* self, void* edx) {
|
int __fastcall DxDriver7_BeginScene(DxDriver7* self, void* edx) {
|
||||||
|
mods::hud::OnDevice(self->device);
|
||||||
if (g_renderWidth != 0 && self->sceneDepth == 0 && self->device) {
|
if (g_renderWidth != 0 && self->sceneDepth == 0 && self->device) {
|
||||||
const bool classic = IsClassicFrame();
|
const bool classic = IsClassicFrame();
|
||||||
Viewport7 viewport = {0, 0, classic ? 1024u : static_cast<DWORD>(g_renderWidth),
|
Viewport7 viewport = {0, 0, classic ? 1024u : static_cast<DWORD>(g_renderWidth),
|
||||||
|
|||||||
@@ -15,4 +15,7 @@ void ApplyRenderSize(game::DxDisplayMode& mode, HWND window);
|
|||||||
// menu, loading screen); the borderless mod then presents only that area.
|
// menu, loading screen); the borderless mod then presents only that area.
|
||||||
bool IsClassicFrame();
|
bool IsClassicFrame();
|
||||||
|
|
||||||
|
// Applied render width (0 while the original 1024x768 is in use).
|
||||||
|
int RenderWidth();
|
||||||
|
|
||||||
} // namespace mods::widescreen
|
} // namespace mods::widescreen
|
||||||
@@ -59,4 +59,22 @@ int InstallHooks() {
|
|||||||
return failed;
|
return failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool AttachRuntimeHook(const char* name, void** original, void* replacement) {
|
||||||
|
const void* target = *original;
|
||||||
|
DetourTransactionBegin();
|
||||||
|
DetourUpdateThread(GetCurrentThread());
|
||||||
|
LONG error = DetourAttach(original, replacement);
|
||||||
|
if (error == NO_ERROR) {
|
||||||
|
error = DetourTransactionCommit();
|
||||||
|
} else {
|
||||||
|
DetourTransactionAbort();
|
||||||
|
}
|
||||||
|
if (error != NO_ERROR) {
|
||||||
|
Log("hook %-24s %p FAILED (error %ld)", name, target, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Log("hook %-24s %p attached at runtime", name, target);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace re
|
} // namespace re
|
||||||
@@ -31,6 +31,11 @@ public:
|
|||||||
// number of hooks that failed to attach.
|
// number of hooks that failed to attach.
|
||||||
int InstallHooks();
|
int InstallHooks();
|
||||||
|
|
||||||
|
// Hooks a function only known at runtime (e.g. a COM method read from a
|
||||||
|
// vtable). `*original` must hold the function's address; it receives the
|
||||||
|
// trampoline. Returns false (and logs) on failure.
|
||||||
|
bool AttachRuntimeHook(const char* name, void** original, void* replacement);
|
||||||
|
|
||||||
} // namespace re
|
} // namespace re
|
||||||
|
|
||||||
// Hooks a Windows API function for the whole process (game and DLLs alike).
|
// Hooks a Windows API function for the whole process (game and DLLs alike).
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
param(
|
param(
|
||||||
[string]$Name = "autotest",
|
[string]$Name = "autotest",
|
||||||
[string]$Ini = "",
|
[string]$Ini = "",
|
||||||
[int]$LoadSeconds = 30
|
[int]$LoadSeconds = 30,
|
||||||
|
# Runs in-game after loading, before the screenshots (e.g. clicks).
|
||||||
|
[scriptblock]$InGame = $null
|
||||||
)
|
)
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
. (Join-Path $PSScriptRoot "game-input.ps1")
|
. (Join-Path $PSScriptRoot "game-input.ps1")
|
||||||
@@ -26,6 +28,7 @@ try {
|
|||||||
Click-At 1720 779 -AfterMs 4000 # main menu: "Spiel laden"
|
Click-At 1720 779 -AfterMs 4000 # main menu: "Spiel laden"
|
||||||
Click-At 2382 568 -AfterMs 2000 # load screen: "Laden" (first save selected)
|
Click-At 2382 568 -AfterMs 2000 # load screen: "Laden" (first save selected)
|
||||||
Click-At 1551 826 -AfterMs ($LoadSeconds * 1000) # confirm: "Ok"
|
Click-At 1551 826 -AfterMs ($LoadSeconds * 1000) # confirm: "Ok"
|
||||||
|
if ($InGame) { & $InGame }
|
||||||
Save-Screen "screenshots\${Name}_ingame.png" | Out-Null
|
Save-Screen "screenshots\${Name}_ingame.png" | Out-Null
|
||||||
Press-Key 0x1B -AfterMs 2500 # ESC menu
|
Press-Key 0x1B -AfterMs 2500 # ESC menu
|
||||||
Save-Screen "screenshots\${Name}_esc.png" | Out-Null
|
Save-Screen "screenshots\${Name}_esc.png" | Out-Null
|
||||||
|
|||||||
Reference in new issue
Block a user