aboutsummaryrefslogtreecommitdiff
path: root/libs/installer_fomod_plus/patchwizard
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-14 02:45:12 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-14 02:45:25 -0600
commit817e8f5cd26739c69d930d21cd9dc4c0b6e4984e (patch)
tree52aed11d6751bb7d20b06acf197d56fac113203d /libs/installer_fomod_plus/patchwizard
parent51a9f8f197727f00896e5de44569b098923527dd (diff)
Add FUSE external mapping support, BG3/Oblivion Remastered fixes, fomod-plus and NaK integration
FUSE VFS now deploys non-data-dir mod mappings (Paks, OBSE, UE4SS, etc.) via real symlinks and injects file-level data-dir mappings (plugins.txt, loadorder.txt) into the VFS tree. Fixes game launches for Oblivion Remastered (Root Builder path resolution, script extender support) and BG3 (Wine prefix documents directory, file mapper symlinks on Linux). Vendors mo2-fomod-plus plugin and NaK crate for FOMOD installer and game finder/runtime support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/installer_fomod_plus/patchwizard')
-rw-r--r--libs/installer_fomod_plus/patchwizard/CMakeLists.txt45
-rw-r--r--libs/installer_fomod_plus/patchwizard/FomodPlusPatchWizard.cpp181
-rw-r--r--libs/installer_fomod_plus/patchwizard/FomodPlusPatchWizard.h54
-rw-r--r--libs/installer_fomod_plus/patchwizard/fomod_plus_patch_wizard_en.ts96
-rw-r--r--libs/installer_fomod_plus/patchwizard/fomod_plus_patchwizard_de.ts19
-rw-r--r--libs/installer_fomod_plus/patchwizard/fomodpluspatchwizard.json8
-rw-r--r--libs/installer_fomod_plus/patchwizard/lib/PatchFinder.cpp86
-rw-r--r--libs/installer_fomod_plus/patchwizard/lib/PatchFinder.h49
-rw-r--r--libs/installer_fomod_plus/patchwizard/resources.qrc6
-rw-r--r--libs/installer_fomod_plus/patchwizard/resources/fomod_icon.pngbin0 -> 1782 bytes
-rw-r--r--libs/installer_fomod_plus/patchwizard/resources/infoscroll.pngbin0 -> 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&ouml;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
new file mode 100644
index 0000000..e044052
--- /dev/null
+++ b/libs/installer_fomod_plus/patchwizard/resources/fomod_icon.png
Binary files differ
diff --git a/libs/installer_fomod_plus/patchwizard/resources/infoscroll.png b/libs/installer_fomod_plus/patchwizard/resources/infoscroll.png
new file mode 100644
index 0000000..0c31777
--- /dev/null
+++ b/libs/installer_fomod_plus/patchwizard/resources/infoscroll.png
Binary files differ