From 7ee008e150bc5bcf76082d726f719ee0fdfda982 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Wed, 11 Feb 2026 02:37:39 -0600 Subject: Fluorine Manager: full Linux port of Mod Organizer 2 Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 --- libs/installer_fomod/src/installerfomod.cpp | 301 ++++++++++++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 libs/installer_fomod/src/installerfomod.cpp (limited to 'libs/installer_fomod/src/installerfomod.cpp') diff --git a/libs/installer_fomod/src/installerfomod.cpp b/libs/installer_fomod/src/installerfomod.cpp new file mode 100644 index 0000000..3558748 --- /dev/null +++ b/libs/installer_fomod/src/installerfomod.cpp @@ -0,0 +1,301 @@ +#include "installerfomod.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "fomodinstallerdialog.h" + +using namespace MOBase; + +const unsigned int InstallerFomod::PROBLEM_IMAGETYPE_UNSUPPORTED; + +InstallerFomod::InstallerFomod() : m_MOInfo(nullptr) {} + +bool InstallerFomod::init(IOrganizer* moInfo) +{ + m_MOInfo = moInfo; + return true; +} + +QString InstallerFomod::name() const +{ + return "Fomod Installer"; +} + +QString InstallerFomod::author() const +{ + return "Tannin & thosrtanner"; +} + +QString InstallerFomod::description() const +{ + return tr("Installer for xml based fomod archives."); +} + +VersionInfo InstallerFomod::version() const +{ + return VersionInfo(1, 7, 0, VersionInfo::RELEASE_FINAL); +} + +QString InstallerFomod::localizedName() const +{ + return tr("Fomod Installer"); +} + +bool InstallerFomod::allowAnyFile() const +{ + return m_MOInfo->pluginSetting(name(), "use_any_file").toBool(); +} + +bool InstallerFomod::checkDisabledMods() const +{ + return m_MOInfo->pluginSetting(name(), "see_disabled_mods").toBool(); +} + +QList InstallerFomod::settings() const +{ + QList result; + result.push_back( + PluginSetting("prefer", "prefer this over the NCC based plugin", QVariant(true))); + result.push_back(PluginSetting("use_any_file", + "allow dependencies on any file, not just esp/esm", + QVariant(false))); + result.push_back(PluginSetting("see_disabled_mods", + "treat disabled mods as inactive rather than missing", + QVariant(false))); + return result; +} + +unsigned int InstallerFomod::priority() const +{ + return m_MOInfo->pluginSetting(name(), "prefer").toBool() ? 110 : 90; +} + +bool InstallerFomod::isManualInstaller() const +{ + return false; +} + +void InstallerFomod::onInstallationStart(QString const&, bool, IModInterface*) +{ + m_InstallerUsed = false; +} + +void InstallerFomod::onInstallationEnd(EInstallResult result, IModInterface* newMod) +{ + if (result == EInstallResult::RESULT_SUCCESS && m_InstallerUsed && + newMod->url().isEmpty()) { + newMod->setUrl(m_Url); + } +} + +std::shared_ptr +InstallerFomod::findFomodDirectory(std::shared_ptr tree) const +{ + auto entry = tree->find("fomod", FileTreeEntry::DIRECTORY); + + if (entry != nullptr) { + return entry->astree(); + } + + if (tree->size() == 1 && tree->at(0)->isDir()) { + return findFomodDirectory(tree->at(0)->astree()); + } + return nullptr; +} + +bool InstallerFomod::isArchiveSupported(std::shared_ptr tree) const +{ + tree = findFomodDirectory(tree); + if (tree != nullptr) { + return tree->exists("ModuleConfig.xml", FileTreeEntry::FILE); + } + return false; +} + +void InstallerFomod::appendImageFiles( + std::vector>& entries, + std::shared_ptr tree) const +{ + static std::set imageSuffixes{"png", "jpg", "jpeg", + "gif", "bmp"}; + for (auto entry : *tree) { + if (entry->isDir()) { + appendImageFiles(entries, entry->astree()); + } else if (imageSuffixes.count(entry->suffix()) > 0) { + entries.push_back(entry); + } + } +} + +std::vector> +InstallerFomod::buildFomodTree(std::shared_ptr tree) const +{ + std::vector> entries; + + auto fomodTree = findFomodDirectory(tree); + + for (auto entry : *fomodTree) { + if (entry->isFile() && + (entry->compare("info.xml") == 0 || entry->compare("ModuleConfig.xml") == 0)) { + entries.push_back(entry); + } + } + + appendImageFiles(entries, tree); + + return entries; +} + +IPluginList::PluginStates InstallerFomod::fileState(const QString& fileName) const +{ + QString ext = QFileInfo(fileName).suffix().toLower(); + if ((ext == "esp") || (ext == "esm") || (ext == "esl")) { + IPluginList::PluginStates state = m_MOInfo->pluginList()->state(fileName); + if (state != IPluginList::STATE_MISSING) { + return state; + } + } else if (allowAnyFile()) { + QFileInfo info(fileName); + QString name = info.fileName(); + QStringList files = + m_MOInfo->findFiles(info.dir().path(), [&, name](const QString& f) -> bool { + return name.compare(QFileInfo(f).fileName(), + FileNameComparator::CaseSensitivity) == 0; + }); + // A note: The list of files produced is somewhat odd as it's the full path + // to the originating mod (or mods). However, all we care about is if it's + // there or not. + if (files.size() != 0) { + return IPluginList::STATE_ACTIVE; + } + } else { + log::warn("A dependency on non esp/esm/esl {} will always find it as missing.", + fileName); + return IPluginList::STATE_MISSING; + } + + // If they are really desparate we look in the full mod list and try that + if (checkDisabledMods()) { + IModList* modList = m_MOInfo->modList(); + QStringList list = modList->allMods(); + for (QString mod : list) { + // Get mod state. if it's active we've already looked. If it's not valid, + // no point in looking. + IModList::ModStates state = modList->state(mod); + if ((state & IModList::STATE_ACTIVE) != 0 || + (state & IModList::STATE_VALID) == 0) { + continue; + } + MOBase::IModInterface* modInfo = m_MOInfo->modList()->getMod(mod); + // Go see if the file is in the mod + QDir modpath(modInfo->absolutePath()); + QFile file(modpath.absoluteFilePath(fileName)); + if (file.exists()) { + return IPluginList::STATE_INACTIVE; + } + } + } + return IPluginList::STATE_MISSING; +} + +IPluginInstaller::EInstallResult +InstallerFomod::install(GuessedValue& modName, + std::shared_ptr& tree, QString& version, int& modID) +{ + auto installerFiles = buildFomodTree(tree); + if (manager()->extractFiles(installerFiles).size() == installerFiles.size()) { + try { + std::shared_ptr fomodTree = findFomodDirectory(tree); + + QString fomodPath = fomodTree->parent()->path(); + FomodInstallerDialog dialog( + this, modName, fomodPath, + std::bind(&InstallerFomod::fileState, this, std::placeholders::_1)); + dialog.initData(m_MOInfo); + if (!dialog.getVersion().isEmpty()) { + version = dialog.getVersion(); + } + if (dialog.getModID() != -1) { + modID = dialog.getModID(); + } + + m_InstallerUsed = true; + m_Url = dialog.getURL(); + + if (!dialog.hasOptions()) { + dialog.transformToSmallInstall(); + } + + auto result = dialog.exec(); + if (result == QDialog::Accepted) { + modName.update(dialog.getName(), GUESS_USER); + return dialog.updateTree(tree); + } else { + if (dialog.manualRequested()) { + modName.update(dialog.getName(), GUESS_USER); + return IPluginInstaller::RESULT_MANUALREQUESTED; + } else if (result == QDialog::Rejected) { + return IPluginInstaller::RESULT_CANCELED; + } else { + return IPluginInstaller::RESULT_FAILED; + } + } + } catch (const std::exception& e) { + reportError(tr("Installation as fomod failed: %1").arg(e.what())); + return IPluginInstaller::RESULT_FAILED; + } + } + return IPluginInstaller::RESULT_CANCELED; +} + +#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) +Q_EXPORT_PLUGIN2(installerFomod, InstallerFomod) +#endif + +std::vector InstallerFomod::activeProblems() const +{ + std::vector result; + QList formats = QImageReader::supportedImageFormats(); + if (!formats.contains("jpg")) { + result.push_back(PROBLEM_IMAGETYPE_UNSUPPORTED); + } + return result; +} + +QString InstallerFomod::shortDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_IMAGETYPE_UNSUPPORTED: + return tr("image formats not supported."); + default: + throw Exception(tr("invalid problem key %1").arg(key)); + } +} + +QString InstallerFomod::fullDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_IMAGETYPE_UNSUPPORTED: + return tr("This indicates that files from dlls/imageformats are missing from your " + "MO installation or outdated. " + "Images in installers may not be displayed. Please re-install MO"); + default: + throw Exception(tr("invalid problem key %1").arg(key)); + } +} + +bool InstallerFomod::hasGuidedFix(unsigned int) const +{ + return false; +} + +void InstallerFomod::startGuidedFix(unsigned int) const {} -- cgit v1.3.1