#include "FomodPlusInstaller.h" #include #include #include #include #include #include #include #include #include "FomodInstallerWindow.h" #include "stringutil.h" #include "integration/FomodDataContent.h" #include "ui/Colors.h" #include "ui/FomodViewModel.h" #include "lib/CrashHandler.h" #include #include using namespace Qt::Literals::StringLiterals; /* -------------------------------------------------------------------------------- Init -------------------------------------------------------------------------------- */ #pragma region Initialization bool FomodPlusInstaller::init(IOrganizer* organizer) { CrashHandler::initialize(); mOrganizer = organizer; mFomodContent = make_shared(organizer); log.setLogFilePath(QDir::currentPath().toStdString() + "/logs/fomodplus.log"); // REMEMBER: This mFomodDB persists beyond the scope of an individual install. Do not do anything nasty to it. mFomodDb = std::make_unique(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 FomodPlusInstaller::shouldShowImages() const { return mOrganizer->pluginSetting(name(), "show_images").value(); } bool FomodPlusInstaller::shouldShowNotifications() const { return mOrganizer->pluginSetting(name(), "show_notifications").value(); } bool FomodPlusInstaller::shouldShowSidebarFilter() const { return mOrganizer->pluginSetting(name(), "show_fomod_filter").value(); } bool FomodPlusInstaller::shouldAutoRestoreChoices() const { return mOrganizer->pluginSetting(name(), "always_restore_choices").value(); } bool FomodPlusInstaller::isWizardIntegrated() const { return mOrganizer->pluginSetting(name(), "wizard_integration").value(); } 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 > 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 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 tree) const { tree = findFomodDirectory(tree); if (tree != nullptr) { return tree->exists(StringConstants::FomodFiles::MODULE_CONFIG.data(), FileTreeEntry::FILE); } return false; } std::pair FomodPlusInstaller::getExistingFomodJson(const GuessedValue& 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 matches; auto parseStepCount = [](const QVariant& fomodData) -> std::pair { 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(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& modName, std::shared_ptr& 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 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(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(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 installTree = window->getFileInstaller()->install(); tree = installTree; mFomodJson = std::make_shared(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& 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 > 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(); 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(); 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 >& entries, const shared_ptr& tree) { static std::set 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 >& entries, const shared_ptr& 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 FomodPlusInstaller::findFomodDirectory(const std::shared_ptr& 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& 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(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" "
"
        + errorText +
        "
" ); msgBox.setIcon(QMessageBox::Critical); msgBox.exec(); }