From 817e8f5cd26739c69d930d21cd9dc4c0b6e4984e Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Sat, 14 Feb 2026 02:45:12 -0600 Subject: 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 --- libs/installer_fomod_plus/scanner/CMakeLists.txt | 54 +++++++++ .../scanner/FomodPlusScanner.cpp | 128 +++++++++++++++++++++ .../scanner/FomodPlusScanner.h | 64 +++++++++++ libs/installer_fomod_plus/scanner/archiveparser.h | 98 ++++++++++++++++ .../scanner/fomod_plus_scanner_de.ts | 57 +++++++++ .../scanner/fomod_plus_scanner_en.ts | 57 +++++++++ .../scanner/fomod_plus_scanner_zh_CN.ts | 57 +++++++++ .../scanner/fomodplusscanner.json | 8 ++ libs/installer_fomod_plus/scanner/resources.qrc | 6 + .../scanner/resources/fomod_icon.png | Bin 0 -> 1782 bytes .../scanner/resources/wizard.gif | Bin 0 -> 5263 bytes 11 files changed, 529 insertions(+) create mode 100644 libs/installer_fomod_plus/scanner/CMakeLists.txt create mode 100644 libs/installer_fomod_plus/scanner/FomodPlusScanner.cpp create mode 100644 libs/installer_fomod_plus/scanner/FomodPlusScanner.h create mode 100644 libs/installer_fomod_plus/scanner/archiveparser.h create mode 100644 libs/installer_fomod_plus/scanner/fomod_plus_scanner_de.ts create mode 100644 libs/installer_fomod_plus/scanner/fomod_plus_scanner_en.ts create mode 100644 libs/installer_fomod_plus/scanner/fomod_plus_scanner_zh_CN.ts create mode 100644 libs/installer_fomod_plus/scanner/fomodplusscanner.json create mode 100644 libs/installer_fomod_plus/scanner/resources.qrc create mode 100644 libs/installer_fomod_plus/scanner/resources/fomod_icon.png create mode 100644 libs/installer_fomod_plus/scanner/resources/wizard.gif (limited to 'libs/installer_fomod_plus/scanner') diff --git a/libs/installer_fomod_plus/scanner/CMakeLists.txt b/libs/installer_fomod_plus/scanner/CMakeLists.txt new file mode 100644 index 0000000..daea477 --- /dev/null +++ b/libs/installer_fomod_plus/scanner/CMakeLists.txt @@ -0,0 +1,54 @@ +cmake_minimum_required(VERSION 3.16) +project(fomod_plus_scanner) + +include(FetchContent) +set(project_type plugin) + +file(GLOB_RECURSE SCANNER_SOURCES CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/*.h + ${CMAKE_CURRENT_SOURCE_DIR}/*.ui + ${CMAKE_CURRENT_SOURCE_DIR}/*.qrc +) + +add_library(fomod_plus_scanner SHARED ${SCANNER_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_scanner + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../share + ${CMAKE_CURRENT_SOURCE_DIR}/../share/FOMODData + ${MO2_UIBASE_INCLUDE_DIRS} + ${MO2_ARCHIVE_INCLUDE_DIRS} +) + +if (MSVC) + target_compile_options( + fomod_plus_scanner + PRIVATE + /bigobj + /W4 + /WX + /wd4201 + /wd4458 + ) +endif () + +target_link_libraries(fomod_plus_scanner PRIVATE mo2::uibase mo2::archive pugixml nlohmann_json::nlohmann_json) +mo2_configure_plugin(fomod_plus_scanner NO_SOURCES WARNINGS OFF PRIVATE_DEPENDS archive) + +if (MSVC) + target_compile_options(fomod_plus_scanner PRIVATE $<$:/Zi>) + target_link_options(fomod_plus_scanner PRIVATE $<$:/DEBUG>) + install(FILES $ + DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO} + CONFIGURATIONS RelWithDebInfo + OPTIONAL) +endif () + +mo2_install_target(fomod_plus_scanner) diff --git a/libs/installer_fomod_plus/scanner/FomodPlusScanner.cpp b/libs/installer_fomod_plus/scanner/FomodPlusScanner.cpp new file mode 100644 index 0000000..4770178 --- /dev/null +++ b/libs/installer_fomod_plus/scanner/FomodPlusScanner.cpp @@ -0,0 +1,128 @@ +#include "FomodPlusScanner.h" + +#include "archiveparser.h" +#include "FomodDBEntry.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "stringutil.h" + +using ScanCallbackFn = std::function; + +bool FomodPlusScanner::init(IOrganizer* organizer) +{ + mOrganizer = organizer; + + mDialog = new QDialog(); + mDialog->setWindowTitle(tr("FOMOD Scanner")); + + const auto layout = new QVBoxLayout(mDialog); + + const QString description = tr("Greetings, traveler.\n" + "This tool will scan your load order for mods installed via FOMOD.\n" + "It will also fix up any erroneous FOMOD flags from previous versions of FOMOD Plus :) \n\n" + "Safe travels, and may your load order be free of conflicts."); + + const auto descriptionLabel = new QLabel(description, mDialog); + descriptionLabel->setWordWrap(true); // Enable word wrap for large text + descriptionLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + descriptionLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); // Align text to the top-left corner + + const auto gifLabel = new QLabel(mDialog); + const auto movie = new QMovie(":/fomod/wizard"); + gifLabel->setMovie(movie); + movie->start(); + + mProgressBar = new QProgressBar(mDialog); + mProgressBar->setRange(0, mOrganizer->modList()->allMods().size()); + mProgressBar->setVisible(false); + + const auto scanButton = new QPushButton(tr("Scan"), mDialog); + const auto cancelButton = new QPushButton(tr("Cancel"), mDialog); + + layout->addWidget(descriptionLabel, 1); + layout->addWidget(gifLabel, 1); + layout->addWidget(mProgressBar, 1); + layout->addWidget(scanButton, 1); + layout->addWidget(cancelButton, 1); + + connect(cancelButton, &QPushButton::clicked, mDialog, &QDialog::reject); + connect(scanButton, &QPushButton::clicked, this, &FomodPlusScanner::onScanClicked); + connect(mDialog, &QDialog::finished, this, &FomodPlusScanner::cleanup); + + mDialog->setLayout(layout); + mDialog->setMinimumSize(400, 300); + mDialog->adjustSize(); + descriptionLabel->adjustSize(); + return true; +} + +void FomodPlusScanner::onScanClicked() const +{ + mProgressBar->setVisible(true); + const int added = scanLoadOrder(setFomodInfoForMod); + mDialog->accept(); + QMessageBox::information(mDialog, tr("Scan Complete"), + tr("The load order scan is complete. Updated filter info for ") + QString::number(added) + tr(" mods.")); + mOrganizer->refresh(); +} + +void FomodPlusScanner::cleanup() const +{ + mProgressBar->reset(); + mProgressBar->setVisible(false); +} + +void FomodPlusScanner::display() const +{ + mDialog->exec(); +} + +int FomodPlusScanner::scanLoadOrder(const ScanCallbackFn& callback) const +{ + int progress = 0; + int modified = 0; + for (const auto& modName : mOrganizer->modList()->allMods()) { + const auto mod = mOrganizer->modList()->getMod(modName); + if (const ScanResult result = openInstallationArchive(mod); callback(mod, result)) { + modified++; + } + mProgressBar->setValue(++progress); + } + return modified; +} + +ScanResult FomodPlusScanner::openInstallationArchive(const IModInterface* mod) const +{ + const auto downloadsDir = mOrganizer->downloadsPath(); + const auto installationFilePath = mod->installationFile(); + return ArchiveParser::scanForFomodFiles(downloadsDir, installationFilePath, mod->name()); +} + +bool FomodPlusScanner::setFomodInfoForMod(IModInterface* mod, const ScanResult result) +{ + const auto pluginName = "FOMOD Plus"; + const auto setting = mod->pluginSetting(pluginName, "fomod", 0); + if (setting == 0 && ScanResult::HAS_FOMOD == result) { + return mod->setPluginSetting(pluginName, "fomod", "{}"); + } + if (setting != 0 && ScanResult::NO_FOMOD == result) { + return mod->setPluginSetting(pluginName, "fomod", 0); + } + return false; +} + +bool FomodPlusScanner::removeFomodInfoFromMod(IModInterface* mod, ScanResult) +{ + const auto pluginName = QString::fromStdString("FOMOD Plus"); + return mod->setPluginSetting(pluginName, "fomod", 0); +} diff --git a/libs/installer_fomod_plus/scanner/FomodPlusScanner.h b/libs/installer_fomod_plus/scanner/FomodPlusScanner.h new file mode 100644 index 0000000..b7893f7 --- /dev/null +++ b/libs/installer_fomod_plus/scanner/FomodPlusScanner.h @@ -0,0 +1,64 @@ +#ifndef FOMODPLUSSCANNER_H +#define FOMODPLUSSCANNER_H + +#include "archiveparser.h" + +#include +#include + +#include +#include + +using namespace MOBase; + +class FomodPlusScanner 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.FomodPlusScanner" FILE "fomodplusscanner.json") +#endif + +public: + ~FomodPlusScanner() override + { + delete mDialog; + mDialog = nullptr; + mProgressBar = nullptr; + } + + bool init(IOrganizer* organizer) override; + + void onScanClicked() const; + + void cleanup() const; + + [[nodiscard]] QString name() const override { return "FOMOD Scanner"; } // This should not be translated + [[nodiscard]] QString author() const override { return "clearing"; } + [[nodiscard]] QString description() const override { return tr("Scans modlist for files installed via FOMOD"); } + [[nodiscard]] VersionInfo version() const override { return {1, 0, 0, VersionInfo::RELEASE_FINAL}; } + + [[nodiscard]] QList settings() const override { return {}; } + + [[nodiscard]] QString displayName() const override { return tr("FOMOD Scanner"); } + + [[nodiscard]] QString tooltip() const override { return tr("Scan modlist for files installed via FOMOD"); } + + [[nodiscard]] QIcon icon() const override { return QIcon(":/fomod/hat"); } + + void display() const override; + + int scanLoadOrder(const std::function &callback) const; + + ScanResult openInstallationArchive(const IModInterface* mod) const; + + static bool setFomodInfoForMod(IModInterface *mod, ScanResult result); + + static bool removeFomodInfoFromMod(IModInterface *mod, ScanResult); + +private: + QDialog* mDialog{ nullptr }; + QProgressBar* mProgressBar{ nullptr }; + IOrganizer* mOrganizer{ nullptr }; +}; + +#endif // FOMODPLUSSCANNER_H \ No newline at end of file diff --git a/libs/installer_fomod_plus/scanner/archiveparser.h b/libs/installer_fomod_plus/scanner/archiveparser.h new file mode 100644 index 0000000..4058d43 --- /dev/null +++ b/libs/installer_fomod_plus/scanner/archiveparser.h @@ -0,0 +1,98 @@ +#pragma once +#include "stringutil.h" + +#include +#include +#include +#include +#include + +inline std::ostream& operator<<(std::ostream& os, const Archive::Error& error) +{ + switch (error) { + case Archive::Error::ERROR_NONE: + os << "No error"; + break; + case Archive::Error::ERROR_ARCHIVE_NOT_FOUND: + os << "File not found"; + break; + case Archive::Error::ERROR_FAILED_TO_OPEN_ARCHIVE: + os << "Failed to open file"; + break; + case Archive::Error::ERROR_INVALID_ARCHIVE_FORMAT: + os << "Invalid archive format"; + break; + default: + os << "Unknown error??"; + } + return os; +} + +inline bool hasFomodFiles(const std::vector& files) +{ + bool hasModuleXml = false; + bool hasInfoXml = false; + + for (const auto* file : files) { + if (endsWithCaseInsensitive(file->getArchiveFilePath(), StringConstants::FomodFiles::W_MODULE_CONFIG.data())) { + hasModuleXml = true; + } + if (endsWithCaseInsensitive(file->getArchiveFilePath(), StringConstants::FomodFiles::W_INFO_XML.data())) { + hasInfoXml = true; + } + } + return hasModuleXml && hasInfoXml; +} + +/* This class can do the following: + * 1.) Detects if an archive has FOMOD files in it (without extracting) + * - This is used by the FOMOD scanner to set content flags + * + * 2.) It may support extracting ESPs and FOMODs, but this might be better served + * by a separate class. Maybe it could return a ModuleConfiguration to use like + * the installer. + */ + +enum class ScanResult { + HAS_FOMOD, + NO_FOMOD, + NO_ARCHIVE +}; + +class ArchiveParser { +public: + static ScanResult scanForFomodFiles(const QString& downloadsPath, const QString& installationFilePath, + const QString& modName) + { + if (installationFilePath.isEmpty()) { + return ScanResult::NO_ARCHIVE; + } + const auto qualifiedInstallerPath = QDir(installationFilePath).isAbsolute() + ? installationFilePath + : downloadsPath + "/" + installationFilePath; + + const auto archive = CreateArchive(); + + if (!archive->isValid()) { + logErrorForMod(modName, "Failed to load the archive module ", archive); + return ScanResult::NO_ARCHIVE; + } + if (!archive->open(qualifiedInstallerPath.toStdWString(), nullptr)) { + logErrorForMod(modName, "Failed to open archive [" + qualifiedInstallerPath + "]", archive); + return ScanResult::NO_ARCHIVE; + } + + if (hasFomodFiles(archive->getFileList())) { + std::cout << "Found FOMOD files in " << qualifiedInstallerPath.toStdString() << std::endl; + return ScanResult::HAS_FOMOD; + } + return ScanResult::NO_FOMOD; + } + +private: + static void logErrorForMod(const QString& modName, const QString& message, const std::unique_ptr& archive) + { + std::cerr << "[" << modName.toStdString() << "] " << message.toStdString() << " (" << archive->getLastError() << + ")" << std::endl; + } +}; \ No newline at end of file diff --git a/libs/installer_fomod_plus/scanner/fomod_plus_scanner_de.ts b/libs/installer_fomod_plus/scanner/fomod_plus_scanner_de.ts new file mode 100644 index 0000000..759824f --- /dev/null +++ b/libs/installer_fomod_plus/scanner/fomod_plus_scanner_de.ts @@ -0,0 +1,57 @@ + + + + + FomodPlusScanner + + + + FOMOD Scanner + + + + + Sei gegrüßt, Reisender. +Dieses Tool scannt deine Ladereihenfolge nach Mods, die über einen FOMOD installiert wurden. +Es wird außerdem fehlerhafte FOMOD-Selektierungen aus früheren Versionen von FOMOD Plus beheben. :) + +Gute Reise, und möge deine Ladereihenfolge frei von Konflikten sein. + + + + + Scannen + + + + + Abbrechen + + + + + Scan Abgeschlossen + + + + + Der Scan der Ladereihenfolge ist abgeschlossen. Aktualisierte Filterinformationen für + + + + + Mods. + + + + + Scannt die Modliste nach Dateien, die über einen FOMOD installiert wurden. + + + + + Modliste nach Dateien durchsuchen, die über einen FOMOD installiert wurden. + + + + diff --git a/libs/installer_fomod_plus/scanner/fomod_plus_scanner_en.ts b/libs/installer_fomod_plus/scanner/fomod_plus_scanner_en.ts new file mode 100644 index 0000000..83a310b --- /dev/null +++ b/libs/installer_fomod_plus/scanner/fomod_plus_scanner_en.ts @@ -0,0 +1,57 @@ + + + + + FomodPlusScanner + + + + FOMOD Scanner + + + + + Greetings, traveler. +This tool will scan your load order for mods installed via FOMOD. +It will also fix up any erroneous FOMOD flags from previous versions of FOMOD Plus :) + +Safe travels, and may your load order be free of conflicts. + + + + + Scan + + + + + Cancel + + + + + Scan Complete + + + + + The load order scan is complete. Updated filter info for + + + + + mods. + + + + + Scans modlist for files installed via FOMOD + + + + + Scan modlist for files installed via FOMOD + + + + diff --git a/libs/installer_fomod_plus/scanner/fomod_plus_scanner_zh_CN.ts b/libs/installer_fomod_plus/scanner/fomod_plus_scanner_zh_CN.ts new file mode 100644 index 0000000..01f84c8 --- /dev/null +++ b/libs/installer_fomod_plus/scanner/fomod_plus_scanner_zh_CN.ts @@ -0,0 +1,57 @@ + + + + + FomodPlusScanner + + + + FOMOD Scanner + + + + + 你好,旅行者。 +该工具将扫描模组列表来查找通过 FOMOD 安装的模组, +并修正旧版本错误的 FOMOD 标志(需保留安装包才能被检测到):) + +一路平安,祝您的 load order 没有冲突。 + + + + + 扫描 + + + + + 取消 + + + + + 扫描完成 + + + + + 模组扫描已完成。更新了 + + + + + 个模组筛选框信息。 + + + + + 扫描模组列表查找通过 FOMOD 安装的文件 + + + + + 扫描模组列表查找通过 FOMOD 安装的文件 + + + + diff --git a/libs/installer_fomod_plus/scanner/fomodplusscanner.json b/libs/installer_fomod_plus/scanner/fomodplusscanner.json new file mode 100644 index 0000000..f6643ea --- /dev/null +++ b/libs/installer_fomod_plus/scanner/fomodplusscanner.json @@ -0,0 +1,8 @@ +{ + "author" : "clearing", + "date" : "2025/02/06", + "name" : "FOMOD Plus - Scanner", + "version" : "1.0.0", + "des" : "Scans load order for FOMOD installations", + "dependencies" : [] +} diff --git a/libs/installer_fomod_plus/scanner/resources.qrc b/libs/installer_fomod_plus/scanner/resources.qrc new file mode 100644 index 0000000..df7c48c --- /dev/null +++ b/libs/installer_fomod_plus/scanner/resources.qrc @@ -0,0 +1,6 @@ + + + resources/fomod_icon.png + resources/wizard.gif + + \ No newline at end of file diff --git a/libs/installer_fomod_plus/scanner/resources/fomod_icon.png b/libs/installer_fomod_plus/scanner/resources/fomod_icon.png new file mode 100644 index 0000000..e044052 Binary files /dev/null and b/libs/installer_fomod_plus/scanner/resources/fomod_icon.png differ diff --git a/libs/installer_fomod_plus/scanner/resources/wizard.gif b/libs/installer_fomod_plus/scanner/resources/wizard.gif new file mode 100644 index 0000000..c1f75d3 Binary files /dev/null and b/libs/installer_fomod_plus/scanner/resources/wizard.gif differ -- cgit v1.3.1