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_bain/src/CMakeLists.txt | 13 ++ .../src/baincomplexinstallerdialog.cpp | 121 +++++++++++ .../src/baincomplexinstallerdialog.h | 94 +++++++++ .../src/baincomplexinstallerdialog.ui | 126 ++++++++++++ libs/installer_bain/src/installer_bain_en.ts | 83 ++++++++ libs/installer_bain/src/installerbain.cpp | 222 +++++++++++++++++++++ libs/installer_bain/src/installerbain.h | 80 ++++++++ 7 files changed, 739 insertions(+) create mode 100644 libs/installer_bain/src/CMakeLists.txt create mode 100644 libs/installer_bain/src/baincomplexinstallerdialog.cpp create mode 100644 libs/installer_bain/src/baincomplexinstallerdialog.h create mode 100644 libs/installer_bain/src/baincomplexinstallerdialog.ui create mode 100644 libs/installer_bain/src/installer_bain_en.ts create mode 100644 libs/installer_bain/src/installerbain.cpp create mode 100644 libs/installer_bain/src/installerbain.h (limited to 'libs/installer_bain/src') diff --git a/libs/installer_bain/src/CMakeLists.txt b/libs/installer_bain/src/CMakeLists.txt new file mode 100644 index 0000000..de57063 --- /dev/null +++ b/libs/installer_bain/src/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.16) + +file(GLOB installer_bain_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_bain SHARED ${installer_bain_SOURCES}) +mo2_configure_plugin(installer_bain NO_SOURCES WARNINGS OFF) +target_link_libraries(installer_bain PRIVATE mo2::uibase) +mo2_install_plugin(installer_bain) diff --git a/libs/installer_bain/src/baincomplexinstallerdialog.cpp b/libs/installer_bain/src/baincomplexinstallerdialog.cpp new file mode 100644 index 0000000..2c25d76 --- /dev/null +++ b/libs/installer_bain/src/baincomplexinstallerdialog.cpp @@ -0,0 +1,121 @@ +/* +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 "ui_baincomplexinstallerdialog.h" + +#include + +#include + +#include "baincomplexinstallerdialog.h" + +using namespace MOBase; + +BainComplexInstallerDialog::BainComplexInstallerDialog( + const std::vector>& subpackages, + const GuessedValue& modName, const QStringList& defaultOptions, + const QString& packageTXT, QWidget* parent) + : TutorableDialog("BainInstaller", parent), ui(new Ui::BainComplexInstallerDialog), + m_Manual(false), m_PackageTXT(packageTXT) +{ + ui->setupUi(this); + + for (auto iter = modName.variants().begin(); iter != modName.variants().end(); + ++iter) { + ui->nameCombo->addItem(*iter); + } + + ui->nameCombo->setCurrentIndex(ui->nameCombo->findText(modName)); + + for (const auto& subpackage : subpackages) { + + auto name = subpackage->name(); + + QListWidgetItem* item = new QListWidgetItem(name, ui->optionsList); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + + if (name.mid(0, 2) == "00" || defaultOptions.contains(name, Qt::CaseInsensitive)) { + item->setCheckState(Qt::Checked); + } else { + item->setCheckState(Qt::Unchecked); + } + + ui->optionsList->addItem(item); + } + + ui->packageBtn->setEnabled(!packageTXT.isEmpty()); + ui->nameCombo->completer()->setCaseSensitivity(Qt::CaseSensitive); +} + +BainComplexInstallerDialog::~BainComplexInstallerDialog() +{ + delete ui; +} + +QString BainComplexInstallerDialog::getName() const +{ + return ui->nameCombo->currentText(); +} + +QStringList BainComplexInstallerDialog::updateTree(std::shared_ptr& tree) +{ + // Retrieve the list of selected names: + std::set selectedNames; + for (int i = 0; i < ui->optionsList->count(); ++i) { + QListWidgetItem* item = ui->optionsList->item(i); + if (item->checkState() == Qt::Checked) { + selectedNames.insert(item->text()); + } + } + + // Create a new empty tree and merge all the selected folder in it: + auto newTree = tree->createOrphanTree(); + for (auto& entry : *tree) { + if (entry->isDir() && selectedNames.count(entry->name()) > 0) { + newTree->merge(entry->astree()); + } + } + + tree = newTree; + + return QStringList(selectedNames.begin(), selectedNames.end()); +} + +void BainComplexInstallerDialog::on_okBtn_clicked() +{ + this->accept(); +} + +void BainComplexInstallerDialog::on_cancelBtn_clicked() +{ + this->reject(); +} + +void BainComplexInstallerDialog::on_manualBtn_clicked() +{ + m_Manual = true; + this->reject(); +} + +void BainComplexInstallerDialog::on_packageBtn_clicked() +{ + TextViewer viewer(m_PackageTXT, this); + viewer.setDescription(""); + viewer.addFile(m_PackageTXT, false); + viewer.exec(); +} diff --git a/libs/installer_bain/src/baincomplexinstallerdialog.h b/libs/installer_bain/src/baincomplexinstallerdialog.h new file mode 100644 index 0000000..f657627 --- /dev/null +++ b/libs/installer_bain/src/baincomplexinstallerdialog.h @@ -0,0 +1,94 @@ +/* +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 BainComplexInstallerDialog; +} + +/** + * @brief Dialog to choose from options offered by a (complex) bain package + **/ +class BainComplexInstallerDialog : public MOBase::TutorableDialog +{ + 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 defaultOptions The default options to select. Can contains options not + *actually present. + * @param packageTXT path to the extracted package.txt file or an empty string if + *there is none + * @param parent parent widget + **/ + explicit BainComplexInstallerDialog( + const std::vector>& subpackages, + const MOBase::GuessedValue& modName, const QStringList& defaultOptions, + const QString& packageTXT, QWidget* parent); + ~BainComplexInstallerDialog(); + + /** + * @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 + **/ + QString getName() const; + + /** + * @brief Remove from the given tree the option not selected by the user. + * + * @param tree The input tree. + * + * @return the list of options selected by the user. + **/ + QStringList updateTree(std::shared_ptr& tree); + +private slots: + + void on_manualBtn_clicked(); + + void on_okBtn_clicked(); + + void on_cancelBtn_clicked(); + + void on_packageBtn_clicked(); + +private: + Ui::BainComplexInstallerDialog* ui; + + bool m_Manual; + QString m_PackageTXT; +}; + +#endif // BAINCOMPLEXINSTALLERDIALOG_H diff --git a/libs/installer_bain/src/baincomplexinstallerdialog.ui b/libs/installer_bain/src/baincomplexinstallerdialog.ui new file mode 100644 index 0000000..0ab5214 --- /dev/null +++ b/libs/installer_bain/src/baincomplexinstallerdialog.ui @@ -0,0 +1,126 @@ + + + BainComplexInstallerDialog + + + + 0 + 0 + 424 + 365 + + + + BAIN Package Installer + + + + + + + + Name + + + + + + + true + + + + + + + + + This looks like a Package prepared for Installation through BAIN. You can select options from the list below. If there is a package.txt file, it should contain detailed information about the options. + + + true + + + + + + + Components of this package. + + + Components of this package. +If there is a component called "00 Core" it is usually required. Options are ordered by priority as set up by the author. + + + + + + + + + Opens a Dialog that allows custom modifications. + + + Opens a Dialog that allows custom modifications. + + + Manual + + + false + + + + + + + The package.txt is often part of BAIN packages and contains details about the options available. + + + The package.txt is often part of BAIN packages and contains details about the options available. + + + Package.txt + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Ok + + + + + + + Cancel + + + false + + + + + + + + + + diff --git a/libs/installer_bain/src/installer_bain_en.ts b/libs/installer_bain/src/installer_bain_en.ts new file mode 100644 index 0000000..fe78e5d --- /dev/null +++ b/libs/installer_bain/src/installer_bain_en.ts @@ -0,0 +1,83 @@ + + + + + BainComplexInstallerDialog + + + BAIN Package Installer + + + + + Name + + + + + This looks like a Package prepared for Installation through BAIN. You can select options from the list below. If there is a package.txt file, it should contain detailed information about the options. + + + + + Components of this package. + + + + + Components of this package. +If there is a component called "00 Core" it is usually required. Options are ordered by priority as set up by the author. + + + + + + Opens a Dialog that allows custom modifications. + + + + + Manual + + + + + + The package.txt is often part of BAIN packages and contains details about the options available. + + + + + Ok + + + + + Cancel + + + + + InstallerBAIN + + + BAIN Installer + + + + + Installer for BAIN archives (originally targeting Wrye Bash) + + + + + May be BAIN installer + + + + + This installer looks like it may contain a BAIN installer but I'm not sure. Install as BAIN installer? + + + + diff --git a/libs/installer_bain/src/installerbain.cpp b/libs/installer_bain/src/installerbain.cpp new file mode 100644 index 0000000..d39590e --- /dev/null +++ b/libs/installer_bain/src/installerbain.cpp @@ -0,0 +1,222 @@ +/* +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 "installerbain.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "baincomplexinstallerdialog.h" + +using namespace MOBase; + +InstallerBAIN::InstallerBAIN() : m_MOInfo(nullptr) {} + +bool InstallerBAIN::init(IOrganizer* moInfo) +{ + m_MOInfo = moInfo; + return true; +} + +QString InstallerBAIN::name() const +{ + return "BAIN Installer"; +} + +QString InstallerBAIN::localizedName() const +{ + return tr("BAIN Installer"); +} + +QString InstallerBAIN::author() const +{ + return "Tannin"; +} + +QString InstallerBAIN::description() const +{ + return tr("Installer for BAIN archives (originally targeting Wrye Bash)"); +} + +VersionInfo InstallerBAIN::version() const +{ + return VersionInfo(1, 1, 0, VersionInfo::RELEASE_FINAL); +} + +QList InstallerBAIN::settings() const +{ + return QList(); +} + +unsigned int InstallerBAIN::priority() const +{ + return 40; +} + +bool InstallerBAIN::isManualInstaller() const +{ + return false; +} + +void InstallerBAIN::onInstallationStart(QString const&, bool, IModInterface* currentMod) +{ + // We reset some field and fetch the previously installed options: + m_InstallerUsed = false; + m_PreviousOptions.clear(); + m_SelectedOptions.clear(); + + if (currentMod) { + auto settings = currentMod->pluginSettings(name()); + for (auto& [name, value] : settings) { + if (name.startsWith("option")) { + m_PreviousOptions.append(value.toString()); + } + } + } +} + +void InstallerBAIN::onInstallationEnd(EInstallResult result, IModInterface* newMod) +{ + if (result == EInstallResult::RESULT_SUCCESS && m_InstallerUsed) { + newMod->clearPluginSettings(name()); + for (auto i = 0; i < m_SelectedOptions.size(); ++i) { + newMod->setPluginSetting(name(), QString("option%1").arg(i), + m_SelectedOptions[i]); + } + } +} + +std::vector> +InstallerBAIN::findSubpackages(std::shared_ptr tree, + std::size_t* invalidFolders) const +{ + // Folders that can be present but do not impact the installation: + static const std::set IGNORED_FOLDERS{ + "fomod", "omod conversion data", "images", "screenshots", "docs"}; + + auto checker = m_MOInfo->gameFeatures()->gameFeature(); + + if (!checker) { + return {}; + } + + // each directory would have to serve as a top-level directory + std::vector> subpackages; + std::size_t ninvalids = 0; + for (auto entry : *tree) { + + if (!entry->isDir()) { + continue; + } + + // ignore fomod in case of combined fomod/bain packages. + // dirs starting with -- are supposed to be ignored + if (IGNORED_FOLDERS.contains(entry->name()) || entry->name().startsWith("--")) { + continue; + } + + if (checker->dataLooksValid(entry->astree()) == + ModDataChecker::CheckReturn::VALID) { + subpackages.push_back(entry); + } else { + ninvalids++; + } + } + + if (invalidFolders) { + *invalidFolders = ninvalids; + } + + return subpackages; +} + +bool InstallerBAIN::isArchiveSupported(std::shared_ptr tree) const +{ + auto checker = m_MOInfo->gameFeatures()->gameFeature(); + + if (!checker) { + return false; + } + + std::size_t numInvalidDirs = 0; + auto subpackages = findSubpackages(tree, &numInvalidDirs); + + if (subpackages.size() < 2) { + // a complex bain package contains at least 2 directories to choose from + return false; + } else if (numInvalidDirs == 0) { + return true; + } else { + return QMessageBox::question(parentWidget(), tr("May be BAIN installer"), + tr("This installer looks like it may contain a BAIN " + "installer but I'm not sure. " + "Install as BAIN installer?"), + QMessageBox::Yes | QMessageBox::No) == + QMessageBox::Yes; + } + + return true; +} + +IPluginInstaller::EInstallResult +InstallerBAIN::install(GuessedValue& modName, std::shared_ptr& tree, + QString&, int&) +{ + auto entry = tree->find("package.txt", FileTreeEntry::FILE); + + QString packageTXT; + if (entry != nullptr) { + packageTXT = manager()->extractFile(entry); + } + + auto subpackages = findSubpackages(tree); + + BainComplexInstallerDialog dialog(subpackages, modName, m_PreviousOptions, packageTXT, + parentWidget()); + + int res = dialog.exec(); + + if (res == QDialog::Accepted) { + modName.update(dialog.getName(), GUESS_USER); + + // Update the tree: + m_SelectedOptions = dialog.updateTree(tree); + m_InstallerUsed = true; + + return IPluginInstaller::RESULT_SUCCESS; + } else { + if (dialog.manualRequested()) { + modName.update(dialog.getName(), GUESS_USER); + return IPluginInstaller::RESULT_MANUALREQUESTED; + } else { + return IPluginInstaller::RESULT_CANCELED; + } + } +} + +#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) +Q_EXPORT_PLUGIN2(installerBAIN, InstallerBAIN) +#endif diff --git a/libs/installer_bain/src/installerbain.h b/libs/installer_bain/src/installerbain.h new file mode 100644 index 0000000..535a31a --- /dev/null +++ b/libs/installer_bain/src/installerbain.h @@ -0,0 +1,80 @@ +/* +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 INSTALLERBAIN_H +#define INSTALLERBAIN_H + +#include + +class InstallerBAIN : 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.InstallerBAIN") +#endif + +public: + InstallerBAIN(); + + 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; + virtual bool isManualInstaller() const; + + 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; + virtual EInstallResult install(MOBase::GuessedValue& modName, + std::shared_ptr& tree, + QString& version, int& modID); + +protected: + /** + * @brief Retrieve the entries corresponding to subpackages in the given tree. + * + * @param tree The tree to check for subpackages. + * @param invalidFolders If not null, will contain the number of invalid folders. + * + * @return the list of entries corresponding to subpackages. + */ + std::vector> + findSubpackages(std::shared_ptr tree, + std::size_t* invalidFolders = nullptr) const; + +private: + const MOBase::IOrganizer* m_MOInfo; + + // Indicates if the installer was used: + bool m_InstallerUsed; + + // List of options currently installed and being installed: + QStringList m_PreviousOptions, m_SelectedOptions; +}; + +#endif // INSTALLERBAIN_H -- cgit v1.3.1