Initial sacred_re scaffold with borderless fullscreen mod

- Launcher (sacred_re.exe) starts sacred.exe with sacred_re.dll injected
  via Detours; the DLL verifies the Sacred Gold 2.28 build before hooking.
- Hook framework: RE_HOOK for game addresses, RE_HOOK_API for Windows
  APIs, per-hook enable/disable in sacred_re.ini, sacred_re.log.
- Game code: Log (0x66f1c0) hooked, CmdLineHasFlag (0x813140) rewritten.
- Borderless mod: forces windowed mode, covers the monitor, scales the
  1024x768 image aspect-correct, remaps mouse input, keeps running in the
  background with keyboard input hidden while unfocused.
- CMake/Ninja/MSVC x86 build, VS Code setup, disasm tool, function map.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-13 23:55:50 +02:00
commit d9f5c85430
31 files changed
+1184

No files matched your search

+49
View File
@@ -0,0 +1,49 @@
// Command-line switches.
//
// Original (0x00813140), called from WinMain with lpCmdLine, e.g. for "log",
// "tabblocker", "win" and the language codes "US", "DE", ... (table 0x00897394).
//
// Status: rewritten. Behaviour matches the original, including its quirks:
// - only the flag is lowercased, not the command line: "-LOG" does not enable
// logging, and the language code "DE" is matched as "de";
// - nothing before the match is checked, so "-xlog" also counts as "log";
// - a match must be followed by a space or the end of the command line.
// Deviation: the original copies the flag into a 256-byte stack buffer
// unchecked; we truncate instead of overflowing.
#include "game/engine/cmdline.h"
#include <cstring>
#include "game/engine/log.h"
#include "re/hook.h"
namespace game {
RE_HOOK(0x00813140, CmdLineHasFlag);
bool __cdecl CmdLineHasFlag(const char* cmdline, const char* flag, bool quiet) {
char needle[256];
strncpy_s(needle, flag, _TRUNCATE);
for (char* c = needle; *c; ++c) {
if (*c >= 'A' && *c <= 'Z') {
*c += 'a' - 'A';
}
}
const size_t length = strlen(needle);
for (const char* match = strstr(cmdline, needle); match; match = strstr(match + 1, needle)) {
const char* space = strchr(match, ' ');
const bool endsAtSpace = space && static_cast<size_t>(space - match) == length;
const bool endsAtEnd = strlen(match) == length;
if (endsAtSpace || endsAtEnd) {
if (!quiet) {
Log("cmd: %s\n", flag);
}
return true;
}
}
return false;
}
} // namespace game
+8
View File
@@ -0,0 +1,8 @@
#pragma once
namespace game {
// sacred.exe 0x00813140
bool __cdecl CmdLineHasFlag(const char* cmdline, const char* flag, bool quiet);
} // namespace game
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <windows.h>
namespace game {
// sacred.exe 0x0066e500. GetCursorPos(), made relative to `window`'s client
// area when `window` is non-null. The game's main source of mouse positions.
void __cdecl GetMouseClientPos(HWND window, POINT* position);
// sacred.exe 0x008122a0. Window procedure of the main "Sacred" window class.
LRESULT CALLBACK MainWindowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam);
} // namespace game
+42
View File
@@ -0,0 +1,42 @@
// Game debug log.
//
// Original (0x0066f1c0): printf-style. Does nothing unless logging is enabled
// (byte 0x017e5ee0, set by the "log" command-line flag in WinMain). When
// enabled it lazily opens DEBUG.LOG and ExpLog.LOG, writes the message to
// DEBUG.LOG, then formats it into a 1024-byte buffer and passes it to an
// optional callback (0x017e5ee4) and to either OutputDebugString or the
// in-game console object (0x017e5edc).
//
// Status: hooked, not rewritten. Every message is mirrored into sacred_re.log
// whether or not the game's own logging is enabled, then forwarded.
#include "game/engine/log.h"
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include "re/hook.h"
#include "re/log.h"
namespace game {
RE_HOOK(0x0066f1c0, Log);
void __cdecl Log(const char* fmt, ...) {
char message[1024];
va_list args;
va_start(args, fmt);
vsnprintf(message, sizeof(message), fmt, args);
va_end(args);
size_t length = strlen(message);
while (length > 0 && (message[length - 1] == '\n' || message[length - 1] == '\r')) {
--length;
}
re::Log("[game] %.*s", static_cast<int>(length), message);
Log_original("%s", message);
}
} // namespace game
+8
View File
@@ -0,0 +1,8 @@
#pragma once
namespace game {
// sacred.exe 0x0066f1c0
void __cdecl Log(const char* fmt, ...);
} // namespace game
+58
View File
@@ -0,0 +1,58 @@
#pragma once
// The engine's DirectDraw 7 / Direct3D 7 renderer ("dxDriver7" in its log
// messages). Single instance, pointer at 0x00cda99c.
//
// These structs mirror the original 32-bit memory layout field by field and
// are only valid inside the game process; only the fields we use are named.
#include <cstddef>
#include <cstdint>
#include <windows.h>
namespace game {
// Display mode request, passed to DxDriver7::SetDisplayMode and copied to
// DxDriver7::mode.
struct DxDisplayMode {
uint8_t unknown00[0x08];
uint16_t height; // +0x08
uint8_t unknown0a[0x02];
uint16_t width; // +0x0c
uint8_t unknown0e[0x6e];
int32_t fullscreen; // +0x7c 1 = exclusive fullscreen, 0 = windowed
uint32_t bitsPerPixel; // +0x80
};
static_assert(sizeof(DxDisplayMode) == 0x84);
static_assert(offsetof(DxDisplayMode, fullscreen) == 0x7c);
struct DxDriver7 {
uint8_t unknown000[0x14];
DxDisplayMode mode; // +0x14
uint8_t unknown098[0x18];
HWND window; // +0xb0
void* directDraw; // +0xb4 IDirectDraw7*
void* primarySurface; // +0xb8 IDirectDrawSurface7*
void* backBuffer; // +0xbc IDirectDrawSurface7*
uint8_t unknown0c0[0x11c];
RECT blitSource; // +0x1dc back-buffer rect copied by Flip (windowed)
RECT blitDest; // +0x1ec screen-space destination of that copy
};
static_assert(offsetof(DxDriver7, window) == 0xb0);
static_assert(offsetof(DxDriver7, blitSource) == 0x1dc);
static_assert(offsetof(DxDriver7, blitDest) == 0x1ec);
// Member functions are __thiscall; we declare them __fastcall with an unused
// second parameter (EDX), which has the same register/stack contract.
// sacred.exe 0x00645390. Creates DirectDraw, the surfaces and the D3D device for
// `mode`. Windowed: resizes the popup window to mode.width x mode.height,
// centred on the desktop. Returns non-zero on success.
int __fastcall DxDriver7_SetDisplayMode(DxDriver7* self, void* edx, void* deviceCaps, const DxDisplayMode* mode,
uint32_t unknown);
// sacred.exe 0x00645f70. Presents the back buffer: Flip() in fullscreen,
// primary->Blt(blitDest, backBuffer, blitSource) in windowed mode.
int __fastcall DxDriver7_Flip(DxDriver7* self, void* edx);
} // namespace game
+102
View File
@@ -0,0 +1,102 @@
// sacred_re.exe: starts sacred.exe with sacred_re.dll injected.
//
// sacred_re.exe [--game-dir <dir>] [game arguments...]
//
// The game directory comes from --game-dir (which is then remembered in
// sacred_re.ini), else from [paths] GameDir in sacred_re.ini, else the
// launcher's own directory. Remaining arguments are passed to the game
// unchanged, e.g. `sacred_re.exe log` enables the game's DEBUG.LOG.
#include <string>
#include <windows.h>
#include <detours.h>
#include <shellapi.h>
namespace {
std::wstring ExeDir() {
wchar_t path[MAX_PATH];
DWORD length = GetModuleFileNameW(nullptr, path, MAX_PATH);
std::wstring dir(path, length);
return dir.substr(0, dir.find_last_of(L"\\/") + 1);
}
bool FileExists(const std::wstring& path) {
DWORD attributes = GetFileAttributesW(path.c_str());
return attributes != INVALID_FILE_ATTRIBUTES && !(attributes & FILE_ATTRIBUTE_DIRECTORY);
}
std::wstring WithTrailingSlash(std::wstring dir) {
if (!dir.empty() && dir.back() != L'\\' && dir.back() != L'/') {
dir += L'\\';
}
return dir;
}
int Fail(const std::wstring& message) {
MessageBoxW(nullptr, message.c_str(), L"sacred_re", MB_OK | MB_ICONERROR);
return 1;
}
} // namespace
int WINAPI wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int) {
const std::wstring launcherDir = ExeDir();
const std::wstring ini = launcherDir + L"sacred_re.ini";
int argc = 0;
wchar_t** argv = CommandLineToArgvW(GetCommandLineW(), &argc);
std::wstring gameDir;
std::wstring gameArgs;
for (int i = 1; i < argc; ++i) {
std::wstring arg = argv[i];
if (arg == L"--game-dir" && i + 1 < argc) {
gameDir = WithTrailingSlash(argv[++i]);
WritePrivateProfileStringW(L"paths", L"GameDir", gameDir.c_str(), ini.c_str());
} else {
gameArgs += L' ';
gameArgs += arg;
}
}
LocalFree(argv);
if (gameDir.empty()) {
wchar_t configured[MAX_PATH] = {};
GetPrivateProfileStringW(L"paths", L"GameDir", L"", configured, MAX_PATH, ini.c_str());
gameDir = configured[0] ? WithTrailingSlash(configured) : launcherDir;
}
const std::wstring gameExe = gameDir + L"sacred.exe";
if (!FileExists(gameExe)) {
return Fail(L"sacred.exe not found in\n" + gameDir +
L"\n\nRun once with: sacred_re.exe --game-dir \"<Sacred Gold folder>\"");
}
const std::wstring dllPath = launcherDir + L"sacred_re.dll";
if (!FileExists(dllPath)) {
return Fail(L"sacred_re.dll not found next to the launcher:\n" + dllPath);
}
// Detours takes the DLL path as ANSI.
char dllPathA[MAX_PATH];
if (!WideCharToMultiByte(CP_ACP, 0, dllPath.c_str(), -1, dllPathA, MAX_PATH, nullptr, nullptr)) {
return Fail(L"DLL path is not representable in the ANSI code page:\n" + dllPath);
}
std::wstring commandLine = L"\"" + gameExe + L"\"" + gameArgs;
STARTUPINFOW startup = {sizeof(startup)};
PROCESS_INFORMATION process = {};
if (!DetourCreateProcessWithDllExW(gameExe.c_str(), commandLine.data(), nullptr, nullptr, FALSE,
CREATE_DEFAULT_ERROR_MODE, nullptr, gameDir.c_str(), &startup, &process,
dllPathA, nullptr)) {
return Fail(L"Failed to start sacred.exe (error " + std::to_wstring(GetLastError()) + L").");
}
WaitForSingleObject(process.hProcess, INFINITE);
DWORD exitCode = 0;
GetExitCodeProcess(process.hProcess, &exitCode);
CloseHandle(process.hThread);
CloseHandle(process.hProcess);
return static_cast<int>(exitCode);
}
+215
View File
@@ -0,0 +1,215 @@
// Borderless fullscreen.
//
// The game renders at a fixed resolution (1024x768). Unmodified, windowed mode
// shows that as a 1024x768 popup centred on the desktop; fullscreen mode
// switches the display mode. With this mod the game always runs windowed, the
// popup covers the whole monitor, and each frame is scaled up into the largest
// centred rectangle with the game's aspect ratio (black bars elsewhere).
//
// Mouse positions are mapped back from that rectangle to game pixels, both in
// GetMouseClientPos (polled by the game) and in mouse window messages.
//
// sacred_re.ini:
// [borderless]
// Enabled=1 ; 0 = original behaviour
// IntegerScale=0 ; 1 = scale by whole multiples only (sharper, larger bars)
// RunInBackground=1 ; keep the game running when it loses focus
//
// Vanilla pauses when the window loses focus (WM_ACTIVATEAPP sets bit 0x20000 in
// 0x0182cdb8 and pauses sound) and stops presenting; a monitor-sized window then
// goes black. With RunInBackground the deactivation is never passed to the
// game. Because the game polls the keyboard with GetAsyncKeyState, which sees
// keys pressed in any application, key state is hidden from it while another
// window has the focus.
//
// Known gap: one code path (0x00610650, cursor nudging) moves the cursor with
// SetCursorPos by a game-pixel delta, which is not scaled to screen pixels.
#include <algorithm>
#include <windows.h>
#include <windowsx.h>
#include "game/engine/input.h"
#include "game/render/dx_driver7.h"
#include "re/config.h"
#include "re/hook.h"
#include "re/log.h"
namespace {
struct Viewport {
bool active = false;
HWND window = nullptr;
bool runInBackground = false;
int gameWidth = 0;
int gameHeight = 0;
RECT client = {}; // scaled game image, window client coordinates
RECT screen = {}; // same rectangle, screen coordinates
};
Viewport g_viewport;
bool Enabled() {
return re::ConfigInt("borderless", "Enabled", 1) != 0;
}
// Largest rectangle with the game's aspect ratio that fits the monitor.
RECT FitRect(int areaWidth, int areaHeight, int width, int height, bool integerScale) {
int scaledWidth;
int scaledHeight;
if (integerScale) {
int scale = std::max(1, std::min(areaWidth / width, areaHeight / height));
scaledWidth = width * scale;
scaledHeight = height * scale;
} else if (static_cast<long long>(areaWidth) * height > static_cast<long long>(areaHeight) * width) {
scaledHeight = areaHeight;
scaledWidth = MulDiv(width, areaHeight, height);
} else {
scaledWidth = areaWidth;
scaledHeight = MulDiv(height, areaWidth, width);
}
int left = (areaWidth - scaledWidth) / 2;
int top = (areaHeight - scaledHeight) / 2;
return {left, top, left + scaledWidth, top + scaledHeight};
}
void ApplyBorderless(game::DxDriver7* driver) {
HWND window = driver->window;
MONITORINFO monitor = {sizeof(monitor)};
GetMonitorInfoW(MonitorFromWindow(window, MONITOR_DEFAULTTONEAREST), &monitor);
const RECT& area = monitor.rcMonitor;
const int areaWidth = area.right - area.left;
const int areaHeight = area.bottom - area.top;
SetWindowLongW(window, GWL_STYLE, WS_POPUP | WS_VISIBLE);
SetWindowPos(window, HWND_TOP, area.left, area.top, areaWidth, areaHeight, SWP_FRAMECHANGED | SWP_SHOWWINDOW);
Viewport viewport;
viewport.window = window;
viewport.runInBackground = re::ConfigInt("borderless", "RunInBackground", 1) != 0;
viewport.gameWidth = driver->mode.width;
viewport.gameHeight = driver->mode.height;
viewport.client = FitRect(areaWidth, areaHeight, viewport.gameWidth, viewport.gameHeight,
re::ConfigInt("borderless", "IntegerScale", 0) != 0);
viewport.screen = viewport.client;
OffsetRect(&viewport.screen, area.left, area.top);
viewport.active = true;
g_viewport = viewport;
// Paint the bars: the window class background brush is black.
InvalidateRect(window, nullptr, TRUE);
re::Log("borderless: %dx%d game -> %ldx%ld monitor, image at (%ld,%ld) %ldx%ld", viewport.gameWidth,
viewport.gameHeight, static_cast<long>(areaWidth), static_cast<long>(areaHeight), viewport.client.left,
viewport.client.top, viewport.client.right - viewport.client.left,
viewport.client.bottom - viewport.client.top);
}
// Window client coordinates -> game pixels.
POINT ToGame(POINT p) {
const Viewport& v = g_viewport;
const int width = v.client.right - v.client.left;
const int height = v.client.bottom - v.client.top;
POINT result;
result.x = std::clamp(MulDiv(p.x - v.client.left, v.gameWidth, width), 0, v.gameWidth - 1);
result.y = std::clamp(MulDiv(p.y - v.client.top, v.gameHeight, height), 0, v.gameHeight - 1);
return result;
}
// Fills everything except the game image with black.
void PaintBars(HDC dc) {
RECT window;
GetClientRect(g_viewport.window, &window);
const int saved = SaveDC(dc);
ExcludeClipRect(dc, g_viewport.client.left, g_viewport.client.top, g_viewport.client.right,
g_viewport.client.bottom);
FillRect(dc, &window, static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH)));
RestoreDC(dc, saved);
}
bool IsMouseMessageWithClientCoords(UINT message) {
// WM_MOUSEWHEEL and WM_MOUSEHWHEEL carry screen coordinates; leave them alone.
return message >= WM_MOUSEFIRST && message <= WM_MOUSELAST && message != WM_MOUSEWHEEL &&
message != WM_MOUSEHWHEEL;
}
} // namespace
namespace game {
RE_HOOK(0x00645390, DxDriver7_SetDisplayMode);
RE_HOOK(0x00645f70, DxDriver7_Flip);
RE_HOOK(0x0066e500, GetMouseClientPos);
RE_HOOK(0x008122a0, MainWindowProc);
int __fastcall DxDriver7_SetDisplayMode(DxDriver7* self, void* edx, void* deviceCaps, const DxDisplayMode* mode,
uint32_t unknown) {
if (!Enabled()) {
return DxDriver7_SetDisplayMode_original(self, edx, deviceCaps, mode, unknown);
}
DxDisplayMode windowed = *mode;
windowed.fullscreen = 0;
g_viewport.active = false;
const int result = DxDriver7_SetDisplayMode_original(self, edx, deviceCaps, &windowed, unknown);
if (result) {
ApplyBorderless(self);
}
return result;
}
int __fastcall DxDriver7_Flip(DxDriver7* self, void* edx) {
// The original computes the blit rectangles once, for a centred window of
// exactly the game's size; override them on every present.
if (g_viewport.active && self->window == g_viewport.window) {
self->blitSource = {0, 0, g_viewport.gameWidth, g_viewport.gameHeight};
self->blitDest = g_viewport.screen;
}
return DxDriver7_Flip_original(self, edx);
}
void __cdecl GetMouseClientPos(HWND window, POINT* position) {
GetMouseClientPos_original(window, position);
if (window && g_viewport.active && window == g_viewport.window) {
*position = ToGame(*position);
}
}
LRESULT CALLBACK MainWindowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam) {
if (!g_viewport.active || window != g_viewport.window) {
return MainWindowProc_original(window, message, wParam, lParam);
}
if (IsMouseMessageWithClientCoords(message)) {
POINT game = ToGame({GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)});
lParam = MAKELPARAM(game.x, game.y);
}
switch (message) {
case WM_ERASEBKGND:
PaintBars(reinterpret_cast<HDC>(wParam));
return 1;
case WM_ACTIVATEAPP:
if (!wParam && g_viewport.runInBackground) {
re::Log("borderless: focus lost, game keeps running");
return 0;
}
break;
}
return MainWindowProc_original(window, message, wParam, lParam);
}
} // namespace game
// The game polls keys with GetAsyncKeyState, which reports keys pressed in any
// application. While it runs in the background, hide them from it.
SHORT WINAPI GetAsyncKeyStateWhenFocused(int key);
RE_HOOK_API(GetAsyncKeyState, GetAsyncKeyStateWhenFocused);
SHORT WINAPI GetAsyncKeyStateWhenFocused(int key) {
if (g_viewport.active && g_viewport.runInBackground && GetForegroundWindow() != g_viewport.window) {
return 0;
}
return GetAsyncKeyStateWhenFocused_original(key);
}
+33
View File
@@ -0,0 +1,33 @@
#include "re/config.h"
#include <cstring>
#include <windows.h>
namespace re {
namespace {
std::wstring g_path;
std::wstring Widen(const char* s) {
return std::wstring(s, s + strlen(s));
}
} // namespace
void SetConfigPath(const std::wstring& path) {
g_path = path;
}
const std::wstring& ConfigPath() {
return g_path;
}
int ConfigInt(const char* section, const char* key, int fallback) {
if (g_path.empty()) {
return fallback;
}
return static_cast<int>(GetPrivateProfileIntW(Widen(section).c_str(), Widen(key).c_str(), fallback, g_path.c_str()));
}
} // namespace re
+12
View File
@@ -0,0 +1,12 @@
#pragma once
// sacred_re.ini next to sacred_re.dll / sacred_re.exe.
#include <string>
namespace re {
void SetConfigPath(const std::wstring& path);
const std::wstring& ConfigPath();
int ConfigInt(const char* section, const char* key, int fallback);
} // namespace re
+75
View File
@@ -0,0 +1,75 @@
// sacred_re.dll entry point. Loaded into sacred.exe by the launcher before the
// game's own entry point runs, so every hook is in place from the first
// instruction of game code.
#include <string>
#include <windows.h>
#include <detours.h>
#include "re/config.h"
#include "re/hook.h"
#include "re/log.h"
namespace {
// The only build we support: Sacred Gold 2.28, sacred.exe
// sha256 fe774c6f985b02c9ce03103c2e55e5d853258c0cc3c737ccd211219c74460cb9.
// Every hook address in this project is specific to it.
constexpr DWORD kExpectedTimestamp = 0x451BBE74;
constexpr DWORD kExpectedSizeOfImage = 0x01999000;
std::wstring ModuleDir(HMODULE module) {
wchar_t path[MAX_PATH];
DWORD length = GetModuleFileNameW(module, path, MAX_PATH);
std::wstring dir(path, length);
return dir.substr(0, dir.find_last_of(L"\\/") + 1);
}
bool IsSupportedGameBuild() {
auto base = reinterpret_cast<const BYTE*>(GetModuleHandleW(nullptr));
auto dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
auto nt = reinterpret_cast<const IMAGE_NT_HEADERS32*>(base + dos->e_lfanew);
re::Log("game image: timestamp 0x%08lx, size 0x%08lx", nt->FileHeader.TimeDateStamp,
nt->OptionalHeader.SizeOfImage);
return nt->FileHeader.TimeDateStamp == kExpectedTimestamp &&
nt->OptionalHeader.SizeOfImage == kExpectedSizeOfImage;
}
void Startup(HMODULE module) {
std::wstring dir = ModuleDir(module);
re::OpenLog((dir + L"sacred_re.log").c_str());
re::SetConfigPath(dir + L"sacred_re.ini");
re::Log("sacred_re loaded");
if (!IsSupportedGameBuild()) {
re::Log("unsupported sacred.exe build, no hooks installed");
MessageBoxW(nullptr,
L"This sacred.exe is not the supported Sacred Gold 2.28 build.\n"
L"sacred_re will not patch it; the game runs unmodified.",
L"sacred_re", MB_OK | MB_ICONWARNING);
return;
}
if (re::ConfigInt("debug", "WaitForDebugger", 0)) {
re::Log("waiting up to 60 s for a debugger to attach");
for (int i = 0; i < 600 && !IsDebuggerPresent(); ++i) {
Sleep(100);
}
re::Log(IsDebuggerPresent() ? "debugger attached" : "no debugger, continuing");
}
re::InstallHooks();
}
} // namespace
BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID) {
if (DetourIsHelperProcess()) {
return TRUE;
}
if (reason == DLL_PROCESS_ATTACH) {
DisableThreadLibraryCalls(module);
DetourRestoreAfterWith();
Startup(module);
}
return TRUE;
}
+62
View File
@@ -0,0 +1,62 @@
#include "re/hook.h"
#include <vector>
#include <windows.h>
#include <detours.h>
#include "re/config.h"
#include "re/log.h"
namespace re {
namespace {
struct HookDef {
const char* name;
uint32_t address;
void** original;
void* replacement;
};
// Function-local static: registrations run during static initialisation, in
// unspecified order across translation units.
std::vector<HookDef>& Registry() {
static std::vector<HookDef> hooks;
return hooks;
}
} // namespace
HookRegistration::HookRegistration(const char* name, uint32_t address, void** original, void* replacement) {
Registry().push_back({name, address, original, replacement});
}
int InstallHooks() {
int failed = 0;
int installed = 0;
for (const HookDef& hook : Registry()) {
if (!ConfigInt("hooks", hook.name, 1)) {
Log("hook %-24s 0x%08x disabled by config", hook.name, hook.address);
continue;
}
// One transaction per hook, so a bad hook does not take the others down.
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
LONG error = DetourAttach(hook.original, hook.replacement);
if (error == NO_ERROR) {
error = DetourTransactionCommit();
} else {
DetourTransactionAbort();
}
if (error == NO_ERROR) {
++installed;
} else {
++failed;
Log("hook %-24s 0x%08x FAILED (error %ld)", hook.name, hook.address, error);
}
}
Log("%d hook(s) installed, %d failed", installed, failed);
return failed;
}
} // namespace re
+51
View File
@@ -0,0 +1,51 @@
#pragma once
// Hook registry: replaces functions at fixed sacred.exe addresses with our code.
//
// Usage (in a game/*.cpp file):
//
// namespace game {
// void __cdecl Log(const char* fmt, ...); // declare our version first
// RE_HOOK(0x0066f1c0, Log); // defines Log_original
// void __cdecl Log(const char* fmt, ...) { // then implement it
// ... // may call Log_original(...)
// }
// }
//
// The signature, including the calling convention, must match the original
// exactly (`__cdecl`, `__stdcall`, `__thiscall` / `__fastcall` stand-in, ...).
// Hooks can be disabled individually at runtime in sacred_re.ini:
//
// [hooks]
// Log=0
#include <cstdint>
namespace re {
class HookRegistration {
public:
HookRegistration(const char* name, uint32_t address, void** original, void* replacement);
};
// Attaches every registered hook that is enabled in the config. Returns the
// number of hooks that failed to attach.
int InstallHooks();
} // namespace re
// Hooks a Windows API function for the whole process (game and DLLs alike).
// `replacement` must have the API's exact signature; defines
// `replacement##_original`. Disabled in sacred_re.ini under the API's name.
#define RE_HOOK_API(api, replacement) \
static decltype(&replacement) replacement##_original = &api; \
static ::re::HookRegistration replacement##_registration( \
#api, static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&api)), \
reinterpret_cast<void**>(&replacement##_original), \
reinterpret_cast<void*>(&replacement))
#define RE_HOOK(address, fn) \
static decltype(&fn) fn##_original = \
reinterpret_cast<decltype(&fn)>(static_cast<uintptr_t>(address)); \
static ::re::HookRegistration fn##_registration( \
#fn, address, reinterpret_cast<void**>(&fn##_original), \
reinterpret_cast<void*>(&fn))
+45
View File
@@ -0,0 +1,45 @@
#include "re/log.h"
#include <cstdarg>
#include <cstdio>
#include <mutex>
#include <windows.h>
namespace re {
namespace {
std::mutex g_mutex;
FILE* g_file = nullptr;
} // namespace
void OpenLog(const wchar_t* path) {
std::lock_guard lock(g_mutex);
if (!g_file) {
g_file = _wfsopen(path, L"w", _SH_DENYWR);
}
}
void Log(const char* fmt, ...) {
char message[1024];
va_list args;
va_start(args, fmt);
vsnprintf(message, sizeof(message), fmt, args);
va_end(args);
SYSTEMTIME t;
GetLocalTime(&t);
char line[1100];
snprintf(line, sizeof(line), "%02d:%02d:%02d.%03d %s\n", t.wHour, t.wMinute, t.wSecond, t.wMilliseconds,
message);
OutputDebugStringA(line);
std::lock_guard lock(g_mutex);
if (g_file) {
fputs(line, g_file);
fflush(g_file);
}
}
} // namespace re
+9
View File
@@ -0,0 +1,9 @@
#pragma once
// sacred_re's own log: sacred_re.log next to sacred_re.dll, plus OutputDebugString.
namespace re {
void OpenLog(const wchar_t* path);
void Log(const char* fmt, ...);
} // namespace re
+4
View File
@@ -0,0 +1,4 @@
LIBRARY sacred_re
EXPORTS
; Detours requires ordinal 1 on any DLL injected via DetourCreateProcessWithDll.
DetourFinishHelperProcess @1 NONAME