Widescreen experiment: native render size via dxwrapper, classic menus

- mods/widescreen: redirect the world view's reads of the 1024.0/768.0
  constants and the 3D ortho extents to our variables; render size =
  monitor; relocate the world view's tile draw list (1750 entries, no
  bounds check) into an enlarged object (16384 entries).
- mods/dxwrapper: route DirectDraw through dxwrapper (Dd7to9, D3D9) since
  Windows' D3D7 cannot render to targets larger than 2048; fix the game's
  device info GUID pointer that dxwrapper's enumeration leaves invalid.
  Pinned release fetched by CMake (hash-checked), installed next to the DLL.
- Main menu (UI thread) and loading screen render in a 1024x768 viewport
  and are presented scaled ("classic" layout); in-game uses the full size.
- re/patch: verified code patches (operand redirect, push immediates,
  displacements) and startup hooks; build.ps1/CMake download fixes.

In-game view untested yet; HUD still at the top-left.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-14 00:47:03 +02:00
1 parent c8dc689bc6
commit 795fe8f087
19 files changed
+860 -28

No files matched your search

+1
View File
@@ -5,3 +5,4 @@
*.rea-snapshot *.rea-snapshot
sacred_re.ini sacred_re.ini
/ghidra/ /ghidra/
/screenshots/
+6 -1
View File
@@ -17,6 +17,7 @@ set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
add_compile_definitions(WIN32_LEAN_AND_MEAN NOMINMAX _CRT_SECURE_NO_WARNINGS) add_compile_definitions(WIN32_LEAN_AND_MEAN NOMINMAX _CRT_SECURE_NO_WARNINGS)
include(cmake/Detours.cmake) include(cmake/Detours.cmake)
include(cmake/DxWrapper.cmake)
set(SACRED_RE_CXX_FLAGS /W4 /permissive- /utf-8) set(SACRED_RE_CXX_FLAGS /W4 /permissive- /utf-8)
@@ -26,14 +27,18 @@ add_library(sacred_re SHARED
src/re/hook.cpp src/re/hook.cpp
src/re/log.cpp src/re/log.cpp
src/re/config.cpp src/re/config.cpp
src/re/patch.cpp
src/re/sacred_re.def src/re/sacred_re.def
src/game/engine/log.cpp src/game/engine/log.cpp
src/game/engine/cmdline.cpp src/game/engine/cmdline.cpp
src/mods/borderless.cpp src/mods/borderless.cpp
src/mods/widescreen.cpp
src/mods/dxwrapper.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})
target_link_libraries(sacred_re PRIVATE detours) target_link_libraries(sacred_re PRIVATE detours ddraw)
sacred_re_install_dxwrapper(sacred_re)
# --- sacred_re.exe: launches sacred.exe with sacred_re.dll injected ---------- # --- sacred_re.exe: launches sacred.exe with sacred_re.dll injected ----------
add_executable(sacred_re_launcher WIN32 src/launcher/main.cpp) add_executable(sacred_re_launcher WIN32 src/launcher/main.cpp)
+3 -1
View File
@@ -15,5 +15,7 @@ $env:PATH = "$(Split-Path $vswhere);$env:PATH"
# re-configuring here would clash with VS Code's CMake Tools, which caches # re-configuring here would clash with VS Code's CMake Tools, which caches
# the full path of cl.exe instead of the preset's plain "cl". # the full path of cl.exe instead of the preset's plain "cl".
$configure = if (Test-Path "build\$Preset\CMakeCache.txt") { "" } else { "cmake --preset $Preset && " } $configure = if (Test-Path "build\$Preset\CMakeCache.txt") { "" } else { "cmake --preset $Preset && " }
cmd /c "`"$vcvars`" x86 >nul && $configure cmake --build --preset $Preset" # 2>&1 inside cmd: Windows PowerShell turns any native stderr line (e.g. CMake's
# download progress) into a terminating error under ErrorActionPreference=Stop.
cmd /c "`"$vcvars`" x86 >nul && $configure cmake --build --preset $Preset 2>&1"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+40
View File
@@ -0,0 +1,40 @@
# dxwrapper (zlib; github.com/elishacloud/dxwrapper): translates the game's
# DirectDraw 7 / Direct3D 7 to Direct3D 9, lifting D3D7's 2048x2048 render
# target limit. Pinned release, verified by hash; installed next to
# sacred_re.dll in dxwrapper/ and loaded by src/mods/dxwrapper.cpp, so the
# game folder stays untouched.
include(FetchContent)
# Some CMake builds (e.g. WinLibs/MinGW) ship curl without a CA store, so
# HTTPS downloads fail certificate verification. Fall back to Git for
# Windows' bundle when nothing else is configured.
if(NOT CMAKE_TLS_CAINFO AND NOT DEFINED ENV{SSL_CERT_FILE})
file(TO_CMAKE_PATH "$ENV{ProgramFiles}" _program_files)
foreach(_bundle "${_program_files}/Git/mingw64/etc/ssl/certs/ca-bundle.crt"
"${_program_files}/Git/mingw64/ssl/certs/ca-bundle.crt")
if(EXISTS "${_bundle}")
set(CMAKE_TLS_CAINFO "${_bundle}")
break()
endif()
endforeach()
endif()
FetchContent_Declare(dxwrapper_bin
URL https://github.com/elishacloud/dxwrapper/releases/download/v1.8.8600.25/dxwrapper.zip
URL_HASH SHA256=3894368ebce69f348d08e189c2f0818c49aa3b18ad01e06348743bf6a29af97a
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
FetchContent_MakeAvailable(dxwrapper_bin)
function(sacred_re_install_dxwrapper target)
set(_dest "$<TARGET_FILE_DIR:${target}>/dxwrapper")
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "${_dest}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${dxwrapper_bin_SOURCE_DIR}/Stub/ddraw.dll"
"${dxwrapper_bin_SOURCE_DIR}/dxwrapper.dll"
"${dxwrapper_bin_SOURCE_DIR}/License.txt"
"${CMAKE_SOURCE_DIR}/config/dxwrapper.ini"
"${_dest}"
VERBATIM)
endfunction()
+152
View File
@@ -0,0 +1,152 @@
;; sacred_re: dxwrapper settings (copied next to dxwrapper.dll at build time).
;; Only change from the release defaults: Dd7to9 = 1 (DirectDraw/D3D7 -> D3D9).
;; Config file for DirectX DLL Wrapper
;;
;; For details, see: https://github.com/elishacloud/dxwrapper/wiki/Configuration
;;
[General]
RealDllPath = AUTO
WrapperMode = AUTO
LoadCustomDllPath =
ExcludeProcess =
IncludeProcess =
RunProcess =
WaitForProcess = 0
DisableLogging = 0
[Plugins]
LoadPlugins = 0
LoadFromScriptsOnly = 0
[Compatibility]
Dd7to9 = 1
D3d8to9 = 0
D3d9to9Ex = 0
D3d9on12 = 0
DDrawCompat = 0
Dinputto8 = 0
DisableGameUX = 0
EnableDdrawWrapper = 0
EnableD3d9Wrapper = 0
EnableDinput8Wrapper = 0
EnableDsoundWrapper = 0
DisableGDIGammaRamp = 0
EnableOpenDialogHook = 0
WinVersionLie = off
WinVersionLieSP = 0
ForceKeyboardLayout = 0
FixPerfCounterUptime = 0
HandleExceptions = 0
SingleProcAffinity = 0
ProcAffinityMask = 0
[DDrawCompat]
DDrawCompat20 = 0
DDrawCompat21 = 0
DDrawCompat32 = 0
DDrawCompatDisableGDIHook = 0
DDrawCompatNoProcAffinity = 0
[ddraw]
DdrawOverrideBitMode = 0
DdrawUseDirect3D9Caps = 0
[Dd7to9]
DdrawAutoFrameSkip = 0
DdrawEmulateSurface = 0
DdrawEmulateLock = 0
DdrawUseShadowSurface = 0
DdrawKeepAllInterfaceCache = 0
DdrawAllowMultiSampling = 0
DdrawForceMipMapAutoGen = 0
DdrawClampVertexZDepth = 0
DdrawEnableByteAlignment = 0
DdrawFixByteAlignment = 0
DdrawIntroVideoFix = 0
DdrawFilterActivateApp = 0
DdrawRemoveScanlines = 0
DdrawRemoveInterlacing = 0
DdrawReadFromGDI = 0
DdrawWriteToGDI = 0
DdrawLimitTextureFormats = 0
DdrawLimitDisplayModeCount = 0
DdrawCustomWidth = 0
DdrawCustomHeight = 0
DdrawLinearTextureFilter = 0
DdrawUseNativeResolution = 0
DdrawOverrideWidth = 0
DdrawOverrideHeight = 0
DdrawOverrideStencilFormat = 0
DdrawIntegerScalingClamp = 0
DdrawMaintainAspectRatio = 0
[d3d9]
AnisotropicFiltering = 0
AntiAliasing = 0
EnableMultisamplingATOC = 0
EnvironmentCubeMapFix = 0
ForceMipMapUsage = 0
DepthBiasFactor = 0
DepthBiasDropOffValue = 0
EnableVSync = 0
ForceVsyncMode = 0
ShowFPSCounter = 0
OverrideRefreshRate = 0
LimitRefreshRates = 0
LimitDisplayModeCount = 0
CustomDisplayWidth = 0
CustomDisplayHeight = 0
LimitPerFrameFPS = 0
EnableWindowMode = 0
WindowModeBorder = 0
WindowModeGammaShader = 0
DisplayBrightness = 0
DisplayContrast = 0
SetInitialWindowPosition = 0
InitialWindowPositionLeft = 0
InitialWindowPositionTop = 0
FullscreenWindowMode = 0
HideWindowFocusChanges = 0
EnableCursorClip = 0
ForceExclusiveFullscreen = 0
ForceMixedVertexProcessing = 0
ForceSystemMemVertexCache = 0
UseShadowBackbuffer = 0
OverrideStencilFormat = 0
FlipEx = 0
SetSwapEffectShim = 0
DisableMaxWindowedMode = 0
GraphicsHybridAdapter = 0
[FullScreen]
FullScreen = 0
ForceWindowResize = 0
WaitForWindowChanges = 0
[dinput8]
DeviceLookupCacheTime = 0
FilterNonActiveInput = 0
InvertForceDirection = 0
FixHighFrequencyMouse = 0
MouseMovementFactor = 0
MouseMovementFactorX = 0
MouseMovementFactorY = 0
MouseMovementPadding = 0
MouseMovementPaddingX = 0
MouseMovementPaddingY = 0
[dsound]
Num2DBuffers = 0
Num3DBuffers = 0
ForceCertification = 0
ForceExclusiveMode = 0
ForceSoftwareMixing = 0
ForceHardwareMixing = 0
ForceHQ3DSoftMixing = 0
ForceNonStaticBuffers = 0
ForceVoiceManagement = 0
ForcePrimaryBufferFormat = 0
PrimaryBufferBits = 16
PrimaryBufferSamples = 44100
PrimaryBufferChannels = 2
AudioClipDetection = 0
+21
View File
@@ -59,6 +59,27 @@ Elsewhere (unknown / UI, leave alone for now): `0x40694a`, `0x4ec036`,
`0x60c1f7`, `0x60da54`, `0x625039`, `0x6283b3`, `0x62858d`, `0x628d3b`. `0x60c1f7`, `0x60da54`, `0x625039`, `0x6283b3`, `0x62858d`, `0x628d3b`.
## Other frame producers (laid out for 1024x768: "classic" frames)
| Address | What | Thread |
|---|---|---|
| `0x00756940` | `cUI_Manager` thread loop: main menu (clear, update, BeginScene `0x646f50`, UI render `0x7587b0` incl. the menu's 3D scene, EndScene `0x646fd0`, Flip) | own UI thread |
| `0x006469d0` | loading screen: GDI BitBlt of bitmap + progress bar + text into the back buffer (fixed 1024x768) | render thread |
| `0x006a0c60` | video/intro playback (unexamined) | |
The widescreen mod gives these frames a 1024x768 D3D viewport (set at
`BeginScene` when the scene depth `+0x1d8` is 0) and the borderless mod
presents only that area, scaled to the monitor.
## Direct3D 7 render target limit
Windows' D3D7 runtime refuses `IDirect3D7::CreateDevice` on any render
target wider or taller than 2048 (`DDERR_INVALIDOBJECT`, 0x88760082),
measured with a standalone test: 2048x1440 works, 2049x1440 / 2560x1080 /
3440x1440 fail. Native ultrawide therefore needs dxwrapper (`Dd7to9`,
D3D7 -> D3D9), see `src/mods/dxwrapper.cpp`; without it the render size is
clamped to 2048.
## Renderer ## Renderer
`DxDriver7::SetDisplayMode` (`0x00645390`) creates back buffer, z-buffer and `DxDriver7::SetDisplayMode` (`0x00645390`) creates back buffer, z-buffer and
+38 -2
View File
@@ -14,6 +14,10 @@ constexpr uint32_t GetMouseClientPos = 0x0066e500;
constexpr uint32_t MainWindowProc = 0x008122a0; constexpr uint32_t MainWindowProc = 0x008122a0;
constexpr uint32_t DxDriver7_SetDisplayMode = 0x00645390; constexpr uint32_t DxDriver7_SetDisplayMode = 0x00645390;
constexpr uint32_t DxDriver7_Flip = 0x00645f70; constexpr uint32_t DxDriver7_Flip = 0x00645f70;
constexpr uint32_t DxDriver7_BeginScene = 0x00646f50;
constexpr uint32_t DxDriver7_LoadingScreen = 0x006469d0; // GDI progress screen, 1024x768
constexpr uint32_t UiManager_ThreadLoop = 0x00756940; // main menu render loop (own thread)
constexpr uint32_t Engine_RenderThreadLoop = 0x0060e3f0; // in-game render loop
constexpr uint32_t WorldView0_SetupView = 0x00624ae0; constexpr uint32_t WorldView0_SetupView = 0x00624ae0;
constexpr uint32_t WorldView0_SetZoom = 0x00628210; constexpr uint32_t WorldView0_SetZoom = 0x00628210;
constexpr uint32_t WorldView0_Render = 0x006322b0; constexpr uint32_t WorldView0_Render = 0x006322b0;
@@ -39,8 +43,40 @@ constexpr uint32_t kViewHeightUsers[] = {
0x0062ea5e, 0x0062f345, 0x0063068a, 0x00630f77, 0x0062ea5e, 0x0062f345, 0x0063068a, 0x00630f77,
}; };
// push sequences of the 3D models' ortho extents (-267, 267, -200, 200). // cWorldView0 is allocated with `push 970F4h; call operator new` here.
constexpr uint32_t kOrthoExtentPushes[] = { constexpr uint32_t kWorldViewAllocSizePush = 0x006101aa;
constexpr uint32_t kWorldViewSize = 0x970f4;
// cWorldView0's tile draw list: entries of 0x98 bytes at +0x3ff2c, count at
// +0x80e3c, room for ~1750 entries and no bounds check (enough for 1024x768
// only). Instructions addressing it by displacement (+0x3ff2c + field):
constexpr uint32_t kTileListOffset = 0x3ff2c;
constexpr uint32_t kTileListEntrySize = 0x98;
constexpr uint32_t kTileListUsers[] = {
0x0062b6d9, 0x0062b6ec, 0x0062b709, 0x0062b730, 0x0062b74b, 0x0062b763, // writer, row renderer 0x62b000
0x0062b777, 0x0062b797, 0x0062b7b7, 0x0062b7d7,
0x0062df47, 0x0062e252, // readers (lea base pointer)
};
// Ortho projection of the 3D models: x -267..267, y -200..200 world units
// span the 1024x768 screen (times zoom). Doubles, read with `fmul qword ptr`.
constexpr uint32_t kOrthoLeftD = 0x0088ee40; // double -267.0
constexpr uint32_t kOrthoRightD = 0x0088ee48; // double 267.0
constexpr uint32_t kOrthoBottomD = 0x0088ee50; // double -200.0
constexpr uint32_t kOrthoTopD = 0x0088ee58; // double 200.0
// World view instructions `fmul st, qword ptr [kOrtho*D]` (zoomed projection),
// four per site, in the order top, bottom, right, left.
constexpr uint32_t kOrthoZoomedUsers[][4] = {
{0x00624fdb, 0x00624ff3, 0x00625001, 0x0062500f},
{0x00628355, 0x0062836d, 0x0062837b, 0x00628389},
{0x0062852f, 0x00628547, 0x00628555, 0x00628563},
{0x00628cd7, 0x00628cef, 0x00628d00, 0x00628d11},
};
// Unzoomed projection pushed as immediates: 4 consecutive `push imm32`
// (5 bytes each) starting here, in the order top, bottom, right, left.
constexpr uint32_t kOrthoPushSequences[] = {
0x0060c1f7, 0x0060da54, 0x00625039, 0x006283b3, 0x0062858d, 0x00628d3b, 0x0060c1f7, 0x0060da54, 0x00625039, 0x006283b3, 0x0062858d, 0x00628d3b,
}; };
+14 -1
View File
@@ -34,11 +34,16 @@ struct DxDriver7 {
void* directDraw; // +0xb4 IDirectDraw7* void* directDraw; // +0xb4 IDirectDraw7*
void* primarySurface; // +0xb8 IDirectDrawSurface7* void* primarySurface; // +0xb8 IDirectDrawSurface7*
void* backBuffer; // +0xbc IDirectDrawSurface7* void* backBuffer; // +0xbc IDirectDrawSurface7*
uint8_t unknown0c0[0x11c]; uint8_t unknown0c0[0x0c];
void* device; // +0xcc IDirect3DDevice7*
uint8_t unknown0d0[0x108];
int32_t sceneDepth; // +0x1d8 nesting of BeginScene/EndScene
RECT blitSource; // +0x1dc back-buffer rect copied by Flip (windowed) RECT blitSource; // +0x1dc back-buffer rect copied by Flip (windowed)
RECT blitDest; // +0x1ec screen-space destination of that copy RECT blitDest; // +0x1ec screen-space destination of that copy
}; };
static_assert(offsetof(DxDriver7, window) == 0xb0); static_assert(offsetof(DxDriver7, window) == 0xb0);
static_assert(offsetof(DxDriver7, device) == 0xcc);
static_assert(offsetof(DxDriver7, sceneDepth) == 0x1d8);
static_assert(offsetof(DxDriver7, blitSource) == 0x1dc); static_assert(offsetof(DxDriver7, blitSource) == 0x1dc);
static_assert(offsetof(DxDriver7, blitDest) == 0x1ec); static_assert(offsetof(DxDriver7, blitDest) == 0x1ec);
@@ -55,4 +60,12 @@ int __fastcall DxDriver7_SetDisplayMode(DxDriver7* self, void* edx, void* device
// primary->Blt(blitDest, backBuffer, blitSource) in windowed mode. // primary->Blt(blitDest, backBuffer, blitSource) in windowed mode.
int __fastcall DxDriver7_Flip(DxDriver7* self, void* edx); int __fastcall DxDriver7_Flip(DxDriver7* self, void* edx);
// sacred.exe 0x00646f50. device->BeginScene() unless already inside a scene
// (counted in sceneDepth). Returns 1.
int __fastcall DxDriver7_BeginScene(DxDriver7* self, void* edx);
// sacred.exe 0x006469d0. Draws the loading screen (bitmap + progress bar +
// text) into the back buffer with GDI at fixed 1024x768 coordinates, then Flip.
void __fastcall DxDriver7_LoadingScreen(DxDriver7* self, void* edx, uint32_t progress, const char* text);
} // namespace game } // namespace game
+10
View File
@@ -0,0 +1,10 @@
#pragma once
namespace game {
// sacred.exe 0x00756940. cUI_Manager's thread body: renders the main menu
// (clear, UI update, BeginScene, UI render 0x7587b0, EndScene, Flip) in a loop
// until the manager is stopped. In-game the UI is drawn by the render thread.
void __fastcall UiManager_ThreadLoop(void* self, void* edx);
} // namespace game
+54 -23
View File
@@ -26,12 +26,15 @@
// SetCursorPos by a game-pixel delta, which is not scaled to screen pixels. // SetCursorPos by a game-pixel delta, which is not scaled to screen pixels.
#include <algorithm> #include <algorithm>
#include <atomic>
#include <windows.h> #include <windows.h>
#include <windowsx.h> #include <windowsx.h>
#include "game/engine/input.h" #include "game/engine/input.h"
#include "game/render/dx_driver7.h" #include "game/render/dx_driver7.h"
#include "mods/dxwrapper.h"
#include "mods/widescreen.h"
#include "re/config.h" #include "re/config.h"
#include "game/addresses.h" #include "game/addresses.h"
#include "re/hook.h" #include "re/hook.h"
@@ -39,17 +42,28 @@
namespace { namespace {
struct Viewport { // Where a gameWidth x gameHeight image appears in the window.
bool active = false; struct Layout {
HWND window = nullptr;
bool runInBackground = false;
int gameWidth = 0; int gameWidth = 0;
int gameHeight = 0; int gameHeight = 0;
RECT client = {}; // scaled game image, window client coordinates RECT client = {}; // scaled game image, window client coordinates
RECT screen = {}; // same rectangle, screen coordinates RECT screen = {}; // same rectangle, screen coordinates
}; };
struct Viewport {
bool active = false;
HWND window = nullptr;
bool runInBackground = false;
Layout full; // the whole back buffer
Layout classic; // its top-left 1024x768 (menus, loading screen; widescreen mod)
};
Viewport g_viewport; Viewport g_viewport;
std::atomic<bool> g_classicShown = false; // which layout the last presented frame used
const Layout& CurrentLayout() {
return g_classicShown ? g_viewport.classic : g_viewport.full;
}
bool Enabled() { bool Enabled() {
return re::ConfigInt("borderless", "Enabled", 1) != 0; return re::ConfigInt("borderless", "Enabled", 1) != 0;
@@ -86,35 +100,43 @@ void ApplyBorderless(game::DxDriver7* driver) {
SetWindowLongW(window, GWL_STYLE, WS_POPUP | WS_VISIBLE); SetWindowLongW(window, GWL_STYLE, WS_POPUP | WS_VISIBLE);
SetWindowPos(window, HWND_TOP, area.left, area.top, areaWidth, areaHeight, SWP_FRAMECHANGED | SWP_SHOWWINDOW); SetWindowPos(window, HWND_TOP, area.left, area.top, areaWidth, areaHeight, SWP_FRAMECHANGED | SWP_SHOWWINDOW);
const bool integerScale = re::ConfigInt("borderless", "IntegerScale", 0) != 0;
auto makeLayout = [&](int width, int height) {
Layout layout;
layout.gameWidth = width;
layout.gameHeight = height;
layout.client = FitRect(areaWidth, areaHeight, width, height, integerScale);
layout.screen = layout.client;
OffsetRect(&layout.screen, area.left, area.top);
return layout;
};
Viewport viewport; Viewport viewport;
viewport.window = window; viewport.window = window;
viewport.runInBackground = re::ConfigInt("borderless", "RunInBackground", 1) != 0; viewport.runInBackground = re::ConfigInt("borderless", "RunInBackground", 1) != 0;
viewport.gameWidth = driver->mode.width; viewport.full = makeLayout(driver->mode.width, driver->mode.height);
viewport.gameHeight = driver->mode.height; viewport.classic = makeLayout(std::min<int>(driver->mode.width, 1024), std::min<int>(driver->mode.height, 768));
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; viewport.active = true;
g_viewport = viewport; g_viewport = viewport;
g_classicShown = false;
// Paint the bars: the window class background brush is black. // Paint the bars: the window class background brush is black.
InvalidateRect(window, nullptr, TRUE); InvalidateRect(window, nullptr, TRUE);
re::Log("borderless: %dx%d game -> %ldx%ld monitor, image at (%ld,%ld) %ldx%ld", viewport.gameWidth, const RECT& image = viewport.full.client;
viewport.gameHeight, static_cast<long>(areaWidth), static_cast<long>(areaHeight), viewport.client.left, re::Log("borderless: %dx%d game -> %ldx%ld monitor, image at (%ld,%ld) %ldx%ld", viewport.full.gameWidth,
viewport.client.top, viewport.client.right - viewport.client.left, viewport.full.gameHeight, static_cast<long>(areaWidth), static_cast<long>(areaHeight), image.left,
viewport.client.bottom - viewport.client.top); image.top, image.right - image.left, image.bottom - image.top);
} }
// Window client coordinates -> game pixels. // Window client coordinates -> game pixels.
POINT ToGame(POINT p) { POINT ToGame(POINT p) {
const Viewport& v = g_viewport; const Layout& layout = CurrentLayout();
const int width = v.client.right - v.client.left; const int width = layout.client.right - layout.client.left;
const int height = v.client.bottom - v.client.top; const int height = layout.client.bottom - layout.client.top;
POINT result; POINT result;
result.x = std::clamp(MulDiv(p.x - v.client.left, v.gameWidth, width), 0, v.gameWidth - 1); result.x = std::clamp(MulDiv(p.x - layout.client.left, layout.gameWidth, width), 0, layout.gameWidth - 1);
result.y = std::clamp(MulDiv(p.y - v.client.top, v.gameHeight, height), 0, v.gameHeight - 1); result.y = std::clamp(MulDiv(p.y - layout.client.top, layout.gameHeight, height), 0, layout.gameHeight - 1);
return result; return result;
} }
@@ -122,9 +144,9 @@ POINT ToGame(POINT p) {
void PaintBars(HDC dc) { void PaintBars(HDC dc) {
RECT window; RECT window;
GetClientRect(g_viewport.window, &window); GetClientRect(g_viewport.window, &window);
const RECT& image = CurrentLayout().client;
const int saved = SaveDC(dc); const int saved = SaveDC(dc);
ExcludeClipRect(dc, g_viewport.client.left, g_viewport.client.top, g_viewport.client.right, ExcludeClipRect(dc, image.left, image.top, image.right, image.bottom);
g_viewport.client.bottom);
FillRect(dc, &window, static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH))); FillRect(dc, &window, static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH)));
RestoreDC(dc, saved); RestoreDC(dc, saved);
} }
@@ -146,11 +168,13 @@ RE_HOOK(addr::MainWindowProc, MainWindowProc);
int __fastcall DxDriver7_SetDisplayMode(DxDriver7* self, void* edx, void* deviceCaps, const DxDisplayMode* mode, int __fastcall DxDriver7_SetDisplayMode(DxDriver7* self, void* edx, void* deviceCaps, const DxDisplayMode* mode,
uint32_t unknown) { uint32_t unknown) {
mods::dxwrapper::FixDeviceInfo(deviceCaps);
if (!Enabled()) { if (!Enabled()) {
return DxDriver7_SetDisplayMode_original(self, edx, deviceCaps, mode, unknown); return DxDriver7_SetDisplayMode_original(self, edx, deviceCaps, mode, unknown);
} }
DxDisplayMode windowed = *mode; DxDisplayMode windowed = *mode;
windowed.fullscreen = 0; windowed.fullscreen = 0;
mods::widescreen::ApplyRenderSize(windowed, self->window);
g_viewport.active = false; g_viewport.active = false;
const int result = DxDriver7_SetDisplayMode_original(self, edx, deviceCaps, &windowed, unknown); const int result = DxDriver7_SetDisplayMode_original(self, edx, deviceCaps, &windowed, unknown);
if (result) { if (result) {
@@ -163,8 +187,15 @@ int __fastcall DxDriver7_Flip(DxDriver7* self, void* edx) {
// The original computes the blit rectangles once, for a centred window of // The original computes the blit rectangles once, for a centred window of
// exactly the game's size; override them on every present. // exactly the game's size; override them on every present.
if (g_viewport.active && self->window == g_viewport.window) { if (g_viewport.active && self->window == g_viewport.window) {
self->blitSource = {0, 0, g_viewport.gameWidth, g_viewport.gameHeight}; // Menus and the loading screen only fill the top-left 1024x768 of a
self->blitDest = g_viewport.screen; // widened back buffer; show just that part, scaled like before.
const bool classic = mods::widescreen::IsClassicFrame();
if (g_classicShown.exchange(classic) != classic) {
InvalidateRect(self->window, nullptr, TRUE); // repaint the bars for the new layout
}
const Layout& layout = CurrentLayout();
self->blitSource = {0, 0, layout.gameWidth, layout.gameHeight};
self->blitDest = layout.screen;
} }
return DxDriver7_Flip_original(self, edx); return DxDriver7_Flip_original(self, edx);
} }
+116
View File
@@ -0,0 +1,116 @@
// Route the game's DirectDraw 7 / Direct3D 7 through dxwrapper (Dd7to9:
// translation to Direct3D 9). Windows' own D3D7 runtime cannot render to
// surfaces larger than 2048x2048, which the widescreen mod needs.
//
// The build puts dxwrapper's ddraw.dll stub, dxwrapper.dll and our
// config/dxwrapper.ini into dxwrapper/ next to sacred_re.dll; the game folder
// is not touched. sacred.exe imports three functions from DDRAW.dll; we hook
// them and forward to the wrapper, which is loaded on the first call (never
// from DllMain). Calls made while the wrapper is running (it may use the real
// DirectDraw itself) go to the original functions.
//
// sacred_re.ini:
// [renderer]
// DxWrapper=1 ; 0 = Windows' DirectDraw (render size limited to 2048)
#include "mods/dxwrapper.h"
#include <cstdint>
#include <cstring>
#include <string>
#include <windows.h>
#include <ddraw.h>
#include "re/config.h"
#include "re/hook.h"
#include "re/log.h"
namespace mods::dxwrapper {
namespace {
HMODULE g_wrapper = nullptr;
bool g_loadAttempted = false;
thread_local int g_insideWrapper = 0;
std::wstring WrapperPath() {
return re::ModuleDirectory() + L"dxwrapper\\ddraw.dll";
}
bool Enabled() {
return re::ConfigInt("renderer", "DxWrapper", 1) != 0;
}
FARPROC WrapperFunction(const char* name) {
if (!g_loadAttempted) {
g_loadAttempted = true;
const std::wstring path = WrapperPath();
g_wrapper = LoadLibraryW(path.c_str());
if (g_wrapper) {
re::Log("dxwrapper: loaded %ls", path.c_str());
} else {
re::Log("dxwrapper: could not load %ls (error %lu), using Windows' DirectDraw", path.c_str(),
GetLastError());
}
}
return g_wrapper ? GetProcAddress(g_wrapper, name) : nullptr;
}
// Calls `function` from the wrapper if possible, else `original`.
template <typename Function, typename... Args>
HRESULT Forward(const char* name, Function original, Args... args) {
if (g_insideWrapper == 0 && Enabled()) {
if (auto wrapped = reinterpret_cast<Function>(WrapperFunction(name))) {
++g_insideWrapper;
const HRESULT result = wrapped(args...);
--g_insideWrapper;
return result;
}
}
return original(args...);
}
} // namespace
bool IsActive() {
if (!Enabled()) {
return false;
}
if (g_loadAttempted) {
return g_wrapper != nullptr;
}
return GetFileAttributesW(WrapperPath().c_str()) != INVALID_FILE_ATTRIBUTES;
}
void FixDeviceInfo(void* deviceInfo) {
auto* bytes = static_cast<uint8_t*>(deviceInfo);
const uint32_t guidAddress = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(bytes + 0x3f4));
uint32_t guidPointer;
memcpy(&guidPointer, bytes + 4, 4);
if (guidPointer != guidAddress) {
re::Log("dxwrapper: device info GUID pointer was 0x%08x, set to 0x%08x", guidPointer, guidAddress);
memcpy(bytes + 4, &guidAddress, 4);
}
}
HRESULT WINAPI DirectDrawCreateExViaWrapper(GUID* guid, LPVOID* directDraw, REFIID iid, IUnknown* outer);
HRESULT WINAPI DirectDrawCreateViaWrapper(GUID* guid, LPDIRECTDRAW* directDraw, IUnknown* outer);
HRESULT WINAPI DirectDrawEnumerateExAViaWrapper(LPDDENUMCALLBACKEXA callback, LPVOID context, DWORD flags);
RE_HOOK_API(DirectDrawCreateEx, DirectDrawCreateExViaWrapper);
RE_HOOK_API(DirectDrawCreate, DirectDrawCreateViaWrapper);
RE_HOOK_API(DirectDrawEnumerateExA, DirectDrawEnumerateExAViaWrapper);
HRESULT WINAPI DirectDrawCreateExViaWrapper(GUID* guid, LPVOID* directDraw, REFIID iid, IUnknown* outer) {
return Forward("DirectDrawCreateEx", DirectDrawCreateExViaWrapper_original, guid, directDraw, iid, outer);
}
HRESULT WINAPI DirectDrawCreateViaWrapper(GUID* guid, LPDIRECTDRAW* directDraw, IUnknown* outer) {
return Forward("DirectDrawCreate", DirectDrawCreateViaWrapper_original, guid, directDraw, outer);
}
HRESULT WINAPI DirectDrawEnumerateExAViaWrapper(LPDDENUMCALLBACKEXA callback, LPVOID context, DWORD flags) {
return Forward("DirectDrawEnumerateExA", DirectDrawEnumerateExAViaWrapper_original, callback, context, flags);
}
} // namespace mods::dxwrapper
+16
View File
@@ -0,0 +1,16 @@
#pragma once
namespace mods::dxwrapper {
// True when the game's DirectDraw calls go to dxwrapper (Direct3D 9), which
// has no 2048x2048 render target limit.
bool IsActive();
// The device info the game passes to DxDriver7::SetDisplayMode keeps a pointer
// to its own device GUID at +4 (the GUID itself is at +0x3f4); the game uses
// it for EnumZBufferFormats. Under dxwrapper the game's device enumeration
// leaves garbage there (0x3c0) and dxwrapper crashes dereferencing it.
// Restores the pointer Windows' DirectDraw would have produced.
void FixDeviceInfo(void* deviceInfo);
} // namespace mods::dxwrapper
+249
View File
@@ -0,0 +1,249 @@
// Widescreen: render the world 1:1 at the monitor's resolution, showing more
// of it, instead of scaling a 1024x768 image.
//
// The world view sizes itself from zoom * 1024.0 / 768.0 (float constants that
// the UI also reads) and projects 3D models with an ortho camera of
// +-267 x +-200 world units (times zoom). We redirect only the world view's
// reads of those constants to our own variables, so the UI keeps its
// 1024x768 layout, and set the variables from the render size.
// See docs/widescreen.md.
//
// sacred_re.ini:
// [widescreen]
// Enabled=1
// Width=0 ; 0 = monitor width
// Height=0 ; 0 = monitor height
//
// Windows' Direct3D 7 limits the render size to 2048x2048 unless the dxwrapper
// mod (Direct3D 9) is active; the borderless mod centres a smaller image 1:1.
//
// Experimental: the UI is still drawn at the top-left 1024x768, and creatures
// are only active within the original distance.
#include "mods/widescreen.h"
#include <algorithm>
#include <atomic>
#include <bit>
#include <iterator>
#include <cstdint>
#include <cstring>
#include "game/addresses.h"
#include "game/ui/ui_manager.h"
#include "mods/dxwrapper.h"
#include "re/config.h"
#include "re/hook.h"
#include "re/log.h"
#include "re/patch.h"
namespace mods::widescreen {
namespace {
constexpr float kOriginalWidth = 1024.0f;
constexpr float kOriginalHeight = 768.0f;
constexpr double kOriginalOrthoX = 267.0;
constexpr double kOriginalOrthoY = 200.0;
// Read by the patched world view instructions; the initial values reproduce
// the original behaviour until a render size is applied.
float g_viewWidth = kOriginalWidth;
float g_viewHeight = kOriginalHeight;
double g_orthoLeft = -kOriginalOrthoX;
double g_orthoRight = kOriginalOrthoX;
double g_orthoBottom = -kOriginalOrthoY;
double g_orthoTop = kOriginalOrthoY;
bool g_pushesPatched = false;
// Render size once applied (0 = original 1024x768 behaviour).
int g_renderWidth = 0;
int g_renderHeight = 0;
// Frames from the main menu thread and the loading screen are laid out for
// 1024x768 ("classic"); everything else is the widened in-game view.
std::atomic<DWORD> g_menuThread = 0;
thread_local int g_loadingScreenDepth = 0;
// D3DVIEWPORT7 (d3d.h is not in current Windows SDKs).
struct Viewport7 {
DWORD x, y, width, height;
float minZ, maxZ;
};
bool Enabled() {
return re::ConfigInt("widescreen", "Enabled", 1) != 0;
}
// The world view's tile draw list only holds ~1750 entries (enough for
// 1024x768, unchecked). Allocate the view larger and move the list to the new
// space at its end, by rewriting the field displacements of every instruction
// that addresses it.
constexpr uint32_t kTileListCapacity = 16384;
void RelocateTileList() {
using namespace game::addr;
const uint32_t newOffset = kWorldViewSize;
const uint32_t newSize = kWorldViewSize + kTileListCapacity * kTileListEntrySize;
if (!re::PatchPushImm32(kWorldViewAllocSizePush, kWorldViewSize, newSize)) {
re::Log("widescreen: world view allocation not patched, tile list stays at 1750 entries");
return;
}
int patched = 0;
for (uint32_t instruction : kTileListUsers) {
// The displacement is kTileListOffset + field; find it in the instruction.
const auto* code = reinterpret_cast<const uint8_t*>(static_cast<uintptr_t>(instruction));
for (int offset = 1; offset <= 7; ++offset) {
uint32_t value;
memcpy(&value, code + offset, 4);
if (value >= kTileListOffset && value < kTileListOffset + kTileListEntrySize) {
patched += re::PatchInstructionDword(instruction, value, newOffset + (value - kTileListOffset));
break;
}
}
}
re::Log("widescreen: tile draw list moved to +0x%x (%u entries), %d/%d instructions patched", newOffset,
kTileListCapacity, patched, static_cast<int>(std::size(kTileListUsers)));
}
void RedirectWorldView() {
if (!Enabled()) {
return;
}
int patched = 0;
int total = 0;
for (uint32_t instruction : game::addr::kViewWidthUsers) {
patched += re::RedirectOperand(instruction, game::addr::kScreenWidthF, &g_viewWidth);
++total;
}
for (uint32_t instruction : game::addr::kViewHeightUsers) {
patched += re::RedirectOperand(instruction, game::addr::kScreenHeightF, &g_viewHeight);
++total;
}
for (const auto& site : game::addr::kOrthoZoomedUsers) {
patched += re::RedirectOperand(site[0], game::addr::kOrthoTopD, &g_orthoTop);
patched += re::RedirectOperand(site[1], game::addr::kOrthoBottomD, &g_orthoBottom);
patched += re::RedirectOperand(site[2], game::addr::kOrthoRightD, &g_orthoRight);
patched += re::RedirectOperand(site[3], game::addr::kOrthoLeftD, &g_orthoLeft);
total += 4;
}
re::Log("widescreen: %d/%d world view operands redirected", patched, total);
RelocateTileList();
}
RE_STARTUP(WidescreenRedirect, RedirectWorldView);
uint32_t FloatBits(float value) {
return std::bit_cast<uint32_t>(value);
}
// The unzoomed projection is pushed as immediates; rewrite them once.
void PatchOrthoPushes(float right, float top) {
if (g_pushesPatched) {
return;
}
int patched = 0;
for (uint32_t site : game::addr::kOrthoPushSequences) {
patched += re::PatchPushImm32(site + 0, FloatBits(200.0f), FloatBits(top));
patched += re::PatchPushImm32(site + 5, FloatBits(-200.0f), FloatBits(-top));
patched += re::PatchPushImm32(site + 10, FloatBits(267.0f), FloatBits(right));
patched += re::PatchPushImm32(site + 15, FloatBits(-267.0f), FloatBits(-right));
}
g_pushesPatched = true;
re::Log("widescreen: %d/%d projection immediates patched", patched,
static_cast<int>(std::size(game::addr::kOrthoPushSequences)) * 4);
}
} // namespace
void ApplyRenderSize(game::DxDisplayMode& mode, HWND window) {
if (!Enabled()) {
return;
}
MONITORINFO monitor = {sizeof(monitor)};
GetMonitorInfoW(MonitorFromWindow(window, MONITOR_DEFAULTTOPRIMARY), &monitor);
int width = re::ConfigInt("widescreen", "Width", 0);
int height = re::ConfigInt("widescreen", "Height", 0);
if (width <= 0) {
width = monitor.rcMonitor.right - monitor.rcMonitor.left;
}
if (height <= 0) {
height = monitor.rcMonitor.bottom - monitor.rcMonitor.top;
}
// Windows' Direct3D 7 runtime cannot create a device for a render target
// wider or taller than 2048 pixels (CreateDevice fails with
// DDERR_INVALIDOBJECT); anything larger needs a different renderer.
constexpr int kMaxD3D7Surface = 2048;
if (!mods::dxwrapper::IsActive() && (width > kMaxD3D7Surface || height > kMaxD3D7Surface)) {
re::Log("widescreen: %dx%d exceeds the Direct3D 7 limit, clamping to %d", width, height, kMaxD3D7Surface);
width = std::min(width, kMaxD3D7Surface);
height = std::min(height, kMaxD3D7Surface);
}
// The game's own layout needs at least its original size.
if (width < kOriginalWidth || height < kOriginalHeight) {
re::Log("widescreen: %dx%d is smaller than 1024x768, keeping the original size", width, height);
return;
}
mode.width = static_cast<uint16_t>(width);
mode.height = static_cast<uint16_t>(height);
g_viewWidth = static_cast<float>(width);
g_viewHeight = static_cast<float>(height);
const double scaleX = width / kOriginalWidth;
const double scaleY = height / kOriginalHeight;
g_orthoRight = kOriginalOrthoX * scaleX;
g_orthoLeft = -g_orthoRight;
g_orthoTop = kOriginalOrthoY * scaleY;
g_orthoBottom = -g_orthoTop;
PatchOrthoPushes(static_cast<float>(g_orthoRight), static_cast<float>(g_orthoTop));
g_renderWidth = width;
g_renderHeight = height;
re::Log("widescreen: render size %dx%d, ortho x +-%.1f y +-%.1f", width, height, g_orthoRight, g_orthoTop);
}
bool IsClassicFrame() {
if (g_renderWidth == 0) {
return false;
}
return g_loadingScreenDepth > 0 || GetCurrentThreadId() == g_menuThread.load();
}
} // namespace mods::widescreen
namespace game {
using namespace mods::widescreen;
RE_HOOK(addr::UiManager_ThreadLoop, UiManager_ThreadLoop);
RE_HOOK(addr::DxDriver7_LoadingScreen, DxDriver7_LoadingScreen);
RE_HOOK(addr::DxDriver7_BeginScene, DxDriver7_BeginScene);
void __fastcall UiManager_ThreadLoop(void* self, void* edx) {
g_menuThread = GetCurrentThreadId();
UiManager_ThreadLoop_original(self, edx);
g_menuThread = 0;
}
void __fastcall DxDriver7_LoadingScreen(DxDriver7* self, void* edx, uint32_t progress, const char* text) {
++g_loadingScreenDepth;
DxDriver7_LoadingScreen_original(self, edx, progress, text);
--g_loadingScreenDepth;
}
// Menus draw their 3D parts for a 1024x768 screen: give them a 1024x768
// viewport (the borderless mod then presents just that area, scaled), and the
// world the full render size.
int __fastcall DxDriver7_BeginScene(DxDriver7* self, void* edx) {
if (g_renderWidth != 0 && self->sceneDepth == 0 && self->device) {
const bool classic = IsClassicFrame();
Viewport7 viewport = {0, 0, classic ? 1024u : static_cast<DWORD>(g_renderWidth),
classic ? 768u : static_cast<DWORD>(g_renderHeight), 0.0f, 1.0f};
using SetViewportFn = HRESULT(__stdcall*)(void* device, Viewport7* viewport);
auto setViewport = reinterpret_cast<SetViewportFn>((*static_cast<void***>(self->device))[13]);
setViewport(self->device, &viewport);
}
return DxDriver7_BeginScene_original(self, edx);
}
} // namespace game
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <windows.h>
#include "game/render/dx_driver7.h"
namespace mods::widescreen {
// Called by the borderless mod before the renderer creates its surfaces:
// sets the render size to the monitor (or the configured size) and resizes
// the world view to match. No-op when disabled.
void ApplyRenderSize(game::DxDisplayMode& mode, HWND window);
// True while the current thread renders a frame laid out for 1024x768 (main
// menu, loading screen); the borderless mod then presents only that area.
bool IsClassicFrame();
} // namespace mods::widescreen
+4
View File
@@ -23,6 +23,10 @@ const std::wstring& ConfigPath() {
return g_path; return g_path;
} }
std::wstring ModuleDirectory() {
return g_path.substr(0, g_path.find_last_of(L"\\/") + 1);
}
int ConfigInt(const char* section, const char* key, int fallback) { int ConfigInt(const char* section, const char* key, int fallback) {
if (g_path.empty()) { if (g_path.empty()) {
return fallback; return fallback;
+2
View File
@@ -7,6 +7,8 @@ namespace re {
void SetConfigPath(const std::wstring& path); void SetConfigPath(const std::wstring& path);
const std::wstring& ConfigPath(); const std::wstring& ConfigPath();
// Folder of sacred_re.dll (with trailing separator), derived from the config path.
std::wstring ModuleDirectory();
int ConfigInt(const char* section, const char* key, int fallback); int ConfigInt(const char* section, const char* key, int fallback);
} // namespace re } // namespace re
+2
View File
@@ -10,6 +10,7 @@
#include "re/config.h" #include "re/config.h"
#include "re/hook.h" #include "re/hook.h"
#include "re/log.h" #include "re/log.h"
#include "re/patch.h"
namespace { namespace {
@@ -58,6 +59,7 @@ void Startup(HMODULE module) {
re::Log(IsDebuggerPresent() ? "debugger attached" : "no debugger, continuing"); re::Log(IsDebuggerPresent() ? "debugger attached" : "no debugger, continuing");
} }
re::InstallHooks(); re::InstallHooks();
re::RunStartupFunctions();
} }
} // namespace } // namespace
+81
View File
@@ -0,0 +1,81 @@
#include "re/patch.h"
#include <cstring>
#include <vector>
#include <windows.h>
#include "re/log.h"
namespace re {
namespace {
struct Startup {
const char* name;
void (*function)();
};
std::vector<Startup>& Startups() {
static std::vector<Startup> startups;
return startups;
}
// Longest x86 instruction is 15 bytes; an absolute operand starts within it.
constexpr size_t kOperandSearch = 11;
} // namespace
bool WriteCode(uint32_t address, const void* bytes, size_t size) {
void* target = reinterpret_cast<void*>(static_cast<uintptr_t>(address));
DWORD oldProtect;
if (!VirtualProtect(target, size, PAGE_EXECUTE_READWRITE, &oldProtect)) {
Log("patch 0x%08x: VirtualProtect failed (%lu)", address, GetLastError());
return false;
}
std::memcpy(target, bytes, size);
VirtualProtect(target, size, oldProtect, &oldProtect);
FlushInstructionCache(GetCurrentProcess(), target, size);
return true;
}
bool PatchInstructionDword(uint32_t instruction, uint32_t from, uint32_t to) {
const auto* code = reinterpret_cast<const uint8_t*>(static_cast<uintptr_t>(instruction));
for (size_t offset = 1; offset + 4 <= kOperandSearch; ++offset) {
uint32_t operand;
std::memcpy(&operand, code + offset, 4);
if (operand == from) {
return WriteCode(instruction + static_cast<uint32_t>(offset), &to, 4);
}
}
Log("patch 0x%08x: operand 0x%08x not found, skipped", instruction, from);
return false;
}
bool RedirectOperand(uint32_t instruction, uint32_t from, const void* to) {
return PatchInstructionDword(instruction, from, static_cast<uint32_t>(reinterpret_cast<uintptr_t>(to)));
}
bool PatchPushImm32(uint32_t instruction, uint32_t expected, uint32_t value) {
const auto* code = reinterpret_cast<const uint8_t*>(static_cast<uintptr_t>(instruction));
uint32_t current;
std::memcpy(&current, code + 1, 4);
if (code[0] != 0x68 || current != expected) {
Log("patch 0x%08x: expected push 0x%08x, found %02x %08x, skipped", instruction, expected, code[0],
current);
return false;
}
return WriteCode(instruction + 1, &value, 4);
}
StartupRegistration::StartupRegistration(const char* name, void (*function)()) {
Startups().push_back({name, function});
}
void RunStartupFunctions() {
for (const Startup& startup : Startups()) {
Log("startup: %s", startup.name);
startup.function();
}
}
} // namespace re
+33
View File
@@ -0,0 +1,33 @@
#pragma once
// Verified in-place patches of sacred.exe code. Every function checks that the
// bytes it expects are present before writing, logs and refuses otherwise.
#include <cstddef>
#include <cstdint>
namespace re {
bool WriteCode(uint32_t address, const void* bytes, size_t size);
// The instruction at `instruction` has the absolute memory operand `from`
// (e.g. `fmul st, dword ptr [from]`); make it read `to` instead.
bool RedirectOperand(uint32_t instruction, uint32_t from, const void* to);
// Replace a 32-bit displacement/immediate `from` inside the instruction at
// `instruction` by `to` (e.g. a struct field offset in `[ebp+ecx*8+3FF2Ch]`).
bool PatchInstructionDword(uint32_t instruction, uint32_t from, uint32_t to);
// `push imm32` (68 xx xx xx xx) at `instruction`: replace `expected` by `value`.
bool PatchPushImm32(uint32_t instruction, uint32_t expected, uint32_t value);
// Code that must run once at startup, after the hooks are installed.
class StartupRegistration {
public:
StartupRegistration(const char* name, void (*function)());
};
void RunStartupFunctions();
} // namespace re
#define RE_STARTUP(name, function) \
static ::re::StartupRegistration name##_startup(#name, &function)