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

+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);
}