aboutsummaryrefslogtreecommitdiff
path: root/libs/installer_fomod_plus/installer/lib
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-14 02:45:12 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-14 02:45:25 -0600
commit817e8f5cd26739c69d930d21cd9dc4c0b6e4984e (patch)
tree52aed11d6751bb7d20b06acf197d56fac113203d /libs/installer_fomod_plus/installer/lib
parent51a9f8f197727f00896e5de44569b098923527dd (diff)
Add FUSE external mapping support, BG3/Oblivion Remastered fixes, fomod-plus and NaK integration
FUSE VFS now deploys non-data-dir mod mappings (Paks, OBSE, UE4SS, etc.) via real symlinks and injects file-level data-dir mappings (plugins.txt, loadorder.txt) into the VFS tree. Fixes game launches for Oblivion Remastered (Root Builder path resolution, script extender support) and BG3 (Wine prefix documents directory, file mapper symlinks on Linux). Vendors mo2-fomod-plus plugin and NaK crate for FOMOD installer and game finder/runtime support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/installer_fomod_plus/installer/lib')
-rw-r--r--libs/installer_fomod_plus/installer/lib/ConditionTester.cpp176
-rw-r--r--libs/installer_fomod_plus/installer/lib/ConditionTester.h42
-rw-r--r--libs/installer_fomod_plus/installer/lib/CrashHandler.cpp122
-rw-r--r--libs/installer_fomod_plus/installer/lib/CrashHandler.h20
-rw-r--r--libs/installer_fomod_plus/installer/lib/FileInstaller.cpp274
-rw-r--r--libs/installer_fomod_plus/installer/lib/FileInstaller.h94
-rw-r--r--libs/installer_fomod_plus/installer/lib/FlagMap.h113
-rw-r--r--libs/installer_fomod_plus/installer/lib/Logger.h118
-rw-r--r--libs/installer_fomod_plus/installer/lib/ViewModels.h120
9 files changed, 1079 insertions, 0 deletions
diff --git a/libs/installer_fomod_plus/installer/lib/ConditionTester.cpp b/libs/installer_fomod_plus/installer/lib/ConditionTester.cpp
new file mode 100644
index 0000000..c35bf47
--- /dev/null
+++ b/libs/installer_fomod_plus/installer/lib/ConditionTester.cpp
@@ -0,0 +1,176 @@
+#include "ConditionTester.h"
+
+#include "ui/FomodViewModel.h"
+
+#include <iplugingame.h>
+#include <ipluginlist.h>
+
+std::string setToString(const std::set<int> &set)
+{
+ std::string str;
+ for (const auto& i : set) {
+ str += std::to_string(i) + ", ";
+ }
+ return str;
+}
+
+bool ConditionTester::isStepVisible(const std::shared_ptr<FlagMap>& flags,
+ const CompositeDependency& compositeDependency,
+ const int stepIndex,
+ const std::vector<std::shared_ptr<StepViewModel>>& steps) const
+{
+
+ // first things first: is it visible?
+ if (!testCompositeDependency(flags, compositeDependency)) {
+ return false;
+ }
+
+ const auto flagDependencies = compositeDependency.flagDependencies;
+ if (flagDependencies.empty()) {
+ return true;
+ }
+
+ std::set<int> stepsThatSetThisFlag;
+
+ for (const auto& flagDependency : flagDependencies) {
+ // for this flag, find the plugins that set it
+ for (int i = stepIndex - 1; i >= 0; --i) {
+ for (const auto& group : steps[i]->getGroups()) {
+ for (const auto& plugin : group->getPlugins()) {
+ if (std::ranges::any_of(plugin->getPlugin()->conditionFlags.flags,
+ [&flagDependency](const ConditionFlag& flag) {
+ return flag.name == flagDependency.flag && flag.value == flagDependency.value;
+ })) {
+ stepsThatSetThisFlag.insert(i);
+ }
+ }
+ }
+ }
+ }
+ const auto anyVisible = std::ranges::any_of(stepsThatSetThisFlag, [this, &steps, &flags](const int index) {
+ return isStepVisible(flags, steps[index]->getVisibilityConditions(), index, steps);
+ });
+ if (!anyVisible) {
+ log.logMessage(DEBUG, "Step " + steps[stepIndex]->getName() + " has no dependent steps that are visible.");
+ log.logMessage(DEBUG, "Steps that set this flag: " + setToString(stepsThatSetThisFlag));
+ }
+ return anyVisible;
+
+}
+
+bool ConditionTester::testCompositeDependency(const std::shared_ptr<FlagMap>& flags,
+ const CompositeDependency& compositeDependency) const
+{
+ const auto fileDependencies = compositeDependency.fileDependencies;
+ const auto flagDependencies = compositeDependency.flagDependencies;
+ const auto gameDependencies = compositeDependency.gameDependencies;
+ const auto nestedDependencies = compositeDependency.nestedDependencies;
+ const auto globalOperatorType = compositeDependency.operatorType;
+
+ // For the globalOperatorType
+ // Evaluate all conditions and store the results in a vector<bool>, then return based on operator.
+ // These aren't expensive to calculate so rather than do some fancy logic to short-circuit, just calculate all of 'em.
+ std::vector<bool> results;
+ for (const auto& fileDependency : fileDependencies) {
+ results.emplace_back(testFileDependency(fileDependency));
+ }
+ for (const auto& flagDependency : flagDependencies) {
+ results.emplace_back(testFlagDependency(flags, flagDependency));
+ }
+ for (const auto& gameDependency : gameDependencies) {
+ results.emplace_back(testGameDependency(gameDependency));
+ }
+ for (const auto& nestedDependency : nestedDependencies) {
+ results.emplace_back(testCompositeDependency(flags, nestedDependency));
+ }
+
+ if (globalOperatorType == OperatorTypeEnum::AND) {
+ return std::ranges::all_of(results, [](const bool result) { return result; });
+ }
+ return std::ranges::any_of(results, [](const bool result) { return result; });
+}
+
+
+bool ConditionTester::testFlagDependency(const std::shared_ptr<FlagMap>& flags, const FlagDependency& flagDependency)
+{
+ // Every instance of this flag being set in the map.
+ const auto flagList = flags->getFlagsByKey(flagDependency.flag);
+
+ // Find the first instance of this flag being set (in the order specified by getFlagsByKey)
+ if (flagList.empty()) {
+ // If the dependency value is an empty string, it means this flag should be unset.
+ // So if we don't have any value for this flag, the result is true.
+ return flagDependency.value.empty();
+ }
+
+ return flagList.front().second == flagDependency.value;
+}
+
+bool ConditionTester::testFileDependency(const FileDependency& fileDependency) const
+{
+ const std::string& pluginName = fileDependency.file;
+ const auto pluginState = getFileDependencyStateForPlugin(pluginName);
+ return pluginState == fileDependency.state;
+}
+
+bool ConditionTester::testGameDependency(const GameDependency& gameDependency) const
+{
+ const auto gameVersion = mOrganizer->managedGame()->gameVersion().toStdString();
+ log.logMessage(DEBUG, "Comparing condition version " + gameDependency.version + " against " + gameVersion);
+ if ( gameDependency.version <= gameVersion) {
+ log.logMessage(DEBUG, "Version matches!");
+ }
+ return gameDependency.version <= gameVersion;
+}
+
+FileDependencyTypeEnum ConditionTester::getFileDependencyStateForPlugin(const std::string& pluginName) const
+{
+ if (const auto it = pluginStateCache.find(pluginName); it != pluginStateCache.end()) {
+ return it->second;
+ }
+
+ const QFlags<MOBase::IPluginList::PluginState> pluginState = mOrganizer->pluginList()->state(
+ QString::fromStdString(pluginName));
+
+ FileDependencyTypeEnum state;
+
+ if (pluginState == MOBase::IPluginList::STATE_MISSING) {
+ state = FileDependencyTypeEnum::Missing;
+ } else if (pluginState == MOBase::IPluginList::STATE_INACTIVE) {
+ state = FileDependencyTypeEnum::Inactive;
+ } else if (pluginState == MOBase::IPluginList::STATE_ACTIVE) {
+ state = FileDependencyTypeEnum::Active;
+ } else {
+ state = FileDependencyTypeEnum::UNKNOWN_STATE;
+ }
+
+ pluginStateCache[pluginName] = state;
+ return state;
+}
+
+PluginTypeEnum ConditionTester::getPluginTypeDescriptorState(const std::shared_ptr<Plugin>& plugin,
+ const std::shared_ptr<FlagMap>& flags) const
+{
+ // NOTE: A plugin's ConditionFlags aren't the same thing as a step visibility one.
+ // A plugin's ConditionFlags are toggled based on the selection state of the plugin
+ // We only evaluate the typeDescriptor here.
+
+ // We will return the 'winning' type or the default. If multiple conditions are met,
+ // ...well, I'm not sure.
+ // ReSharper disable once CppTooWideScopeInitStatement
+ const auto& dependencyType = plugin->typeDescriptor.dependencyType;
+ for (const auto& pattern : dependencyType.patterns.patterns) {
+ if (testCompositeDependency(flags, pattern.dependencies)) {
+ return pattern.type;
+ }
+ }
+
+ // Sometimes authors do this.
+ if (plugin->typeDescriptor.type != PluginTypeEnum::Optional) {
+ return plugin->typeDescriptor.type;
+ }
+ if (plugin->typeDescriptor.dependencyType.defaultType.has_value()) {
+ return plugin->typeDescriptor.dependencyType.defaultType.value();
+ }
+ return PluginTypeEnum::Optional;
+} \ No newline at end of file
diff --git a/libs/installer_fomod_plus/installer/lib/ConditionTester.h b/libs/installer_fomod_plus/installer/lib/ConditionTester.h
new file mode 100644
index 0000000..09aa26e
--- /dev/null
+++ b/libs/installer_fomod_plus/installer/lib/ConditionTester.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <imoinfo.h>
+
+#include "FlagMap.h"
+#include "Logger.h"
+#include "xml/ModuleConfiguration.h"
+
+class CompositeDependency;
+class StepViewModel;
+
+class ConditionTester {
+public:
+ explicit ConditionTester(MOBase::IOrganizer* organizer) : mOrganizer(organizer) {}
+
+ bool isStepVisible(const std::shared_ptr<FlagMap>& flags, const CompositeDependency& compositeDependency,
+ int stepIndex,
+ const std::vector<std::shared_ptr<StepViewModel>>& steps) const;
+
+ bool testCompositeDependency(const std::shared_ptr<FlagMap>& flags,
+ const CompositeDependency& compositeDependency) const;
+
+ static bool testFlagDependency(const std::shared_ptr<FlagMap>& flags, const FlagDependency& flagDependency);
+
+ [[nodiscard]] bool testFileDependency(const FileDependency& fileDependency) const;
+
+ bool testGameDependency(const GameDependency& gameDependency) const;
+
+private:
+ Logger& log = Logger::getInstance();
+ MOBase::IOrganizer* mOrganizer;
+
+ friend class FomodViewModel;
+
+ [[nodiscard]] FileDependencyTypeEnum getFileDependencyStateForPlugin(const std::string& pluginName) const;
+
+ PluginTypeEnum getPluginTypeDescriptorState(const std::shared_ptr<Plugin>& plugin,
+ const std::shared_ptr<FlagMap>& flags) const;
+
+ mutable std::unordered_map<std::string, FileDependencyTypeEnum> pluginStateCache;
+
+};
diff --git a/libs/installer_fomod_plus/installer/lib/CrashHandler.cpp b/libs/installer_fomod_plus/installer/lib/CrashHandler.cpp
new file mode 100644
index 0000000..98ed1f3
--- /dev/null
+++ b/libs/installer_fomod_plus/installer/lib/CrashHandler.cpp
@@ -0,0 +1,122 @@
+#include "CrashHandler.h"
+#include <iostream>
+
+#ifdef _WIN32
+#include <windows.h>
+#include <dbghelp.h>
+#include <sstream>
+#include <fstream>
+
+LPTOP_LEVEL_EXCEPTION_FILTER CrashHandler::previousFilter = nullptr;
+PVOID CrashHandler::vectoredHandler = nullptr;
+#endif
+
+void CrashHandler::initialize() {
+#ifdef _WIN32
+ std::cout << "CrashHandler initializing..." << std::endl;
+ vectoredHandler = AddVectoredExceptionHandler(1, vectoredExceptionHandler);
+ previousFilter = SetUnhandledExceptionFilter(unhandledExceptionFilter);
+ std::cout << "CrashHandler initialized successfully" << std::endl;
+#else
+ // No-op on Linux — Windows crash handler not needed.
+#endif
+}
+
+void CrashHandler::cleanup() {
+#ifdef _WIN32
+ if (vectoredHandler) {
+ RemoveVectoredExceptionHandler(vectoredHandler);
+ vectoredHandler = nullptr;
+ }
+ if (previousFilter) {
+ SetUnhandledExceptionFilter(previousFilter);
+ previousFilter = nullptr;
+ }
+#endif
+}
+
+#ifdef _WIN32
+LONG WINAPI CrashHandler::vectoredExceptionHandler(EXCEPTION_POINTERS* exceptionInfo) {
+ // Log all exceptions but only create dumps for serious ones
+ std::cout << "Exception caught: 0x" << std::hex << exceptionInfo->ExceptionRecord->ExceptionCode << std::endl;
+
+ // Only create dumps for serious crashes, not benign exceptions
+ if (exceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION ||
+ exceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_STACK_OVERFLOW ||
+ exceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_ILLEGAL_INSTRUCTION) {
+
+ // For stack overflow, we need to handle it on a different thread with its own stack
+ if (exceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_STACK_OVERFLOW) {
+ // Create a new thread to handle the crash dump since our stack is corrupted
+ HANDLE dumpThread = CreateThread(nullptr, 0, [](LPVOID param) -> DWORD {
+ auto* exceptionInfo = static_cast<EXCEPTION_POINTERS*>(param);
+ logCrashInfo(exceptionInfo);
+
+ HANDLE file = CreateFileA("fomod_plus_stack_overflow.dmp", GENERIC_WRITE, 0, nullptr,
+ CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
+
+ if (file != INVALID_HANDLE_VALUE) {
+ MINIDUMP_EXCEPTION_INFORMATION dumpInfo = {0};
+ dumpInfo.ThreadId = GetCurrentThreadId();
+ dumpInfo.ExceptionPointers = exceptionInfo;
+ dumpInfo.ClientPointers = FALSE;
+
+ MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), file,
+ MiniDumpNormal, &dumpInfo, nullptr, nullptr);
+ CloseHandle(file);
+
+ std::cout << "Stack overflow crash dump written to fomod_plus_stack_overflow.dmp" << std::endl;
+ }
+ return 0;
+ }, exceptionInfo, 0, nullptr);
+
+ if (dumpThread) {
+ WaitForSingleObject(dumpThread, 5000); // Wait up to 5 seconds
+ CloseHandle(dumpThread);
+ }
+ } else {
+ logCrashInfo(exceptionInfo);
+
+ // Generate minidump
+ HANDLE file = CreateFileA("fomod_plus_crash.dmp", GENERIC_WRITE, 0, nullptr,
+ CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
+
+ if (file != INVALID_HANDLE_VALUE) {
+ MINIDUMP_EXCEPTION_INFORMATION dumpInfo = {0};
+ dumpInfo.ThreadId = GetCurrentThreadId();
+ dumpInfo.ExceptionPointers = exceptionInfo;
+ dumpInfo.ClientPointers = FALSE;
+
+ MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), file,
+ MiniDumpNormal, &dumpInfo, nullptr, nullptr);
+ CloseHandle(file);
+
+ std::cout << "Crash dump written to fomod_plus_crash.dmp" << std::endl;
+ }
+ }
+ }
+
+ return EXCEPTION_CONTINUE_SEARCH; // Let other handlers process it
+}
+
+LONG WINAPI CrashHandler::unhandledExceptionFilter(EXCEPTION_POINTERS* exceptionInfo) {
+ logCrashInfo(exceptionInfo);
+ return EXCEPTION_EXECUTE_HANDLER;
+}
+
+void CrashHandler::logCrashInfo(const EXCEPTION_POINTERS* exceptionInfo) {
+ std::ostringstream oss;
+ oss << "FOMOD PLUS CRASH DETECTED!" << std::endl;
+ oss << "Exception Code: 0x" << std::hex << exceptionInfo->ExceptionRecord->ExceptionCode << std::endl;
+ oss << "Exception Address: 0x" << std::hex << exceptionInfo->ExceptionRecord->ExceptionAddress << std::endl;
+
+ // Write to stdout immediately
+ std::cout << oss.str() << std::endl;
+
+ // Also write to crash log file
+ std::ofstream crashLog("fomod_plus_crash.log", std::ios::app);
+ if (crashLog.is_open()) {
+ crashLog << oss.str() << std::endl;
+ }
+}
+#endif
diff --git a/libs/installer_fomod_plus/installer/lib/CrashHandler.h b/libs/installer_fomod_plus/installer/lib/CrashHandler.h
new file mode 100644
index 0000000..78c8552
--- /dev/null
+++ b/libs/installer_fomod_plus/installer/lib/CrashHandler.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#ifdef _WIN32
+#include <windows.h>
+#endif
+
+class CrashHandler {
+public:
+ static void initialize();
+ static void cleanup();
+
+private:
+#ifdef _WIN32
+ static LONG WINAPI unhandledExceptionFilter(EXCEPTION_POINTERS* exceptionInfo);
+ static LONG WINAPI vectoredExceptionHandler(EXCEPTION_POINTERS* exceptionInfo);
+ static void logCrashInfo(const EXCEPTION_POINTERS* exceptionInfo);
+ static LPTOP_LEVEL_EXCEPTION_FILTER previousFilter;
+ static PVOID vectoredHandler;
+#endif
+};
diff --git a/libs/installer_fomod_plus/installer/lib/FileInstaller.cpp b/libs/installer_fomod_plus/installer/lib/FileInstaller.cpp
new file mode 100644
index 0000000..e47e088
--- /dev/null
+++ b/libs/installer_fomod_plus/installer/lib/FileInstaller.cpp
@@ -0,0 +1,274 @@
+#include "FileInstaller.h"
+
+#include <utility>
+
+#include "ui/FomodViewModel.h"
+
+using namespace MOBase;
+
+FileInstaller::FileInstaller(
+ IOrganizer* organizer,
+ QString fomodPath,
+ const std::shared_ptr<IFileTree>& fileTree,
+ std::unique_ptr<ModuleConfiguration> fomodFile,
+ const std::shared_ptr<FlagMap>& flagMap,
+ const std::vector<std::shared_ptr<StepViewModel> >& steps) : mOrganizer(organizer),
+ mFomodPath(std::move(fomodPath)),
+ mFileTree(fileTree),
+ mFomodFile(std::move(fomodFile)),
+ mFlagMap(flagMap),
+ mConditionTester(organizer), mSteps(steps)
+{
+ const auto requiredCount =
+ (mFomodFile != nullptr) ? mFomodFile->requiredInstallFiles.files.size() : 0;
+ const auto stepCount = mSteps.size();
+ logMessage(DEBUG,
+ "FileInstaller constructed. steps=" + std::to_string(stepCount) + ", required files=" +
+ std::to_string(requiredCount) + ", fomodPath=" + mFomodPath.toStdString());
+}
+
+std::shared_ptr<IFileTree> FileInstaller::install() const
+{
+ logMessage(DEBUG, "Starting FileInstaller::install()");
+ const auto filesToInstall = collectFilesToInstall();
+ logMessage(INFO, "Installing " + std::to_string(filesToInstall.size()) + " files");
+ logMessage(INFO, "FlagMap");
+ logMessage(INFO, mFlagMap->toString());
+
+ // update the file tree with the new files
+ const std::shared_ptr<IFileTree> installTree = mFileTree->createOrphanTree();
+
+ for (const auto& file : filesToInstall) {
+
+ logMessage(DEBUG,
+ "Processing install entry source=" + file.source + ", destination=" +
+ (file.destination.has_value() ? file.destination.value() : "<default>") +
+ ", priority=" + std::to_string(file.priority));
+ const auto sourcePath = getQualifiedFilePath(file.source);
+ const auto sourceNode = mFileTree->find(QString::fromStdString(sourcePath));
+ if (sourceNode == nullptr) {
+ logMessage(ERR, "Could not find source: " + file.source);
+ continue;
+ }
+ const auto targetPath = file.destination.has_value()
+ ? QString::fromStdString(file.destination.value())
+ : QString::fromStdString(sourcePath);
+
+ // If it's a folder, copy the contents of the folder, not the folder itself.
+ if (sourceNode->isDir()) {
+ logMessage(DEBUG,
+ "Copying directory '" + file.source + "' into target '" + targetPath.toStdString() +
+ "'");
+ // TODO: Check if target path is literally undefined/null
+ const auto& tree = sourceNode->astree();
+ for (auto it = tree->begin(); it != tree->end(); ++it) {
+ const auto& entry = *it;
+ const auto path = targetPath.isEmpty() ? entry->name() : targetPath + "/" + entry->name();
+ logMessage(DEBUG,
+ " Copying entry '" + entry->name().toStdString() + "' to '" +
+ path.toStdString() + "'");
+ installTree->copy(entry, path, IFileTree::InsertPolicy::MERGE);
+ }
+ } else {
+ logMessage(DEBUG,
+ "Copying file '" + file.source + "' to '" + targetPath.toStdString() + "'");
+ installTree->copy(sourceNode, targetPath, IFileTree::InsertPolicy::MERGE);
+ }
+ }
+
+ // This file will be written by the InstallationManager later.
+ const auto jsonFilePath = "fomod.json";
+ installTree->addFile(QString::fromStdString(jsonFilePath), true);
+ logMessage(DEBUG, "Added fomod.json placeholder file to install tree.");
+
+ logMessage(DEBUG, "FileInstaller::install completed.");
+ return installTree;
+}
+
+nlohmann::json FileInstaller::generateFomodJson() const
+{
+ nlohmann::json fomodJson;
+ logMessage(DEBUG, "Generating fomod.json representation for " + std::to_string(mSteps.size()) +
+ " steps.");
+
+ fomodJson["steps"] = nlohmann::json::array();
+ for (const auto& stepViewModel : mSteps) {
+ auto stepJson = nlohmann::json::object();
+ stepJson["name"] = stepViewModel->getName();
+ stepJson["groups"] = nlohmann::json::array();
+ logMessage(DEBUG, "Serializing step '" + stepViewModel->getName() + "'");
+
+ for (const auto& groupViewModel : stepViewModel->getGroups()) {
+ auto groupJson = nlohmann::json::object();
+ groupJson["name"] = groupViewModel->getName();
+ auto pluginArray = nlohmann::json::array();
+ auto deselectedArray = nlohmann::json::array();
+ logMessage(DEBUG,
+ " Serializing group '" + groupViewModel->getName() +
+ "' with " + std::to_string(groupViewModel->getPlugins().size()) + " plugins");
+
+ for (const auto& pluginViewModel : groupViewModel->getPlugins()) {
+ logMessage(DEBUG,
+ " Plugin '" + pluginViewModel->getName() + "' selected=" +
+ (pluginViewModel->isSelected() ? "true" : "false") + ", manually-set=" +
+ (pluginViewModel->wasManuallySet() ? "true" : "false"));
+ if (pluginViewModel->isSelected()) {
+ pluginArray.emplace_back(pluginViewModel->getName());
+ }
+ // Add deselected plugins here. (TODO: This will be replaced with an embedded db.)
+ if (!pluginViewModel->isSelected() && pluginViewModel->wasManuallySet()) {
+ deselectedArray.emplace_back(pluginViewModel->getName());
+ }
+ }
+ groupJson["plugins"] = pluginArray;
+ groupJson["deselected"] = deselectedArray;
+ stepJson["groups"].emplace_back(groupJson);
+ }
+ fomodJson["steps"].emplace_back(stepJson);
+ }
+ return fomodJson;
+}
+
+std::string FileInstaller::getQualifiedFilePath(const std::string& treePath) const
+{
+ // We need to prepend the fomod path to whatever source we reference. Guess we're passing that path around.
+ const auto qualifiedPath = mFomodPath.toStdString() + "/" + treePath;
+ logMessage(DEBUG, "Qualifying tree path '" + treePath + "' to '" + qualifiedPath + "'");
+ return qualifiedPath;
+}
+
+std::vector<std::string> FileInstaller::collectPositiveFileNamesFromDependencyPatterns(
+ const std::vector<DependencyPattern>& patterns)
+{
+ std::vector<std::string> usableFileDependencyPluginNames = {};
+ logMessage(DEBUG,
+ "Collecting positive file names from " + std::to_string(patterns.size()) + " patterns.");
+
+ for (const auto& pattern : patterns) {
+ if (pattern.type == PluginTypeEnum::NotUsable) {
+ logMessage(DEBUG, "Skipping pattern marked as NotUsable.");
+ continue;
+ }
+
+ if (pattern.dependencies.fileDependencies.empty() && pattern.dependencies.nestedDependencies.empty()) {
+ logMessage(DEBUG, "Skipping pattern with no dependencies.");
+ continue;
+ }
+
+ const auto fileDependencies = pattern.dependencies.fileDependencies;
+ const auto nestedDependencies = pattern.dependencies.nestedDependencies;
+
+ for (const auto& fileDependency : fileDependencies) {
+ if (fileDependency.state != FileDependencyTypeEnum::Active) {
+ continue;
+ }
+ usableFileDependencyPluginNames.emplace_back(fileDependency.file);
+ logMessage(DEBUG, "Adding active file dependency: " + fileDependency.file);
+ }
+
+ for (const auto& nestedDependency : nestedDependencies) {
+ for (const auto& fileDependency : nestedDependency.fileDependencies) {
+ if (fileDependency.state != FileDependencyTypeEnum::Active) {
+ continue;
+ }
+ usableFileDependencyPluginNames.emplace_back(fileDependency.file);
+ logMessage(DEBUG, "Adding nested active file dependency: " + fileDependency.file);
+ }
+ }
+ // Not handling twice-nested dependencies now. IDK if that's even feasible.
+ }
+
+ logMessage(DEBUG,
+ "CollectPositiveFileNamesFromDependencyPatterns found " +
+ std::to_string(usableFileDependencyPluginNames.size()) + " entries.");
+ return usableFileDependencyPluginNames;
+}
+
+// Generic vector appender
+void FileInstaller::addFiles(std::vector<File>& main, std::vector<File> toAdd) const
+{
+ for (const auto& add : toAdd) {
+ logMessage(INFO, "Adding file with source: " + add.source);
+ }
+ main.insert(main.end(), toAdd.begin(), toAdd.end());
+ logMessage(DEBUG,
+ "addFiles merged " + std::to_string(toAdd.size()) + " files. Total is now " +
+ std::to_string(main.size()));
+}
+
+// TODO: Unclear if we're copying. oh well.
+// TODO: Rebuild flagmap and step indeces before installing
+std::vector<File> FileInstaller::collectFilesToInstall() const
+{
+ logMessage(DEBUG, "collectFilesToInstall started.");
+ std::vector<File> allFiles;
+
+ // Required files from FOMOD
+ const FileList requiredInstallFiles = mFomodFile->requiredInstallFiles;
+ logMessage(DEBUG,
+ "Adding " + std::to_string(requiredInstallFiles.files.size()) +
+ " required install files from fomod.");
+ addFiles(allFiles, requiredInstallFiles.files);
+
+ // Selected files from visible steps
+ for (const auto& stepViewModel : mSteps) {
+ if (!mConditionTester.testCompositeDependency(mFlagMap, stepViewModel->getVisibilityConditions())) {
+ logMessage(DEBUG,
+ "Skipping invisible step '" + stepViewModel->getName() + "'");
+ continue;
+ }
+ logMessage(DEBUG,
+ "Processing visible step '" + stepViewModel->getName() + "' with " +
+ std::to_string(stepViewModel->getGroups().size()) + " groups.");
+ for (const auto& groupViewModel : stepViewModel->getGroups()) {
+ logMessage(DEBUG,
+ " Processing group '" + groupViewModel->getName() + "' with " +
+ std::to_string(groupViewModel->getPlugins().size()) + " plugins.");
+ for (const auto& pluginViewModel : groupViewModel->getPlugins()) {
+ if (pluginViewModel->isSelected()) {
+ logMessage(DEBUG,
+ " Adding selected plugin '" + pluginViewModel->getName() +
+ "' with " +
+ std::to_string(pluginViewModel->getPlugin()->files.files.size()) +
+ " files.");
+ addFiles(allFiles, pluginViewModel->getPlugin()->files.files);
+ } else {
+ logMessage(DEBUG,
+ " Skipping plugin '" + pluginViewModel->getName() +
+ "' (not selected).");
+ }
+ }
+ }
+ }
+
+ // ConditionalInstall files
+ for (const auto conditionals = mFomodFile->conditionalFileInstalls; const auto& pattern : conditionals.patterns) {
+ //<folder source="CR\Dagi-Raht LL\VLrn_Custom Race - Dagi-Raht LL" destination="" priority="2" />
+
+ if (mConditionTester.testCompositeDependency(mFlagMap, pattern.dependencies)) {
+ // also check if the plugins setting these flags are visible. at least one
+
+ addFiles(allFiles, pattern.files.files);
+ logMessage(DEBUG,
+ "Conditional install pattern matched; added " +
+ std::to_string(pattern.files.files.size()) + " files.");
+ } else {
+ logMessage(DEBUG, "Conditional install pattern did not match.");
+ }
+ }
+
+ // Files will all have a default priority of 0 if not specified, so the order should also be informed by the
+ // order they appear within XML. That's why we put conditionalFileInstalls after.
+ logMessage(DEBUG, "Sorting " + std::to_string(allFiles.size()) + " files by priority.");
+ std::ranges::sort(allFiles, [](const auto& a, const auto& b) {
+ return a.priority < b.priority;
+ });
+
+ for (const auto& toInstall : allFiles) {
+ logMessage(DEBUG, "File to install: " + toInstall.source);
+ }
+ logMessage(DEBUG,
+ "collectFilesToInstall completed with " + std::to_string(allFiles.size()) + " files.");
+
+ return allFiles;
+}
diff --git a/libs/installer_fomod_plus/installer/lib/FileInstaller.h b/libs/installer_fomod_plus/installer/lib/FileInstaller.h
new file mode 100644
index 0000000..1c122a7
--- /dev/null
+++ b/libs/installer_fomod_plus/installer/lib/FileInstaller.h
@@ -0,0 +1,94 @@
+#pragma once
+
+#include <ifiletree.h>
+#include <nlohmann/json.hpp>
+
+#include "ConditionTester.h"
+#include "FlagMap.h"
+#include "Logger.h"
+#include "xml/ModuleConfiguration.h"
+
+
+/* This is what legacy fomodInstaller does:
+ modName.update(dialog.getName(), GUESS_USER);
+ return dialog.updateTree(tree);
+
+ This is a link to the legacy updateTree method:
+ https://github.com/ModOrganizer2/modorganizer-installer_fomod/blob/fc263f2d923c704b4853c11ed4f8b8cf3920f30d/src/fomodinstallerdialog.cpp#L546
+
+*/
+
+// Things this should do:
+// - Copy all requiredInstall files
+// - Copy all conditionalInstall files
+// - Copy all selected files from steps that were visible to the user at the point of install
+
+using namespace MOBase;
+
+using FileGlobalIndex = int;
+using FileDescriptor = std::pair<File, FileGlobalIndex>;
+
+class StepViewModel;
+
+class FileInstaller {
+public:
+ FileInstaller(
+ IOrganizer* organizer,
+ QString fomodPath,
+ const std::shared_ptr<IFileTree>& fileTree,
+ std::unique_ptr<ModuleConfiguration> fomodFile,
+ const std::shared_ptr<FlagMap>& flagMap,
+ const std::vector<std::shared_ptr<StepViewModel> >& steps);
+
+ std::shared_ptr<IFileTree> install() const;
+
+ /**
+ * @brief Create a 'fomod.json' file to add to the base of the installTree. Functionally similar to MO2's meta.ini.
+ *
+ * Until there's more utility in the JSON structure itself, it will simply be of this format:
+ * @code
+ * {
+ * "steps": [
+ * {
+ * "name": "Step 1",
+ * "groups": [
+ * {
+ * "name": "Group 1",
+ * "plugins: [
+ * "Plugin1.esp",
+ * "Plugin2.esp"
+ * ]
+ * }
+ * ]
+ * }
+ * ]
+ * }
+ * @endcode
+ *
+ * @return nhlohmann::json
+ */
+ nlohmann::json generateFomodJson() const;
+
+ std::string getQualifiedFilePath(const std::string& treePath) const;
+
+ std::vector<std::string> collectPositiveFileNamesFromDependencyPatterns(const std::vector<DependencyPattern> &patterns);
+
+ void addFiles(std::vector<File>& main, std::vector<File> toAdd) const;
+
+private:
+ IOrganizer* mOrganizer;
+ Logger& log = Logger::getInstance();
+ QString mFomodPath;
+ std::shared_ptr<IFileTree> mFileTree;
+ std::unique_ptr<ModuleConfiguration> mFomodFile;
+ std::shared_ptr<FlagMap> mFlagMap;
+ ConditionTester mConditionTester;
+ std::vector<std::shared_ptr<StepViewModel> > mSteps; // TODO: Maybe this is nasty. Idk.
+
+ std::vector<File> collectFilesToInstall() const;
+
+ void logMessage(LogLevel level, const std::string& message) const
+ {
+ log.logMessage(level, "[INSTALLER] " + message);
+ }
+};
diff --git a/libs/installer_fomod_plus/installer/lib/FlagMap.h b/libs/installer_fomod_plus/installer/lib/FlagMap.h
new file mode 100644
index 0000000..4959cec
--- /dev/null
+++ b/libs/installer_fomod_plus/installer/lib/FlagMap.h
@@ -0,0 +1,113 @@
+#pragma once
+
+#include "ViewModels.h"
+#include "stringutil.h"
+
+#include <ranges>
+#include <string>
+#include <unordered_map>
+
+using Flag = std::pair<std::string, std::string>;
+using FlagList = std::vector<Flag>;
+
+class FlagMap {
+public:
+
+ [[nodiscard]] std::vector<std::shared_ptr<PluginViewModel>> getPluginsSettingFlag(const std::string& key, const std::string& value) const
+ {
+ std::vector<std::shared_ptr<PluginViewModel>> result;
+ for (const auto& [plugin, theseFlags] : flags) {
+ for (const auto& [fst, snd] : theseFlags) {
+ if (toLower(fst) == toLower(key) && snd == value) {
+ result.emplace_back(plugin);
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ *
+ * @param key The flag key
+ * @return A list of flags currently set in this map with the given key. The list is ordered by step descending, then plugin ascending.
+ * So if steps 1, 2, and 3 set flag X in their first two plugins, it'll be ordered [3:1, 3:2, 2:1, 2:2, 1:2, 1:1]
+ */
+ [[nodiscard]] FlagList getFlagsByKey(const std::string& key) const
+ {
+ FlagList result;
+ std::vector<std::pair<int, std::shared_ptr<PluginViewModel>>> orderedPlugins;
+
+ // Collect all plugins with their stepIndex and ownIndex
+ for (const auto& plugin : flags | std::views::keys) {
+ orderedPlugins.emplace_back(plugin->getStepIndex(), plugin);
+ }
+
+ // Sort plugins by stepIndex and ownIndex, stepIndex descending and ownIndex ascending
+ // TODO: Might need group sorting too. How can I just do the natural order??
+ std::ranges::sort(orderedPlugins, [](const auto& a, const auto& b) {
+ return a.first > b.first || (a.first == b.first && a.second->getOwnIndex() < b.second->getOwnIndex());
+ });
+
+ // Collect flags in the sorted order
+ for (const auto& plugin : orderedPlugins | std::views::values) {
+ for (const auto& flag : flags.at(plugin)) {
+ if (toLower(flag.first) == toLower(key)) {
+ result.emplace_back(flag);
+ }
+ }
+ }
+ return result;
+ }
+
+ void setFlagsForPlugin(PluginRef plugin)
+ {
+ // Don't clutter the map with empty key-vals
+ if (plugin->getConditionFlags().empty()) {
+ return;
+ }
+ unsetFlagsForPlugin(plugin);
+
+ FlagList flagList;
+ for (const auto& conditionFlag : plugin->getConditionFlags()) {
+ flagList.emplace_back(toLower(conditionFlag.name), conditionFlag.value);
+ }
+ flags[plugin] = flagList;
+ }
+
+ void unsetFlagsForPlugin(PluginRef plugin)
+ {
+ if (flags.contains(plugin)) {
+ flags.erase(plugin);
+ }
+ }
+
+ std::string toString()
+ {
+ auto result = std::string();
+ result += "FlagMap:\n";
+
+ for (const auto& [plugin, theseFlags] : flags) {
+ result += plugin->getName() + " [";
+ for (const auto& [fst, snd] : theseFlags) {
+ result += fst + ": " + snd + ", ";
+ }
+ result.erase(result.size() - 2);
+ result += "]\n";
+ }
+ return result;
+ }
+
+ void clearAll()
+ {
+ flags.clear();
+ }
+
+ [[nodiscard]] size_t getFlagCount() const
+ {
+ return flags.size();
+ }
+
+
+private:
+ std::unordered_map<std::shared_ptr<PluginViewModel>, FlagList> flags;
+};
diff --git a/libs/installer_fomod_plus/installer/lib/Logger.h b/libs/installer_fomod_plus/installer/lib/Logger.h
new file mode 100644
index 0000000..fdeb1f0
--- /dev/null
+++ b/libs/installer_fomod_plus/installer/lib/Logger.h
@@ -0,0 +1,118 @@
+#pragma once
+#include <fstream>
+#include <iostream>
+#include <mutex>
+#include <regex>
+
+
+// Log levels
+enum LogLevel {
+ DEBUG = 0,
+ INFO = 1,
+ WARN = 2,
+ ERR = 3
+};
+
+// Convert LogLevel to string
+inline const char* logLevelToString(const LogLevel level) {
+ switch (level) {
+ case DEBUG: return "DEBUG";
+ case INFO: return "INFO";
+ case WARN: return "WARN";
+ case ERR: return "ERROR";
+ default: return "UNKNOWN";
+ }
+}
+
+
+class Logger {
+public:
+ static Logger& getInstance()
+ {
+ static Logger instance;
+ return instance;
+ }
+
+ void setLogFilePath(const std::string& filePath)
+ {
+ std::lock_guard lock(mMutex);
+ if (mLogFile.is_open()) {
+ mLogFile.close();
+ }
+ mLogFile.open(filePath, std::ios::out);
+ // std::ios::app is an option for appending but dont wanna grow it forever.
+ }
+
+ void setDebugMode(bool debug)
+ {
+ std::lock_guard lock(mMutex);
+ mDebugMode = debug;
+ }
+
+ void logMessage(const LogLevel level, const std::string& message)
+ {
+
+#if defined(__GNUC__) || defined(__clang__)
+ std::string functionName = __PRETTY_FUNCTION__;
+#elif defined(_MSC_VER)
+ std::string functionName = __FUNCSIG__;
+#else
+ std::string functionName = "UnknownFunction";
+#endif
+
+ std::regex classNameRegex(R"((\w+)::\w+\()");
+ std::smatch match;
+ std::string className = "UnknownClass";
+
+ if (std::regex_search(functionName, match, classNameRegex) && match.size() > 1) {
+ className = match.str(1);
+ }
+
+ std::lock_guard lock(mMutex);
+
+ auto writeLog = [&](std::ostream& stream) {
+ switch (level) {
+ case DEBUG:
+ stream << "[DEBUG] " << message << std::endl;
+ break;
+ case INFO:
+ stream << "[INFO] " << message << std::endl;
+ break;
+ case WARN:
+ stream << "[WARN] " << message << std::endl;
+ break;
+ case ERR:
+ stream << "[ERROR] " << message << std::endl;
+ break;
+ }
+ };
+
+ if (mLogFile.is_open()) {
+ writeLog(mLogFile);
+ }
+
+ writeLog(std::cout);
+ }
+
+ Logger& operator=(const Logger&) = delete;
+
+private:
+ Logger() = default;
+
+ ~Logger()
+ {
+ if (mLogFile.is_open()) {
+ mLogFile.close();
+ }
+ }
+
+ Logger(const Logger&) = delete;
+
+ std::ofstream mLogFile;
+ std::mutex mMutex;
+#if !defined(NDEBUG) || defined(CMAKE_BUILD_TYPE_RELWITHDEBINFO)
+ bool mDebugMode = true; // Auto-enable in debug/RelWithDebInfo builds
+#else
+ bool mDebugMode = false; // Disable in release builds
+#endif
+}; \ No newline at end of file
diff --git a/libs/installer_fomod_plus/installer/lib/ViewModels.h b/libs/installer_fomod_plus/installer/lib/ViewModels.h
new file mode 100644
index 0000000..062445a
--- /dev/null
+++ b/libs/installer_fomod_plus/installer/lib/ViewModels.h
@@ -0,0 +1,120 @@
+#pragma once
+
+#include <memory>
+#include <vector>
+
+#include "xml/ModuleConfiguration.h"
+
+
+template <typename T>
+using shared_ptr_list = std::vector<std::shared_ptr<T> >;
+
+
+/*
+--------------------------------------------------------------------------------
+ Plugins
+--------------------------------------------------------------------------------
+*/
+class PluginViewModel {
+public:
+ PluginViewModel(const std::shared_ptr<Plugin>& plugin_, const bool selected, bool, const int index)
+ : ownIndex(index), selected(selected), enabled(true), manuallySet(false), plugin(plugin_) {}
+
+ void setSelected(const bool selected) { this->selected = selected; }
+ void setEnabled(const bool enabled) { this->enabled = enabled; }
+ [[nodiscard]] std::string getName() const { return plugin ? plugin->name : std::string(); }
+ [[nodiscard]] std::string getDescription() const { return plugin ? plugin->description : std::string(); }
+ [[nodiscard]] std::string getImagePath() const { return plugin ? plugin->image.path : std::string(); }
+ [[nodiscard]] bool isSelected() const { return selected; }
+ [[nodiscard]] bool isEnabled() const { return enabled; }
+ [[nodiscard]] int getOwnIndex() const { return ownIndex; }
+ [[nodiscard]] std::vector<ConditionFlag> getConditionFlags() const { return plugin->conditionFlags.flags; }
+ [[nodiscard]] PluginTypeEnum getCurrentPluginType() const { return currentPluginType; }
+ [[nodiscard]] bool wasManuallySet() const { return manuallySet; }
+
+ void setCurrentPluginType(const PluginTypeEnum type) { currentPluginType = type; }
+ void setStepIndex(const int stepIndex) { this->stepIndex = stepIndex; }
+ void setGroupIndex(const int groupIndex) { this->groupIndex = groupIndex; }
+
+ [[nodiscard]] int getStepIndex() const { return stepIndex; }
+ [[nodiscard]] int getGroupIndex() const { return groupIndex; }
+
+ friend class FomodViewModel;
+ friend class FileInstaller;
+ friend class ConditionTester;
+
+protected:
+ [[nodiscard]] std::shared_ptr<Plugin> getPlugin() const { return plugin; }
+
+private:
+ int ownIndex;
+ bool selected;
+ bool enabled;
+ bool manuallySet;
+ PluginTypeEnum currentPluginType = PluginTypeEnum::UNKNOWN;
+ std::shared_ptr<Plugin> plugin;
+
+ int stepIndex{ -1 };
+ int groupIndex{ -1 };
+};
+
+/*
+--------------------------------------------------------------------------------
+ Groups
+--------------------------------------------------------------------------------
+*/
+class GroupViewModel {
+public:
+ GroupViewModel(const std::shared_ptr<Group>& group_, const shared_ptr_list<PluginViewModel>& plugins,
+ const int index, const int stepIndex)
+ : plugins(plugins), group(group_), ownIndex(index), stepIndex(stepIndex) {}
+
+ void addPlugin(const std::shared_ptr<PluginViewModel>& plugin) { plugins.emplace_back(plugin); }
+
+ [[nodiscard]] std::string getName() const { return group->name; }
+ [[nodiscard]] GroupTypeEnum getType() const { return group->type; }
+ [[nodiscard]] const shared_ptr_list<PluginViewModel>& getPlugins() const { return plugins; }
+ [[nodiscard]] int getOwnIndex() const { return ownIndex; }
+ [[nodiscard]] int getStepIndex() const { return stepIndex; }
+
+private:
+ shared_ptr_list<PluginViewModel> plugins;
+ std::shared_ptr<Group> group;
+ int ownIndex;
+ int stepIndex;
+};
+
+/*
+--------------------------------------------------------------------------------
+ Steps
+--------------------------------------------------------------------------------
+*/
+class StepViewModel {
+public:
+ StepViewModel(const std::shared_ptr<InstallStep>& installStep_, const shared_ptr_list<GroupViewModel>& groups,
+ const int index)
+ : installStep(installStep_), groups(groups), ownIndex(index) {}
+
+ [[nodiscard]] CompositeDependency& getVisibilityConditions() const { return installStep->visible; }
+ [[nodiscard]] std::string getName() const { return installStep->name; }
+ [[nodiscard]] const shared_ptr_list<GroupViewModel>& getGroups() const { return groups; }
+ [[nodiscard]] int getOwnIndex() const { return ownIndex; }
+ [[nodiscard]] bool getHasVisited() const { return visited; }
+ void setVisited(const bool visited) { this->visited = visited; }
+
+private:
+ bool visited{ false };
+ std::shared_ptr<InstallStep> installStep;
+ shared_ptr_list<GroupViewModel> groups;
+ int ownIndex;
+};
+
+
+/*
+--------------------------------------------------------------------------------
+ Outbound Types
+--------------------------------------------------------------------------------
+*/
+using StepRef = const std::shared_ptr<StepViewModel>&;
+using GroupRef = const std::shared_ptr<GroupViewModel>&;
+using PluginRef = const std::shared_ptr<PluginViewModel>&;