diff options
Diffstat (limited to 'libs/installer_fomod_plus/installer/FomodPlusInstaller.cpp')
| -rw-r--r-- | libs/installer_fomod_plus/installer/FomodPlusInstaller.cpp | 512 |
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(); + +} |
