User folders: save games and Settings.cfg in Documents\Sacred
The game's ANSI file API calls are hooked; paths under save\ and Settings.cfg are redirected to Documents\Sacred\save and Documents\Sacred\config, copied there once from the game folder. The shared chest uses the same save folder. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
198c21dbfa
commit
6c467e22c4
7 files changed
+337
-9
No files matched your search
@@ -70,6 +70,7 @@ add_library(sacredK SHARED
|
|||||||
src/mods/charexport.cpp
|
src/mods/charexport.cpp
|
||||||
src/mods/options.cpp
|
src/mods/options.cpp
|
||||||
src/mods/lootfilter.cpp
|
src/mods/lootfilter.cpp
|
||||||
|
src/mods/userfolders.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(sacredK PRIVATE src)
|
target_include_directories(sacredK PRIVATE src)
|
||||||
target_compile_options(sacredK PRIVATE ${SACREDK_CXX_FLAGS})
|
target_compile_options(sacredK PRIVATE ${SACREDK_CXX_FLAGS})
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
|
|
||||||
The chest gets a second tab that belongs to no single hero: a chest shared by
|
The chest gets a second tab that belongs to no single hero: a chest shared by
|
||||||
all characters. Decided: stored in one file in the save folder
|
all characters. Decided: stored in one file in the save folder
|
||||||
(`save\shared_chest.json`), two tabs ("Truhe" / "Gemeinsam"), same size as the
|
(`shared_chest.json` in the save folder, `Documents\Sacred\save`, see
|
||||||
|
`user-folders.md`), two tabs ("Truhe" / "Gemeinsam"), same size as the
|
||||||
chest (20 x 10). Potions stack in it like in every player inventory
|
chest (20 x 10). Potions stack in it like in every player inventory
|
||||||
(docs/item-stacks.md). Follow-up (same day): text tabs were barely visible;
|
(docs/item-stacks.md). Follow-up (same day): text tabs were barely visible;
|
||||||
use the inventory's Rucksack / Kampfkünste / Combos header look with its
|
use the inventory's Rucksack / Kampfkünste / Combos header look with its
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# User folders (Documents\Sacred)
|
||||||
|
|
||||||
|
Request (2026-10-19): the game keeps its save games and settings in the
|
||||||
|
user's Documents folder instead of the game folder:
|
||||||
|
|
||||||
|
| Folder | Contents |
|
||||||
|
|---|---|
|
||||||
|
| `Documents\Sacred\save\` | `GAMEnn.PAK` (save rows), `Heronn.pax` (exported heroes), `shared_chest.json` (sacredK) |
|
||||||
|
| `Documents\Sacred\config\` | `Settings.cfg` (the game's options) |
|
||||||
|
|
||||||
|
`sacredK.ini` (key bindings, loot filter, game folder) stays next to the DLL.
|
||||||
|
|
||||||
|
## Measured
|
||||||
|
|
||||||
|
Vanilla keeps everything inside the game folder: no AppData, Documents or
|
||||||
|
registry use (the `Ascaron Entertainment` folders there belong to Sacred 2).
|
||||||
|
`sacred.exe` names the files with paths relative to the working directory
|
||||||
|
(the game folder), from 37 code sites:
|
||||||
|
|
||||||
|
| String | Address | Users |
|
||||||
|
|---|---|---|
|
||||||
|
| `./SAVE/GAME%.2d.PAK` | 0x95c530 | 0x60b58b, 0x60c0d3, 0x60e38f, 0x60e645, 0x71cda7 |
|
||||||
|
| `SAVE/GAME%.2d.PAK` | 0x95c884 | 0x60e35a, 0x60e60a, 0x619c38 |
|
||||||
|
| `./SAVE/GAMEF%d.PAK` | 0x95c870 | 0x60e3b9, 0x60e674 |
|
||||||
|
| `./SAVE/SAVE02.PAK` | 0x95c85c | 0x60dc2c |
|
||||||
|
| `Save\Hero%.2d.pax` | 0x95c4ec | 0x60bf16, 0x614ceb, 0x70d96c, 0x70dd8d, 0x70df1b, 0x70e091 |
|
||||||
|
| `.\Save`, `Save` | 0x95d5f4, 0x95d5ec | 0x619cd4/ec, 0x61cdc0/d1 |
|
||||||
|
| `SAVE\` | 0x9e15a0 | 0x70bf07 |
|
||||||
|
| `Settings.cfg` | 0xa18dc4 | 0x7ed6a2..0x7eeab3 (13), WinMain 0x816dc0..0x816dd6 |
|
||||||
|
|
||||||
|
All file access goes through kernel32's ANSI functions: `CreateFileA`,
|
||||||
|
`DeleteFileA`, `FindFirstFileA`, `GetFileAttributes(Ex)A`,
|
||||||
|
`SetFileAttributesA`, `CreateDirectoryA`, `CopyFileA` (no `MoveFile*`, no
|
||||||
|
`SetCurrentDirectory`).
|
||||||
|
|
||||||
|
## How the mod works (`src/mods/userfolders.cpp`)
|
||||||
|
|
||||||
|
- Those eight functions are hooked (`RE_HOOK_API`). A path is rewritten when,
|
||||||
|
after an optional game-folder prefix and `.\`/`./`, its first component is
|
||||||
|
`save` (any case) or it is exactly `Settings.cfg`; every other path passes
|
||||||
|
unchanged. One choke point instead of 37 patched operands, and no long
|
||||||
|
absolute path in the game's fixed-size buffers.
|
||||||
|
- The Documents folder comes from `SHGetKnownFolderPath(FOLDERID_Documents)`
|
||||||
|
(follows OneDrive/redirected Documents), looked up on the first redirected
|
||||||
|
path: the hooks are installed under the loader lock, where shell calls must
|
||||||
|
not run. If it is unusable (lookup fails, or a character outside the ANSI
|
||||||
|
code page), the game folder stays in use.
|
||||||
|
- Migration: when `save\` or `config\` does not exist yet, the game folder's
|
||||||
|
save files / `Settings.cfg` are copied there once with the wide file
|
||||||
|
functions (not hooked). The originals stay in the game folder.
|
||||||
|
- `mods::userfolders::SaveDir()` gives sacredK's own files the same folder
|
||||||
|
(shared chest).
|
||||||
|
|
||||||
|
Not covered: `Config.exe` and `gameserver.exe` (`GameServer.cfg`) are
|
||||||
|
separate programs without the DLL and still use the game folder.
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
// serializer (vf1c(stream, 1, 0)); loadHero recreates them with
|
// serializer (vf1c(stream, 1, 0)); loadHero recreates them with
|
||||||
// cObjectManager::Create(type, 0, 0, 0), vf1c(stream, 0, file version) and
|
// cObjectManager::Create(type, 0, 0, 0), vf1c(stream, 0, file version) and
|
||||||
// gives them their new id. The shared chest does the same into
|
// gives them their new id. The shared chest does the same into
|
||||||
// save\shared_chest.json: written whenever a hero is saved, read at the start
|
// shared_chest.json in the save folder (mods/userfolders.cpp): written whenever a hero is saved, read at the start
|
||||||
// of each game session; cleared when the session ends (the ids die with the
|
// of each game session; cleared when the session ends (the ids die with the
|
||||||
// object manager).
|
// object manager).
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
#include "mods/itemstack.h"
|
#include "mods/itemstack.h"
|
||||||
#include "mods/sharedchest.h"
|
#include "mods/sharedchest.h"
|
||||||
#include "mods/skillslots.h"
|
#include "mods/skillslots.h"
|
||||||
#include "re/config.h"
|
#include "mods/userfolders.h"
|
||||||
#include "re/hook.h"
|
#include "re/hook.h"
|
||||||
#include "re/log.h"
|
#include "re/log.h"
|
||||||
|
|
||||||
@@ -102,11 +102,7 @@ bool SharedOpen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string FilePath() {
|
std::string FilePath() {
|
||||||
std::string dir = re::ConfigString("paths", "GameDir", "");
|
return mods::userfolders::SaveDir() + "shared_chest.json";
|
||||||
if (!dir.empty() && dir.back() != '\\' && dir.back() != '/') {
|
|
||||||
dir += '\\';
|
|
||||||
}
|
|
||||||
return dir + "save\\shared_chest.json";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- items <-> JSON ------------------------------------------------------------
|
// --- items <-> JSON ------------------------------------------------------------
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace mods::sharedchest {
|
namespace mods::sharedchest {
|
||||||
|
|
||||||
// Writes the shared chest (save\shared_chest.json); called after a hero is saved.
|
// Writes the shared chest (shared_chest.json in the save folder); called after a hero is saved.
|
||||||
void Save();
|
void Save();
|
||||||
|
|
||||||
} // namespace mods::sharedchest
|
} // namespace mods::sharedchest
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
// User folders (request of 2026-10-19): save games and the game's settings
|
||||||
|
// live in the user's Documents instead of the game folder:
|
||||||
|
// Documents\Sacred\save\ GAMEnn.PAK, Heronn.pax, shared_chest.json
|
||||||
|
// Documents\Sacred\config\ Settings.cfg
|
||||||
|
//
|
||||||
|
// Measured: sacred.exe names these files with paths relative to the game
|
||||||
|
// folder (the working directory), from 37 code sites: "./SAVE/GAME%.2d.PAK",
|
||||||
|
// "SAVE/GAME%.2d.PAK", "./SAVE/GAMEF%d.PAK", "./SAVE/SAVE02.PAK",
|
||||||
|
// "Save\Hero%.2d.pax", ".\Save", "Save", "SAVE\" and "Settings.cfg"
|
||||||
|
// (0x95c4ec..0x95d5f4, 0x9e15a0, 0xa18dc4). It reaches files only through
|
||||||
|
// kernel32's ANSI file functions, so those are hooked: a path whose first
|
||||||
|
// component is "save", or which is "Settings.cfg", is rewritten into the user
|
||||||
|
// folder; every other path passes unchanged. One choke point instead of 37
|
||||||
|
// patched operands, and no long absolute path in the game's fixed buffers.
|
||||||
|
//
|
||||||
|
// Migration: the first time a folder is used and does not exist yet, the game
|
||||||
|
// folder's files are copied into it (the originals stay where they were).
|
||||||
|
// Config.exe and gameserver.exe are separate programs and still use the game
|
||||||
|
// folder.
|
||||||
|
|
||||||
|
#include "mods/userfolders.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <shlobj.h>
|
||||||
|
|
||||||
|
#include "re/hook.h"
|
||||||
|
#include "re/log.h"
|
||||||
|
|
||||||
|
namespace mods::userfolders {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr char kSaveFolder[] = "save";
|
||||||
|
constexpr char kSettingsFile[] = "Settings.cfg";
|
||||||
|
constexpr wchar_t kUserFolder[] = L"Sacred";
|
||||||
|
constexpr wchar_t kUserSaveFolder[] = L"save";
|
||||||
|
constexpr wchar_t kUserConfigFolder[] = L"config";
|
||||||
|
|
||||||
|
bool IsSeparator(char c) {
|
||||||
|
return c == '\\' || c == '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EqualsNoCase(std::string_view a, std::string_view b) {
|
||||||
|
return a.size() == b.size() && _strnicmp(a.data(), b.data(), a.size()) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The game folder (sacred.exe's), with trailing separator.
|
||||||
|
const std::string& GameDir() {
|
||||||
|
static const std::string dir = [] {
|
||||||
|
char path[MAX_PATH];
|
||||||
|
DWORD length = GetModuleFileNameA(nullptr, path, MAX_PATH);
|
||||||
|
std::string result(path, length);
|
||||||
|
return result.substr(0, result.find_last_of("\\/") + 1);
|
||||||
|
}();
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring Widen(const std::string& text) {
|
||||||
|
std::wstring result(MultiByteToWideChar(CP_ACP, 0, text.c_str(), -1, nullptr, 0), L'\0');
|
||||||
|
MultiByteToWideChar(CP_ACP, 0, text.c_str(), -1, result.data(), static_cast<int>(result.size()));
|
||||||
|
result.pop_back();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `path` in the game's code page; empty when a character has no ANSI form.
|
||||||
|
std::string Narrow(const std::wstring& path) {
|
||||||
|
char buffer[MAX_PATH];
|
||||||
|
BOOL lossy = FALSE;
|
||||||
|
int length = WideCharToMultiByte(CP_ACP, WC_NO_BEST_FIT_CHARS, path.c_str(), -1, buffer, MAX_PATH, nullptr,
|
||||||
|
&lossy);
|
||||||
|
return length > 0 && !lossy ? std::string(buffer) : std::string();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Exists(const std::wstring& path) {
|
||||||
|
return GetFileAttributesW(path.c_str()) != INVALID_FILE_ATTRIBUTES;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copies the files of `from` into `to` (not recursive; the save folder is flat).
|
||||||
|
void CopyFiles(const std::wstring& from, const std::wstring& to) {
|
||||||
|
WIN32_FIND_DATAW found;
|
||||||
|
HANDLE find = FindFirstFileW((from + L"\\*").c_str(), &found);
|
||||||
|
if (find == INVALID_HANDLE_VALUE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int copied = 0;
|
||||||
|
do {
|
||||||
|
if (!(found.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
|
||||||
|
CopyFileW((from + L"\\" + found.cFileName).c_str(), (to + L"\\" + found.cFileName).c_str(), TRUE)) {
|
||||||
|
++copied;
|
||||||
|
}
|
||||||
|
} while (FindNextFileW(find, &found));
|
||||||
|
FindClose(find);
|
||||||
|
re::Log("user folders: copied %d files from the game's save folder", copied);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The user folders without trailing separator, in the game's code page; both
|
||||||
|
// empty when the Documents folder can't be used (the game folder stays).
|
||||||
|
struct Folders {
|
||||||
|
std::string save;
|
||||||
|
std::string config;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wide file functions only: the hooked ANSI ones would map the game folder's
|
||||||
|
// files onto themselves.
|
||||||
|
Folders FindFolders() {
|
||||||
|
PWSTR documents = nullptr;
|
||||||
|
HRESULT result = SHGetKnownFolderPath(FOLDERID_Documents, 0, nullptr, &documents);
|
||||||
|
std::wstring root = SUCCEEDED(result) ? std::wstring(documents) + L"\\" + kUserFolder : std::wstring();
|
||||||
|
CoTaskMemFree(documents);
|
||||||
|
|
||||||
|
Folders folders;
|
||||||
|
if (root.empty() || Narrow(root).empty()) {
|
||||||
|
re::Log("user folders: Documents folder unusable (0x%08lx), files stay in the game folder", result);
|
||||||
|
return folders;
|
||||||
|
}
|
||||||
|
const std::wstring save = root + L"\\" + kUserSaveFolder;
|
||||||
|
const std::wstring config = root + L"\\" + kUserConfigFolder;
|
||||||
|
const std::wstring gameDir = Widen(GameDir());
|
||||||
|
CreateDirectoryW(root.c_str(), nullptr);
|
||||||
|
if (!Exists(save)) {
|
||||||
|
CreateDirectoryW(save.c_str(), nullptr);
|
||||||
|
CopyFiles(gameDir + Widen(kSaveFolder), save);
|
||||||
|
}
|
||||||
|
if (!Exists(config)) {
|
||||||
|
CreateDirectoryW(config.c_str(), nullptr);
|
||||||
|
if (CopyFileW((gameDir + Widen(kSettingsFile)).c_str(), (config + L"\\" + Widen(kSettingsFile)).c_str(),
|
||||||
|
TRUE)) {
|
||||||
|
re::Log("user folders: copied the game's %s", kSettingsFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
folders.save = Narrow(save);
|
||||||
|
folders.config = Narrow(config);
|
||||||
|
re::Log("user folders: save games in %s, settings in %s", folders.save.c_str(), folders.config.c_str());
|
||||||
|
return folders;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looked up on first use, not at startup: the hooks are installed under the
|
||||||
|
// loader lock, where the shell's folder lookup must not run.
|
||||||
|
const Folders& UserFolders() {
|
||||||
|
static const Folders folders = FindFolders();
|
||||||
|
return folders;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `path` as the game means it, redirected into the user folders; nullptr when
|
||||||
|
// it stays as it is. `buffer` holds the result.
|
||||||
|
const char* Redirect(const char* path, std::string& buffer) {
|
||||||
|
if (!path) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
std::string_view rest(path);
|
||||||
|
const std::string& gameDir = GameDir();
|
||||||
|
if (rest.size() >= gameDir.size() && EqualsNoCase(rest.substr(0, gameDir.size()), gameDir)) {
|
||||||
|
rest.remove_prefix(gameDir.size());
|
||||||
|
} else if ((rest.size() >= 2 && rest[1] == ':') || (!rest.empty() && IsSeparator(rest[0]))) {
|
||||||
|
return nullptr; // absolute, elsewhere
|
||||||
|
}
|
||||||
|
while (rest.size() >= 2 && rest[0] == '.' && IsSeparator(rest[1])) {
|
||||||
|
rest.remove_prefix(2);
|
||||||
|
}
|
||||||
|
const size_t end = rest.find_first_of("\\/");
|
||||||
|
const std::string_view first = rest.substr(0, end);
|
||||||
|
const bool save = EqualsNoCase(first, kSaveFolder);
|
||||||
|
if (!save && !(end == std::string_view::npos && EqualsNoCase(first, kSettingsFile))) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
const Folders& folders = UserFolders();
|
||||||
|
if (folders.save.empty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (save) {
|
||||||
|
buffer = folders.save;
|
||||||
|
for (char c : rest.substr(first.size())) {
|
||||||
|
buffer += c == '/' ? '\\' : c;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buffer = folders.config + "\\" + kSettingsFile;
|
||||||
|
}
|
||||||
|
return buffer.c_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The path to hand to the original function.
|
||||||
|
const char* Mapped(const char* path, std::string& buffer) {
|
||||||
|
const char* redirected = Redirect(path, buffer);
|
||||||
|
return redirected ? redirected : path;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::string SaveDir() {
|
||||||
|
const Folders& folders = UserFolders();
|
||||||
|
return (folders.save.empty() ? GameDir() + kSaveFolder : folders.save) + "\\";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace mods::userfolders
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using mods::userfolders::Mapped;
|
||||||
|
|
||||||
|
HANDLE WINAPI CreateFileInUserFolders(LPCSTR path, DWORD access, DWORD share, LPSECURITY_ATTRIBUTES security,
|
||||||
|
DWORD disposition, DWORD flags, HANDLE templateFile);
|
||||||
|
BOOL WINAPI DeleteFileInUserFolders(LPCSTR path);
|
||||||
|
HANDLE WINAPI FindFirstFileInUserFolders(LPCSTR pattern, LPWIN32_FIND_DATAA found);
|
||||||
|
DWORD WINAPI GetFileAttributesInUserFolders(LPCSTR path);
|
||||||
|
BOOL WINAPI GetFileAttributesExInUserFolders(LPCSTR path, GET_FILEEX_INFO_LEVELS level, LPVOID info);
|
||||||
|
BOOL WINAPI SetFileAttributesInUserFolders(LPCSTR path, DWORD attributes);
|
||||||
|
BOOL WINAPI CreateDirectoryInUserFolders(LPCSTR path, LPSECURITY_ATTRIBUTES security);
|
||||||
|
BOOL WINAPI CopyFileInUserFolders(LPCSTR from, LPCSTR to, BOOL failIfExists);
|
||||||
|
|
||||||
|
RE_HOOK_API(CreateFileA, CreateFileInUserFolders);
|
||||||
|
RE_HOOK_API(DeleteFileA, DeleteFileInUserFolders);
|
||||||
|
RE_HOOK_API(FindFirstFileA, FindFirstFileInUserFolders);
|
||||||
|
RE_HOOK_API(GetFileAttributesA, GetFileAttributesInUserFolders);
|
||||||
|
RE_HOOK_API(GetFileAttributesExA, GetFileAttributesExInUserFolders);
|
||||||
|
RE_HOOK_API(SetFileAttributesA, SetFileAttributesInUserFolders);
|
||||||
|
RE_HOOK_API(CreateDirectoryA, CreateDirectoryInUserFolders);
|
||||||
|
RE_HOOK_API(CopyFileA, CopyFileInUserFolders);
|
||||||
|
|
||||||
|
HANDLE WINAPI CreateFileInUserFolders(LPCSTR path, DWORD access, DWORD share, LPSECURITY_ATTRIBUTES security,
|
||||||
|
DWORD disposition, DWORD flags, HANDLE templateFile) {
|
||||||
|
std::string buffer;
|
||||||
|
return CreateFileInUserFolders_original(Mapped(path, buffer), access, share, security, disposition, flags,
|
||||||
|
templateFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL WINAPI DeleteFileInUserFolders(LPCSTR path) {
|
||||||
|
std::string buffer;
|
||||||
|
return DeleteFileInUserFolders_original(Mapped(path, buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
HANDLE WINAPI FindFirstFileInUserFolders(LPCSTR pattern, LPWIN32_FIND_DATAA found) {
|
||||||
|
std::string buffer;
|
||||||
|
return FindFirstFileInUserFolders_original(Mapped(pattern, buffer), found);
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD WINAPI GetFileAttributesInUserFolders(LPCSTR path) {
|
||||||
|
std::string buffer;
|
||||||
|
return GetFileAttributesInUserFolders_original(Mapped(path, buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL WINAPI GetFileAttributesExInUserFolders(LPCSTR path, GET_FILEEX_INFO_LEVELS level, LPVOID info) {
|
||||||
|
std::string buffer;
|
||||||
|
return GetFileAttributesExInUserFolders_original(Mapped(path, buffer), level, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL WINAPI SetFileAttributesInUserFolders(LPCSTR path, DWORD attributes) {
|
||||||
|
std::string buffer;
|
||||||
|
return SetFileAttributesInUserFolders_original(Mapped(path, buffer), attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL WINAPI CreateDirectoryInUserFolders(LPCSTR path, LPSECURITY_ATTRIBUTES security) {
|
||||||
|
std::string buffer;
|
||||||
|
return CreateDirectoryInUserFolders_original(Mapped(path, buffer), security);
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL WINAPI CopyFileInUserFolders(LPCSTR from, LPCSTR to, BOOL failIfExists) {
|
||||||
|
std::string fromBuffer;
|
||||||
|
std::string toBuffer;
|
||||||
|
return CopyFileInUserFolders_original(Mapped(from, fromBuffer), Mapped(to, toBuffer), failIfExists);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace mods::userfolders {
|
||||||
|
|
||||||
|
// Folder of the save games, with trailing separator: Documents\Sacred\save\,
|
||||||
|
// or the game's own save\ when the Documents folder is unusable.
|
||||||
|
std::string SaveDir();
|
||||||
|
|
||||||
|
} // namespace mods::userfolders
|
||||||
Reference in new issue
Block a user