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/CMakeLists.txt | 11 + libs/installer_fomod_csharp/src/base_script.cpp | 685 +++++++++++++++++++++ libs/installer_fomod_csharp/src/base_script.h | 552 +++++++++++++++++ .../src/csharp_interface.cpp | 157 +++++ libs/installer_fomod_csharp/src/csharp_interface.h | 40 ++ libs/installer_fomod_csharp/src/csharp_utils.h | 41 ++ .../src/installer_fomod_csharp.cpp | 193 ++++++ .../src/installer_fomod_csharp.h | 84 +++ .../src/installer_fomod_csharp_en.ts | 125 ++++ .../src/installer_fomod_csharp_postdialog.ui | 99 +++ .../src/installer_fomod_csharp_predialog.ui | 89 +++ .../src/installer_fomod_postdialog.h | 87 +++ .../src/installer_fomod_predialog.h | 72 +++ libs/installer_fomod_csharp/src/psettings.h | 108 ++++ libs/installer_fomod_csharp/src/xml_info_reader.h | 140 +++++ 15 files changed, 2483 insertions(+) create mode 100644 libs/installer_fomod_csharp/src/CMakeLists.txt create mode 100644 libs/installer_fomod_csharp/src/base_script.cpp create mode 100644 libs/installer_fomod_csharp/src/base_script.h create mode 100644 libs/installer_fomod_csharp/src/csharp_interface.cpp create mode 100644 libs/installer_fomod_csharp/src/csharp_interface.h create mode 100644 libs/installer_fomod_csharp/src/csharp_utils.h create mode 100644 libs/installer_fomod_csharp/src/installer_fomod_csharp.cpp create mode 100644 libs/installer_fomod_csharp/src/installer_fomod_csharp.h create mode 100644 libs/installer_fomod_csharp/src/installer_fomod_csharp_en.ts create mode 100644 libs/installer_fomod_csharp/src/installer_fomod_csharp_postdialog.ui create mode 100644 libs/installer_fomod_csharp/src/installer_fomod_csharp_predialog.ui create mode 100644 libs/installer_fomod_csharp/src/installer_fomod_postdialog.h create mode 100644 libs/installer_fomod_csharp/src/installer_fomod_predialog.h create mode 100644 libs/installer_fomod_csharp/src/psettings.h create mode 100644 libs/installer_fomod_csharp/src/xml_info_reader.h (limited to 'libs/installer_fomod_csharp/src') diff --git a/libs/installer_fomod_csharp/src/CMakeLists.txt b/libs/installer_fomod_csharp/src/CMakeLists.txt new file mode 100644 index 0000000..09b3015 --- /dev/null +++ b/libs/installer_fomod_csharp/src/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(mo2-cmake CONFIG REQUIRED) +find_package(mo2-uibase CONFIG REQUIRED) + +add_library(installer_fomod_csharp SHARED) +mo2_configure_plugin(installer_fomod_csharp WARNINGS OFF CLI ON) +target_link_libraries(installer_fomod_csharp PRIVATE mo2::uibase) +mo2_install_plugin(installer_fomod_csharp) + +set_target_properties(installer_fomod_csharp PROPERTIES CXX_STANDARD 20) 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; + } + +} diff --git a/libs/installer_fomod_csharp/src/base_script.h b/libs/installer_fomod_csharp/src/base_script.h new file mode 100644 index 0000000..abfa152 --- /dev/null +++ b/libs/installer_fomod_csharp/src/base_script.h @@ -0,0 +1,552 @@ +/* +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 . +*/ + +// clang-format off + +#ifndef BASE_SCRIPT_H +#define BASE_SCRIPT_H + +#using +#using +#using + +#include + +/** + * Note: The specification of BaseScript where taken from the Nexus-Mods installer_fomod extension + * for Vortex: https://github.com/Nexus-Mods/fomod-installer + */ +namespace CSharp { + + using namespace System; + using namespace System::Drawing; + using namespace System::Windows::Forms; + + /// + /// Describes the options to display in a select form. + /// + public ref struct SelectOption { + public: + /// + /// The name of the selection item. + /// + String^ Item; + + /// + /// The path to the preview image of the item. + /// + String^ Preview; + + /// + /// The description of the selection item. + /// + String^ Desc; + + /// + /// A simple constructor that initializes the struct with the given values. + /// + /// The name of the selection item. + /// The path to the preview image of the item. + /// The description of the selection item. + SelectOption(String^ item, String^ preview, String^ desc) { + Item = item; + Preview = preview; + Desc = desc; + } + }; + + /// + /// The base class for C# scripts. + /// + public ref class BaseScriptImpl { + public: + + static String^ LastError; + + /// + /// Returns the last error that occurred. + /// + /// The last error that occurred. + static String^ GetLastError() { + return LastError; + } + + /// + /// Performs a basic install of the mod. + /// + /// + /// A basic install installs all of the file in the mod to the Data directory + /// or activates all esp and esm files. + /// + /// true if the installation succeed; + /// false otherwise. + static bool PerformBasicInstall(); + + /// + /// Installs the specified file from the mod to the specified location on the file system. + /// + /// + /// This is the legacy form of . It now just calls + /// . + /// + /// The path of the file in the mod to install. + /// The path on the file system where the file is to be created. + /// true if the file was written; false otherwise. + /// + static bool CopyDataFile(String^ p_strFrom, String^ p_strTo) { + return InstallFileFromMod(p_strFrom, p_strTo); + } + + /// + /// Installs the specified file from the mod to the specified location on the file system. + /// + /// The path of the file in the mod to install. + /// The path on the file system where the file is to be created. + /// true if the file was written; false otherwise. + static bool InstallFileFromMod(String^ p_strFrom, String^ p_strTo); + + /// + /// Installs the speified file from the mod to the file system. + /// + /// The path of the file to install. + /// true if the file was written; false otherwise. + static bool InstallFileFromMod(String^ p_strFile) { + return InstallFileFromMod(p_strFile, p_strFile); + } + + /// + /// Installs the specified file from the mod to the specified location on the file system. + /// + /// The path of the file in the mod to install. + /// The path on the file system where the file is to be created. + /// true if the file was written; false otherwise. + static bool InstallFileFromFomod(String^ p_strFrom, String^ p_strTo) { + return InstallFileFromMod(p_strFrom, p_strTo); + } + + /// + /// Installs the speified file from the mod to the file system. + /// + /// The path of the file to install. + /// true if the file was written; false otherwise. + static bool InstallFileFromFomod(String^ p_strFile) { + return InstallFileFromMod(p_strFile, p_strFile); + } + + /// + /// Retrieves the list of files in the mod. + /// + /// The list of files in the mod. + static array^ GetModFileList(); + + /// + /// Retrieves the list of files in the mod. + /// + /// The list of files in the mod. + static array^ GetFomodFileList() { + return GetModFileList(); + } + + /// + /// Retrieves the specified file from the mod. + /// + /// The file to retrieve. + /// The requested file data. + static array^ GetFileFromMod(String^ p_strFile); + + /// + /// Retrieves the specified file from the mod. + /// + /// The file to retrieve. + /// The requested file data. + static array^ GetFileFromFomod(String^ p_strFile) { + return GetFileFromMod(p_strFile); + } + + /// + /// Gets a filtered list of all files in a user's Data directory. + /// + /// The subdirectory of the Data directory from which to get the listing. + /// The pattern against which to filter the file paths. + /// Whether or not to search through subdirectories. + /// A filtered list of all files in a user's Data directory. + static array^ GetExistingDataFileList(String^ p_strPath, String^ p_strPattern, bool p_booAllFolders); + + /// + /// Determines if the specified file exists in the user's Data directory. + /// + /// The path of the file whose existence is to be verified. + /// true if the specified file exists; false + /// otherwise. + static bool DataFileExists(String^ p_strPath); + + /// + /// Gets the speified file from the user's Data directory. + /// + /// The path of the file to retrieve. + /// The specified file, or null if the file does not exist. + static array^ GetExistingDataFile(String^ p_strPath); + + /// + /// Writes the file represented by the given byte array to the given path. + /// + /// + /// This method writes the given data as a file at the given path. If the file + /// already exists the user is prompted to overwrite the file. + /// + /// The path where the file is to be created. + /// The data that is to make up the file. + /// true if the file was written; false otherwise. + static bool GenerateDataFile(String^ p_strPath, array^ p_bteData); + + /// + /// Shows a message box with the given message. + /// + /// The message to display in the message box. + static void MessageBox(String^ p_strMessage) { + MessageBox(p_strMessage, nullptr); + } + + /// + /// Shows a message box with the given message and title. + /// + /// The message to display in the message box. + /// The message box's title, display in the title bar. + static void MessageBox(String^ p_strMessage, String^ p_strTitle) { + MessageBox(p_strMessage, p_strTitle, MessageBoxButtons::OK); + } + + /// + /// Shows a message box with the given message, title, and buttons. + /// + /// The message to display in the message box. + /// The message box's title, display in the title bar. + /// The buttons to show in the message box. + static DialogResult MessageBox(String^ p_strMessage, String^ p_strTitle, MessageBoxButtons p_mbbButtons) { + return MessageBox(p_strMessage, p_strTitle, p_mbbButtons, MessageBoxIcon::Information); + } + + static DialogResult MessageBox(String^ p_strMessage, String^ p_strTitle, MessageBoxButtons p_mbbButtons, MessageBoxIcon p_mdiIcon) { + return ExtendedMessageBox(p_strMessage, p_strTitle, nullptr, p_mbbButtons, p_mdiIcon); + } + + /// + /// Shows an extended message box with the given message, title, details, buttons, and icon. + /// + /// The message to display in the message box. + /// The message box's title, displayed in the title bar. + /// The message box's details, displayed in the details area. + /// The buttons to show in the message box. + /// The icon to display in the message box. + static DialogResult ExtendedMessageBox(String^ p_strMessage, String^ p_strTitle, String^ p_strDetails, MessageBoxButtons p_mbbButtons, MessageBoxIcon p_mdiIcon); + + /// + /// Displays a selection form to the user. + /// + /// The options from which to select. + /// The title of the selection form. + /// Whether more than one item can be selected. + /// The indices of the selected items. + static array^ Select(array^ p_sopOptions, String^ p_strTitle, bool p_booSelectMany); + + /// + /// Displays a selection form to the user. + /// + /// + /// The items, previews, and descriptions are repectively ordered. In other words, + /// the i-th item in uses the i-th preview in + /// and the i-th description in . + /// + /// Similarly, the idices return as results correspond to the indices of the items in + /// . + /// + /// The items from which to select. + /// The preview image file names for the items. + /// The descriptions of the items. + /// The title of the selection form. + /// Whether more than one item can be selected. + /// The indices of the selected items. + static array^ Select(array^ p_strItems, array^ p_strPreviewPaths, array^ p_strDescriptions, String^ p_strTitle, bool p_booSelectMany) { + const int size = p_strItems->GetLength(0); + array^ options = gcnew array(size); + for (int i = 0; i < size; ++i) { + options[i] = gcnew SelectOption(p_strItems[i], p_strPreviewPaths[i], p_strDescriptions[i]); + } + return Select(options, p_strTitle, p_booSelectMany); + } + + /// + /// Displays a selection form to the user. + /// + /// + /// The items, previews, and descriptions are repectively ordered. In other words, + /// the i-th item in uses the i-th preview in + /// and the i-th description in . + /// + /// Similarly, the idices return as results correspond to the indices of the items in + /// . + /// + /// The items from which to select. + /// The preview images for the items. + /// The descriptions of the items. + /// The title of the selection form. + /// Whether more than one item can be selected. + /// The indices of the selected items. + static array^ ImageSelect(array^ p_strItems, array^ /* p_imgPreviews */, array^ p_strDescriptions, String^ p_strTitle, bool p_booSelectMany) { + return Select(p_strItems, gcnew array(p_strItems->Length), p_strDescriptions, p_strTitle, p_booSelectMany); + } + + /// + /// Creates a form that can be used in custom mod scripts. + /// + /// A form that can be used in custom mod scripts. + static Form^ CreateCustomForm() { + Form^ form = gcnew Form; + return form; + } + + /// + /// Gets the version of the mod manager. + /// + /// The version of the mod manager. + static System::Version^ GetModManagerVersion(); + + /// + /// Gets the version of the game that is installed. + /// + /// The version of the game, or null if Fallout + /// is not installed. + static System::Version^ GetGameVersion(); + + // This is not in the spec., but I do not see a point in checking each + // script extender in a different way. + static System::Version^ GetScriptExtenderVersion(); + + /// + /// Gets the script extender version or null if it's not installed + /// + /// + static System::Version^ GetSkseVersion() { + return GetScriptExtenderVersion(); + } + + /// + /// Gets the script extender version or null if it's not installed + /// + /// + static System::Version^ GetFoseVersion() { + return GetScriptExtenderVersion(); + } + + /// + /// Gets the script extender version or null if it's not installed + /// + /// + static System::Version^ GetNvseVersion() { + return GetScriptExtenderVersion(); + } + + /// + /// Gets the version of the mod manager. + /// + /// The version of the mod manager. + static System::Version^ GetFommVersion() { + return GetModManagerVersion(); + } + + /// + /// Gets the version of the game that is installed. + /// + /// The version of the game, or null if Fallout + /// is not installed. + static System::Version^ GetFalloutVersion() { + // I think this is an old function... What Fallout anyway? So just going + // to return the game version, whatever current game. + return GetGameVersion(); + } + + /// + /// Determins if the script extender for the game is installed + /// (this checks for the script extender of the game for which the + /// mod is being installed) + /// + /// + static bool ScriptExtenderPresent(); + + /// + /// Gets a list of all install plugins. + /// + /// A list of all install plugins. + static array^ GetAllPlugins(); + + /// + /// Retrieves a list of currently active plugins. + /// + /// A list of currently active plugins. + static array^ GetActivePlugins(); + + /// + /// Sets the activated status of a plugin (i.e., and esp or esm file). + /// + /// The path to the plugin to activate or deactivate. + /// Whether to activate the plugin. + static void SetPluginActivation(String^ /* p_strPluginPath */, bool /* p_booActivate */) { + // throw gcnew NotImplementedException("SetPluginActivation"); + } + + /// + /// Sets the load order of the specifid plugin. + /// + /// The path to the plugin file whose load order is to be set. + /// The new load order index of the plugin. + static void SetPluginOrderIndex(String^ /* p_strPlugin */, int /* p_intNewIndex */) { + // throw gcnew NotImplementedException("SetPluginOrderIndex"); + } + + /// + /// Sets the load order of the plugins. + /// + /// + /// Each plugin will be moved from its current index to its indices' position + /// in . + /// + /// The new load order of the plugins. Each entry in this array + /// contains the current index of a plugin. This array must contain all current indices. + static void SetLoadOrder(array^ /* p_intPlugins */) { + // throw gcnew NotImplementedException("SetLoadOrder"); + } + + /// + /// Moves the specified plugins to the given position in the load order. + /// + /// + /// Note that the order of the given list of plugins is not maintained. They are re-ordered + /// to be in the same order as they are in the before-operation load order. This, I think, + /// is somewhat counter-intuitive and may change, though likely not so as to not break + /// backwards compatibility. + /// + /// The list of plugins to move to the given position in the + /// load order. Each entry in this array contains the current index of a plugin. + /// The position in the load order to which to move the specified + /// plugins. + static void SetLoadOrder(array^ /* p_intPlugins */, int /* p_intPosition */) { + // throw gcnew NotImplementedException("SetLoadOrder"); + } + + /// + /// Retrieves the specified settings value as a string. + /// + /// The name of the settings file from which to retrieve the value. + /// The section containing the value to retrieve. + /// The key of the value to retrieve. + /// The specified value as a string. + static String^ GetIniString(String^ settingsFileName, String^ section, String^ key); + + /// + /// Retrieves the specified settings value as an integer. + /// + /// The name of the settings file from which to retrieve the value. + /// The section containing the value to retrieve. + /// The key of the value to retrieve. + /// The specified value as an integer. + static int GetIniInt(String^ settingsFileName, String^ section, String^ key); + + /// + /// Retrieves the specified Fallout.ini value as a string. + /// + /// The section containing the value to retrieve. + /// The key of the value to retrieve. + /// The specified value as a string. + /// + static String^ GetFalloutIniString(String^ section, String^ key) { + return GetIniString("Fallout.ini", section, key); + } + + /// + /// Retrieves the specified Fallout.ini value as an integer. + /// + /// The section containing the value to retrieve. + /// The key of the value to retrieve. + /// + static int GetFalloutIniInt(String^ section, String^ key) { + return GetIniInt("Fallout.ini", section, key); + } + + /// + /// Retrieves the specified FalloutPrefs.ini value as a string. + /// + /// The section containing the value to retrieve. + /// The key of the value to retrieve. + /// The specified value as a string. + /// + static String^ GetPrefsIniString(String^ p_strSection, String^ p_strKey) { + // This looks wrong? Yes! But that's what used in other mod managers... + return GetIniString("FalloutPrefs.ini", p_strSection, p_strKey); + } + + /// + /// Retrieves the specified FalloutPrefs.ini value as an integer. + /// + /// The section containing the value to retrieve. + /// The key of the value to retrieve. + /// The specified value as an integer. + /// + static int GetPrefsIniInt(String^ p_strSection, String^ p_strKey) { + // This looks wrong? Yes! But that's what used in other mod managers... + return GetIniInt("FalloutPrefs.ini", p_strSection, p_strKey); + } + + /// + /// Sets the specified value in the specified Ini file to the given value. + /// + /// The name of the settings file to edit. + /// The section in the Ini file to edit. + /// The key in the Ini file to edit. + /// The value to which to set the key. + /// true if the value was set; false + /// if the user chose not to overwrite the existing value. + static bool EditIni(String^ p_strSettingsFileName, String^ p_strSection, String^ p_strKey, String^ p_strValue); + + /// + /// Sets the specified value in the Fallout.ini file to the given value. + /// + /// The section in the Ini file to edit. + /// The key in the Ini file to edit. + /// The value to which to set the key. + /// Not used. + /// true if the value was set; false + /// if the user chose not to overwrite the existing value. + static bool EditFalloutINI(String^ p_strSection, String^ p_strKey, String^ p_strValue, bool /* p_booSaveOld */) { + return EditIni("Fallout.ini", p_strSection, p_strKey, p_strValue); + } + + }; + + /** + * @brief Post-install script. + */ + MOBase::IPluginInstaller::EInstallResult postInstall(std::shared_ptr& tree); +} + +// BaseScript cannot be in a namespace: +public ref struct SelectOption: public CSharp::SelectOption { + SelectOption(System::String^ item, System::String^ preview, System::String^ desc) : + CSharp::SelectOption(item, preview, desc) { } +}; +public ref class BaseScript: public CSharp::BaseScriptImpl { }; + +#endif diff --git a/libs/installer_fomod_csharp/src/csharp_interface.cpp b/libs/installer_fomod_csharp/src/csharp_interface.cpp new file mode 100644 index 0000000..91a4a0d --- /dev/null +++ b/libs/installer_fomod_csharp/src/csharp_interface.cpp @@ -0,0 +1,157 @@ +#include "csharp_interface.h" + +#include +#include +#include + +#include + +#include "base_script.h" +#include "csharp_utils.h" + +// clang-format off + +#using + +using namespace MOBase; + +/** + * This is a assembly resolve handler that does only one thing: returns the assembly + * containing BaseScript (usually the DLL) when requested. + * + * I don't know why this must be done manually... But I did not find any better solution. + */ +System::Reflection::Assembly^ currentDomain_AssemblyResolve(System::Object^, System::ResolveEventArgs^ args) +{ + using namespace System::Reflection; + + try { + Assembly^ baseScriptAssembly = System::Reflection::Assembly::GetAssembly(BaseScript::typeid); + if (args->Name->Equals(baseScriptAssembly->FullName)) { + return baseScriptAssembly; + } + } + catch (...) + { + } + + return nullptr; +} + +IPluginInstaller::EInstallResult executeScript(System::String^ script) { + + using namespace System; + using namespace System::CodeDom; + using namespace System::CodeDom::Compiler; + using namespace System::Collections::Generic; + + AppDomain^ currentDomain = AppDomain::CurrentDomain; + currentDomain->AssemblyResolve += gcnew ResolveEventHandler(currentDomain_AssemblyResolve); + + // From Nexus-Mods/fomod-installer: + Dictionary^ dicOptions = gcnew Dictionary(10); + dicOptions->Add("CompilerVersion", "v4.0"); + + // List of assemblies (including BaseScript) - From Nexus-Mods/fomod-installer: + array^ referenceAssemblies = { + "System.dll", + "System.Runtime.dll", + "System.Drawing.dll", + "System.Windows.Forms.dll", + "System.Xml.dll", + System::Reflection::Assembly::GetAssembly(BaseScript::typeid)->Location + }; + + CompilerParameters^ cp = gcnew CompilerParameters(referenceAssemblies); + cp->GenerateExecutable = false; + cp->IncludeDebugInformation = false; + cp->GenerateInMemory = true; + cp->TreatWarningsAsErrors = false; + CodeDomProvider^ provider = CodeDomProvider::CreateProvider("CSharp", dicOptions); + + // Compile the script + auto result = provider->CompileAssemblyFromSource(cp, script); + + int errorCount = 0; + for each (CompilerError ^ error in result->Errors) { + if (error->IsWarning) { + log::warn("C# [{}]: {}", error->Line, CSharp::to_string(error->ErrorText)); + } + else { + log::error("C# [{}]: {}", error->Line, CSharp::to_string(error->ErrorText)); + ++errorCount; + } + } + + if (errorCount > 0) { + return IPluginInstaller::EInstallResult::RESULT_FAILED; + } + + // Execute the script: + try { + auto scriptClass = result->CompiledAssembly->GetType("Script"); + BaseScript^ scriptObject = (BaseScript^)System::Activator::CreateInstance(scriptClass); + auto onActivateMethod = scriptObject->GetType()->GetMethod("OnActivate"); + + auto success = (bool)(onActivateMethod->IsStatic ? onActivateMethod->Invoke(nullptr, nullptr) : onActivateMethod->Invoke(scriptObject, nullptr)); + return success ? IPluginInstaller::EInstallResult::RESULT_SUCCESS : IPluginInstaller::EInstallResult::RESULT_CANCELED; + } + catch (System::Exception^ ex) { + log::error("C# ({}): {}\n{}", CSharp::to_string(ex->GetType()->FullName), CSharp::to_string(ex->Message), CSharp::to_string(ex->StackTrace)); + System::Exception^ innerEx = ex->InnerException; + if (innerEx) { + log::error("C# ({}): {}\n{}", CSharp::to_string(innerEx->GetType()->FullName), CSharp::to_string(innerEx->Message), CSharp::to_string(innerEx->StackTrace)); + } + return IPluginInstaller::EInstallResult::RESULT_FAILED; + } + +} + +namespace CSharp { + + IPluginInstaller::EInstallResult executeCSharpScript(QString scriptPath, std::shared_ptr& tree) { + + using namespace System; + using namespace System::IO; + using namespace System::Text::RegularExpressions; + + // Note: Using C# stuff here to mimicate NMM since there are some encoding issues, and + // some regex do not work in C++: + array^ scriptBytes = File::ReadAllBytes(from_string(scriptPath.toStdWString())); + + // Read the script (using C# to "auto-detect" encoding in a C# way): + String^ script; + { + auto memoryStream = gcnew MemoryStream(scriptBytes); + auto reader = gcnew StreamReader(memoryStream, true); + + script = reader->ReadToEnd(); + + reader->Close(); + memoryStream->Close(); + + delete reader; + delete memoryStream; + } + + + Regex^ regScriptClass = gcnew Regex(R"re((class\s+Script\s*:.*?)(\S*BaseScript))re"); + Regex^ regFommUsing = gcnew Regex(R"re(\s*using\s*fomm.Scripting\s*;)re"); + + String^ strBaseScriptClassName = regScriptClass->Match(script)->Groups[2]->ToString(); + Regex^ regOtherScriptClasses = gcnew Regex(String::Format(R"re((class\s+\S+\s*:.*?)(?Replace(strCode, "$1BaseScript"); + strCode = regOtherScriptClasses->Replace(strCode, "$1BaseScript"); + strCode = regFommUsing->Replace(strCode, ""); + + auto result = executeScript(strCode); + + if (result != IPluginInstaller::EInstallResult::RESULT_SUCCESS) { + return result; + } + + return postInstall(tree); + } + +} diff --git a/libs/installer_fomod_csharp/src/csharp_interface.h b/libs/installer_fomod_csharp/src/csharp_interface.h new file mode 100644 index 0000000..4f22c20 --- /dev/null +++ b/libs/installer_fomod_csharp/src/csharp_interface.h @@ -0,0 +1,40 @@ +#ifndef CSHARP_INTERFACE_H +#define CSHARP_INTERFACE_H + +#include + +#include +#include + +namespace CSharp +{ + +void init(MOBase::IOrganizer* moInfo); + +/** + * @brief Initialize the C# interface before starting an installation. + * + * @param installer The FOMOD C# installer. + * @param manager The installation manager from the installer. + * @param parentWidget The parent widget from the installer. + * @param tree The archive tree. + * @param extractedEntries A map from extracted entries to their extracted path. + */ +void beforeInstall( + MOBase::IPlugin const* installer, MOBase::IInstallationManager* manager, + QWidget* parentWidget, std::shared_ptr tree, + std::map, QString> extractedEntries); + +/** + * @brief Clear the C# interface after an installation. + * + * @param scriptPath Path to the script to execute. + * @param tree Reference where the final tree will be stored (in case of success. + * + * @return the installation result after performing post-installation. + */ +MOBase::IPluginInstaller::EInstallResult +executeCSharpScript(QString scriptPath, std::shared_ptr& tree); + +} // namespace CSharp +#endif diff --git a/libs/installer_fomod_csharp/src/csharp_utils.h b/libs/installer_fomod_csharp/src/csharp_utils.h new file mode 100644 index 0000000..5ede874 --- /dev/null +++ b/libs/installer_fomod_csharp/src/csharp_utils.h @@ -0,0 +1,41 @@ +// clang-format off + +#ifndef CSHARP_UTILS_H +#define CSHARP_UTILS_H + +#include +#include + +#include + +#include + +#using + +namespace CSharp { + + /** + * Handy functions. + */ + inline std::string to_string(System::String^ value) { + return msclr::interop::marshal_as(value); + } + inline std::wstring to_wstring(System::String^ value) { + return msclr::interop::marshal_as(value); + } + inline QString to_qstring(System::String^ value) { + msclr::interop::marshal_context ctx; + return QString::fromWCharArray(ctx.marshal_as(value)); + } + + template + inline System::String^ from_string(Str const& string) { + return msclr::interop::marshal_as(string); + } + inline System::String^ from_string(QString const& string) { + return msclr::interop::marshal_as(string.toStdWString().c_str()); + } + +} + +#endif diff --git a/libs/installer_fomod_csharp/src/installer_fomod_csharp.cpp b/libs/installer_fomod_csharp/src/installer_fomod_csharp.cpp new file mode 100644 index 0000000..02d3a3e --- /dev/null +++ b/libs/installer_fomod_csharp/src/installer_fomod_csharp.cpp @@ -0,0 +1,193 @@ +/* +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 + +#include "csharp_interface.h" +#include "installer_fomod_csharp.h" +#include "installer_fomod_predialog.h" +#include "xml_info_reader.h" + +using namespace MOBase; + +bool InstallerFomodCSharp::init(IOrganizer* moInfo) +{ + m_MOInfo = moInfo; + CSharp::init(moInfo); + return true; +} + +std::shared_ptr +InstallerFomodCSharp::findFomodDirectory(std::shared_ptr tree) const +{ + auto entry = tree->find("fomod", FileTreeEntry::DIRECTORY); + + if (entry != nullptr) { + return entry->astree(); + } + + if (tree->empty()) { + return nullptr; + } + + // We need at least a directory: + if (!tree->at(0)->isDir()) { + return nullptr; + } + + // But not two: + if (tree->size() > 1 && tree->at(1)->isDir()) { + return nullptr; + } + + return findFomodDirectory(tree->at(0)->astree()); +} + +std::shared_ptr +InstallerFomodCSharp::findScriptFile(std::shared_ptr tree) const +{ + auto fomodDirectory = findFomodDirectory(tree); + + if (fomodDirectory == nullptr) { + return nullptr; + } + + for (auto e : *fomodDirectory) { + if (e->isFile() && e->suffix().compare("cs", Qt::CaseInsensitive) == 0) { + return e; + } + } + + return nullptr; +} + +std::shared_ptr +InstallerFomodCSharp::findInfoFile(std::shared_ptr tree) const +{ + auto fomodDirectory = findFomodDirectory(tree); + + if (fomodDirectory == nullptr) { + return nullptr; + } + + for (auto e : *fomodDirectory) { + if (e->isFile() && e->compare("info.xml") == 0) { + return e; + } + } + + return nullptr; +} + +bool InstallerFomodCSharp::isArchiveSupported( + std::shared_ptr tree) const +{ + return findScriptFile(tree) != nullptr; +} + +InstallerFomodCSharp::EInstallResult +InstallerFomodCSharp::install(MOBase::GuessedValue& modName, + std::shared_ptr& tree, + QString& version, int& modID) +{ + static std::set imageSuffixes{"png", "jpg", "jpeg", + "gif", "bmp"}; + + // Extract the script file: + auto scriptFile = findScriptFile(tree); + if (scriptFile == nullptr) { + return EInstallResult::RESULT_NOTATTEMPTED; + } + + // Check if there is a info.xml: + auto infoFile = findInfoFile(tree); + + // Set containing everything to extract except the script and the info file: + std::set> toExtractSet{scriptFile}; + + if (infoFile != nullptr) { + toExtractSet.insert(infoFile); + } + + // Extract all the images: + tree->walk([&](const QString&, auto entry) { + if (entry->isFile() && imageSuffixes.count(entry->suffix()) > 0) { + toExtractSet.insert(entry); + } + return IFileTree::WalkReturn::CONTINUE; + }); + + // Extract everything from the fomod/ folder: + auto fomodFolder = findFomodDirectory(tree); + fomodFolder->walk([&](const QString&, auto entry) { + if (entry->isFile()) { + toExtractSet.insert(entry); + } + return IFileTree::WalkReturn::CONTINUE; + }); + + // Convert to vector: + std::vector toExtract(std::begin(toExtractSet), std::end(toExtractSet)); + QStringList paths(manager()->extractFiles(toExtract)); + + // If user cancelled: + if (toExtract.size() != static_cast(paths.size())) { + return EInstallResult::RESULT_CANCELED; + } + + // Create a map from entry to file path: + std::map, QString> entryToPath; + for (std::size_t i = 0; i < toExtract.size(); ++i) { + entryToPath[toExtract[i]] = paths[i]; + } + + if (infoFile != nullptr) { + QFile file(entryToPath[infoFile]); + if (file.open(QIODevice::ReadOnly)) { + auto info = FomodInfoReader::readXml(file, &FomodInfoReader::parseInfo); + if (!std::get<0>(info).isEmpty()) { + modName.update(std::get<0>(info), GUESS_META); + } + if (std::get<1>(info) != -1) { + modID = std::get<1>(info); + } + if (!std::get<2>(info).isEmpty()) { + version = std::get<2>(info); + } + } + } + + // Show the dialog: + InstallerFomodPredialog dialog(modName, parentWidget()); + if (dialog.exec() != QDialog::Accepted) { + if (dialog.manualRequested()) { + modName.update(dialog.getName(), GUESS_USER); + return EInstallResult::RESULT_MANUALREQUESTED; + } else { + return EInstallResult::RESULT_CANCELED; + } + } + modName.update(dialog.getName(), GUESS_USER); + + // Run the C# script: + const QString scriptPath = entryToPath[scriptFile]; + CSharp::beforeInstall( + this, manager(), parentWidget(), + std::const_pointer_cast(scriptFile->parent()->parent()), + std::move(entryToPath)); + return CSharp::executeCSharpScript(scriptPath, tree); +} diff --git a/libs/installer_fomod_csharp/src/installer_fomod_csharp.h b/libs/installer_fomod_csharp/src/installer_fomod_csharp.h new file mode 100644 index 0000000..530db37 --- /dev/null +++ b/libs/installer_fomod_csharp/src/installer_fomod_csharp.h @@ -0,0 +1,84 @@ +/* +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 . +*/ + +#ifndef INSTALLER_FOMOD_CSHARP_H +#define INSTALLER_FOMOD_CSHARP_H + +#include + +class InstallerFomodCSharp : public MOBase::IPluginInstallerSimple +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerSimple) + Q_PLUGIN_METADATA(IID "org.holt59.InstallerFomodCSharp") + +public: + InstallerFomodCSharp() {} + + virtual bool init(MOBase::IOrganizer* moInfo) override; + + virtual QString name() const override { return "Fomod Installer C#"; } + + virtual QString localizedName() const override { return tr("Fomod Installer C#"); } + + virtual QString author() const override { return "Holt59"; } + + virtual QString description() const override + { + return tr("Installer for C# based FOMOD archives."); + } + + virtual MOBase::VersionInfo version() const override + { + return MOBase::VersionInfo(1, 0, 0, MOBase::VersionInfo::RELEASE_BETA); + } + + virtual QList settings() const override + { + return { + MOBase::PluginSetting("enabled", "check to enable this plugin", QVariant(true)), + MOBase::PluginSetting("prefer", "prefer this over the NCC based plugin", + QVariant(true))}; + } + + virtual unsigned int priority() const override + { + // It's the same priority as the FOMOD installer but those should never conflict: + return m_MOInfo->pluginSetting(name(), "prefer").toBool() ? 110 : 90; + } + + virtual bool isManualInstaller() const override { return false; } + + 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: + MOBase::IOrganizer* m_MOInfo; + + std::shared_ptr + findFomodDirectory(std::shared_ptr tree) const; + std::shared_ptr + findScriptFile(std::shared_ptr tree) const; + std::shared_ptr + findInfoFile(std::shared_ptr tree) const; +}; + +#endif diff --git a/libs/installer_fomod_csharp/src/installer_fomod_csharp_en.ts b/libs/installer_fomod_csharp/src/installer_fomod_csharp_en.ts new file mode 100644 index 0000000..7c13b6c --- /dev/null +++ b/libs/installer_fomod_csharp/src/installer_fomod_csharp_en.ts @@ -0,0 +1,125 @@ + + + + + FomodCSharpPostDialog + + + Settings modification required + + + + + The installer needs to edit the following settings. You can either apply them, discard them or move them to the mod installation folder (under INI Tweaks). + + + + + Apply the settings to the INI files corresponding to the current profile. + + + + + Apply + + + + + Discard the settings. + + + + + Discard + + + + + Create files under "INI Tweaks" in the mod folder with these settings. + + + + + INI Tweaks + + + + + Cancel the installation. + + + + + Cancel + + + + + FomodCSharpPredialog + + + FOMOD C# Installer + + + + + Name + + + + + + Opens a Dialog that allows custom modifications. + + + + + Manual + + + + + Start + + + + + Cancel + + + + + FomodInfoReader + + + Failed to parse %1. See console for details. + + + + + InstallerFomodCSharp + + + Fomod Installer C# + + + + + Installer for C# based FOMOD archives. + + + + + QObject + + + Choose any: + + + + + Choose one: + + + + diff --git a/libs/installer_fomod_csharp/src/installer_fomod_csharp_postdialog.ui b/libs/installer_fomod_csharp/src/installer_fomod_csharp_postdialog.ui new file mode 100644 index 0000000..89a6106 --- /dev/null +++ b/libs/installer_fomod_csharp/src/installer_fomod_csharp_postdialog.ui @@ -0,0 +1,99 @@ + + + FomodCSharpPostDialog + + + + 0 + 0 + 589 + 399 + + + + Settings modification required + + + + + + The installer needs to edit the following settings. You can either apply them, discard them or move them to the mod installation folder (under INI Tweaks). + + + true + + + + + + + + + -1 + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Apply the settings to the INI files corresponding to the current profile. + + + Apply + + + + + + + Discard the settings. + + + Discard + + + + + + + Create files under "INI Tweaks" in the mod folder with these settings. + + + INI Tweaks + + + + + + + Cancel the installation. + + + Cancel + + + + + + + + + + diff --git a/libs/installer_fomod_csharp/src/installer_fomod_csharp_predialog.ui b/libs/installer_fomod_csharp/src/installer_fomod_csharp_predialog.ui new file mode 100644 index 0000000..0aa08c3 --- /dev/null +++ b/libs/installer_fomod_csharp/src/installer_fomod_csharp_predialog.ui @@ -0,0 +1,89 @@ + + + FomodCSharpPredialog + + + + 0 + 0 + 400 + 83 + + + + FOMOD C# Installer + + + + + + + + Name + + + + + + + true + + + + + + + + + + + Opens a Dialog that allows custom modifications. + + + Opens a Dialog that allows custom modifications. + + + Manual + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 20 + 20 + + + + + + + + Start + + + true + + + + + + + Cancel + + + + + + + + + + diff --git a/libs/installer_fomod_csharp/src/installer_fomod_postdialog.h b/libs/installer_fomod_csharp/src/installer_fomod_postdialog.h new file mode 100644 index 0000000..9804610 --- /dev/null +++ b/libs/installer_fomod_csharp/src/installer_fomod_postdialog.h @@ -0,0 +1,87 @@ +#ifndef INSTALLER_FOMOD_POSTDIALOG_H +#define INSTALLER_FOMOD_POSTDIALOG_H + +#include "ui_installer_fomod_csharp_postdialog.h" + +#include + +#include "psettings.h" + +/** + * @brief Dialog for the installation of a simple archive + * a simple archive is one that doesn't require any manual changes to work correctly + **/ +class InstallerFomodPostDialog : public QDialog +{ + Q_OBJECT + +public: + enum class Result + { + APPLY, + DISCARD, + MOVE, + }; + + /** + * @brief constructor + * + * @param preset suggested name for the mod + * @param parent parent widget + **/ + explicit InstallerFomodPostDialog(QWidget* parent = 0) + : QDialog(parent), ui(new Ui::FomodCSharpPostDialog) + { + ui->setupUi(this); + setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); + } + + ~InstallerFomodPostDialog() {} + + /** + * @return the result of this dialog if the user did not cancel. + */ + Result result() const { return m_Result; } + + /** + * + */ + void setIniSettings(std::map const& settings) + { + for (auto p : settings) { + QTextEdit* widget = new QTextEdit(this); + widget->append(p.second.toString()); + widget->setReadOnly(true); + ui->tabWidget->addTab(widget, p.first); + } + } + +private slots: + + void on_discardBtn_clicked() + { + m_Result = Result::DISCARD; + this->accept(); + } + + void on_applyBtn_clicked() + { + m_Result = Result::APPLY; + this->accept(); + } + + void on_moveBtn_clicked() + { + m_Result = Result::MOVE; + this->accept(); + } + + void on_cancelBtn_clicked() { this->reject(); } + +private: + std::unique_ptr ui; + + Result m_Result{Result::APPLY}; +}; + +#endif diff --git a/libs/installer_fomod_csharp/src/installer_fomod_predialog.h b/libs/installer_fomod_csharp/src/installer_fomod_predialog.h new file mode 100644 index 0000000..5c57fdf --- /dev/null +++ b/libs/installer_fomod_csharp/src/installer_fomod_predialog.h @@ -0,0 +1,72 @@ +#ifndef INSTALLER_FOMOD_PREDIALOG_H +#define INSTALLER_FOMOD_PREDIALOG_H + +#include "ui_installer_fomod_csharp_predialog.h" + +#include + +#include + +/** + * @brief Dialog for the installation of a simple archive + * a simple archive is one that doesn't require any manual changes to work correctly + **/ +class InstallerFomodPredialog : public QDialog +{ + Q_OBJECT + +public: + /** + * @brief constructor + * + * @param preset suggested name for the mod + * @param parent parent widget + **/ + explicit InstallerFomodPredialog(const MOBase::GuessedValue& preset, + QWidget* parent = 0) + : QDialog(parent), ui(new Ui::FomodCSharpPredialog), m_Manual(false) + { + + ui->setupUi(this); + setWindowTitle(preset + " - " + windowTitle()); + + for (auto iter = preset.variants().begin(); iter != preset.variants().end(); + ++iter) { + ui->nameCombo->addItem(*iter); + } + + ui->nameCombo->setCurrentIndex(ui->nameCombo->findText(preset)); + setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); + ui->nameCombo->completer()->setCaseSensitivity(Qt::CaseSensitive); + } + + ~InstallerFomodPredialog() {} + + /** + * @return true if the user requested a manual installation. + **/ + bool manualRequested() const { return m_Manual; } + + /** + * @return the (user-modified) mod name + **/ + QString getName() const { return ui->nameCombo->currentText(); } + +private slots: + + void on_okBtn_clicked() { this->accept(); } + + void on_cancelBtn_clicked() { this->reject(); } + + void on_manualBtn_clicked() + { + m_Manual = true; + this->reject(); + } + +private: + std::unique_ptr ui; + bool m_Manual; +}; + +#endif diff --git a/libs/installer_fomod_csharp/src/psettings.h b/libs/installer_fomod_csharp/src/psettings.h new file mode 100644 index 0000000..649c7da --- /dev/null +++ b/libs/installer_fomod_csharp/src/psettings.h @@ -0,0 +1,108 @@ +#ifndef PSETTINGS_H +#define PSETTINGS_H + +#include +#include +#include + +/** + * This is a small class that can be used to store INI settings in memory since + * QSettings is a pain to use without an actual file. + * + * It is a much simpler structure since it stores everything as string. + */ +struct PSettings +{ + + /** + * + */ + PSettings() = default; + +public: // Value read/write. + /** + * @brief Set the value of the given section/key. + * + * @param section The section of the value. + * @param key The key of the value. + * @param value The value to set. + */ + void setValue(QString section, QString key, QString value) + { + m_Values[std::make_pair(section, key)] = value; + } + + /** + * @brief Return the value at the given section/key. + * + * @param section The section of the value. + * @param key The key of the value. + * + * @return the corresponding value, or an empty string if the section/key does not + * exist. + */ + QString value(QString section, QString key) const + { + auto it = m_Values.find(std::make_pair(section, key)); + return it == m_Values.end() ? QString() : it->second; + } + + /** + * @brief Check if the given section/key exists in these settings. + * + * @param section The section of the value. + * @param key The key of the value. + * + * @return true if the section/key exist. + */ + bool hasValue(QString section, QString key) const + { + return m_Values.find(std::make_pair(section, key)) != m_Values.end(); + } + +public: // Output: + /** + * @brief Convert this PSettings to a string. + * + * @return a string representing the content of a valid INI file corresponding + * to this PSettings. + */ + QString toString() const + { + QString result = ""; + QString cSection; + for (auto& p : m_Values) { + if (cSection != p.first.first) { + if (!cSection.isEmpty()) { + result += '\n'; + } + cSection = p.first.first; + result += "[" + cSection + "]\n"; + } + result += p.first.second + "=" + p.second + "\n"; + } + return result; + } + + /** + * @brief Update the given QSettings with all the value in this. + * + * @param settings The settings to update. + */ + void update(QSettings& settings) const + { + for (auto& p : m_Values) { + if (p.first.first == "General") { + settings.setValue(p.first.second, p.second); + } else { + settings.setValue(p.first.first + "/" + p.first.second, p.second); + } + } + } + +private: + // Map from to value: + std::map, QString> m_Values; +}; + +#endif // !PSETTINGS_H diff --git a/libs/installer_fomod_csharp/src/xml_info_reader.h b/libs/installer_fomod_csharp/src/xml_info_reader.h new file mode 100644 index 0000000..91d6d9a --- /dev/null +++ b/libs/installer_fomod_csharp/src/xml_info_reader.h @@ -0,0 +1,140 @@ +#ifndef XML_INFO_READER_H +#define XML_INFO_READER_H + +#include +#include +#include +#include +#include +#include + +#include +#include + +// This is from installer_fomod, but should probably not be duplicated here. + +struct FomodInfoReader : QObject +{ + + Q_OBJECT + +public: + struct XmlParseError : MOBase::Exception + { + XmlParseError(const QString& message) : MOBase::Exception(message) {} + }; + + static QByteArray skipXmlHeader(QIODevice& file) + { + static const unsigned char UTF16LE_BOM[] = {0xFF, 0xFE}; + static const unsigned char UTF16BE_BOM[] = {0xFE, 0xFF}; + static const unsigned char UTF8_BOM[] = {0xEF, 0xBB, 0xBF}; + static const unsigned char UTF16LE[] = {0x3C, 0x00, 0x3F, 0x00}; + static const unsigned char UTF16BE[] = {0x00, 0x3C, 0x00, 0x3F}; + static const unsigned char UTF8[] = {0x3C, 0x3F, 0x78, 0x6D}; + + file.seek(0); + QByteArray rawBytes = file.read(4); + QTextStream stream(&file); + int bom = 0; + if (rawBytes.startsWith((const char*)UTF16LE_BOM)) { + stream.setEncoding(QStringEncoder::Encoding::Utf16LE); + bom = 2; + } else if (rawBytes.startsWith((const char*)UTF16BE_BOM)) { + stream.setEncoding(QStringEncoder::Encoding::Utf16BE); + bom = 2; + } else if (rawBytes.startsWith((const char*)UTF8_BOM)) { + stream.setEncoding(QStringEncoder::Encoding::Utf8); + bom = 3; + } else if (rawBytes.startsWith(QByteArray((const char*)UTF16LE, 4))) { + stream.setEncoding(QStringEncoder::Encoding::Utf16LE); + } else if (rawBytes.startsWith(QByteArray((const char*)UTF16BE, 4))) { + stream.setEncoding(QStringEncoder::Encoding::Utf16BE); + } else if (rawBytes.startsWith(QByteArray((const char*)UTF8, 4))) { + stream.setEncoding(QStringEncoder::Encoding::Utf8); + } // otherwise maybe the textstream knows the encoding? + + stream.seek(bom); + QString header = stream.readLine(); + if (!header.startsWith(" + static auto readXml(QFile& file, Fn&& fn) + { + // List of encodings to try: + static const std::vector encodings{ + QStringConverter::Utf16, QStringConverter::Utf8, QStringConverter::Latin1}; + + std::string errorMessage; + try { + QXmlStreamReader reader(&file); + return fn(reader); + } catch (const XmlParseError& e) { + MOBase::log::warn( + "The {} in this file is incorrectly encoded ({}). Applying heuristics...", + file.fileName(), e.what()); + } + + // nmm's xml parser is less strict than the one from qt and allows files with + // wrong encoding in the header. Being strict here would be bad user experience + // this works around bad headers. + QByteArray headerlessData = skipXmlHeader(file); + + // try parsing the file with several encodings to support broken files + for (auto encoding : encodings) { + MOBase::log::debug("Trying encoding {} for {}... ", + QStringConverter::nameForEncoding(encoding), file.fileName()); + try { + QStringEncoder encoder(encoding); + QXmlStreamReader reader( + encoder.encode(QString("") + .arg(encoder.name())) + + headerlessData); + MOBase::log::debug("Interpreting {} as {}.", file.fileName(), encoder.name()); + return fn(reader); + } catch (const XmlParseError& e) { + MOBase::log::debug("Not {}: {}.", QStringConverter::nameForEncoding(encoding), + e.what()); + } + } + + throw XmlParseError( + tr("Failed to parse %1. See console for details.").arg(file.fileName())); + } + + static std::tuple parseInfo(QXmlStreamReader& reader) + { + std::tuple info{"", -1, ""}; + while (!reader.atEnd()) { + switch (reader.readNext()) { + case QXmlStreamReader::StartElement: { + if (reader.name().toString() == "Name") { + std::get<0>(info) = reader.readElementText(); + } else if (reader.name().toString() == "Author") { + } else if (reader.name().toString() == "Version") { + std::get<2>(info) = reader.readElementText(); + } else if (reader.name().toString() == "Id") { + std::get<1>(info) = reader.readElementText().toInt(); + } else if (reader.name().toString() == "Website") { + } + } break; + default: { + } break; + } + } + if (reader.hasError()) { + throw XmlParseError( + QString("%1 in line %2").arg(reader.errorString()).arg(reader.lineNumber())); + } + return info; + } +}; + +#endif -- cgit v1.3.1