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

+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