DS2Revival/mods/common/plugin.h

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