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_csharp/src/base_script.cpp | 685 ++++++++++++++++++++++++ 1 file changed, 685 insertions(+) create mode 100644 libs/installer_fomod_csharp/src/base_script.cpp (limited to 'libs/installer_fomod_csharp/src/base_script.cpp') diff --git a/libs/installer_fomod_csharp/src/base_script.cpp b/libs/installer_fomod_csharp/src/base_script.cpp new file mode 100644 index 0000000..e3aa4a7 --- /dev/null +++ b/libs/installer_fomod_csharp/src/base_script.cpp @@ -0,0 +1,685 @@ +/* +Copyright (C) 2020 Holt59. All rights reserved. + +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 "base_script.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "csharp_interface.h" +#include "csharp_utils.h" +#include "installer_fomod_postdialog.h" +#include "psettings.h" + +using namespace MOBase; + +// clang-format off + +namespace CSharp { + + // Pointer to object: + static MOBase::IOrganizer* g_Organizer = nullptr; + + // Per-install globals: + struct Globals { + IInstallationManager* InstallManager; + QWidget* ParentWidget; + std::shared_ptr SourceTree; + std::shared_ptr DestinationTree; + + // Map from path in destination entry to the original entry: + std::map, + std::shared_ptr> InstalledEntries; + + // Map extracted entries (in the original tree) to (temporary) paths: + std::map, QString> ExtractedEntries; + + // Map creted entries (in the destination tree) to (temporary) paths: + std::map, QString> CreatedEntries; + + // List of modified settings values: + std::map Settings; + + Globals() { } + Globals( + IPlugin const* plugin, MOBase::IInstallationManager* manager, QWidget* parentWidget, + std::shared_ptr tree, std::map, QString> entries) : + m_Plugin(plugin), InstallManager(manager), ParentWidget(parentWidget), SourceTree(tree), DestinationTree(tree->createOrphanTree()), ExtractedEntries(std::move(entries)) { + + } + + Globals(Globals const&) = delete; + Globals(Globals&&) = default; + + Globals& operator=(Globals const&) = delete; + Globals& operator=(Globals&&) = default; + + private: + IPlugin const* m_Plugin; + }; + static Globals g; + + + + void init(MOBase::IOrganizer* moInfo) { + + // Do this only once: + if (g_Organizer == nullptr) { + Application::EnableVisualStyles(); + Application::SetCompatibleTextRenderingDefault(false); + } + + g_Organizer = moInfo; + } + + void beforeInstall(IPlugin const* plugin, MOBase::IInstallationManager* manager, QWidget* parentWidget, + std::shared_ptr tree, std::map, QString> entries) { + g = { plugin, manager, parentWidget, tree, std::move(entries) }; + } + + IPluginInstaller::EInstallResult postInstall(std::shared_ptr& tree) { + + if (!g.Settings.empty()) { + + InstallerFomodPostDialog* dialog = new InstallerFomodPostDialog(g.ParentWidget); + + dialog->setIniSettings(g.Settings); + + // Installation cancelled: + if (dialog->exec() == QDialog::Rejected) { + return IPluginInstaller::EInstallResult::RESULT_CANCELED; + } + + switch (dialog->result()) { + + // Discard, nothing do to: + case InstallerFomodPostDialog::Result::DISCARD: break; + + // Apply, must fetch the profile INI settings and apply the settings: + case InstallerFomodPostDialog::Result::APPLY: { + for (auto& p : g.Settings) { + QDir path(g_Organizer->profilePath()); + if (!g_Organizer->profile()->localSettingsEnabled()) { + path = QDir(g_Organizer->managedGame()->documentsDirectory()); + } + + QSettings settings(path.filePath(p.first), QSettings::IniFormat); + + if (settings.status() != QSettings::NoError) { + return IPluginInstaller::EInstallResult::RESULT_FAILED; + } + + p.second.update(settings); + } + } break; + + // Move, must create the INI files and apply the settings: + case InstallerFomodPostDialog::Result::MOVE: { + for (auto& p : g.Settings) { + auto e = g.DestinationTree->addFile("INI Tweaks/" + p.first, false); + if (e == nullptr) { + return IPluginInstaller::EInstallResult::RESULT_FAILED; + } + QString path = g.InstallManager->createFile(e); + if (path.isEmpty()) { + return IPluginInstaller::EInstallResult::RESULT_FAILED; + } + QSettings settings(path, QSettings::IniFormat); + if (settings.status() != QSettings::NoError) { + return IPluginInstaller::EInstallResult::RESULT_FAILED; + } + p.second.update(settings); + } + } break; + } + + } + + tree = g.DestinationTree; + + // Clear up: + g = Globals(); + + + return IPluginInstaller::EInstallResult::RESULT_SUCCESS; + } + +} + +namespace CSharp { + + using namespace System::IO; + + bool isFomodEntry(std::shared_ptr entry) { + return entry->pathFrom(g.SourceTree).compare("fomod", Qt::CaseInsensitive) == 0; + } + + bool BaseScriptImpl::PerformBasicInstall() { + for (auto e : *g.SourceTree) { + if (!isFomodEntry(e)) { + auto ce = g.DestinationTree->copy(e, "", IFileTree::InsertPolicy::MERGE); + g.InstalledEntries[ce] = e; + } + } + return true; + } + + bool BaseScriptImpl::InstallFileFromMod(String^ p_strFrom, String^ p_strTo) { + auto sourceEntry = g.SourceTree->find(to_qstring(p_strFrom)); + + if (!sourceEntry) { + log::warn("File '{}' not found in the archive.", to_wstring(p_strFrom)); + return false; + } + + if (auto ce = g.DestinationTree->copy(sourceEntry, to_qstring(p_strTo)); ce != nullptr) { + g.InstalledEntries[ce] = sourceEntry; + return true; + } + + return false; + } + + array^ BaseScriptImpl::GetModFileList() { + // Cannot directly fill a, e.g., List^ because I cannot capture it: + std::vector paths; + g.SourceTree->walk([&](QString const& path, std::shared_ptr entry) { + // Discard fomod folder: + if (isFomodEntry(entry)) { + return IFileTree::WalkReturn::SKIP; + } + if (entry->isFile()) { + paths.push_back(path + entry->name()); + } + return IFileTree::WalkReturn::CONTINUE; + }, "/"); + + // Convert to C#: + const auto size = static_cast(paths.size()); + array^ result = gcnew array(size); + for (int i = 0; i < size; ++i) { + result[i] = from_string(paths[i].toStdWString()); + } + + return result; + } + + + /** + * @brief Extract the given entry. + * + * If the entry has already been extracted, the existing paths is returned. + * + * @param entry The entry to extract. + * + * @return path to the temporary file corresponding to the entry. + */ + QString extractFile(std::shared_ptr entry) { + + QString qPath; + if (auto it = g.ExtractedEntries.find(entry); it != g.ExtractedEntries.end()) { + qPath = it->second; + } + else { + qPath = g.InstallManager->extractFile(entry, true); + + if (qPath.isEmpty()) { + return QString(); + } + + g.ExtractedEntries[entry] = qPath; + } + + return qPath; + } + + array^ BaseScriptImpl::GetFileFromMod(String^ p_strFile) { + auto entry = g.SourceTree->find(to_qstring(p_strFile)); + + if (!entry) { + return gcnew array(0); + } + + QString qPath = extractFile(entry); + if (qPath.isEmpty()) { + return gcnew array(0); + } + + String^ path = from_string(qPath.toStdWString()); + return File::ReadAllBytes(path); + } + + QStringList getDataFiles(QString folder, QString pattern, bool allFolders) { + QStringList files = g_Organizer->findFiles(folder, [pattern](QString const& filepath) { + return QDir::match(pattern, QFileInfo(filepath).fileName()); + }); + if (allFolders) { + QStringList directories = g_Organizer->listDirectories(folder); + for (QString directory : directories) { + // MO2 does not like path with . or / (I think), so creating the path manually: + files.append(getDataFiles((folder.isEmpty() ? "" : folder + QDir::separator()) + directory, pattern, allFolders)); + } + } + return files; + } + + array^ BaseScriptImpl::GetExistingDataFileList(String^ p_strPath, String^ p_strPattern, bool p_booAllFolders) { + QStringList files = getDataFiles(to_qstring(p_strPath), to_qstring(p_strPattern), p_booAllFolders); + array^ result = gcnew array(files.size()); + + QDir modsDir(g_Organizer->modsPath()); + for (int i = 0; i < files.size(); ++i) { + // We need to trim the path to the actual folder containing the mod (did not find a better way): + QString rpath = modsDir.relativeFilePath(files[i]).replace("/", QDir::separator()); + result[i] = from_string(rpath.mid(rpath.indexOf(QDir::separator()) + 1).toStdWString()); + } + return result; + } + + /** + * @brief Find the data-file path corresponding to the given path. + * + * This methods first lookup the file in the destination tree of the mod (for files + * that have been extracted / created by the mod), and if it does not find it there + * lookup files from other mods. + * + * @param p_strPath Path to the file to lookup, relative to the data folder. + * + * @return a path to an actual corresponding file, or a null pointer if the + * file was not found. + */ + String^ getDataFilePath(String^ p_strPath) { + + // Check if the file is in the output tree: + QString qPath = to_qstring(p_strPath); + if (auto e = g.DestinationTree->find(qPath); e != nullptr) { + + // Check if it's a created entry: + if (auto it = g.CreatedEntries.find(e); it != g.CreatedEntries.end()) { + return from_string(g.CreatedEntries[e]); + } + + // Find the source entry - We need to check for parent: + std::shared_ptr originalEntry; + if (auto it = g.InstalledEntries.find(e); it != g.InstalledEntries.end()) { + originalEntry = it->second; + } + else { + std::shared_ptr tree = e->parent(); + decltype(tree) oTree = nullptr; + while (oTree == nullptr && tree != nullptr) { + auto it = g.InstalledEntries.find(tree); + if (it != g.InstalledEntries.end()) { + oTree = it->second->astree(); + } + else { + tree = tree->parent(); + } + } + originalEntry = oTree->find(e->pathFrom(tree)); + } + QString path = extractFile(originalEntry); + if (path.isEmpty()) { + return nullptr; + } + return from_string(path); + } + + // Convert to path and normalize separator: + auto path = std::filesystem::path(qPath.toStdWString()).make_preferred(); + QStringList paths = g_Organizer->findFiles(ToQString(path.parent_path().native()), [name = ToQString(path.filename())](QString const& filepath) { + return QFileInfo(filepath).fileName().compare(name, Qt::CaseInsensitive) == 0; + }); + + if (paths.isEmpty()) { + return nullptr; + } + + return from_string(paths[0]); + } + + bool BaseScriptImpl::DataFileExists(String^ p_strPath) { + return getDataFilePath(p_strPath) != nullptr; + } + + array^ BaseScriptImpl::GetExistingDataFile(String^ p_strPath) { + + // Convert to path and normalize separator: + String^ datapath = getDataFilePath(p_strPath); + + if (datapath == nullptr) { + return nullptr; + } + + // Read the first file (should be only one): + return File::ReadAllBytes(datapath); + } + + bool BaseScriptImpl::GenerateDataFile(String^ p_strPath, array^ p_bteData) { + + // Check if we already have created an entry for this: + QString qPath = to_qstring(p_strPath); + auto entry = g.DestinationTree->find(qPath); + + QString qAbsPath; + // If the entry is in the list of created files (note: if the entry does not exist, + // find return a nullptr, which is never in g.CreatedEntries). + if (auto it = g.CreatedEntries.find(entry); it != g.CreatedEntries.end()) { + qAbsPath = g.CreatedEntries.at(entry); + } + // Otherwize: Create the entry and the temporary file: + else { + entry = g.DestinationTree->addFile(qPath, true); + qAbsPath = g.InstallManager->createFile(entry); + if (qAbsPath.isEmpty()) { + // Remove the entry from the tree: + entry->detach(); + return false; + } + + // Store the created entry: + g.CreatedEntries[entry] = qAbsPath; + } + + String^ absPath = from_string(qAbsPath); + File::WriteAllBytes(absPath, p_bteData); + return true; + } + + // UI methods: + + DialogResult BaseScriptImpl::ExtendedMessageBox(String^ p_strMessage, String^ p_strTitle, String^ p_strDetails, MessageBoxButtons p_mbbButtons, MessageBoxIcon p_mdiIcon) { + QMessageBox messageBox(g.ParentWidget); + if (!String::IsNullOrEmpty(p_strTitle)) { + messageBox.setWindowTitle(to_qstring(p_strTitle)); + } + messageBox.setText(to_qstring(p_strMessage)); + + if (!String::IsNullOrEmpty(p_strDetails)) { + messageBox.setDetailedText(to_qstring(p_strDetails)); + } + + // For whatever reason MessageBoxIcon has duplicated entries... + switch (p_mdiIcon) { + case MessageBoxIcon::Error: + // case MessageBoxIcon::Stop: + messageBox.setIcon(QMessageBox::Icon::Critical); + break; + case MessageBoxIcon::Asterisk: + // case MessageBoxIcon::Information: + messageBox.setIcon(QMessageBox::Icon::Information); + break; + case MessageBoxIcon::Question: + messageBox.setIcon(QMessageBox::Icon::Question); + break; + case MessageBoxIcon::Exclamation: + // case MessageBoxIcon::Hand: + // case MessageBoxIcon::Warning: + // case MessageBoxIcon::None: + messageBox.setIcon(QMessageBox::Icon::Warning); + case MessageBoxIcon::None: + messageBox.setIcon(QMessageBox::Icon::NoIcon); + break; + } + + QMessageBox::StandardButtons buttons; + + switch (p_mbbButtons) { + case MessageBoxButtons::AbortRetryIgnore: + buttons = QMessageBox::StandardButton::Abort | QMessageBox::StandardButton::Retry | QMessageBox::StandardButton::Ignore; + break; + case MessageBoxButtons::OK: + buttons = QMessageBox::StandardButton::Ok; + break; + case MessageBoxButtons::OKCancel: + buttons = QMessageBox::StandardButton::Ok | QMessageBox::Cancel; + break; + case MessageBoxButtons::RetryCancel: + buttons = QMessageBox::StandardButton::Cancel | QMessageBox::StandardButton::Retry; + break; + case MessageBoxButtons::YesNoCancel: + buttons = QMessageBox::StandardButton::Yes | QMessageBox::No | QMessageBox::Cancel; + break; + case MessageBoxButtons::YesNo: + buttons = QMessageBox::StandardButton::Yes | QMessageBox::No; + break; + } + + messageBox.setStandardButtons(buttons); + + // Only some case are possible here: + switch (messageBox.exec()) { + case QMessageBox::Button::Abort: + return DialogResult::Abort; + case QMessageBox::Button::Cancel: + return DialogResult::Cancel; + case QMessageBox::Button::Ignore: + return DialogResult::Ignore; + case QMessageBox::Button::No: + return DialogResult::No; + case QMessageBox::Button::Ok: + return DialogResult::OK; + case QMessageBox::Button::Retry: + return DialogResult::Retry; + case QMessageBox::Button::Yes: + return DialogResult::Yes; + } + + return DialogResult::None; + } + + + array^ BaseScriptImpl::Select(array^ p_sopOptions, String^ p_strTitle, bool p_booSelectMany) { + using namespace System::Collections::Generic; + + QDialog* inputDialog = new QDialog(); + QVBoxLayout* layout = new QVBoxLayout(inputDialog); + inputDialog->setWindowTitle(to_qstring(p_strTitle)); + inputDialog->setLayout(layout); + + layout->setSizeConstraint(QLayout::SetFixedSize); + + if (p_booSelectMany) { + layout->addWidget(new QLabel(QObject::tr("Choose any:"), inputDialog)); + } + else { + layout->addWidget(new QLabel(QObject::tr("Choose one:"), inputDialog)); + } + + QList items; + for each (SelectOption ^ opt in p_sopOptions) { + QAbstractButton* btn; + if (p_booSelectMany) { + btn = new QCheckBox(to_qstring(opt->Item), inputDialog); + } + else { + btn = new QRadioButton(to_qstring(opt->Item), inputDialog); + } + + if (!String::IsNullOrEmpty(opt->Desc)) { + btn->setToolTip(to_qstring(opt->Desc)); + } + + layout->addWidget(btn); + items.append(btn); + } + + if (!p_booSelectMany && items.size() > 0) { + items[0]->setChecked(true); + } + + QDialogButtonBox* buttonBox = new QDialogButtonBox( + QDialogButtonBox::Cancel | QDialogButtonBox::Ok, inputDialog); + layout->addWidget(buttonBox); + + // Using old signal/slot syntax since the new one does not work here (probably + // due to the C++/CLR nature): + QObject::connect(buttonBox, SIGNAL(accepted()), inputDialog, SLOT(accept())); + QObject::connect(buttonBox, SIGNAL(rejected()), inputDialog, SLOT(reject())); + + inputDialog->setModal(true); + if (inputDialog->exec() != QDialog::Accepted) { + return gcnew array(0); + } + + List^ selected = gcnew List(items.size()); + for (int i = 0; i < items.size(); ++i) { + if (items[i]->isChecked()) { + selected->Add(i); + } + } + + return selected->ToArray(); + } + + // Versioning / INIs: + + // Convert to Version^ from a MO2 version: + inline System::Version^ make_version(VersionInfo version) { + auto qversion = version.asQVersionNumber(); + return gcnew System::Version(qversion.majorVersion(), qversion.minorVersion(), qversion.microVersion()); + + } + + System::Version^ BaseScriptImpl::GetModManagerVersion() { + const auto version = g_Organizer->version(); + return gcnew System::Version(version.major(), version.minor(), version.patch()); + } + + System::Version^ BaseScriptImpl::GetGameVersion() { + return make_version(g_Organizer->managedGame()->gameVersion()); + } + + System::Version^ BaseScriptImpl::GetScriptExtenderVersion() { + auto scriptExtender = g_Organizer->gameFeatures()->gameFeature(); + + if (!scriptExtender || !scriptExtender->isInstalled()) { + return nullptr; + } + + return gcnew System::Version(msclr::interop::marshal_as(scriptExtender->getExtenderVersion().toStdString())); + } + + bool BaseScriptImpl::ScriptExtenderPresent() { + auto scriptExtender = g_Organizer->gameFeatures()->gameFeature(); + return scriptExtender && scriptExtender->isInstalled(); + } + + // Plugins: + array^ BaseScriptImpl::GetAllPlugins() { + QStringList names = g_Organizer->pluginList()->pluginNames(); + array^ result = gcnew array(names.size()); + for (int i = 0; i < names.size(); ++i) { + result[i] = from_string(names[i].toStdString()); + } + return result; + } + + array^ BaseScriptImpl::GetActivePlugins() { + auto pluginList = g_Organizer->pluginList(); + QStringList names = pluginList->pluginNames(); + QStringList activeNames; + for (auto& name : names) { + if (pluginList->state(name) == IPluginList::STATE_ACTIVE) { + activeNames.append(name); + } + } + array^ result = gcnew array(activeNames.size()); + for (int i = 0; i < activeNames.size(); ++i) { + result[i] = from_string(activeNames[i].toStdString()); + } + return result; + } + + // INIs: + String^ BaseScriptImpl::GetIniString(String^ settingsFileName, String^ section, String^ key) { + + // Check if we have already set this within this installation: + auto fIt = g.Settings.find(to_qstring(settingsFileName)); + if (fIt != g.Settings.end()) { + QString value = fIt->second.value(to_qstring(section), to_qstring(key)); + if (!value.isEmpty()) { + return from_string(value); + } + } + + // Otherwize, look-up the file: + QDir path(g_Organizer->profilePath()); + if (!g_Organizer->profile()->localSettingsEnabled()) { + path = QDir(g_Organizer->managedGame()->documentsDirectory()); + } + + QSettings settings(path.filePath(to_qstring(settingsFileName)), QSettings::IniFormat); + + if (settings.status() != QSettings::NoError) { + return nullptr; + } + + QString name = to_qstring(section + "/" + key); + if (section->Equals("General", System::StringComparison::CurrentCultureIgnoreCase)) { + name = to_qstring(key); + } + + QVariant value = settings.value(name); + if (!value.isValid()) { + return nullptr; + } + + return from_string(value.toString().toStdString()); + } + + int BaseScriptImpl::GetIniInt(String^ settingsFileName, String^ section, String^ key) { + return Convert::ToInt32(GetIniString(settingsFileName, section, key)); + } + + bool BaseScriptImpl::EditIni(String^ p_strSettingsFileName, String^ p_strSection, String^ p_strKey, String^ p_strValue) { + // Check that the file is supported: + bool iniFound = false; + for (auto ini : g_Organizer->managedGame()->iniFiles()) { + if (ini.compare(to_qstring(p_strSettingsFileName), Qt::CaseInsensitive) == 0) { + iniFound = true; + } + } + + if (!iniFound) { + return false; + } + + g.Settings[to_qstring(p_strSettingsFileName)].setValue(to_qstring(p_strSection), to_qstring(p_strKey), to_qstring(p_strValue)); + return true; + } + +} -- cgit v1.3.1