776 lines
38 KiB
C++
776 lines
38 KiB
C++
// DS2Toolkit.asi - runs GPG's Dungeon Siege II Tool Kit 1.1 (SiegeEditor2.exe, DS2AnimViewer.exe, DS2TankViewer.exe,
|
|
// DungeonSiege2Mod.exe; Nov 2005) against the Steam game. Loaded by Ultimate ASI Loader (version.dll) placed next to
|
|
// the tool kit's programs: SiegeEditor2 imports VERSION.dll.
|
|
//
|
|
// The tool kit finds its paths through machine-wide registry keys its installer and the retail game wrote:
|
|
// HKLM\Software\Microsoft\Microsoft Games\DungeonSiege2 InstallationDirectory -> the game ("ds_install_path",
|
|
// "%ds_exe_path%/DungeonSiege2.exe", "%ds_exe_path%/Maps/")
|
|
// HKLM\Software\Gas Powered Games\DS2TK the tool kit's folder ("dstk_path", "%dstk_path%/Icons/*.ico")
|
|
// HKLM\Software\Gas Powered Games\SiegeEditor2 machine-wide editor settings, opened once per setting
|
|
// Steam installs have none of them (the tool kit's installer refused with "Dungeon Siege II does not appear to be
|
|
// installed on this machine!"; SiegeEditor2 started without the installer stops on "%dstk_path%"). This plugin answers
|
|
// them in memory when they are missing - nothing is written to the registry; a real key always wins. The game folder is
|
|
// [Toolkit] GameDir, else the folder above the tool kit's, else the Steam library that has Dungeon Siege 2. Every key
|
|
// and value the tools ask for is logged once (DS2Toolkit.log).
|
|
#include "../common/patch.h"
|
|
#include "bwenums.h"
|
|
#include <commctrl.h>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <cwctype>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
char g_gameDir[MAX_PATH], g_toolDir[MAX_PATH];
|
|
std::string g_autoMap, g_autoRegion; // region opened at start-up (ds2toolkit_map= / ds2toolkit_region=)
|
|
bool g_quietErrors = false; // Broken World mode: report_errors=false (errors to the log, no dialog each)
|
|
const HKEY kGameKey = reinterpret_cast<HKEY>(static_cast<ULONG_PTR>(0x7D520011));
|
|
const HKEY kToolKitKey = reinterpret_cast<HKEY>(static_cast<ULONG_PTR>(0x7D520012));
|
|
const HKEY kEditorKey = reinterpret_cast<HKEY>(static_cast<ULONG_PTR>(0x7D520013));
|
|
bool IsFake(HKEY k) { return k == kGameKey || k == kToolKitKey || k == kEditorKey; }
|
|
|
|
using RegOpenKeyExA_t = LSTATUS(WINAPI*)(HKEY, LPCSTR, DWORD, REGSAM, PHKEY);
|
|
using RegOpenKeyA_t = LSTATUS(WINAPI*)(HKEY, LPCSTR, PHKEY);
|
|
using RegQueryValueExA_t = LSTATUS(WINAPI*)(HKEY, LPCSTR, LPDWORD, LPDWORD, LPBYTE, LPDWORD);
|
|
using RegQueryValueA_t = LSTATUS(WINAPI*)(HKEY, LPCSTR, LPSTR, PLONG);
|
|
using RegCloseKey_t = LSTATUS(WINAPI*)(HKEY);
|
|
RegOpenKeyExA_t g_RegOpenKeyExA;
|
|
RegOpenKeyA_t g_RegOpenKeyA;
|
|
RegQueryValueExA_t g_RegQueryValueExA;
|
|
RegQueryValueA_t g_RegQueryValueA;
|
|
RegCloseKey_t g_RegCloseKey;
|
|
|
|
bool HasGame(const std::string& dir) {
|
|
return GetFileAttributesA((dir + "\\DungeonSiege2.exe").c_str()) != INVALID_FILE_ATTRIBUTES &&
|
|
GetFileAttributesA((dir + "\\Resources\\Logic.ds2res").c_str()) != INVALID_FILE_ATTRIBUTES;
|
|
}
|
|
|
|
// Steam: HKCU\Software\Valve\Steam SteamPath, then every "path" in steamapps\libraryfolders.vdf.
|
|
bool FindSteamGame(std::string& out) {
|
|
HKEY k;
|
|
char steam[MAX_PATH] = {};
|
|
DWORD size = sizeof(steam);
|
|
if (RegOpenKeyExA(HKEY_CURRENT_USER, "Software\\Valve\\Steam", 0, KEY_READ, &k) != ERROR_SUCCESS) return false;
|
|
LSTATUS r = RegQueryValueExA(k, "SteamPath", nullptr, nullptr, reinterpret_cast<LPBYTE>(steam), &size);
|
|
RegCloseKey(k);
|
|
if (r != ERROR_SUCCESS || !steam[0]) return false;
|
|
std::string libs = std::string(steam) + "\\steamapps\\libraryfolders.vdf";
|
|
FILE* f = nullptr;
|
|
std::string text;
|
|
if (fopen_s(&f, libs.c_str(), "rb") == 0 && f) {
|
|
char buf[4096];
|
|
size_t n;
|
|
while ((n = fread(buf, 1, sizeof(buf), f)) > 0) text.append(buf, n);
|
|
fclose(f);
|
|
}
|
|
std::string candidates[64];
|
|
int count = 0;
|
|
candidates[count++] = steam;
|
|
for (size_t at = text.find("\"path\""); at != std::string::npos && count < 64; at = text.find("\"path\"", at + 6)) {
|
|
size_t a = text.find('"', at + 6), b = a == std::string::npos ? a : text.find('"', a + 1);
|
|
if (b == std::string::npos) break;
|
|
std::string p;
|
|
for (size_t i = a + 1; i < b; ++i) // VDF escapes backslashes
|
|
if (!(text[i] == '\\' && i + 1 < b && text[i + 1] == '\\')) p += text[i];
|
|
candidates[count++] = p;
|
|
}
|
|
for (int i = 0; i < count; ++i) {
|
|
std::string dir = candidates[i] + "\\steamapps\\common\\Dungeon Siege 2";
|
|
for (char& c : dir)
|
|
if (c == '/') c = '\\';
|
|
if (HasGame(dir)) {
|
|
out = dir;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
HKEY FakeFor(HKEY root, LPCSTR sub) {
|
|
if (root != HKEY_LOCAL_MACHINE || !sub) return nullptr;
|
|
char lower[512];
|
|
strncpy_s(lower, sub, _TRUNCATE);
|
|
_strlwr_s(lower);
|
|
if (std::strstr(lower, "microsoft games\\") && std::strstr(lower, "dungeon")) return g_gameDir[0] ? kGameKey : nullptr;
|
|
if (std::strstr(lower, "gas powered games\\ds2tk")) return kToolKitKey;
|
|
if (std::strstr(lower, "gas powered games\\siegeeditor")) return kEditorKey;
|
|
return nullptr;
|
|
}
|
|
|
|
// Each line is logged once (the editor opens its settings key thousands of times).
|
|
void LogOnce(const char* what) {
|
|
static std::string seen;
|
|
std::string k = std::string("|") + what + "|";
|
|
if (seen.find(k) != std::string::npos || seen.size() > 64 * 1024) return;
|
|
seen += k;
|
|
ds2::log().Write("%s", what);
|
|
}
|
|
|
|
LSTATUS Fake(LSTATUS r, HKEY root, LPCSTR sub, PHKEY out, const char* api) {
|
|
HKEY fake = FakeFor(root, sub);
|
|
if (root == HKEY_LOCAL_MACHINE && sub) {
|
|
char line[600];
|
|
sprintf_s(line, "%s HKLM\\%s -> %s", api, sub, r == ERROR_SUCCESS ? "real key" : fake ? "served" : "missing");
|
|
LogOnce(line);
|
|
}
|
|
if (r == ERROR_SUCCESS || !out || !fake) return r;
|
|
*out = fake;
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
LSTATUS WINAPI HookRegOpenKeyExA(HKEY root, LPCSTR sub, DWORD opt, REGSAM sam, PHKEY out) {
|
|
return Fake(g_RegOpenKeyExA(root, sub, opt, sam, out), root, sub, out, "RegOpenKeyExA");
|
|
}
|
|
|
|
LSTATUS WINAPI HookRegOpenKeyA(HKEY root, LPCSTR sub, PHKEY out) {
|
|
return Fake(g_RegOpenKeyA(root, sub, out), root, sub, out, "RegOpenKeyA");
|
|
}
|
|
|
|
extern bool g_bwMode;
|
|
extern std::string g_resPaths, g_mapPaths;
|
|
|
|
char g_iniPath[MAX_PATH];
|
|
bool g_askGame = false; // ask which game at the editor's first settings lookup (outside the loader lock)
|
|
void AskGame();
|
|
|
|
LSTATUS ServeValue(HKEY key, const char* name, LPDWORD type, LPBYTE data, LPDWORD size) {
|
|
static const char* const kPathNames[] = {"InstallationDirectory", "AppPath", "EXE Path", "InstallDir", "Path",
|
|
"InstallPath", "(default)"};
|
|
const char* dir = key == kGameKey ? g_gameDir : key == kToolKitKey ? g_toolDir : nullptr;
|
|
// [Settings] in DS2Toolkit.ini: any Siege Editor 2 setting (the editor reads them from this key, e.g.
|
|
// contentdb_jit, micro_templates, bits), answered before the plugin's own values.
|
|
if (key == kEditorKey && g_askGame) {
|
|
g_askGame = false;
|
|
AskGame();
|
|
}
|
|
if (key == kEditorKey && g_iniPath[0]) {
|
|
char v[1024];
|
|
GetPrivateProfileStringA("Settings", name, "\x01", v, sizeof(v), g_iniPath);
|
|
if (v[0] != '\x01') {
|
|
char line[1200];
|
|
sprintf_s(line, "served SiegeEditor2 key %s = %s ([Settings])", name, v);
|
|
LogOnce(line);
|
|
DWORD len = static_cast<DWORD>(std::strlen(v) + 1), cap = size ? *size : 0;
|
|
if (type) *type = REG_SZ;
|
|
if (size) *size = len;
|
|
if (!data) return ERROR_SUCCESS;
|
|
if (cap < len) return ERROR_MORE_DATA;
|
|
std::memcpy(data, v, len);
|
|
return ERROR_SUCCESS;
|
|
}
|
|
}
|
|
// Broken World mode: also skrit_retry=false. Broken World's archives replace base-game AI skrits with versions that
|
|
// call functions/enums only its exe has (e.g. ist_blood_assassin); the 2005 editor cannot compile them and asked
|
|
// "Strikes: 1 / AI failed to find, load, or compile Skrit ... Take another swing?" for each.
|
|
// report_errors=false ([Toolkit] QuietErrors=1, Broken World mode only; default 0): no error dialogs. It was the
|
|
// default while Broken World's enum values (ist_blood_assassin, the new ALTER_* enchantment types) raised errors at
|
|
// every start; ExtendEnums now teaches the editor those names, so errors are reported again by default.
|
|
// ds2dll_path: the folder with BWCompat.ds2dll (tools/bwcompat_gen.py): FuBi imports a *.ds2dll's exports as script
|
|
// functions, and this one exports, as stubs, the functions Broken World's exe added that its skrits call.
|
|
// map / region: Siege Editor 2 opens this region at start-up. From the command line: ds2toolkit_map=<map>
|
|
// ds2toolkit_region=<region> (e.g. ds2x_world x2_02x_prison) - for testing without the Load Region dialog.
|
|
if (key == kEditorKey && ((!_stricmp(name, "map") && !g_autoMap.empty()) || (!_stricmp(name, "region") && !g_autoRegion.empty()))) {
|
|
const std::string& v = !_stricmp(name, "map") ? g_autoMap : g_autoRegion;
|
|
char line[300];
|
|
sprintf_s(line, "served SiegeEditor2 key %s = %s", name, v.c_str());
|
|
LogOnce(line);
|
|
DWORD len = static_cast<DWORD>(v.size() + 1), cap = size ? *size : 0;
|
|
if (type) *type = REG_SZ;
|
|
if (size) *size = len;
|
|
if (!data) return ERROR_SUCCESS;
|
|
if (cap < len) return ERROR_MORE_DATA;
|
|
std::memcpy(data, v.c_str(), len);
|
|
return ERROR_SUCCESS;
|
|
}
|
|
static const std::string kFalse = "false";
|
|
static const std::string bwDlls = std::string(g_toolDir) + "\\bw";
|
|
if (key == kEditorKey && g_bwMode &&
|
|
(!_stricmp(name, "res_paths") || !_stricmp(name, "map_paths") || !_stricmp(name, "skrit_retry") ||
|
|
!_stricmp(name, "ds2dll_path") || (!_stricmp(name, "report_errors") && g_quietErrors))) {
|
|
const std::string& v = !_stricmp(name, "res_paths") ? g_resPaths : !_stricmp(name, "map_paths") ? g_mapPaths
|
|
: !_stricmp(name, "ds2dll_path") ? bwDlls : kFalse;
|
|
char line[700];
|
|
sprintf_s(line, "served SiegeEditor2 key %s = %s", name, v.c_str());
|
|
LogOnce(line);
|
|
DWORD len = static_cast<DWORD>(v.size() + 1), cap = size ? *size : 0;
|
|
if (type) *type = REG_SZ;
|
|
if (size) *size = len;
|
|
if (!data) return ERROR_SUCCESS;
|
|
if (cap < len) return ERROR_MORE_DATA;
|
|
std::memcpy(data, v.c_str(), len);
|
|
return ERROR_SUCCESS;
|
|
}
|
|
const char* which = key == kGameKey ? "game key" : key == kToolKitKey ? "DS2TK key" : "SiegeEditor2 key";
|
|
char line[300];
|
|
if (dir)
|
|
for (const char* n : kPathNames)
|
|
if (_stricmp(name, n) == 0) {
|
|
sprintf_s(line, "served %s %s = %s", which, name, dir);
|
|
LogOnce(line);
|
|
DWORD len = static_cast<DWORD>(std::strlen(dir) + 1), cap = size ? *size : 0;
|
|
if (type) *type = REG_SZ;
|
|
if (size) *size = len;
|
|
if (!data) return ERROR_SUCCESS;
|
|
if (cap < len) return ERROR_MORE_DATA;
|
|
std::memcpy(data, dir, len);
|
|
return ERROR_SUCCESS;
|
|
}
|
|
sprintf_s(line, "%s value %s: not answered (defaults)", which, name);
|
|
LogOnce(line);
|
|
return ERROR_FILE_NOT_FOUND;
|
|
}
|
|
|
|
LSTATUS WINAPI HookRegQueryValueExA(HKEY key, LPCSTR name, LPDWORD res, LPDWORD type, LPBYTE data, LPDWORD size) {
|
|
if (!IsFake(key)) return g_RegQueryValueExA(key, name, res, type, data, size);
|
|
return ServeValue(key, name && *name ? name : "(default)", type, data, size);
|
|
}
|
|
|
|
LSTATUS WINAPI HookRegQueryValueA(HKEY key, LPCSTR sub, LPSTR data, PLONG size) {
|
|
if (!IsFake(key)) return g_RegQueryValueA(key, sub, data, size);
|
|
DWORD s = size ? static_cast<DWORD>(*size) : 0;
|
|
LSTATUS r = ServeValue(key, sub && *sub ? sub : "(default)", nullptr, reinterpret_cast<LPBYTE>(data), size ? &s : nullptr);
|
|
if (size) *size = static_cast<LONG>(s);
|
|
return r;
|
|
}
|
|
|
|
LSTATUS WINAPI HookRegCloseKey(HKEY key) { return IsFake(key) ? ERROR_SUCCESS : g_RegCloseKey(key); }
|
|
|
|
// --- Broken World mode (command line "ds2toolkit_bw=1", or [Toolkit] BrokenWorld=1) --------------------------------
|
|
// The tool kit mounts only the base game's archives, so Broken World's maps fail on its own content ("Unable to open
|
|
// file 't_xxx_dun2x_flr_base_04x08-a'": a mesh in Broken World's xTerrain.ds2res). Siege Editor 2 reads its archive
|
|
// folders from its machine-wide settings key (values res_paths, map_paths, mod_paths with *.ds2res, *.ds2map,
|
|
// *.ds2mod; defaults Resources, Maps, Mods - table at 0x1044bf0, defaults set at 0x8bba2b). In this mode the served
|
|
// SiegeEditor2 key answers res_paths and map_paths with the base game's folders and Broken World's, the way Broken
|
|
// World itself mounts both. (Tried first and not taken: res_paths= on the command line, and Broken World's archives
|
|
// listed in the Mods folder - as *.ds2res they are skipped, as *.ds2mod they are listed but not mounted.)
|
|
bool g_bwMode = false;
|
|
std::string g_resPaths, g_mapPaths;
|
|
|
|
// "key=value" from the command line (value up to the next space), or "".
|
|
std::string CommandValue(const char* key) {
|
|
std::string cmd = GetCommandLineA(), k = std::string(key) + "=";
|
|
size_t at = cmd.find(k);
|
|
if (at == std::string::npos) return "";
|
|
size_t end = cmd.find_first_of(" \"", at + k.size());
|
|
return cmd.substr(at + k.size(), end == std::string::npos ? std::string::npos : end - at - k.size());
|
|
}
|
|
|
|
std::string BrokenWorldDir(const ds2::Ini& ini) {
|
|
char bw[MAX_PATH];
|
|
ini.String("Toolkit", "BrokenWorldDir", "", bw, sizeof(bw));
|
|
return bw[0] ? std::string(bw) : std::string(g_gameDir) + "\\Broken World";
|
|
}
|
|
|
|
bool BrokenWorldInstalled(const ds2::Ini& ini) {
|
|
return GetFileAttributesA((BrokenWorldDir(ini) + "\\Resources\\xTerrain.ds2res").c_str()) != INVALID_FILE_ATTRIBUTES;
|
|
}
|
|
|
|
void InstallBrokenWorldMode(const ds2::Ini& ini) {
|
|
std::string dir = BrokenWorldDir(ini);
|
|
if (GetFileAttributesA((dir + "\\Resources\\xTerrain.ds2res").c_str()) == INVALID_FILE_ATTRIBUTES) {
|
|
ds2::log().Write("Broken World mode: no Resources\\xTerrain.ds2res in %s - set [Toolkit] BrokenWorldDir=", dir.c_str());
|
|
return;
|
|
}
|
|
g_resPaths = std::string(g_gameDir) + "\\Resources;" + dir + "\\Resources";
|
|
g_mapPaths = std::string(g_gameDir) + "\\Maps;" + dir + "\\Maps";
|
|
g_bwMode = true;
|
|
ds2::log().Write("Broken World mode: res_paths=%s map_paths=%s", g_resPaths.c_str(), g_mapPaths.c_str());
|
|
}
|
|
|
|
// --- Broken World enum names (mods/toolkit/bwenums.h, generated by tools/se2_bwenums_gen.py) ----------------------
|
|
// Broken World added values to two enums its content names: eItemSkillType (ist_blood_assassin, ist_fist_of_stone,
|
|
// used by k_inc_character_utils.skrit - without them rules.skrit and efct_spell_wrath_of_the_wind do not compile:
|
|
// "'ist_blood_assassin': undeclared identifier") and eAlteration (32 ALTER_* enchantment types in pcontent.gas:
|
|
// "Enchantment must specify valid alteration type"). The editor builds both string tables in static initialisers;
|
|
// this runs before them (Ultimate ASI Loader loads plugins from the CRT start-up's first kernel32 call, before
|
|
// _initterm) and points each initialiser's table/count immediates at a table holding the editor's own entries, read
|
|
// from its image, followed by Broken World's. The editor's own values keep their numbers.
|
|
bool ExtendEnums() {
|
|
bool all = true;
|
|
for (const bwenums::Enum& e : bwenums::kEnums) {
|
|
if (*reinterpret_cast<const DWORD*>(e.object) != 0) { // the object is zero-initialised data until then
|
|
ds2::log().Write("SKIP %s names: its table is already built (plugin loaded after static initialisers)", e.name);
|
|
all = false;
|
|
continue;
|
|
}
|
|
const DWORD n = e.count, total = n + static_cast<DWORD>(e.extraCount);
|
|
DWORD table;
|
|
if (e.pairs) {
|
|
auto* t = new DWORD[2 * total]; // {const char* name, value}; lives as long as the process
|
|
std::memcpy(t, reinterpret_cast<const void*>(e.table), 8 * n);
|
|
for (int i = 0; i < e.extraCount; ++i) {
|
|
t[2 * (n + i)] = reinterpret_cast<DWORD>(e.extra[i].name);
|
|
t[2 * (n + i) + 1] = e.extra[i].value;
|
|
}
|
|
table = reinterpret_cast<DWORD>(t);
|
|
} else {
|
|
auto* t = new const char*[total];
|
|
std::memcpy(t, reinterpret_cast<const void*>(e.table), 4 * n);
|
|
for (int i = 0; i < e.extraCount; ++i) t[n + i] = e.extra[i].name;
|
|
table = reinterpret_cast<DWORD>(t);
|
|
}
|
|
char what[96];
|
|
sprintf_s(what, "%s table (+%d Broken World names)", e.name, e.extraCount);
|
|
const BYTE count8 = static_cast<BYTE>(n), total8 = static_cast<BYTE>(total);
|
|
bool ok = e.countSize == 1 ? ds2::PatchBytes(e.countImm, &count8, &total8, 1, what)
|
|
: ds2::PatchBytes(e.countImm, &n, &total, 4, what);
|
|
ok = ok && ds2::PatchBytes(e.tableImm, &e.table, &table, 4, what);
|
|
all = all && ok;
|
|
}
|
|
return all;
|
|
}
|
|
|
|
// --- [Toolkit] SkipArchives: archives the editor does not mount ----------------------------------------------------
|
|
// The editor mounts every *.ds2res / *.ds2map / *.ds2mod in its res_paths / map_paths / mod_paths folders, found with
|
|
// FindFirstFile/FindNextFile. Third-party archives dropped into the game's folders come along - e.g. the Widescreen/FoV
|
|
// mod's "Widescreen Fix.ds2res" (KillerGremal, 2015) redefines template party_humanoid, and the editor reports
|
|
// "Duplicate template detected: 'party_humanoid' is already in content database" at every start. Names listed in
|
|
// SkipArchives (separated by ';', case-insensitive, file names without folder) are hidden from those searches.
|
|
std::string g_skipArchives; // ";name1;name2;" lower case
|
|
|
|
bool SkipFound(const char* name) {
|
|
if (g_skipArchives.empty() || !name) return false;
|
|
std::string k = std::string(";") + name + ";";
|
|
for (char& c : k) c = static_cast<char>(tolower(static_cast<unsigned char>(c)));
|
|
if (g_skipArchives.find(k) == std::string::npos) return false;
|
|
char line[400];
|
|
sprintf_s(line, "SkipArchives: not mounting %s", name);
|
|
LogOnce(line);
|
|
return true;
|
|
}
|
|
|
|
bool SkipFoundW(const wchar_t* name) {
|
|
char a[MAX_PATH] = {};
|
|
WideCharToMultiByte(CP_ACP, 0, name, -1, a, sizeof(a), nullptr, nullptr);
|
|
return SkipFound(a);
|
|
}
|
|
|
|
using FindFirstFileA_t = HANDLE(WINAPI*)(LPCSTR, LPWIN32_FIND_DATAA);
|
|
using FindNextFileA_t = BOOL(WINAPI*)(HANDLE, LPWIN32_FIND_DATAA);
|
|
using FindFirstFileW_t = HANDLE(WINAPI*)(LPCWSTR, LPWIN32_FIND_DATAW);
|
|
using FindNextFileW_t = BOOL(WINAPI*)(HANDLE, LPWIN32_FIND_DATAW);
|
|
FindFirstFileA_t g_FindFirstFileA;
|
|
FindNextFileA_t g_FindNextFileA;
|
|
FindFirstFileW_t g_FindFirstFileW;
|
|
FindNextFileW_t g_FindNextFileW;
|
|
|
|
BOOL WINAPI HookFindNextFileA(HANDLE h, LPWIN32_FIND_DATAA d) {
|
|
BOOL r;
|
|
while ((r = g_FindNextFileA(h, d)) && SkipFound(d->cFileName)) {}
|
|
return r;
|
|
}
|
|
|
|
HANDLE WINAPI HookFindFirstFileA(LPCSTR pattern, LPWIN32_FIND_DATAA d) {
|
|
HANDLE h = g_FindFirstFileA(pattern, d);
|
|
if (h == INVALID_HANDLE_VALUE || !SkipFound(d->cFileName) || HookFindNextFileA(h, d)) return h;
|
|
FindClose(h); // the only match was skipped
|
|
SetLastError(ERROR_FILE_NOT_FOUND);
|
|
return INVALID_HANDLE_VALUE;
|
|
}
|
|
|
|
BOOL WINAPI HookFindNextFileW(HANDLE h, LPWIN32_FIND_DATAW d) {
|
|
BOOL r;
|
|
while ((r = g_FindNextFileW(h, d)) && SkipFoundW(d->cFileName)) {}
|
|
return r;
|
|
}
|
|
|
|
HANDLE WINAPI HookFindFirstFileW(LPCWSTR pattern, LPWIN32_FIND_DATAW d) {
|
|
HANDLE h = g_FindFirstFileW(pattern, d);
|
|
if (h == INVALID_HANDLE_VALUE || !SkipFoundW(d->cFileName) || HookFindNextFileW(h, d)) return h;
|
|
FindClose(h);
|
|
SetLastError(ERROR_FILE_NOT_FOUND);
|
|
return INVALID_HANDLE_VALUE;
|
|
}
|
|
|
|
void InstallSkipArchives(const ds2::Ini& ini) {
|
|
char v[1024];
|
|
ini.String("Toolkit", "SkipArchives", "", v, sizeof(v));
|
|
if (!v[0]) return;
|
|
g_skipArchives = std::string(";") + v + ";";
|
|
for (char& c : g_skipArchives) c = static_cast<char>(tolower(static_cast<unsigned char>(c)));
|
|
for (size_t at; (at = g_skipArchives.find("; ")) != std::string::npos;) g_skipArchives.erase(at + 1, 1);
|
|
for (size_t at; (at = g_skipArchives.find(" ;")) != std::string::npos;) g_skipArchives.erase(at, 1);
|
|
ds2::log().Write("SkipArchives: %s", v);
|
|
g_FindFirstFileA = static_cast<FindFirstFileA_t>(ds2::HookImport("KERNEL32.dll", "FindFirstFileA", reinterpret_cast<void*>(HookFindFirstFileA), "FindFirstFileA (SkipArchives)"));
|
|
g_FindNextFileA = static_cast<FindNextFileA_t>(ds2::HookImport("KERNEL32.dll", "FindNextFileA", reinterpret_cast<void*>(HookFindNextFileA), "FindNextFileA (SkipArchives)"));
|
|
g_FindFirstFileW = static_cast<FindFirstFileW_t>(ds2::HookImport("KERNEL32.dll", "FindFirstFileW", reinterpret_cast<void*>(HookFindFirstFileW), "FindFirstFileW (SkipArchives)"));
|
|
g_FindNextFileW = static_cast<FindNextFileW_t>(ds2::HookImport("KERNEL32.dll", "FindNextFileW", reinterpret_cast<void*>(HookFindNextFileW), "FindNextFileW (SkipArchives)"));
|
|
if (!g_FindNextFileA) g_FindNextFileA = FindNextFileA; // not hooked: the First hook still needs the original
|
|
if (!g_FindNextFileW) g_FindNextFileW = FindNextFileW;
|
|
}
|
|
|
|
// --- clothmap_rollover in info/start_positions.gas -----------------------------------------------------------------
|
|
// Every start group in the retail start_positions.gas (Dungeon Siege II and Broken World) names its journal map
|
|
// label: "clothmap_rollover = journal_map_location_...;". The editor's start-position writer (the function that writes
|
|
// clothmap_pos_x/y/z with the other start-group keys, string ref at 0x44cbe4) has no such field, so File > Save
|
|
// rewrites the map's start_positions.gas without any clothmap_rollover line (56 lost in ds2_world, 20 in ds2x_world).
|
|
// When the editor opens that file for writing, the plugin first reads the old file's rollovers per start group; when
|
|
// the editor closes the handle, it adds them back to the groups that still exist and lack one (after clothmap_pos_z).
|
|
struct RolloverWatch {
|
|
std::wstring path;
|
|
std::string groups; // "\n<group>\t<value>" per start group of the old file with a rollover
|
|
};
|
|
CRITICAL_SECTION g_watchLock;
|
|
volatile LONG g_watchCount = 0;
|
|
HANDLE g_watchHandle[8];
|
|
RolloverWatch g_watch[8];
|
|
|
|
std::string ReadWholeW(const std::wstring& path) {
|
|
std::string out;
|
|
HANDLE h = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr);
|
|
if (h == INVALID_HANDLE_VALUE) return out;
|
|
char buf[65536];
|
|
DWORD n;
|
|
while (ReadFile(h, buf, sizeof(buf), &n, nullptr) && n) out.append(buf, n);
|
|
CloseHandle(h);
|
|
return out;
|
|
}
|
|
|
|
// Replace path with text via a temporary sibling, so a failed write leaves the existing file as it was.
|
|
bool WriteWholeW(const std::wstring& path, const std::string& text) {
|
|
const std::wstring tmp = path + L".rollover.tmp";
|
|
HANDLE h = CreateFileW(tmp.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
|
if (h == INVALID_HANDLE_VALUE) return false;
|
|
DWORD n = 0;
|
|
bool ok = WriteFile(h, text.data(), static_cast<DWORD>(text.size()), &n, nullptr) && n == text.size() && FlushFileBuffers(h);
|
|
CloseHandle(h);
|
|
if (ok) ok = MoveFileExW(tmp.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0;
|
|
if (!ok) DeleteFileW(tmp.c_str());
|
|
return ok;
|
|
}
|
|
|
|
bool IsStartPositions(const std::wstring& path) {
|
|
static const wchar_t kTail[] = L"\\info\\start_positions.gas";
|
|
const size_t n = sizeof(kTail) / sizeof(wchar_t) - 1;
|
|
if (path.size() < n) return false;
|
|
std::wstring tail = path.substr(path.size() - n);
|
|
for (wchar_t& c : tail) c = c == L'/' ? L'\\' : towlower(c);
|
|
return tail == kTail;
|
|
}
|
|
|
|
std::string Trim(const std::string& s) {
|
|
size_t a = s.find_first_not_of(" \t\r\n"), b = s.find_last_not_of(" \t\r\n");
|
|
return a == std::string::npos ? std::string() : s.substr(a, b - a + 1);
|
|
}
|
|
|
|
// Walk the start groups ([t:start_group,n:NAME] blocks) of a start_positions.gas text: f(line index, group name in
|
|
// lower case, kind) for every line inside one. Level = a line of the group's own block (not of a nested
|
|
// [start_position] block).
|
|
enum class GroupLine { Open, Level, Nested, Close };
|
|
|
|
template <class F>
|
|
void ForEachGroupLine(const std::vector<std::string>& lines, F f) {
|
|
int depth = 0, groupDepth = -1;
|
|
std::string group, pending;
|
|
static const std::string kHead = "[t:start_group,n:";
|
|
for (size_t i = 0; i < lines.size(); ++i) {
|
|
std::string t = Trim(lines[i].substr(0, lines[i].find("//")));
|
|
std::string flat;
|
|
for (char c : t)
|
|
if (!isspace(static_cast<unsigned char>(c))) flat += static_cast<char>(tolower(static_cast<unsigned char>(c)));
|
|
if (flat.compare(0, kHead.size(), kHead) == 0 && flat.back() == ']') {
|
|
pending = flat.substr(kHead.size(), flat.size() - kHead.size() - 1);
|
|
continue;
|
|
}
|
|
const int depthBefore = depth;
|
|
bool opened = false, closed = false;
|
|
for (char c : t) {
|
|
if (c == '{') {
|
|
++depth;
|
|
if (!pending.empty() && group.empty()) {
|
|
group = pending;
|
|
groupDepth = depth;
|
|
pending.clear();
|
|
opened = true;
|
|
}
|
|
} else if (c == '}') {
|
|
if (depth == groupDepth) closed = true;
|
|
--depth;
|
|
}
|
|
}
|
|
if (group.empty()) continue;
|
|
if (opened) {
|
|
f(i, group, GroupLine::Open);
|
|
} else if (closed) {
|
|
f(i, group, GroupLine::Close);
|
|
group.clear();
|
|
groupDepth = -1;
|
|
} else {
|
|
f(i, group, depthBefore == groupDepth && depth == groupDepth ? GroupLine::Level : GroupLine::Nested);
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<std::string> SplitLines(const std::string& text) {
|
|
std::vector<std::string> lines;
|
|
size_t at = 0;
|
|
while (at < text.size()) {
|
|
size_t e = text.find('\n', at);
|
|
lines.push_back(text.substr(at, e == std::string::npos ? std::string::npos : e - at + 1));
|
|
at = e == std::string::npos ? text.size() : e + 1;
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
// key of a "key = value;" line (lower case, type prefix dropped) and its value, or false.
|
|
bool KeyValue(const std::string& line, std::string& key, std::string& value) {
|
|
std::string t = Trim(line);
|
|
size_t eq = t.find('='), semi = t.rfind(';');
|
|
if (eq == std::string::npos || semi == std::string::npos || semi < eq || t[0] == '[') return false;
|
|
std::string k = Trim(t.substr(0, eq));
|
|
size_t sp = k.find_last_of(" \t");
|
|
key = sp == std::string::npos ? k : k.substr(sp + 1);
|
|
for (char& c : key) c = static_cast<char>(tolower(static_cast<unsigned char>(c)));
|
|
value = Trim(t.substr(eq + 1, semi - eq - 1));
|
|
return true;
|
|
}
|
|
|
|
std::string CollectRollovers(const std::string& text) {
|
|
std::string out;
|
|
std::vector<std::string> lines = SplitLines(text);
|
|
ForEachGroupLine(lines, [&](size_t i, const std::string& group, GroupLine kind) {
|
|
std::string k, v;
|
|
if (kind == GroupLine::Level && KeyValue(lines[i], k, v) && k == "clothmap_rollover")
|
|
out += "\n" + group + "\t" + v;
|
|
});
|
|
return out;
|
|
}
|
|
|
|
std::string MergeRollovers(const std::string& text, const std::string& groups, int& added) {
|
|
std::vector<std::string> lines = SplitLines(text);
|
|
const std::string eol = text.find("\r\n") != std::string::npos ? "\r\n" : "\n";
|
|
std::vector<std::string> insertAfter(lines.size());
|
|
size_t anchor = 0;
|
|
bool has = false;
|
|
std::string indent;
|
|
ForEachGroupLine(lines, [&](size_t i, const std::string& group, GroupLine kind) {
|
|
std::string k, v;
|
|
if (kind == GroupLine::Open) {
|
|
anchor = i;
|
|
has = false;
|
|
size_t ws = lines[i].find_first_not_of(" \t");
|
|
indent = lines[i].substr(0, ws == std::string::npos ? 0 : ws) + "\t";
|
|
} else if (kind == GroupLine::Level && KeyValue(lines[i], k, v)) {
|
|
if (k == "clothmap_rollover") has = true;
|
|
if (k == "clothmap_pos_z") anchor = i;
|
|
} else if (kind == GroupLine::Close) {
|
|
size_t at = groups.find("\n" + group + "\t");
|
|
if (!has && at != std::string::npos) {
|
|
size_t b = at + group.size() + 2, e = groups.find('\n', b);
|
|
insertAfter[anchor] += indent + "clothmap_rollover = " +
|
|
groups.substr(b, e == std::string::npos ? std::string::npos : e - b) + ";" + eol;
|
|
++added;
|
|
}
|
|
}
|
|
});
|
|
std::string out;
|
|
for (size_t i = 0; i < lines.size(); ++i) {
|
|
out += lines[i];
|
|
if (!insertAfter[i].empty()) {
|
|
if (out.empty() || out.back() != '\n') out += eol;
|
|
out += insertAfter[i];
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
void WatchStartPositions(HANDLE h, const std::wstring& path, const std::string& before) {
|
|
std::string groups = CollectRollovers(before);
|
|
if (groups.empty()) return;
|
|
EnterCriticalSection(&g_watchLock);
|
|
for (int i = 0; i < 8; ++i)
|
|
if (!g_watchHandle[i]) {
|
|
g_watchHandle[i] = h;
|
|
g_watch[i].path = path;
|
|
g_watch[i].groups = groups;
|
|
InterlockedIncrement(&g_watchCount);
|
|
break;
|
|
}
|
|
LeaveCriticalSection(&g_watchLock);
|
|
}
|
|
|
|
using CreateFileA_t = HANDLE(WINAPI*)(LPCSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE);
|
|
using CreateFileW_t = HANDLE(WINAPI*)(LPCWSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE);
|
|
using CloseHandle_t = BOOL(WINAPI*)(HANDLE);
|
|
CreateFileA_t g_CreateFileA;
|
|
CreateFileW_t g_CreateFileW;
|
|
CloseHandle_t g_CloseHandle;
|
|
|
|
HANDLE WINAPI HookCreateFileW(LPCWSTR name, DWORD access, DWORD share, LPSECURITY_ATTRIBUTES sa, DWORD disp, DWORD flags, HANDLE tmpl) {
|
|
std::wstring path;
|
|
std::string before;
|
|
bool watch = name && (access & (GENERIC_WRITE | GENERIC_ALL)) && IsStartPositions(path = name);
|
|
if (watch) before = ReadWholeW(path); // before the editor truncates it
|
|
HANDLE h = g_CreateFileW(name, access, share, sa, disp, flags, tmpl);
|
|
if (watch && h != INVALID_HANDLE_VALUE) WatchStartPositions(h, path, before);
|
|
return h;
|
|
}
|
|
|
|
HANDLE WINAPI HookCreateFileA(LPCSTR name, DWORD access, DWORD share, LPSECURITY_ATTRIBUTES sa, DWORD disp, DWORD flags, HANDLE tmpl) {
|
|
std::wstring path;
|
|
std::string before;
|
|
bool watch = false;
|
|
if (name && (access & (GENERIC_WRITE | GENERIC_ALL))) {
|
|
wchar_t w[MAX_PATH * 2];
|
|
if (MultiByteToWideChar(CP_ACP, 0, name, -1, w, MAX_PATH * 2) && IsStartPositions(path = w)) {
|
|
watch = true;
|
|
before = ReadWholeW(path);
|
|
}
|
|
}
|
|
HANDLE h = g_CreateFileA(name, access, share, sa, disp, flags, tmpl);
|
|
if (watch && h != INVALID_HANDLE_VALUE) WatchStartPositions(h, path, before);
|
|
return h;
|
|
}
|
|
|
|
BOOL WINAPI HookCloseHandle(HANDLE h) {
|
|
BOOL r = g_CloseHandle(h);
|
|
if (!g_watchCount) return r;
|
|
RolloverWatch w;
|
|
bool found = false;
|
|
EnterCriticalSection(&g_watchLock);
|
|
for (int i = 0; i < 8; ++i)
|
|
if (g_watchHandle[i] == h) {
|
|
g_watchHandle[i] = nullptr;
|
|
w = g_watch[i];
|
|
g_watch[i] = RolloverWatch();
|
|
InterlockedDecrement(&g_watchCount);
|
|
found = true;
|
|
break;
|
|
}
|
|
LeaveCriticalSection(&g_watchLock);
|
|
if (found) {
|
|
int added = 0;
|
|
std::string text = ReadWholeW(w.path), merged = MergeRollovers(text, w.groups, added);
|
|
bool ok = !added || WriteWholeW(w.path, merged);
|
|
char path[MAX_PATH * 2] = {};
|
|
WideCharToMultiByte(CP_ACP, 0, w.path.c_str(), -1, path, sizeof(path), nullptr, nullptr);
|
|
ds2::log().Write("start_positions.gas saved: %d clothmap_rollover line(s) put back%s (%s)", added, ok ? "" : " - WRITE FAILED", path);
|
|
}
|
|
return r;
|
|
}
|
|
|
|
void InstallRolloverKeeper() {
|
|
InitializeCriticalSection(&g_watchLock);
|
|
g_CreateFileA = static_cast<CreateFileA_t>(ds2::HookImport("KERNEL32.dll", "CreateFileA", reinterpret_cast<void*>(HookCreateFileA), "CreateFileA (clothmap_rollover)"));
|
|
g_CreateFileW = static_cast<CreateFileW_t>(ds2::HookImport("KERNEL32.dll", "CreateFileW", reinterpret_cast<void*>(HookCreateFileW), "CreateFileW (clothmap_rollover)"));
|
|
g_CloseHandle = static_cast<CloseHandle_t>(ds2::HookImport("KERNEL32.dll", "CloseHandle", reinterpret_cast<void*>(HookCloseHandle), "CloseHandle (clothmap_rollover)"));
|
|
if (!g_CloseHandle) g_CloseHandle = CloseHandle;
|
|
if (!g_CreateFileA) g_CreateFileA = CreateFileA;
|
|
if (!g_CreateFileW) g_CreateFileW = CreateFileW;
|
|
}
|
|
|
|
// The start-up question: which game to edit. A task dialog with two command links and "Remember my choice" (saved as
|
|
// [Toolkit] Game=ds2/bw); a Yes/No message box where task dialogs are not available.
|
|
void AskGame() {
|
|
ds2::Ini ini(g_iniPath);
|
|
using TaskDialogIndirect_t = HRESULT(WINAPI*)(const TASKDIALOGCONFIG*, int*, int*, BOOL*);
|
|
auto td = reinterpret_cast<TaskDialogIndirect_t>(GetProcAddress(LoadLibraryA("comctl32.dll"), "TaskDialogIndirect"));
|
|
bool bw = false, remember = false;
|
|
if (td) {
|
|
TASKDIALOG_BUTTON buttons[] = {
|
|
{100, L"Dungeon Siege II\nThe base game's maps and content"},
|
|
{101, L"Broken World\nThe expansion's maps, with both games' content loaded"},
|
|
};
|
|
TASKDIALOGCONFIG c = {sizeof(c)};
|
|
c.dwFlags = TDF_USE_COMMAND_LINKS | TDF_POSITION_RELATIVE_TO_WINDOW;
|
|
c.pszWindowTitle = L"Siege Editor 2";
|
|
c.pszMainIcon = TD_INFORMATION_ICON;
|
|
c.pszMainInstruction = L"Which game do you want to edit?";
|
|
c.pszContent = L"Hold Shift while Siege Editor 2 starts to see this question again.";
|
|
c.cButtons = 2;
|
|
c.pButtons = buttons;
|
|
c.nDefaultButton = 100;
|
|
c.pszVerificationText = L"Remember my choice";
|
|
int pressed = 100;
|
|
BOOL verified = FALSE;
|
|
if (SUCCEEDED(td(&c, &pressed, nullptr, &verified))) {
|
|
bw = pressed == 101;
|
|
remember = verified != FALSE;
|
|
}
|
|
} else {
|
|
bw = MessageBoxA(nullptr, "Edit Broken World?\n\nYes = Broken World (both games' content)\nNo = Dungeon Siege II",
|
|
"Siege Editor 2", MB_YESNO | MB_ICONQUESTION) == IDYES;
|
|
}
|
|
if (remember) ini.Set("Toolkit", "Game", bw ? "bw" : "ds2");
|
|
ds2::log().Write("start-up choice: %s%s", bw ? "Broken World" : "Dungeon Siege II", remember ? " (remembered)" : "");
|
|
if (bw) InstallBrokenWorldMode(ini);
|
|
g_quietErrors = g_bwMode && ini.Bool("Toolkit", "QuietErrors", false);
|
|
}
|
|
|
|
bool IsToolKitExe() {
|
|
static const char* const kTools[] = {"SiegeEditor2.exe", "DS2AnimViewer.exe", "DS2TankViewer.exe", "DungeonSiege2Mod.exe"};
|
|
char path[MAX_PATH];
|
|
GetModuleFileNameA(nullptr, path, MAX_PATH);
|
|
const char* base = std::strrchr(path, '\\');
|
|
base = base ? base + 1 : path;
|
|
for (const char* t : kTools)
|
|
if (_stricmp(base, t) == 0) return true;
|
|
return false;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
BOOL WINAPI DllMain(HMODULE self, DWORD reason, LPVOID) {
|
|
if (reason != DLL_PROCESS_ATTACH) return TRUE;
|
|
DisableThreadLibraryCalls(self);
|
|
if (!IsToolKitExe()) return TRUE; // e.g. the loader also sits next to the game: nothing to do there
|
|
char path[MAX_PATH];
|
|
ds2::SiblingPath(self, ".log", path, sizeof(path));
|
|
ds2::log().Open(path);
|
|
ds2::log().SetDetail(true); // the tool kit plugin keeps its own small log next to itself: everything goes in
|
|
ds2::SiblingPath(self, ".ini", path, sizeof(path));
|
|
ds2::Ini ini(path);
|
|
strcpy_s(g_iniPath, path);
|
|
GetModuleFileNameA(nullptr, g_toolDir, MAX_PATH); // the tool kit's folder ("dstk_path")
|
|
if (char* slash = std::strrchr(g_toolDir, '\\')) *slash = '\0';
|
|
ini.String("Toolkit", "GameDir", "", g_gameDir, sizeof(g_gameDir));
|
|
std::string dir = g_gameDir;
|
|
if (dir.empty()) {
|
|
std::string up = g_toolDir;
|
|
up = up.substr(0, up.find_last_of('\\')); // the folder above the tool kit's
|
|
if (HasGame(up)) dir = up;
|
|
else FindSteamGame(dir);
|
|
}
|
|
strncpy_s(g_gameDir, dir.c_str(), _TRUNCATE);
|
|
ds2::log().Write("tool kit folder %s", g_toolDir);
|
|
if (g_gameDir[0])
|
|
ds2::log().Write("game folder %s", g_gameDir);
|
|
else
|
|
ds2::log().Write("no Dungeon Siege 2 folder found - set [Toolkit] GameDir= in DS2Toolkit.ini");
|
|
g_RegOpenKeyExA = static_cast<RegOpenKeyExA_t>(ds2::HookImport("ADVAPI32.dll", "RegOpenKeyExA", reinterpret_cast<void*>(HookRegOpenKeyExA), "RegOpenKeyExA (tool kit keys)"));
|
|
g_RegOpenKeyA = static_cast<RegOpenKeyA_t>(ds2::HookImport("ADVAPI32.dll", "RegOpenKeyA", reinterpret_cast<void*>(HookRegOpenKeyA), "RegOpenKeyA (tool kit keys)"));
|
|
g_RegQueryValueExA = static_cast<RegQueryValueExA_t>(ds2::HookImport("ADVAPI32.dll", "RegQueryValueExA", reinterpret_cast<void*>(HookRegQueryValueExA), "RegQueryValueExA (tool kit keys)"));
|
|
g_RegQueryValueA = static_cast<RegQueryValueA_t>(ds2::HookImport("ADVAPI32.dll", "RegQueryValueA", reinterpret_cast<void*>(HookRegQueryValueA), "RegQueryValueA (tool kit keys)"));
|
|
g_RegCloseKey = static_cast<RegCloseKey_t>(ds2::HookImport("ADVAPI32.dll", "RegCloseKey", reinterpret_cast<void*>(HookRegCloseKey), "RegCloseKey (tool kit keys)"));
|
|
// Which game to edit: ds2toolkit_bw=1/0 on the command line, else [Toolkit] Game=bw/ds2, else (Broken World
|
|
// installed) a chooser at start-up - shown at the editor's first settings lookup, not here under the loader lock.
|
|
// Holding Shift while the editor starts asks again.
|
|
std::string bwArg = CommandValue("ds2toolkit_bw");
|
|
char game[16];
|
|
ini.String("Toolkit", "Game", "ask", game, sizeof(game));
|
|
bool shift = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0;
|
|
if (!g_gameDir[0] || !BrokenWorldInstalled(ini)) {
|
|
// only the base game
|
|
} else if (bwArg == "1" || (bwArg.empty() && !shift && !_stricmp(game, "bw"))) {
|
|
InstallBrokenWorldMode(ini);
|
|
} else if (bwArg.empty() && (shift || _stricmp(game, "ds2") != 0)) {
|
|
g_askGame = true;
|
|
}
|
|
// Broken World's enum names: whenever Broken World may be edited in this session (also when the start-up question
|
|
// is still to come - it is asked after the static initialisers ran). Base-game content never uses these names.
|
|
char exe[MAX_PATH];
|
|
GetModuleFileNameA(nullptr, exe, MAX_PATH);
|
|
const char* exeName = std::strrchr(exe, '\\');
|
|
const bool editor = !_stricmp(exeName ? exeName + 1 : exe, "SiegeEditor2.exe");
|
|
if (editor && (g_bwMode || g_askGame) && ds2::ExeTimestamp() == bwenums::kEditorTimestamp) ExtendEnums();
|
|
InstallSkipArchives(ini);
|
|
if (editor) InstallRolloverKeeper();
|
|
g_quietErrors = g_bwMode && ini.Bool("Toolkit", "QuietErrors", false);
|
|
g_autoMap = CommandValue("ds2toolkit_map");
|
|
g_autoRegion = CommandValue("ds2toolkit_region");
|
|
if (!g_autoMap.empty() || !g_autoRegion.empty())
|
|
ds2::log().Write("opening map %s region %s at start-up", g_autoMap.c_str(), g_autoRegion.c_str());
|
|
return TRUE;
|
|
}
|