aboutsummaryrefslogtreecommitdiff
path: root/libs/installer_fomod_plus/installer/FomodPlusInstaller.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'libs/installer_fomod_plus/installer/FomodPlusInstaller.cpp')
-rw-r--r--libs/installer_fomod_plus/installer/FomodPlusInstaller.cpp512
1 files changed, 512 insertions, 0 deletions
diff --git a/libs/installer_fomod_plus/installer/FomodPlusInstaller.cpp b/libs/installer_fomod_plus/installer/FomodPlusInstaller.cpp
new file mode 100644
index 0000000..690bdbb
--- /dev/null
+++ b/libs/installer_fomod_plus/installer/FomodPlusInstaller.cpp
@@ -0,0 +1,512 @@
+#include "FomodPlusInstaller.h"
+
+#include <igamefeatures.h>
+#include <iinstallationmanager.h>
+#include <iplugingame.h>
+#include <QEventLoop>
+#include <QTreeWidget>
+#include <xml/FomodInfoFile.h>
+#include <xml/ModuleConfiguration.h>
+#include <xml/XmlParseException.h>
+
+#include "FomodInstallerWindow.h"
+#include "stringutil.h"
+#include "integration/FomodDataContent.h"
+#include "ui/Colors.h"
+#include "ui/FomodViewModel.h"
+#include "lib/CrashHandler.h"
+
+#include <QMessageBox>
+#include <QSettings>
+
+using namespace Qt::Literals::StringLiterals;
+
+/*
+--------------------------------------------------------------------------------
+ Init
+--------------------------------------------------------------------------------
+*/
+#pragma region Initialization
+
+bool FomodPlusInstaller::init(IOrganizer* organizer)
+{
+ CrashHandler::initialize();
+ mOrganizer = organizer;
+ mFomodContent = make_shared<FomodDataContent>(organizer);
+ log.setLogFilePath(QDir::currentPath().toStdString() + "/logs/fomodplus.log");
+ std::cout << "QDir::currentPath(): " << QDir::currentPath().toStdString() << std::endl;
+ std::cout << "mOrganizer->basePath() : " << mOrganizer->basePath().toStdString() << std::endl;
+
+ // REMEMBER: This mFomodDB persists beyond the scope of an individual install. Do not do anything nasty to it.
+ mFomodDb = std::make_unique<FomodDB>(mOrganizer->basePath().toStdString());
+ setupUiInjection();
+ return true;
+}
+
+void FomodPlusInstaller::setupUiInjection() const
+{
+ if (shouldShowSidebarFilter()) {
+ mOrganizer->gameFeatures()->registerFeature(mFomodContent, 9999, false);
+ }
+
+ mOrganizer->onPluginSettingChanged([this](const QString& pluginName, const QString& key, const QVariant& oldValue, const QVariant& newValue) {
+ if (pluginName == name() && key == "show_fomod_filter") {
+ toggleFeature(newValue.toBool());
+ }
+ });
+}
+
+void FomodPlusInstaller::toggleFeature(const bool enabled) const
+{
+ if (enabled) {
+ mOrganizer->gameFeatures()->registerFeature(mFomodContent, 9999, false);
+ } else {
+ mOrganizer->gameFeatures()->unregisterFeature(mFomodContent);
+ }
+ mOrganizer->refresh();
+}
+
+#pragma endregion
+
+/*
+--------------------------------------------------------------------------------
+ Settings
+--------------------------------------------------------------------------------
+*/
+#pragma region Settings
+bool FomodPlusInstaller::shouldFallbackToLegacyInstaller() const
+{
+ return mOrganizer->pluginSetting(name(), "fallback_to_legacy").value<bool>();
+}
+
+bool FomodPlusInstaller::shouldShowImages() const
+{
+ return mOrganizer->pluginSetting(name(), "show_images").value<bool>();
+}
+
+bool FomodPlusInstaller::shouldShowNotifications() const
+{
+ return mOrganizer->pluginSetting(name(), "show_notifications").value<bool>();
+}
+
+bool FomodPlusInstaller::shouldShowSidebarFilter() const
+{
+ return mOrganizer->pluginSetting(name(), "show_fomod_filter").value<bool>();
+}
+
+bool FomodPlusInstaller::shouldAutoRestoreChoices() const
+{
+ return mOrganizer->pluginSetting(name(), "always_restore_choices").value<bool>();
+}
+
+bool FomodPlusInstaller::isWizardIntegrated() const
+{
+ return mOrganizer->pluginSetting(name(), "wizard_integration").value<bool>();
+}
+
+void FomodPlusInstaller::toggleShouldShowImages() const
+{
+ const bool showImages = shouldShowImages();
+ mOrganizer->setPluginSetting(name(), "show_images", !showImages);
+}
+
+QString FomodPlusInstaller::getSelectedColor() const
+{
+ const auto colorName = mOrganizer->pluginSetting(name(), "color_theme").toString();
+ const auto it = UiColors::colorStyles.find(colorName);
+ return it != UiColors::colorStyles.end() ? it->first : "Blue";
+}
+
+std::vector<std::shared_ptr<const IPluginRequirement> > FomodPlusInstaller::requirements() const
+{
+ return { Requirements::gameDependency(
+ { u"Morrowind"_s, u"Oblivion"_s, u"Fallout 3"_s, u"New Vegas"_s, u"Skyrim"_s, u"Enderal"_s,
+ u"Fallout 4"_s, u"Skyrim Special Edition"_s, u"Enderal Special Edition"_s,
+ u"Skyrim VR"_s, u"Fallout 4 VR"_s, u"Starfield"_s }) };
+}
+
+QList<PluginSetting> FomodPlusInstaller::settings() const
+{
+ return {
+ { u"fallback_to_legacy"_s, u"When hitting cancel, fall back to the legacy FOMOD installer."_s, false },
+ { u"always_restore_choices"_s, u"Restore previous choices without clicking the magic button"_s, false },
+ { u"show_images"_s, u"Show image previews and the image carousel in installer windows."_s, true },
+ { u"color_theme"_s, u"Select the color theme for the installer"_s, QString("Blue") }, // Default color name
+ { u"show_notifications"_s, u"Show the notifications panel"_s, false }, //WIP
+ { u"wizard_integration"_s, u"Integrate the installer with patch wizard."_s, true }, //WIP
+ { u"show_fomod_filter"_s, u"Show the filter in the sidebar (may break other content filters)"_s, true }
+ };
+}
+
+#pragma endregion
+
+bool FomodPlusInstaller::isArchiveSupported(std::shared_ptr<const IFileTree> tree) const
+{
+ tree = findFomodDirectory(tree);
+ if (tree != nullptr) {
+ return tree->exists(StringConstants::FomodFiles::MODULE_CONFIG.data(), FileTreeEntry::FILE);
+ }
+ return false;
+}
+
+
+std::pair<nlohmann::json, IModInterface*> FomodPlusInstaller::getExistingFomodJson(const GuessedValue<QString>& modName,
+ const int& nexusId,
+ const int& stepsInCurrentFomod) const
+{
+ logMessage(DEBUG, "FomodPlusInstaller::getExistingFomodJson - modName: " + modName->toStdString() +
+ " nexusId: " + std::to_string(nexusId));
+
+ // Need to have better mod matching based on presence of FOMOD plugin data.
+ struct ModMatch {
+ MOBase::IModInterface* mod;
+ bool hasFomodData;
+ int stepCount;
+ nlohmann::json fomodJson;
+ };
+ std::vector<ModMatch> matches;
+
+ auto parseStepCount = [](const QVariant& fomodData) -> std::pair<bool, int> {
+ try {
+ if (fomodData.isValid() && !fomodData.isNull()) {
+ if (auto json = nlohmann::json::parse(fomodData.toString().toStdString()); json.contains("steps") && json["steps"].is_array()) {
+ return { true, static_cast<int>(json["steps"].size()) };
+ }
+ }
+ } catch (...) {
+ // Ignore parsing errors
+ }
+ return { false, 0 };
+ };
+
+ // Check exact match first
+ const auto modList = mOrganizer->modList();
+ if (modList == nullptr) {
+ return {};
+ }
+
+ if (const auto exactMod = modList->getMod(modName)) {
+ const auto fomodData = exactMod->pluginSetting(name(), "fomod", 0);
+ if (auto [valid, stepCount] = parseStepCount(fomodData); valid) {
+ matches.push_back({
+ exactMod,
+ true,
+ stepCount,
+ nlohmann::json::parse(fomodData.toString().toStdString())
+ });
+ } else {
+ matches.push_back({
+ exactMod,
+ false,
+ 0,
+ nlohmann::json()
+ });
+ }
+ }
+
+ // Check all variants
+ for (const auto& variant : modName.variants()) {
+ if (const auto variantMod = modList->getMod(variant)) {
+ const auto fomodData = variantMod->pluginSetting(name(), "fomod", 0);
+ if (auto [valid, stepCount] = parseStepCount(fomodData); valid) {
+ matches.push_back({
+ variantMod,
+ true,
+ stepCount,
+ nlohmann::json::parse(fomodData.toString().toStdString())
+ });
+ } else {
+ matches.push_back({
+ variantMod,
+ false,
+ 0,
+ nlohmann::json()
+ });
+ }
+ }
+ }
+
+ // No matches found
+ if (matches.empty()) {
+ return {};
+ }
+
+ // First try to find exact step count match
+ const auto exactMatch = ranges::find_if(matches,
+ [stepsInCurrentFomod](const ModMatch& match) {
+ return match.hasFomodData && match.stepCount == stepsInCurrentFomod;
+ });
+
+ if (exactMatch != matches.end()) {
+ logMessage(DEBUG, "Found exact step count match in mod: " +
+ exactMatch->mod->name().toStdString() +
+ " with " + std::to_string(exactMatch->stepCount) + " steps");
+ return std::make_pair(exactMatch->fomodJson, exactMatch->mod);
+ }
+
+ // Find the closest step count among mods with FOMOD data
+ const auto closestMatch = ranges::min_element(matches,
+ [stepsInCurrentFomod](const ModMatch& a, const ModMatch& b) {
+ if (!a.hasFomodData || !b.hasFomodData)
+ return false;
+ // The min difference between the step counts
+ return std::abs(a.stepCount - stepsInCurrentFomod) <
+ std::abs(b.stepCount - stepsInCurrentFomod);
+ });
+
+ if (closestMatch != matches.end() && closestMatch->hasFomodData) {
+ logMessage(DEBUG, "Using closest step count match from mod: " +
+ closestMatch->mod->name().toStdString() +
+ " with " + std::to_string(closestMatch->stepCount) + " steps");
+ return std::make_pair(closestMatch->fomodJson, closestMatch->mod);
+ }
+
+ // Fallback to first mod with any FOMOD data
+ const auto anyFomod = ranges::find_if(matches,
+ [](const ModMatch& match) { return match.hasFomodData; });
+
+ if (anyFomod != matches.end()) {
+ logMessage(DEBUG, "Using first available FOMOD data from mod: " +
+ anyFomod->mod->name().toStdString());
+ return std::make_pair(anyFomod->fomodJson, anyFomod->mod);
+ }
+
+ logMessage(DEBUG, "No matching FOMOD data found");
+ return {};
+}
+
+void FomodPlusInstaller::clearPriorInstallData()
+{
+ mInstallerUsed = false;
+ mFomodJson = nullptr;
+ mFomodPath = "";
+}
+
+/**
+ *
+ * @param modName
+ * @param tree
+ * @param version
+ * @param nexusID
+ * @return
+ */
+IPluginInstaller::EInstallResult FomodPlusInstaller::install(GuessedValue<QString>& modName,
+ std::shared_ptr<IFileTree>& tree, QString& version,
+ int& nexusID)
+{
+ clearPriorInstallData();
+
+ logMessage(INFO, std::format("FomodPlusInstaller::install - modName: {}, version: {}, nexusID: {}",
+ modName->toStdString(),
+ version.toStdString(),
+ nexusID
+ ));
+ logMessage(INFO, std::format("FomodPlusInstaller::install - tree size: {}", tree->size()));
+
+ auto [infoFile, moduleConfigFile, filePaths] = parseFomodFiles(tree);
+
+ if (infoFile == nullptr || moduleConfigFile == nullptr) {
+ return RESULT_FAILED;
+ }
+
+ std::vector<QString> pluginPaths = {};
+ for (const auto& filePath : filePaths) {
+ if (isPluginFile(filePath)) {
+ pluginPaths.emplace_back(filePath);
+ }
+ }
+
+ const auto dbEntry = mFomodDb->getEntryFromFomod(moduleConfigFile.get(), pluginPaths, nexusID);
+
+ // create ui & pass xml classes to ui
+ auto [json, matchMod] = getExistingFomodJson(modName, nexusID, static_cast<int>(moduleConfigFile->installSteps.installSteps.size()));
+ if (matchMod != nullptr) {
+ modName.update(matchMod->name(), GUESS_USER);
+ }
+ auto fomodViewModel = FomodViewModel::create(mOrganizer, std::move(moduleConfigFile), std::move(infoFile));
+ const auto window = std::make_shared<FomodInstallerWindow>(this, modName, tree, mFomodPath, fomodViewModel, json);
+
+ // ReSharper disable once CppTooWideScopeInitStatement
+ const QDialog::DialogCode result = showInstallerWindow(window);
+ if (result == QDialog::Accepted) {
+ // modname was updated in window
+ mInstallerUsed = true;
+ const std::shared_ptr<IFileTree> installTree = window->getFileInstaller()->install();
+ tree = installTree;
+ mFomodJson = std::make_shared<nlohmann::json>(window->getFileInstaller()->generateFomodJson());
+
+ try {
+ mFomodDb->addEntry(dbEntry);
+ mFomodDb->saveToFile();
+ } catch ([[maybe_unused]] Exception& e) {
+ logMessage(ERR, "Failed to add FomodDB entries.");
+ logMessage(ERR, e.what());
+ }
+ return RESULT_SUCCESS;
+ }
+ if (window->isManualInstall()) {
+ return RESULT_MANUALREQUESTED;
+ }
+ if (shouldFallbackToLegacyInstaller()) {
+ return RESULT_NOTATTEMPTED;
+ }
+ return RESULT_CANCELED;
+}
+
+
+/**
+ *
+ * @param tree
+ * @return
+ */
+ParsedFilesTuple FomodPlusInstaller::parseFomodFiles(const std::shared_ptr<IFileTree>& tree)
+{
+ const auto emptyResult = std::make_tuple(nullptr, nullptr, QStringList());
+
+ const auto fomodDir = findFomodDirectory(tree);
+ if (fomodDir == nullptr) {
+ logMessage(ERR, "FomodPlusInstaller::install - fomod directory not found");
+ return emptyResult;
+ }
+
+ // This is a strange place to set this value but okay for now.
+ mFomodPath = fomodDir->parent()->path();
+
+ const auto infoXML = fomodDir->find(
+ StringConstants::FomodFiles::INFO_XML.data(),
+ FileTreeEntry::FILE
+ );
+ const auto moduleConfig = fomodDir->find(
+ StringConstants::FomodFiles::MODULE_CONFIG.data(),
+ FileTreeEntry::FILE
+ );
+
+ // Extract files first.
+ vector<std::shared_ptr<const FileTreeEntry> > toExtract = {};
+ if (moduleConfig) {
+ toExtract.push_back(moduleConfig);
+ } else {
+ logMessage(ERR, "FomodPlusInstaller::install - error parsing moduleConfig.xml: Not Present");
+ return emptyResult;
+ }
+ if (infoXML) {
+ toExtract.push_back(infoXML);
+ }
+ appendImageFiles(toExtract, tree);
+ appendPluginFiles(toExtract, tree); // For patch wizard data collection
+ auto paths = manager()->extractFiles(toExtract);
+ // Normalize backslash separators from MO2's internal file tree to forward slashes for Linux
+ for (auto& p : paths) {
+ p.replace('\\', '/');
+ }
+
+ auto moduleConfiguration = std::make_unique<ModuleConfiguration>();
+ try {
+ moduleConfiguration->deserialize(paths.at(0));
+ } catch (XmlParseException& e) {
+ logMessage(ERR, std::format("FomodPlusInstaller::install - error parsing moduleConfig.xml: {}", e.what()));
+ return emptyResult;
+ }
+
+ auto infoFile = std::make_unique<FomodInfoFile>();
+ if (infoXML) {
+ try {
+ infoFile->deserialize(paths.at(1));
+ } catch (XmlParseException& e) {
+ logMessage(ERR, std::format("FomodPlusInstaller::install - error parsing info.xml: {}", e.what()));
+ }
+ }
+
+ return std::make_tuple(std::move(infoFile), std::move(moduleConfiguration), paths);
+}
+
+// Taken from https://github.com/ModOrganizer2/modorganizer-installer_fomod/blob/master/src/installerfomod.cpp#L123
+void FomodPlusInstaller::appendImageFiles(vector<shared_ptr<const FileTreeEntry> >& entries,
+ const shared_ptr<const IFileTree>& tree)
+{
+ static std::set<QString, FileNameComparator> imageSuffixes{ "png", "jpg", "jpeg", "gif", "bmp" };
+ for (auto entry : *tree) {
+ if (entry->isDir()) {
+ appendImageFiles(entries, entry->astree());
+ } else if (imageSuffixes.contains(entry->suffix())) {
+ entries.push_back(entry);
+ }
+ }
+}
+
+void FomodPlusInstaller::appendPluginFiles(vector<shared_ptr<const FileTreeEntry> >& entries,
+ const shared_ptr<const IFileTree>& tree)
+{
+ // Don't bother with this stuff if the user doesn't care about the wizard.
+ // It may slightly bloat the temp file directory if not needed.
+ if (isWizardIntegrated()) {
+ for (auto entry : *tree) {
+ if (entry->isDir()) {
+ appendPluginFiles(entries, entry->astree());
+ } else if (isPluginFile(entry->suffix())) {
+ entries.push_back(entry);
+ }
+ }
+ }
+}
+
+void FomodPlusInstaller::onInstallationStart(QString const& archive, const bool reinstallation,
+ IModInterface* currentMod)
+{
+ IPluginInstallerSimple::onInstallationStart(archive, reinstallation, currentMod);
+}
+
+void FomodPlusInstaller::onInstallationEnd(const EInstallResult result, IModInterface* newMod)
+{
+ IPluginInstallerSimple::onInstallationEnd(result, newMod);
+
+ // Update the meta.ini file with the fomod information
+ if (mFomodJson != nullptr && result == RESULT_SUCCESS && newMod != nullptr && mInstallerUsed) {
+ newMod->setPluginSetting(this->name(), "fomod", mFomodJson->dump().c_str());
+ mOrganizer->refresh();
+ }
+ clearPriorInstallData();
+}
+
+// Borrowed from https://github.com/ModOrganizer2/modorganizer-installer_fomod/blob/master/src/installerfomod.cpp
+std::shared_ptr<const IFileTree> FomodPlusInstaller::findFomodDirectory(const std::shared_ptr<const IFileTree>& tree)
+{
+ // ReSharper disable once CppTooWideScopeInitStatement
+ const auto entry = tree->find(StringConstants::FomodFiles::FOMOD_DIR.data(), FileTreeEntry::DIRECTORY);
+
+ if (entry != nullptr) {
+ return entry->astree();
+ }
+
+ if (tree->size() == 1 && tree->at(0)->isDir()) {
+ return findFomodDirectory(tree->at(0)->astree());
+ }
+ return nullptr;
+}
+
+QDialog::DialogCode FomodPlusInstaller::showInstallerWindow(const std::shared_ptr<FomodInstallerWindow>& window)
+{
+ QEventLoop loop;
+ connect(window.get(), SIGNAL(accepted()), &loop, SLOT(quit()));
+ connect(window.get(), SIGNAL(rejected()), &loop, SLOT(quit()));
+ window->show();
+ loop.exec();
+ return static_cast<QDialog::DialogCode>(window->result());
+}
+
+void showError(const Exception& e)
+{
+ const QString errorText = "Mod Name: \n" "Nexus ID: " "\n" "Exception: " + QString(e.what()) + "\n";
+
+ QMessageBox msgBox;
+ msgBox.setWindowTitle("FOMOD Plus Error :(");
+ msgBox.setTextFormat(Qt::RichText);
+ msgBox.setText("Sorry this happened. Please copy the following error and report it to me on Nexus or GitHub.\n"
+ "<pre style='background-color: #f0f0f0; padding: 10px;'>"
+ + errorText +
+ "</pre>"
+ );
+ msgBox.setIcon(QMessageBox::Critical);
+ msgBox.exec();
+
+}