DS2Revival/mods/revival_loader/revival_loader.cpp
DS2Revival a1e2f8f9b5
Some checks failed
build / build (push) Failing after 1m35s
CI: build on the project's own runner at git.dsii.uk
2026-10-27 22:02:02 -04:00

120 lines
6.4 KiB
C++

// DS2Revival.asi - the Broken World side of the plugin set. It is the only plugin in "Broken World\scripts".
//
// In a Broken World process it loads the release plugins from the main game's scripts folder (..\scripts of the
// Broken World folder), so both games run the very same plugin files: one set to install and to update, one settings file
// (the main game's DS2Revival.ini), one [Plugins] switch per plugin. Each plugin finds out for itself (mods/common/
// plugin.h) that it runs in Broken World from the main folder.
//
// Never twice: when "Broken World\scripts" (or another folder the ASI loader reads) still holds a full copy of a plugin from
// an older install, the plugin of that name is not loaded from the main folder - the old copy keeps working as before - and
// the log tells the player to delete the old copies. A plugin also refuses to start when a module of its name is already
// loaded (plugin.h), which covers copies this check cannot see.
//
// Without a main game next to the Broken World folder (a Broken World folder on its own) it does nothing: the folder's own
// plugins stand alone. In the Steam game it does nothing.
//
// Its log is the main game's scripts\logs\DS2Revival.log (in a Broken World folder on its own: its own scripts\logs).
#include "../common/plugin.h"
namespace {
// The plugins that may be loaded, in the order the ASI loader used to load them (alphabetical): the release plugins
// (mods/build.py RELEASE; tools/test_settings_ini.py checks the list) and the opt-in controller preview.
const char* const kPlugins[] = {"DS2BrokenWorld", "DS2Controller", "DS2Fixes", "DS2Graphics", "DS2ModLoader", "DS2Multiplayer", "DS2Widescreen"};
// Folders of the Broken World exe in which the ASI loader looks for .asi files.
const char* const kLoaderFolders[] = {"", "\\scripts", "\\plugins", "\\update"};
// The main game's scripts folder (next to the Broken World folder, with the base game's exe above it): true and the path in
// `scripts`, else false. `base` (optional) gets the main game's folder.
bool MainScripts(char* scripts, size_t cap, char* base = nullptr) {
char bwDir[MAX_PATH], parent[MAX_PATH], baseExe[MAX_PATH];
ds2::ExeDir(bwDir, sizeof(bwDir));
strcpy_s(parent, bwDir);
if (char* s = std::strrchr(parent, '\\')) *s = 0;
sprintf_s(baseExe, "%s\\DungeonSiege2.exe", parent);
sprintf_s(scripts, cap, "%s\\scripts", parent);
if (base) strcpy_s(base, MAX_PATH, parent);
return ds2::FileExists(scripts) && ds2::FileExists(baseExe);
}
void LoadFromMainGame() {
char bwDir[MAX_PATH], base[MAX_PATH], scripts[MAX_PATH];
ds2::ExeDir(bwDir, sizeof(bwDir));
if (!MainScripts(scripts, sizeof(scripts), base)) {
ds2::log().Write("no Dungeon Siege II with a scripts folder next to the Broken World folder (%s): nothing to load, "
"the plugins in this folder run on their own", base);
return;
}
ds2::log().Write("main game: %s", base);
std::string oldCopies;
int loaded = 0, skipped = 0;
for (const char* name : kPlugins) {
char file[MAX_PATH];
sprintf_s(file, "%s\\%s.asi", scripts, name);
if (!ds2::FileExists(file)) {
ds2::log().Write("%s: not in %s (deleted there = off for Broken World too)", name, scripts);
continue;
}
std::string old; // a copy of the same plugin that the ASI loader loads from the Broken World folder
for (const char* sub : kLoaderFolders) {
char copy[MAX_PATH];
sprintf_s(copy, "%s%s\\%s.asi", bwDir, sub, name);
if (ds2::FileExists(copy)) { old = copy; break; }
}
if (!old.empty()) {
ds2::log().Write("%s: NOT loaded from the main game - an old copy is here: %s (it keeps working as before)", name, old.c_str());
oldCopies += (oldCopies.empty() ? "" : ", ") + old.substr(std::strlen(bwDir) + 1);
++skipped;
continue;
}
if (HMODULE m = GetModuleHandleA((std::string(name) + ".asi").c_str())) {
char at[MAX_PATH] = {};
GetModuleFileNameA(m, at, MAX_PATH);
ds2::log().Write("%s: already loaded from %s - not loaded again", name, at);
++skipped;
continue;
}
HMODULE m = LoadLibraryA(file);
if (m) {
ds2::log().Write("%s: loaded from %s", name, file);
++loaded;
} else {
ds2::log().Write("%s: FAILED to load %s (Windows error %lu)", name, file, GetLastError());
}
}
if (!oldCopies.empty())
ds2::log().Write("old copies in Broken World\\scripts: delete them (%s). Broken World runs the plugins of the main game's "
"scripts folder; the old copies are older versions and are why those plugins are not loaded from there.",
oldCopies.c_str());
ds2::log().Write("done: %d plugin(s) loaded from the main game, %d left to old copies / already loaded", loaded, skipped);
}
} // namespace
BOOL WINAPI DllMain(HMODULE self, DWORD reason, LPVOID) {
if (reason != DLL_PROCESS_ATTACH) return TRUE;
DisableThreadLibraryCalls(self);
// The log is the run's shared one (patch.h Log::OpenShared): the main game's scripts\logs\DS2Revival.bw.log when this runs in the
// Steam game's Broken World folder (the plugins it loads write the same file), else (a Broken World folder on its own) its own
// scripts folder.
char own[MAX_PATH], mainScripts[MAX_PATH];
GetModuleFileNameA(self, own, MAX_PATH);
if (char* s = std::strrchr(own, '\\')) *s = 0;
const bool inMainGame = MainScripts(mainScripts, sizeof(mainScripts));
ds2::RetireOldLogs(own); // the loader's own old log (the main game's folder is cleaned by its plugins)
ds2::log().OpenShared(inMainGame ? mainScripts : own, "DS2Revival", ds2::ExeKind() == ds2::Exe::BrokenWorld && inMainGame);
ds2::Exe exe = ds2::ExeKind();
ds2::log().Write("DS2Revival loaded; exe timestamp %08X (%s)", ds2::ExeTimestamp(),
exe == ds2::Exe::Steam23 ? "Steam v2.3" : exe == ds2::Exe::BrokenWorld ? "Broken World" : "unknown");
if (exe != ds2::Exe::BrokenWorld) {
ds2::log().Write("not Broken World: nothing to do (this plugin belongs in \"Broken World\\scripts\" only)");
return TRUE;
}
if (ds2::OtherCopyLoaded(self).size()) {
ds2::log().Write("another DS2Revival.asi is loaded: nothing to do");
return TRUE;
}
LoadFromMainGame();
return TRUE;
}