diff options
Diffstat (limited to 'libs/installer_fomod_plus/patchwizard')
| -rw-r--r-- | libs/installer_fomod_plus/patchwizard/CMakeLists.txt | 45 | ||||
| -rw-r--r-- | libs/installer_fomod_plus/patchwizard/FomodPlusPatchWizard.cpp | 181 | ||||
| -rw-r--r-- | libs/installer_fomod_plus/patchwizard/FomodPlusPatchWizard.h | 54 | ||||
| -rw-r--r-- | libs/installer_fomod_plus/patchwizard/fomod_plus_patch_wizard_en.ts | 96 | ||||
| -rw-r--r-- | libs/installer_fomod_plus/patchwizard/fomod_plus_patchwizard_de.ts | 19 | ||||
| -rw-r--r-- | libs/installer_fomod_plus/patchwizard/fomodpluspatchwizard.json | 8 | ||||
| -rw-r--r-- | libs/installer_fomod_plus/patchwizard/lib/PatchFinder.cpp | 86 | ||||
| -rw-r--r-- | libs/installer_fomod_plus/patchwizard/lib/PatchFinder.h | 49 | ||||
| -rw-r--r-- | libs/installer_fomod_plus/patchwizard/resources.qrc | 6 | ||||
| -rw-r--r-- | libs/installer_fomod_plus/patchwizard/resources/fomod_icon.png | bin | 0 -> 1782 bytes | |||
| -rw-r--r-- | libs/installer_fomod_plus/patchwizard/resources/infoscroll.png | bin | 0 -> 3682 bytes |
11 files changed, 544 insertions, 0 deletions
diff --git a/libs/installer_fomod_plus/patchwizard/CMakeLists.txt b/libs/installer_fomod_plus/patchwizard/CMakeLists.txt new file mode 100644 index 0000000..be46b32 --- /dev/null +++ b/libs/installer_fomod_plus/patchwizard/CMakeLists.txt @@ -0,0 +1,45 @@ +cmake_minimum_required(VERSION 3.16) +project(fomod_plus_patch_wizard) + +include(FetchContent) +set(project_type plugin) + +file(GLOB_RECURSE PATCHWIZARD_SOURCES CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/*.h + ${CMAKE_CURRENT_SOURCE_DIR}/*.ui + ${CMAKE_CURRENT_SOURCE_DIR}/*.qrc +) +file(GLOB SHARE_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/../share/**/*.cpp") + +add_library(fomod_plus_patch_wizard SHARED ${PATCHWIZARD_SOURCES} ${SHARE_SOURCES}) + +FetchContent_Declare(json URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz) +FetchContent_Declare(pugixml GIT_REPOSITORY https://github.com/zeux/pugixml GIT_TAG v1.14) +FetchContent_MakeAvailable(pugixml json) + +target_include_directories( + fomod_plus_patch_wizard + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../share + ${CMAKE_CURRENT_SOURCE_DIR}/../share/FOMODData + ${CMAKE_CURRENT_SOURCE_DIR}/../share/xml + ${MO2_ARCHIVE_INCLUDE_DIRS} +) + +if (MSVC) + target_compile_options( + fomod_plus_patch_wizard + PRIVATE + /bigobj + /W4 + /WX + /wd4201 + /wd4458 + ) +endif () + +target_link_libraries(fomod_plus_patch_wizard PRIVATE mo2::uibase pugixml nlohmann_json::nlohmann_json) +mo2_configure_plugin(fomod_plus_patch_wizard NO_SOURCES WARNINGS OFF PRIVATE_DEPENDS archive) +mo2_install_target(fomod_plus_patch_wizard) diff --git a/libs/installer_fomod_plus/patchwizard/FomodPlusPatchWizard.cpp b/libs/installer_fomod_plus/patchwizard/FomodPlusPatchWizard.cpp new file mode 100644 index 0000000..00be8d5 --- /dev/null +++ b/libs/installer_fomod_plus/patchwizard/FomodPlusPatchWizard.cpp @@ -0,0 +1,181 @@ +#include "FomodPlusPatchWizard.h" + +#include <QApplication> +#include <QDialog> +#include <QHBoxLayout> +#include <QLabel> +#include <QMessageBox> +#include <QProgressDialog> +#include <QPushButton> +#include <QVBoxLayout> + +#include "lib/PatchFinder.h" +#include <FomodRescan.h> + +bool FomodPlusPatchWizard::init(IOrganizer* organizer) +{ + mOrganizer = organizer; + mDialog = new QDialog(); + mDialog->setWindowTitle(tr("Patch Wizard")); + mDialog->setMinimumSize(400, 200); + log.setLogFilePath(QDir::currentPath().toStdString() + "/logs/fomodplus-patchwizard.log"); + + mOrganizer->onUserInterfaceInitialized([this](QMainWindow*) { + logMessage(DEBUG, "patches populated."); + mPatchFinder = std::make_unique<PatchFinder>(mOrganizer); + mPatchFinder->populateInstalledPlugins(); + mAvailablePatches = mPatchFinder->getAvailablePatchesForModList(); + logMessage(DEBUG, "Available Patches: " + std::to_string(mAvailablePatches.size())); + }); + + return true; +} + +void FomodPlusPatchWizard::display() const +{ + // Clear any existing layout + if (mDialog->layout() != nullptr) { + QLayoutItem* item; + while ((item = mDialog->layout()->takeAt(0)) != nullptr) { + delete item->widget(); + delete item; + } + delete mDialog->layout(); + } + + if (mAvailablePatches.empty()) { + setupEmptyState(); + } else { + setupPatchList(); + } + + mDialog->exec(); +} + +void FomodPlusPatchWizard::setupEmptyState() const +{ + auto* mainLayout = new QVBoxLayout(mDialog); + mainLayout->setAlignment(Qt::AlignCenter); + + auto* contentWidget = new QWidget(mDialog); + auto* contentLayout = new QHBoxLayout(contentWidget); + contentLayout->setAlignment(Qt::AlignCenter); + contentLayout->setSpacing(16); + + auto* imageLabel = new QLabel(contentWidget); + imageLabel->setPixmap(QPixmap(":/fomod/infoscroll").scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + + auto* textLabel = new QLabel( + tr("Nothing of interest yet. The wizard gets wiser as you\ninstall FOMODs, so check back later!"), + contentWidget + ); + textLabel->setAlignment(Qt::AlignVCenter | Qt::AlignLeft); + + contentLayout->addWidget(imageLabel); + contentLayout->addWidget(textLabel); + + mainLayout->addWidget(contentWidget); + + auto* rescanButton = new QPushButton(tr("Rescan Load Order"), mDialog); + // Use const_cast since setupEmptyState is const but onRescanClicked modifies state + connect(rescanButton, &QPushButton::clicked, const_cast<FomodPlusPatchWizard*>(this), + &FomodPlusPatchWizard::onRescanClicked); + mainLayout->addWidget(rescanButton, 0, Qt::AlignCenter); +} + +void FomodPlusPatchWizard::onRescanClicked() +{ + const auto confirmResult = QMessageBox::question( + mDialog, + tr("Rescan Load Order"), + tr("Rescanning will populate as many existing choices and options as we can, " + "but if some downloads are deleted it may be missing things! " + "It may take a few minutes depending on the size of your load order and all that."), + QMessageBox::Ok | QMessageBox::Cancel, + QMessageBox::Cancel + ); + + if (confirmResult != QMessageBox::Ok) { + return; + } + + logMessage(DEBUG, "Rescan requested by user"); + + // Create progress dialog + QProgressDialog progress(tr("Scanning mods..."), tr("Cancel"), 0, 100, mDialog); + progress.setWindowModality(Qt::WindowModal); + progress.setMinimumDuration(0); + progress.setValue(0); + + bool cancelled = false; + + // Perform the rescan + FomodRescan rescan(mOrganizer, mPatchFinder->mFomodDb.get()); + auto result = rescan.scanAllModsWithChoices([&](int current, int total, const QString& modName) { + if (progress.wasCanceled()) { + cancelled = true; + return; + } + const int percent = total > 0 ? (current * 100 / total) : 0; + progress.setValue(percent); + progress.setLabelText(tr("Scanning: %1 (%2/%3)").arg(modName).arg(current).arg(total)); + QApplication::processEvents(); + }); + + progress.setValue(100); + + if (cancelled) { + QMessageBox::information( + mDialog, + tr("Rescan Cancelled"), + tr("The rescan was cancelled. Partial results may have been saved.") + ); + logMessage(INFO, "Rescan cancelled by user"); + } else { + // Show result summary + QString summary = tr("Rescan complete!\n\n" + "Mods processed: %1\n" + "Successfully scanned: %2\n" + "Missing archives: %3\n" + "Parse errors: %4") + .arg(result.totalModsProcessed) + .arg(result.successfullyScanned) + .arg(result.missingArchives) + .arg(result.parseErrors); + + if (!result.failedMods.empty() && result.failedMods.size() <= 10) { + summary += tr("\n\nFailed mods:"); + for (const auto& mod : result.failedMods) { + summary += QString("\n- %1").arg(QString::fromStdString(mod)); + } + } else if (result.failedMods.size() > 10) { + summary += tr("\n\n%1 mods failed (see log for details)").arg(result.failedMods.size()); + for (const auto& mod : result.failedMods) { + logMessage(INFO, "Failed mod: " + mod); + } + } + + QMessageBox::information(mDialog, tr("Rescan Complete"), summary); + + logMessage(INFO, "Rescan complete: " + std::to_string(result.successfullyScanned) + + "/" + std::to_string(result.totalModsProcessed) + " successful"); + } + + // Refresh available patches + mPatchFinder->populateInstalledPlugins(); + mAvailablePatches = mPatchFinder->getAvailablePatchesForModList(); + logMessage(DEBUG, "Available Patches after rescan: " + std::to_string(mAvailablePatches.size())); + + // Refresh the UI + display(); +} + +void FomodPlusPatchWizard::setupPatchList() const +{ + auto* mainLayout = new QVBoxLayout(mDialog); + + auto* label = new QLabel(tr("Available patches: %1").arg(mAvailablePatches.size()), mDialog); + mainLayout->addWidget(label); + + // TODO: Implement actual patch list UI +}
\ No newline at end of file diff --git a/libs/installer_fomod_plus/patchwizard/FomodPlusPatchWizard.h b/libs/installer_fomod_plus/patchwizard/FomodPlusPatchWizard.h new file mode 100644 index 0000000..2324fb0 --- /dev/null +++ b/libs/installer_fomod_plus/patchwizard/FomodPlusPatchWizard.h @@ -0,0 +1,54 @@ +#pragma once +#include "../installer/lib/Logger.h" +#include "lib/PatchFinder.h" + +#include <iplugintool.h> +#include <qtmetamacros.h> + +using namespace MOBase; + +class FomodPlusPatchWizard final : public IPluginTool { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginTool) +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + Q_PLUGIN_METADATA(IID "io.clearing.FomodPlusPatchWizard" FILE "fomodpluspatchwizard.json") +#endif + +public: + bool init(IOrganizer* organizer) override; + + [[nodiscard]] QString name() const override { return tr("Patch Wizard"); }; + + [[nodiscard]] QString author() const override { return "clearing"; }; + + [[nodiscard]] QString description() const override { return tr("Find missing patches from FOMODs in your load order."); }; + + [[nodiscard]] VersionInfo version() const override { return { 1, 0, 0, VersionInfo::RELEASE_BETA }; }; + + [[nodiscard]] QList<PluginSetting> settings() const override { return {}; }; + + [[nodiscard]] QString displayName() const override { return tr("Patch Wizard"); }; + + [[nodiscard]] QString tooltip() const override { return tr("Find missing patches from FOMODs in your load order."); }; + + [[nodiscard]] QIcon icon() const override { return QIcon(":/fomod/hat"); } + + void display() const override; + +private: + Logger& log = Logger::getInstance(); + QDialog* mDialog{ nullptr }; + IOrganizer* mOrganizer{ nullptr }; + std::unique_ptr<PatchFinder> mPatchFinder{ nullptr }; + std::vector<AvailablePatch> mAvailablePatches; + + void setupEmptyState() const; + void setupPatchList() const; + void onRescanClicked(); + + void logMessage(const LogLevel level, const std::string& message) const + { + log.logMessage(level, "[PATCHFINDER] " + message); + } + +};
\ No newline at end of file diff --git a/libs/installer_fomod_plus/patchwizard/fomod_plus_patch_wizard_en.ts b/libs/installer_fomod_plus/patchwizard/fomod_plus_patch_wizard_en.ts new file mode 100644 index 0000000..7da04f1 --- /dev/null +++ b/libs/installer_fomod_plus/patchwizard/fomod_plus_patch_wizard_en.ts @@ -0,0 +1,96 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE TS> +<TS version="2.1" language="en_US"> +<context> + <name>FomodPlusPatchWizard</name> + <message> + <location filename="FomodPlusPatchWizard.cpp" line="19"/> + <location filename="FomodPlusPatchWizard.h" line="20"/> + <location filename="FomodPlusPatchWizard.h" line="30"/> + <source>Patch Wizard</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="FomodPlusPatchWizard.cpp" line="69"/> + <source>Nothing of interest yet. The wizard gets wiser as you +install FOMODs, so check back later!</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="FomodPlusPatchWizard.cpp" line="79"/> + <location filename="FomodPlusPatchWizard.cpp" line="90"/> + <source>Rescan Load Order</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="FomodPlusPatchWizard.cpp" line="91"/> + <source>Rescanning will populate as many existing choices and options as we can, but if some downloads are deleted it may be missing things! It may take a few minutes depending on the size of your load order and all that.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="FomodPlusPatchWizard.cpp" line="105"/> + <source>Scanning mods...</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="FomodPlusPatchWizard.cpp" line="105"/> + <source>Cancel</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="FomodPlusPatchWizard.cpp" line="121"/> + <source>Scanning: %1 (%2/%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="FomodPlusPatchWizard.cpp" line="130"/> + <source>Rescan Cancelled</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="FomodPlusPatchWizard.cpp" line="131"/> + <source>The rescan was cancelled. Partial results may have been saved.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="FomodPlusPatchWizard.cpp" line="136"/> + <source>Rescan complete! + +Mods processed: %1 +Successfully scanned: %2 +Missing archives: %3 +Parse errors: %4</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="FomodPlusPatchWizard.cpp" line="147"/> + <source> + +Failed mods:</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="FomodPlusPatchWizard.cpp" line="152"/> + <source> + +%1 mods failed (see log for details)</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="FomodPlusPatchWizard.cpp" line="158"/> + <source>Rescan Complete</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="FomodPlusPatchWizard.cpp" line="177"/> + <source>Available patches: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="FomodPlusPatchWizard.h" line="24"/> + <location filename="FomodPlusPatchWizard.h" line="32"/> + <source>Find missing patches from FOMODs in your load order.</source> + <translation type="unfinished"></translation> + </message> +</context> +</TS> diff --git a/libs/installer_fomod_plus/patchwizard/fomod_plus_patchwizard_de.ts b/libs/installer_fomod_plus/patchwizard/fomod_plus_patchwizard_de.ts new file mode 100644 index 0000000..aaa26e2 --- /dev/null +++ b/libs/installer_fomod_plus/patchwizard/fomod_plus_patchwizard_de.ts @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE TS> +<TS version="2.1" language="de_DE"> +<context> + <name>FomodPlusPatchWizard</name> + <message> + <location filename="FomodPlusPatchWizard.cpp" line="26"/> + <location filename="FomodPlusPatchWizard.cpp" line="51"/> + <location filename="FomodPlusPatchWizard.cpp" line="74"/> + <source>Patch Wizard</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="FomodPlusPatchWizard.cpp" line="36"/> + <source>Finde Patches, die du aus den FOMODs in deiner Modliste installieren möchtest.</source> + <translation type="unfinished"></translation> + </message> +</context> +</TS> diff --git a/libs/installer_fomod_plus/patchwizard/fomodpluspatchwizard.json b/libs/installer_fomod_plus/patchwizard/fomodpluspatchwizard.json new file mode 100644 index 0000000..2d6025f --- /dev/null +++ b/libs/installer_fomod_plus/patchwizard/fomodpluspatchwizard.json @@ -0,0 +1,8 @@ +{ + "author": "clearing", + "date": "2025/02/06", + "name": "FOMOD Plus - Patch Wizard", + "version": "1.0.0", + "des": "Find missing patches in your modlist", + "dependencies": [] +} diff --git a/libs/installer_fomod_plus/patchwizard/lib/PatchFinder.cpp b/libs/installer_fomod_plus/patchwizard/lib/PatchFinder.cpp new file mode 100644 index 0000000..cd06b3a --- /dev/null +++ b/libs/installer_fomod_plus/patchwizard/lib/PatchFinder.cpp @@ -0,0 +1,86 @@ +#include "PatchFinder.h" + +std::vector<AvailablePatch> PatchFinder::getAvailablePatchesForMod(const MOBase::IModInterface* mod) +{ + std::vector<AvailablePatch> available_patches = {}; + + // Verify that the mod has plugins in some shape or form. + if (const auto it = m_installedPlugins.find(mod); it == m_installedPlugins.end()) { + return available_patches; + } + + // Look through the database + for (const auto& fomodDbEntries = mFomodDb->getEntries(); const auto& entry : fomodDbEntries) { + for (const auto& option : entry->getOptions()) { + // Skip options that are already selected/installed + if (option.selectionState == SelectionState::Selected) { + continue; + } + + // Extract just the filename from the path (handle both / and \) + const auto fileName = option.fileName.substr(option.fileName.find_last_of("/\\") + 1); + + // Skip if already installed in the modlist + if (m_installedPluginsCacheSet.contains(fileName)) { + continue; + } + + // Technically without this check, we're doing a lookup for the entire modlist, which actually + // kinda works, but isn't the intention of this function. Might want to consider reworking + // it to map to mod names with one pass of the DB instead of a DB pass per mod. + if (!std::ranges::any_of(option.masters, [&mod](const std::string& master) { + return master == mod->name().toStdString(); + })) { + continue; + } + + // If we have all of this patch's masters in our modlist, and it's not installed, add it to the results. + if (std::ranges::all_of(option.masters, [this](const std::string& master) { + return m_installedPluginsCacheSet.contains(master); + })) { + AvailablePatch patch{ + option, + entry->getDisplayName(), + mod->name().toStdString(), + false, // not installed + false, // not hidden + option.selectionState == SelectionState::Deselected // userDeselected + }; + available_patches.push_back(patch); + } + } + } + + return available_patches; +} + +std::vector<AvailablePatch> PatchFinder::getAvailablePatchesForModList() +{ + std::vector<AvailablePatch> available_patches = {}; + for (const auto& modName : m_organizer->modList()->allMods()) { + const auto mod = m_organizer->modList()->getMod(modName); + if (mod == nullptr) { + continue; + } + for (const auto& available_patch : getAvailablePatchesForMod(mod)) { + available_patches.emplace_back(available_patch); + } + } + + return available_patches; +} + +void PatchFinder::populateInstalledPlugins() +{ + for (const auto& modName : m_organizer->modList()->allMods()) { + const auto mod = m_organizer->modList()->getMod(modName); + const auto mod_tree = mod->fileTree(); + for (auto it = mod_tree->begin(); it != mod_tree->end(); ++it) { + if ((*it)->isFile() && isPluginFile((*it)->name())) { + std::cout << "Plugin: " << (*it)->name().toStdString() << std::endl; + m_installedPlugins[mod].emplace_back((*it)->name().toStdString()); + m_installedPluginsCacheSet.insert((*it)->name().toStdString()); + } + } + } +}
\ No newline at end of file diff --git a/libs/installer_fomod_plus/patchwizard/lib/PatchFinder.h b/libs/installer_fomod_plus/patchwizard/lib/PatchFinder.h new file mode 100644 index 0000000..02327e3 --- /dev/null +++ b/libs/installer_fomod_plus/patchwizard/lib/PatchFinder.h @@ -0,0 +1,49 @@ +#pragma once + +#include "../../installer/lib/Logger.h" + +#include <FomodDB.h> +#include <imodinterface.h> +#include <imoinfo.h> +#include <ifiletree.h> + +struct AvailablePatch { + FomodOption fomod_option; + std::string installer_name; + std::string patch_for_mod; + bool installed = false; + bool hidden = false; + bool userDeselected = false; // True if user explicitly chose not to install +}; + +class PatchFinder { +friend class FomodPlusPatchWizard; + +public: + explicit PatchFinder(MOBase::IOrganizer* m_organizer) : m_organizer(m_organizer) + { + mFomodDb = std::make_unique<FomodDB>(m_organizer->basePath().toStdString()); + logMessage(DEBUG, "mFomodDb loaded."); + } + + std::vector<AvailablePatch> getAvailablePatchesForMod(const MOBase::IModInterface* mod); + std::vector<AvailablePatch> getAvailablePatchesForModList(); + +protected: + void populateInstalledPlugins(); + +private: + Logger& log = Logger::getInstance(); + MOBase::IOrganizer* m_organizer; + std::unique_ptr<FomodDB> mFomodDb; + + // Map of { pluginPtr: [1.esp, 2.esp, 3.esp] } + std::unordered_map<const MOBase::IModInterface*, std::vector<std::string> > m_installedPlugins; + std::unordered_set<std::string> m_installedPluginsCacheSet; + + void logMessage(const LogLevel level, const std::string& message) const + { + log.logMessage(level, "[PATCHFINDER] " + message); + } + +};
\ No newline at end of file diff --git a/libs/installer_fomod_plus/patchwizard/resources.qrc b/libs/installer_fomod_plus/patchwizard/resources.qrc new file mode 100644 index 0000000..fe6971e --- /dev/null +++ b/libs/installer_fomod_plus/patchwizard/resources.qrc @@ -0,0 +1,6 @@ +<RCC> + <qresource prefix="/fomod"> + <file alias="hat">resources/fomod_icon.png</file> + <file alias="infoscroll">resources/infoscroll.png</file> + </qresource> +</RCC> diff --git a/libs/installer_fomod_plus/patchwizard/resources/fomod_icon.png b/libs/installer_fomod_plus/patchwizard/resources/fomod_icon.png Binary files differnew file mode 100644 index 0000000..e044052 --- /dev/null +++ b/libs/installer_fomod_plus/patchwizard/resources/fomod_icon.png diff --git a/libs/installer_fomod_plus/patchwizard/resources/infoscroll.png b/libs/installer_fomod_plus/patchwizard/resources/infoscroll.png Binary files differnew file mode 100644 index 0000000..0c31777 --- /dev/null +++ b/libs/installer_fomod_plus/patchwizard/resources/infoscroll.png |
