226 lines
12 KiB
C++
226 lines
12 KiB
C++
// Start-up shared by the DS2 plugins (DS2Multiplayer, DS2Widescreen, DS2Fixes, DS2BrokenWorld).
|
|
//
|
|
// Every plugin works on its own and is turned off by deleting its .asi. Settings are optional: the plugins share
|
|
// one settings file, DS2Revival.ini, in the game folder next to DungeonSiege2.exe (one [section] per plugin; versions 1.2.0
|
|
// and 1.2.1 kept it in scripts, it is moved up on the first start). The zip does not contain it: the first plugin to start
|
|
// creates it from the default text compiled into the plugins (settings_file.h, which also moves the values of old per-plugin
|
|
// files into it) and later starts add settings it lacks. Without it (folder not writable, test instances) a plugin falls
|
|
// back to the old per-plugin file <its name>.ini in its scripts folder; without either, built-in defaults.
|
|
// If both exist DS2Revival.ini wins and the plugin logs that the old file is ignored.
|
|
// Logs go to scripts\logs (patch.h LogPath): <plugin>.log, <plugin>.bw.log for a plugin run by Broken World from the main
|
|
// scripts folder, <plugin>.<pid>.log in a ds2dev test instance. The scripts folder itself holds only plugins.
|
|
// Broken World (its own exe in a "Broken World" subfolder of the game) runs the same plugin files: "Broken World\scripts"
|
|
// holds only DS2Revival.asi (mods/revival_loader), which loads the release plugins from the main game's scripts folder
|
|
// into the Broken World process. Such a plugin reads the main game's settings file (the main game folder's, one file for both
|
|
// games), and deleting it there turns it off for both games. A Broken World folder on its own keeps the file in its folder. Older installs (and a Broken World folder on its own) hold full copies of the plugins in the Broken
|
|
// World folder; those copies follow the base game's scripts folder when it exists (they stay off when the same .asi was
|
|
// deleted there), else they stand alone. A plugin never starts when a module of the same name is already loaded.
|
|
#pragma once
|
|
#include "patch.h"
|
|
#include <psapi.h>
|
|
#include "settings_file.h"
|
|
|
|
namespace ds2 {
|
|
|
|
// Folder of the main exe (no trailing backslash).
|
|
inline void ExeDir(char* out, size_t cap) {
|
|
GetModuleFileNameA(nullptr, out, static_cast<DWORD>(cap));
|
|
if (char* s = std::strrchr(out, '\\')) *s = 0;
|
|
}
|
|
|
|
// A Dungeon Siege II install (its base resources are there).
|
|
inline bool LooksLikeDs2Folder(const char* dir) {
|
|
char p[MAX_PATH];
|
|
sprintf_s(p, "%s\\Resources\\Logic.ds2res", dir);
|
|
return FileExists(p);
|
|
}
|
|
|
|
// True when `dir` (no trailing backslash) is `parent` or inside it (case-insensitive).
|
|
inline bool InsideFolder(const char* dir, const char* parent) {
|
|
size_t n = std::strlen(parent);
|
|
return n && _strnicmp(dir, parent, n) == 0 && (dir[n] == 0 || dir[n] == '\\');
|
|
}
|
|
|
|
// The plugin file at `self` is in the folder of the running exe (or below it), not in another game's scripts folder.
|
|
inline bool InExeFolder(HMODULE self) {
|
|
char own[MAX_PATH], exe[MAX_PATH];
|
|
GetModuleFileNameA(self, own, MAX_PATH);
|
|
if (char* s = std::strrchr(own, '\\')) *s = 0;
|
|
ExeDir(exe, sizeof(exe));
|
|
return InsideFolder(own, exe);
|
|
}
|
|
|
|
// Another module with the same file name as `self` (a second copy of the plugin, e.g. in "Broken World\scripts") is
|
|
// loaded in this process: its path, else empty.
|
|
inline std::string OtherCopyLoaded(HMODULE self) {
|
|
char mine[MAX_PATH];
|
|
GetModuleFileNameA(self, mine, MAX_PATH);
|
|
const char* slash = std::strrchr(mine, '\\');
|
|
const char* name = slash ? slash + 1 : mine;
|
|
HMODULE mods[1024];
|
|
DWORD bytes = 0;
|
|
if (!K32EnumProcessModules(GetCurrentProcess(), mods, sizeof(mods), &bytes)) return "";
|
|
for (DWORD i = 0; i < bytes / sizeof(HMODULE) && i < 1024; ++i) {
|
|
char path[MAX_PATH];
|
|
if (mods[i] == self || !GetModuleFileNameA(mods[i], path, MAX_PATH)) continue;
|
|
const char* file = std::strrchr(path, '\\');
|
|
if (file && _stricmp(file + 1, name) == 0) return path;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
struct Plugin {
|
|
Exe exe = Exe::Unknown;
|
|
char ini[MAX_PATH] = {}; // settings file (may not exist: defaults apply)
|
|
char baseDir[MAX_PATH] = {}; // Broken World: the folder above its own (normally the base game); else empty
|
|
};
|
|
|
|
// Chooses p.ini (see the top of this file) and logs which file is used. `gameDir` = the folder where DS2Revival.ini lives (next
|
|
// to DungeonSiege2.exe, or for Broken World started by the Steam game: the main game folder); `oldDirs` = folders whose old
|
|
// <name>.ini may stand in for it, best first (the plugins' scripts folders). Without any file p.ini is the place where the
|
|
// shared file would be (the old file's place for a plugin that does not use it).
|
|
inline void ChooseSettings(HMODULE self, const char* name, bool shared, const char* gameDir, const std::vector<std::string>& oldDirs, Plugin& p) {
|
|
char revival[MAX_PATH], ignored[MAX_PATH] = {};
|
|
sprintf_s(revival, "%s\\%s", gameDir, kSharedIni);
|
|
bool found = false;
|
|
auto oldFile = [&](const std::string& dir, char* out) {
|
|
sprintf_s(out, MAX_PATH, "%s\\%s.ini", dir.c_str(), name);
|
|
return FileExists(out);
|
|
};
|
|
if (shared && FileExists(revival)) {
|
|
strcpy_s(p.ini, revival);
|
|
found = true;
|
|
for (const std::string& dir : oldDirs)
|
|
if (oldFile(dir, ignored)) break;
|
|
else ignored[0] = 0;
|
|
} else {
|
|
for (const std::string& dir : oldDirs)
|
|
if (oldFile(dir, p.ini)) { found = true; break; }
|
|
}
|
|
if (!found) {
|
|
if (shared) strcpy_s(p.ini, revival);
|
|
else SiblingPath(self, ".ini", p.ini, sizeof(p.ini));
|
|
log().Write("settings: no %s found - built-in defaults", shared ? kSharedIni : "settings file");
|
|
return;
|
|
}
|
|
const char* file = std::strrchr(p.ini, '\\');
|
|
if (!shared || (file && !_stricmp(file + 1, kSharedIni)))
|
|
log().Write("settings: %s", p.ini);
|
|
else
|
|
log().Write("settings: %s (the old per-plugin file; %s does not exist)", p.ini, kSharedIni);
|
|
if (ignored[0]) log().Write("settings: %s is ignored because %s exists (it wins)", ignored, kSharedIni);
|
|
}
|
|
|
|
// Opens the plugin's log, identifies the exe and finds the settings (shared = DS2Revival.ini applies; the
|
|
// diagnostic probes and the opt-in controller / tactics plugins pass false and keep their own <name>.ini). False = apply nothing (unknown exe, or a Broken
|
|
// World copy whose twin was deleted from the base game's scripts folder).
|
|
// [Plugins] <name>=... in the settings file: 1 = on in both games (the default), 0 = off without deleting the .asi,
|
|
// ds2 = Dungeon Siege II only, bw = Broken World only (Broken World uses the main game's file, so one file covers both).
|
|
inline bool SwitchedOn(const char* name, const Plugin& p) {
|
|
char v[32];
|
|
Ini(p.ini).String("Plugins", name, "1", v, sizeof(v));
|
|
char* e = v + std::strlen(v);
|
|
while (e > v && (e[-1] == ' ' || e[-1] == '\t')) *--e = 0;
|
|
const char* t = v + std::strspn(v, " \t");
|
|
const bool inBw = p.exe == Exe::BrokenWorld;
|
|
if (!_stricmp(t, "ds2") || !_stricmp(t, "bw")) {
|
|
if ((_stricmp(t, "bw") == 0) == inBw) return true;
|
|
log().Write("%s is off in %s: [Plugins] %s=%s in %s", name, inBw ? "Broken World" : "Dungeon Siege II", name, t, p.ini);
|
|
return false;
|
|
}
|
|
const bool number = (*t >= '0' && *t <= '9') || *t == '-';
|
|
const bool words = !_stricmp(t, "on") || !_stricmp(t, "off") || !_stricmp(t, "true") || !_stricmp(t, "false") ||
|
|
!_stricmp(t, "yes") || !_stricmp(t, "no");
|
|
if (*t && !number && !words) log().Write("[Plugins] %s=%s is not 1, 0, ds2 or bw - treated as 1", name, t);
|
|
const bool off = (number && std::atoi(t) == 0) || !_stricmp(t, "off") || !_stricmp(t, "false") || !_stricmp(t, "no");
|
|
if (!off) return true;
|
|
log().Write("%s is off: [Plugins] %s=0 in %s", name, name, p.ini);
|
|
return false;
|
|
}
|
|
|
|
inline bool StartPlugin(HMODULE self, const char* name, Plugin& p, bool shared = true) {
|
|
char path[MAX_PATH], ext[32] = ".log";
|
|
if (GetEnvironmentVariableA("DS2DEV_DOCS", nullptr, 0)) // test instances (several per PC): one log each
|
|
sprintf_s(ext, ".%lu.log", GetCurrentProcessId());
|
|
else if (ExeKind() == Exe::BrokenWorld && !InExeFolder(self)) // run from the main scripts folder by Broken World: keep the Steam game's log
|
|
strcpy_s(ext, ".bw.log");
|
|
LogPath(self, ext, path, sizeof(path)); // <scripts>\logs\<name><ext>
|
|
log().Open(path);
|
|
p.exe = ExeKind();
|
|
log().Write("%s loaded; exe timestamp %08X (%s)", name, ExeTimestamp(),
|
|
p.exe == Exe::Steam23 ? "Steam v2.3" : p.exe == Exe::BrokenWorld ? "Broken World" : "unknown");
|
|
if (p.exe == Exe::Unknown) {
|
|
log().Write("unknown DungeonSiege2.exe build - no patches applied");
|
|
return false;
|
|
}
|
|
std::string other = OtherCopyLoaded(self);
|
|
if (!other.empty()) {
|
|
log().Write("%s is already loaded from %s - this copy stays off (a plugin never runs twice)", name, other.c_str());
|
|
return false;
|
|
}
|
|
// An older install's all-in-one ds2fix.asi beside us: the same hooks twice would break the display and the
|
|
// relay. The first plugin to start renames it to ds2fix.asi.old; if it was not loaded yet the loader can no
|
|
// longer load it and the new plugins take over at once. If it is already loaded (or cannot be renamed) it
|
|
// handles this session and the new plugins stay off until the next start.
|
|
char old[MAX_PATH];
|
|
GetModuleFileNameA(self, old, MAX_PATH);
|
|
if (char* s = std::strrchr(old, '\\')) strcpy_s(s + 1, MAX_PATH - (s + 1 - old), "ds2fix.asi");
|
|
bool renamed = false;
|
|
if (FileExists(old)) {
|
|
char to[MAX_PATH];
|
|
sprintf_s(to, "%s.old", old);
|
|
renamed = MoveFileExA(old, to, MOVEFILE_REPLACE_EXISTING) != 0;
|
|
log().Write(renamed ? "renamed the old %s to ds2fix.asi.old (replaced by the separate plugins)"
|
|
: "could not rename the old %s - remove it to use the separate plugins", old);
|
|
}
|
|
if (GetModuleHandleA("ds2fix.asi") || (FileExists(old) && !renamed)) {
|
|
log().Write("the old ds2fix.asi handles this session - %s stays off until the next start", name);
|
|
return false;
|
|
}
|
|
char own[MAX_PATH]; // this plugin's folder
|
|
GetModuleFileNameA(self, own, MAX_PATH);
|
|
const char* file = std::strrchr(own, '\\');
|
|
std::string ownFile = file ? file : "\\"; // "\DS2Fixes.asi"
|
|
if (char* s = std::strrchr(own, '\\')) *s = 0;
|
|
char exeDir[MAX_PATH];
|
|
ExeDir(exeDir, sizeof(exeDir)); // the folder of DungeonSiege2.exe: where DS2Revival.ini lives, unless Broken World follows the main game
|
|
if (p.exe != Exe::BrokenWorld) {
|
|
if (shared) { // old files of a Broken World copy are retired with them
|
|
std::vector<std::string> others;
|
|
std::string bw = std::string(exeDir) + "\\Broken World\\scripts";
|
|
if (FileExists(bw.c_str())) others.push_back(bw);
|
|
settings::Ensure(exeDir, own, others);
|
|
}
|
|
ChooseSettings(self, name, shared, exeDir, {own}, p);
|
|
return SwitchedOn(name, p);
|
|
}
|
|
strcpy_s(p.baseDir, exeDir);
|
|
if (char* s = std::strrchr(p.baseDir, '\\')) *s = 0;
|
|
if (!InExeFolder(self)) { // loaded by DS2Revival.asi from the main game's scripts folder: this is the main set (no twin to check)
|
|
if (shared) {
|
|
std::vector<std::string> others;
|
|
std::string bwScripts = std::string(exeDir) + "\\scripts";
|
|
if (FileExists(bwScripts.c_str())) others.push_back(bwScripts);
|
|
settings::Ensure(p.baseDir, own, others); // the main game's file
|
|
}
|
|
ChooseSettings(self, name, shared, p.baseDir, {own}, p);
|
|
return SwitchedOn(name, p);
|
|
}
|
|
char scripts[MAX_PATH], twin[MAX_PATH];
|
|
sprintf_s(scripts, "%s\\scripts", p.baseDir);
|
|
if (!FileExists(scripts)) { // no plugins in the base game: this copy stands alone (its settings file is in its own folder)
|
|
if (shared) settings::Ensure(exeDir, own, {});
|
|
ChooseSettings(self, name, shared, exeDir, {own}, p);
|
|
return SwitchedOn(name, p);
|
|
}
|
|
sprintf_s(twin, "%s%s", scripts, ownFile.c_str());
|
|
if (!FileExists(twin)) {
|
|
log().Write("%s is not in %s - off for Broken World too", ownFile.c_str() + 1, scripts);
|
|
return false;
|
|
}
|
|
if (shared) settings::Ensure(p.baseDir, scripts, {own}); // the main game's file (never one in Broken World)
|
|
ChooseSettings(self, name, shared, p.baseDir, {scripts, own}, p); // the base game's file first, else an old file of this copy
|
|
return SwitchedOn(name, p);
|
|
}
|
|
|
|
} // namespace ds2
|