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:
commit
d9f5c85430
31 files changed
+1184
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
LIBRARY sacred_re
|
||||
EXPORTS
|
||||
; Detours requires ordinal 1 on any DLL injected via DetourCreateProcessWithDll.
|
||||
DetourFinishHelperProcess @1 NONAME
|
||||
Reference in new issue
Block a user