-
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathfs_override.cpp
More file actions
202 lines (178 loc) · 8.65 KB
/
Copy pathfs_override.cpp
File metadata and controls
202 lines (178 loc) · 8.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
// SacredSDK — filesystem override layer.
//
// Hooks the Sacred.exe IAT slot for `CreateFileA` (and `_lopen` / `OpenFile`
// if they're imported) so we can transparently redirect ANY file open to a
// shadow copy under `<game_dir>\custom\`.
//
// Design goals
// ------------
// - **Zero changes to the Steam install**. Modders drop files in
// `<game_dir>\custom\<sub>\<file>` mirroring the original tree.
// Steam's "Verify integrity" never sees them, never overwrites them.
// - **Two trees, the player's first**. A read is served from
// `<game_dir>\custom\<rel>` if it exists, otherwise from
// `<game_dir>\sdk\custom\<rel>` -- the framework shipped with the SDK.
// So a player can override anything the SDK ships without touching it.
// - **Read-only redirection**. We only swap the path for READ opens.
// Sacred's write opens (logs, save games) go through untouched.
// - **No content-type assumptions**. The same hook handles
// `scripts/us/global.res`, `bin/TYPE_NPC_X/FunkCode.bin`,
// `bin/Balance.bin`, `pak/textures.pak`, etc.
// - **Cheap on the hot path**. Per-open cost is a single
// `GetFileAttributesA` of the candidate `custom\` path; if it doesn't
// exist, we fall back to the original path immediately.
//
// What we deliberately DON'T hook
// -------------------------------
// - `Sacred.exe` itself — that's our entry point, never goes through us.
// - DLLs (`granny.dll`, `ijl15.dll`, etc.) — loaded by Windows loader,
// not via CreateFileA.
// - The override directory itself (no recursive lookups).
// - Anything outside the game install dir.
// - Write opens (GENERIC_WRITE | OPEN_ALWAYS | CREATE_*).
#include "sdk.h"
#include "iat_hook.h"
#include <cstdio>
#include <cstring>
namespace sdk { namespace fs_override {
volatile long g_total_opens = 0;
volatile long g_redirected = 0;
volatile long g_verbose = 0; // when set, log every CreateFileA call
static char g_last_redirect[MAX_PATH * 2] = "";
const char* last_redirect() { return g_last_redirect; }
static char g_game_dir[MAX_PATH] = {0}; // e.g. E:\SteamLibrary\...\Sacred Gold
static size_t g_game_dir_len = 0;
static char g_custom_dir[MAX_PATH] = {0}; // <game_dir>\custom (the player's)
static char g_sdk_custom_dir[MAX_PATH] = {0}; // <game_dir>\sdk\custom (the SDK's)
static bool path_starts_with_i(const char* p, const char* prefix, size_t prefix_len) {
return _strnicmp(p, prefix, (int)prefix_len) == 0;
}
// Normalise a path to <game_dir>-relative form if possible.
// Returns nullptr if the path is outside the game install or is one of the
// blocklisted top-level files we never override.
//
// Accepts:
// "scripts\us\global.res" (already relative)
// ".\scripts\us\global.res" (dot-relative)
// "E:\...\Sacred Gold\scripts\us\..." (absolute inside game)
// "scripts/us/global.res" (forward slashes — normalised)
static bool to_relative(const char* in, char out_rel[MAX_PATH]) {
if (!in || !*in) return false;
char buf[MAX_PATH];
size_t n = strlen(in);
if (n >= MAX_PATH) return false;
memcpy(buf, in, n + 1);
// Normalise slashes
for (size_t i = 0; i < n; i++)
if (buf[i] == '/') buf[i] = '\\';
const char* rel = buf;
if (n >= 2 && buf[0] == '.' && buf[1] == '\\') rel = buf + 2;
else if (path_starts_with_i(buf, g_game_dir, g_game_dir_len)
&& buf[g_game_dir_len] == '\\') {
rel = buf + g_game_dir_len + 1;
}
// Reject anything still containing a drive letter (X:\)
if (rel[0] && rel[1] == ':') return false;
// Reject root-level / blocklisted names
if (!strchr(rel, '\\')) {
// top-level file — these are EXE, DLLs, configs etc. never override.
return false;
}
// Reject anything that's already inside either override tree.
if (_strnicmp(rel, "custom\\", 7) == 0) return false;
if (_strnicmp(rel, "sdk\\custom\\", 11) == 0) return false;
// Reject parent-dir escapes for safety.
if (strstr(rel, "..\\")) return false;
strncpy_s(out_rel, MAX_PATH, rel, _TRUNCATE);
return true;
}
// Build <root>\<rel>
static bool build_override_path(const char* root, const char* rel, char out[MAX_PATH]) {
if (!root[0]) return false;
int r = _snprintf_s(out, MAX_PATH, _TRUNCATE, "%s\\%s", root, rel);
return r > 0;
}
static bool custom_exists(const char* custom_path) {
DWORD attrs = GetFileAttributesA(custom_path);
if (attrs == INVALID_FILE_ATTRIBUTES) return false;
if (attrs & FILE_ATTRIBUTE_DIRECTORY) return false;
return true;
}
// ----- hook -----------------------------------------------------------
using CreateFileA_t = HANDLE (WINAPI*)(LPCSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES,
DWORD, DWORD, HANDLE);
static CreateFileA_t orig_CreateFileA = nullptr;
static HANDLE WINAPI hook_CreateFileA(LPCSTR lpFileName, DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecAttrs,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile)
{
InterlockedIncrement(&g_total_opens);
if (g_verbose && lpFileName) {
bool wants_write = (dwDesiredAccess & GENERIC_WRITE) != 0;
sdk_log("[fs] %s disp=%lu acc=%#lx '%s'",
wants_write ? "WRITE" : "read ",
dwCreationDisposition, dwDesiredAccess, lpFileName);
}
if (!lpFileName) {
return orig_CreateFileA(lpFileName, dwDesiredAccess, dwShareMode,
lpSecAttrs, dwCreationDisposition,
dwFlagsAndAttributes, hTemplateFile);
}
// Only redirect pure-read opens. If the caller is creating or writing,
// pass through — that's a save game / log file, not a content read.
bool is_read_only =
(dwDesiredAccess & GENERIC_WRITE) == 0 &&
(dwCreationDisposition == OPEN_EXISTING ||
dwCreationDisposition == OPEN_ALWAYS);
if (is_read_only) {
char rel[MAX_PATH];
if (to_relative(lpFileName, rel)) {
// The player's tree first, then the SDK's.
const char* roots[2] = { g_custom_dir, g_sdk_custom_dir };
const char* labels[2] = { "custom", "sdk\\custom" };
for (int i = 0; i < 2; i++) {
char path[MAX_PATH];
if (build_override_path(roots[i], rel, path) && custom_exists(path)) {
_snprintf_s(g_last_redirect, _TRUNCATE,
"%s -> %s\\%s", lpFileName, labels[i], rel);
InterlockedIncrement(&g_redirected);
sdk_log("[fs_override] %s -> %s", lpFileName, path);
return orig_CreateFileA(path, dwDesiredAccess, dwShareMode,
lpSecAttrs, dwCreationDisposition,
dwFlagsAndAttributes, hTemplateFile);
}
}
}
}
return orig_CreateFileA(lpFileName, dwDesiredAccess, dwShareMode,
lpSecAttrs, dwCreationDisposition,
dwFlagsAndAttributes, hTemplateFile);
}
// ----- install --------------------------------------------------------
void install() {
// Resolve game dir from g_attach.exe_path (set at DllMain).
if (g_attach.exe_path[0]) {
strncpy_s(g_game_dir, _TRUNCATE, g_attach.exe_path, _TRUNCATE);
char* slash = strrchr(g_game_dir, '\\');
if (slash) *slash = 0;
g_game_dir_len = strlen(g_game_dir);
}
_snprintf_s(g_custom_dir, _TRUNCATE, "%s\\custom", g_game_dir);
_snprintf_s(g_sdk_custom_dir, _TRUNCATE, "%s\\sdk\\custom", g_game_dir);
// NB: do NOT call CreateDirectoryA / any filesystem API from DllMain —
// the Windows loader holds its lock during DLL init and a filesystem
// call can recurse into LoadLibrary (e.g. for shell-extension DLLs) and
// deadlock or crash. The user / setup creates `custom/` manually; if it
// doesn't exist, the redirect simply doesn't match anything and Sacred
// falls through to the vanilla file. Safe to ship.
// Patch IAT (just memory writes — loader-safe).
HMODULE exe = GetModuleHandleA(nullptr);
orig_CreateFileA = (CreateFileA_t)iat::patch(
exe, "KERNEL32.dll", "CreateFileA", (void*)hook_CreateFileA);
sdk_log("[fs_override] installed. game_dir='%s' custom='%s' sdk_custom='%s' orig_CreateFileA=%p",
g_game_dir, g_custom_dir, g_sdk_custom_dir, orig_CreateFileA);
}
}} // namespace sdk::fs_override