diff options
Diffstat (limited to 'libs/installer_fomod_plus/installer/lib')
| -rw-r--r-- | libs/installer_fomod_plus/installer/lib/ConditionTester.cpp | 176 | ||||
| -rw-r--r-- | libs/installer_fomod_plus/installer/lib/ConditionTester.h | 42 | ||||
| -rw-r--r-- | libs/installer_fomod_plus/installer/lib/CrashHandler.cpp | 122 | ||||
| -rw-r--r-- | libs/installer_fomod_plus/installer/lib/CrashHandler.h | 20 | ||||
| -rw-r--r-- | libs/installer_fomod_plus/installer/lib/FileInstaller.cpp | 274 | ||||
| -rw-r--r-- | libs/installer_fomod_plus/installer/lib/FileInstaller.h | 94 | ||||
| -rw-r--r-- | libs/installer_fomod_plus/installer/lib/FlagMap.h | 113 | ||||
| -rw-r--r-- | libs/installer_fomod_plus/installer/lib/Logger.h | 118 | ||||
| -rw-r--r-- | libs/installer_fomod_plus/installer/lib/ViewModels.h | 120 |
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>&; |
