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_bundle/src/CMakeLists.txt | 14 ++ libs/installer_bundle/src/installer_bundle_en.ts | 46 ++++++ libs/installer_bundle/src/installerbundle.cpp | 194 +++++++++++++++++++++++ libs/installer_bundle/src/installerbundle.h | 88 ++++++++++ libs/installer_bundle/src/multiarchivedialog.cpp | 40 +++++ libs/installer_bundle/src/multiarchivedialog.h | 78 +++++++++ libs/installer_bundle/src/multiarchivedialog.ui | 77 +++++++++ 7 files changed, 537 insertions(+) create mode 100644 libs/installer_bundle/src/CMakeLists.txt create mode 100644 libs/installer_bundle/src/installer_bundle_en.ts create mode 100644 libs/installer_bundle/src/installerbundle.cpp create mode 100644 libs/installer_bundle/src/installerbundle.h create mode 100644 libs/installer_bundle/src/multiarchivedialog.cpp create mode 100644 libs/installer_bundle/src/multiarchivedialog.h create mode 100644 libs/installer_bundle/src/multiarchivedialog.ui (limited to 'libs/installer_bundle/src') diff --git a/libs/installer_bundle/src/CMakeLists.txt b/libs/installer_bundle/src/CMakeLists.txt new file mode 100644 index 0000000..7b946ce --- /dev/null +++ b/libs/installer_bundle/src/CMakeLists.txt @@ -0,0 +1,14 @@ + +cmake_minimum_required(VERSION 3.16) + +file(GLOB installer_bundle_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(installer_bundle SHARED ${installer_bundle_SOURCES}) +mo2_configure_plugin(installer_bundle NO_SOURCES WARNINGS OFF) +target_link_libraries(installer_bundle PRIVATE mo2::uibase) +mo2_install_plugin(installer_bundle) diff --git a/libs/installer_bundle/src/installer_bundle_en.ts b/libs/installer_bundle/src/installer_bundle_en.ts new file mode 100644 index 0000000..d38444a --- /dev/null +++ b/libs/installer_bundle/src/installer_bundle_en.ts @@ -0,0 +1,46 @@ + + + + + InstallerBundle + + + Bundle Installer + + + + + Proxy-Installer to handle bundles containing the actual mod archive + + + + + MultiArchiveDialog + + + Bundled archives + + + + + This archive contains multiple archives that can be installed separately. Please select the archive you want to extract and install. + + + + + + Opens a Dialog that allows custom modifications. + + + + + Manual + + + + + Cancel + + + + diff --git a/libs/installer_bundle/src/installerbundle.cpp b/libs/installer_bundle/src/installerbundle.cpp new file mode 100644 index 0000000..65ea02f --- /dev/null +++ b/libs/installer_bundle/src/installerbundle.cpp @@ -0,0 +1,194 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "installerbundle.h" + +#include + +#include + +#include "multiarchivedialog.h" + +using namespace MOBase; + +InstallerBundle::InstallerBundle() {} + +bool InstallerBundle::init(IOrganizer* organizer) +{ + m_Organizer = organizer; + return true; +} + +QString InstallerBundle::name() const +{ + return "Bundle Installer"; +} + +QString InstallerBundle::localizedName() const +{ + return tr("Bundle Installer"); +} + +QString InstallerBundle::author() const +{ + return "Tannin"; +} + +QString InstallerBundle::description() const +{ + return tr("Proxy-Installer to handle bundles containing the actual mod archive"); +} + +VersionInfo InstallerBundle::version() const +{ + return VersionInfo(1, 1, 0, VersionInfo::RELEASE_FINAL); +} + +QList InstallerBundle::settings() const +{ + return {PluginSetting("auto_reinstall", + "when reinstalling from an archive containing multiple mods, " + "automatically select the previously installed", + true)}; +} + +unsigned int InstallerBundle::priority() const +{ + return 10; +} + +bool InstallerBundle::isManualInstaller() const +{ + return false; +} + +void InstallerBundle::onInstallationStart(QString const& archive, bool reinstallation, + IModInterface* currentMod) +{ + // We reset some field and fetch the previously installed file: + m_InstallationFile = archive; + m_InstallerUsed = false; + m_SelectedFile = ""; + m_PreviousFile = ""; + + if (reinstallation && m_Organizer->pluginSetting(name(), "auto_reinstall").toBool()) { + m_PreviousFile = currentMod->pluginSetting(name(), "archive", QString()).toString(); + } +} +void InstallerBundle::onInstallationEnd(EInstallResult result, IModInterface* newMod) +{ + if (result == EInstallResult::RESULT_SUCCESS && m_InstallerUsed) { + newMod->setInstallationFile(m_InstallationFile); + if (!m_SelectedFile.isEmpty()) { + newMod->setPluginSetting(name(), "archive", m_SelectedFile); + } + } +} + +std::vector> +InstallerBundle::findObjects(std::shared_ptr tree) const +{ + std::vector> entries; + // Check if we have an archive: + auto managerExtensions = manager()->getSupportedExtensions(); + + // Look for a file with a valid extension: + int nDirs = 0; + for (auto entry : *tree) { + if (entry->isFile()) { + if (managerExtensions.contains(entry->suffix(), + FileNameComparator::CaseSensitivity)) { + entries.push_back(entry); + } + } else { + nDirs++; + } + } + + if (!entries.empty()) { + return entries; + } + + // Arguably this should also check the name of the directory we're looking at + // is the same as the module name: + if (nDirs != 1) { + return {}; + } + + // Retrieve the first folder which is the first item: + return findObjects(tree->at(0)->astree()); +} + +bool InstallerBundle::isArchiveSupported(std::shared_ptr tree) const +{ + return !findObjects(tree).empty(); +} + +IPluginInstaller::EInstallResult +InstallerBundle::install(GuessedValue& modName, + std::shared_ptr& tree, QString&, int& modId) +{ + // Find a valid "object": + auto entries = findObjects(tree); + if (entries.empty()) { + return IPluginInstaller::RESULT_NOTATTEMPTED; + } + + std::shared_ptr entry = nullptr; + if (entries.size() == 1) { + entry = entries[0]; + } else { + if (!m_PreviousFile.isEmpty()) { + entry = tree->find(m_PreviousFile); + } + + if (entry == nullptr) { + MultiArchiveDialog dialog(entries, parentWidget()); + if (dialog.exec() == QDialog::Accepted) { + entry = dialog.selectedEntry(); + m_SelectedFile = entry->pathFrom(tree); + } else { + if (dialog.manualRequested()) { + return IPluginInstaller::RESULT_MANUALREQUESTED; + } else { + return IPluginInstaller::RESULT_CANCELED; + } + } + } + } + + if (entry == nullptr) { + return IPluginInstaller::RESULT_CANCELED; + } + + m_InstallerUsed = true; + + // Extract it: + QString tempFile = manager()->extractFile(entry); + IPluginInstaller::EInstallResult res = + manager()->installArchive(modName, tempFile, modId); + if (res == IPluginInstaller::RESULT_SUCCESS) { + res = IPluginInstaller::RESULT_SUCCESSCANCEL; + } + return res; +} + +#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) +Q_EXPORT_PLUGIN2(installerBundle, InstallerBundle) +#endif diff --git a/libs/installer_bundle/src/installerbundle.h b/libs/installer_bundle/src/installerbundle.h new file mode 100644 index 0000000..59cc28c --- /dev/null +++ b/libs/installer_bundle/src/installerbundle.h @@ -0,0 +1,88 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef INSTALLERBUNDLE_H +#define INSTALLERBUNDLE_H + +#include + +class InstallerBundle : public MOBase::IPluginInstallerSimple +{ + + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerSimple) +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + Q_PLUGIN_METADATA(IID "org.tannin.InstallerBundle") +#endif + +public: + InstallerBundle(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; + + virtual unsigned int priority() const override; + virtual bool isManualInstaller() const override; + + virtual void onInstallationStart(QString const& archive, bool reinstallation, + MOBase::IModInterface* currentMod) override; + virtual void onInstallationEnd(EInstallResult result, + MOBase::IModInterface* newMod) override; + + virtual bool + isArchiveSupported(std::shared_ptr tree) const override; + virtual EInstallResult install(MOBase::GuessedValue& modName, + std::shared_ptr& tree, + QString& version, int& modID) override; + +private: + /** + * @brief Find the entries that can be extracted from this archive. + * + * @param tree The tree to look the entry in. + * + * @return the entry, if one was found, or a null pointer. + */ + std::vector> + findObjects(std::shared_ptr tree) const; + +private: + MOBase::IOrganizer* m_Organizer; + + // The archive being installed: + QString m_InstallationFile; + + // Indicates if this installer was used during the installation process: + bool m_InstallerUsed; + + // Contains the file installed (when multiple archives are present), or an + // empty string: + QString m_SelectedFile; + + // Contains the file previously installed (when multiple archives are present), or an + // empty string: + QString m_PreviousFile; +}; + +#endif // INSTALLERBUNDLE_H diff --git a/libs/installer_bundle/src/multiarchivedialog.cpp b/libs/installer_bundle/src/multiarchivedialog.cpp new file mode 100644 index 0000000..03774f6 --- /dev/null +++ b/libs/installer_bundle/src/multiarchivedialog.cpp @@ -0,0 +1,40 @@ +#include "multiarchivedialog.h" +#include "ui_multiarchivedialog.h" + +using namespace MOBase; + +MultiArchiveDialog::MultiArchiveDialog( + std::vector> entries, QWidget* parent) + : QDialog(parent), ui(new Ui::MultiArchiveDialog), m_Manual(false), + m_SelectedEntry(nullptr) +{ + ui->setupUi(this); + + auto layout = ui->middleLayout; + for (auto& entry : entries) { + QPushButton* button = new QPushButton(entry->name(), this); + button->setStyleSheet("text-align: left; padding: 10px; font-weight: bold;"); + layout->addWidget(button); + + connect(button, &QPushButton::clicked, [this, entry]() { + m_SelectedEntry = entry; + this->accept(); + }); + } +} + +MultiArchiveDialog::~MultiArchiveDialog() +{ + delete ui; +} + +void MultiArchiveDialog::on_cancelBtn_clicked() +{ + this->reject(); +} + +void MultiArchiveDialog::on_manualBtn_clicked() +{ + m_Manual = true; + this->reject(); +} diff --git a/libs/installer_bundle/src/multiarchivedialog.h b/libs/installer_bundle/src/multiarchivedialog.h new file mode 100644 index 0000000..39949b9 --- /dev/null +++ b/libs/installer_bundle/src/multiarchivedialog.h @@ -0,0 +1,78 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef BAINCOMPLEXINSTALLERDIALOG_H +#define BAINCOMPLEXINSTALLERDIALOG_H + +#include + +#include + +#include + +namespace Ui +{ +class MultiArchiveDialog; +} + +/** + * + **/ +class MultiArchiveDialog : public QDialog +{ + Q_OBJECT + +public: + /** + * @brief Constructor + * + * @param tree the directory tree of the archive. The caller is resonsible to verify + *this actually qualifies as a bain installer + * @param modName proposed name for the mod. The dialog allows the user to change this + * @param packageTXT path to the extracted package.txt file or an empty string if + *there is none + * @param parent parent widget + **/ + explicit MultiArchiveDialog( + std::vector> entries, + QWidget* parent); + ~MultiArchiveDialog(); + + /** + * @return bool true if the user requested the manual dialog + **/ + bool manualRequested() const { return m_Manual; } + + /** + * @return QString the (user-modified) name to be used for the mod + **/ + auto selectedEntry() const { return m_SelectedEntry; } + +private slots: + + void on_manualBtn_clicked(); + void on_cancelBtn_clicked(); + +private: + Ui::MultiArchiveDialog* ui; + bool m_Manual; + std::shared_ptr m_SelectedEntry; +}; + +#endif // BAINCOMPLEXINSTALLERDIALOG_H diff --git a/libs/installer_bundle/src/multiarchivedialog.ui b/libs/installer_bundle/src/multiarchivedialog.ui new file mode 100644 index 0000000..47d5d8b --- /dev/null +++ b/libs/installer_bundle/src/multiarchivedialog.ui @@ -0,0 +1,77 @@ + + + MultiArchiveDialog + + + + 0 + 0 + 424 + 146 + + + + Bundled archives + + + + + + This archive contains multiple archives that can be installed separately. Please select the archive you want to extract and install. + + + true + + + + + + + + + + + + Opens a Dialog that allows custom modifications. + + + Opens a Dialog that allows custom modifications. + + + Manual + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Cancel + + + false + + + + + + + + + + -- cgit v1.3.1