aboutsummaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-06-17 01:46:09 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-06-17 01:46:09 -0500
commit05aa96d20ca726908a7d8b1943f86f0c4e11bf18 (patch)
treebd45913d1c7a033088f3456e40c8b44c72818ace /libs
parent30d2c801a01eeb140ddd5adf8676f2626ecff3fa (diff)
Prepare Nexus-safe release cleanup
Diffstat (limited to 'libs')
-rw-r--r--libs/game_bethesda/src/games/starfield/gamestarfield.cpp11
-rw-r--r--libs/installer_bsplugins/src/CMakeLists.txt2
-rw-r--r--libs/installer_fomod_plus/.clang-format4
-rw-r--r--libs/installer_fomod_plus/.clangd16
-rw-r--r--libs/installer_fomod_plus/.editorconfig16
-rw-r--r--libs/installer_fomod_plus/CMakeLists.txt36
-rw-r--r--libs/installer_fomod_plus/cmake/patch_pugixml.cmake20
-rw-r--r--libs/installer_fomod_plus/installer/CMakeLists.txt53
-rw-r--r--libs/installer_fomod_plus/installer/FomodInstallerWindow.cpp842
-rw-r--r--libs/installer_fomod_plus/installer/FomodInstallerWindow.h214
-rw-r--r--libs/installer_fomod_plus/installer/FomodPlusInstaller.cpp510
-rw-r--r--libs/installer_fomod_plus/installer/FomodPlusInstaller.h115
-rw-r--r--libs/installer_fomod_plus/installer/fomod_plus_de.ts4
-rw-r--r--libs/installer_fomod_plus/installer/fomod_plus_en.ts4
-rw-r--r--libs/installer_fomod_plus/installer/fomod_plus_installer_de.ts75
-rw-r--r--libs/installer_fomod_plus/installer/fomod_plus_installer_en.ts79
-rw-r--r--libs/installer_fomod_plus/installer/fomod_plus_installer_zh_CN.ts75
-rw-r--r--libs/installer_fomod_plus/installer/fomod_plus_zh_CN.ts4
-rw-r--r--libs/installer_fomod_plus/installer/fomodplus.json4
-rw-r--r--libs/installer_fomod_plus/installer/integration/FomodDataContent.cpp50
-rw-r--r--libs/installer_fomod_plus/installer/integration/FomodDataContent.h21
-rw-r--r--libs/installer_fomod_plus/installer/lib/ConditionTester.cpp176
-rw-r--r--libs/installer_fomod_plus/installer/lib/ConditionTester.h42
-rw-r--r--libs/installer_fomod_plus/installer/lib/CrashHandler.cpp122
-rw-r--r--libs/installer_fomod_plus/installer/lib/CrashHandler.h20
-rw-r--r--libs/installer_fomod_plus/installer/lib/FileInstaller.cpp274
-rw-r--r--libs/installer_fomod_plus/installer/lib/FileInstaller.h94
-rw-r--r--libs/installer_fomod_plus/installer/lib/FlagMap.h113
-rw-r--r--libs/installer_fomod_plus/installer/lib/Logger.h116
-rw-r--r--libs/installer_fomod_plus/installer/lib/ViewModels.h120
-rw-r--r--libs/installer_fomod_plus/installer/resources.qrc14
-rw-r--r--libs/installer_fomod_plus/installer/resources/delete.pngbin2125 -> 0 bytes
-rw-r--r--libs/installer_fomod_plus/installer/resources/fomod_icon.pngbin1782 -> 0 bytes
-rw-r--r--libs/installer_fomod_plus/installer/resources/image_icon.pngbin727 -> 0 bytes
-rw-r--r--libs/installer_fomod_plus/installer/resources/left-chevron.pngbin1732 -> 0 bytes
-rw-r--r--libs/installer_fomod_plus/installer/resources/right-chevron.pngbin1808 -> 0 bytes
-rw-r--r--libs/installer_fomod_plus/installer/ui/ClickableWidget.h24
-rw-r--r--libs/installer_fomod_plus/installer/ui/Colors.h155
-rw-r--r--libs/installer_fomod_plus/installer/ui/FomodImageViewer.cpp284
-rw-r--r--libs/installer_fomod_plus/installer/ui/FomodImageViewer.h92
-rw-r--r--libs/installer_fomod_plus/installer/ui/FomodViewModel.cpp746
-rw-r--r--libs/installer_fomod_plus/installer/ui/FomodViewModel.h154
-rw-r--r--libs/installer_fomod_plus/installer/ui/ScaleLabel.cpp120
-rw-r--r--libs/installer_fomod_plus/installer/ui/ScaleLabel.h45
-rw-r--r--libs/installer_fomod_plus/installer/ui/UIHelper.cpp97
-rw-r--r--libs/installer_fomod_plus/installer/ui/UIHelper.h80
-rw-r--r--libs/installer_fomod_plus/patchwizard/CMakeLists.txt45
-rw-r--r--libs/installer_fomod_plus/patchwizard/FomodPlusPatchWizard.cpp181
-rw-r--r--libs/installer_fomod_plus/patchwizard/FomodPlusPatchWizard.h54
-rw-r--r--libs/installer_fomod_plus/patchwizard/fomod_plus_patch_wizard_en.ts96
-rw-r--r--libs/installer_fomod_plus/patchwizard/fomod_plus_patchwizard_de.ts19
-rw-r--r--libs/installer_fomod_plus/patchwizard/fomodpluspatchwizard.json8
-rw-r--r--libs/installer_fomod_plus/patchwizard/lib/PatchFinder.cpp85
-rw-r--r--libs/installer_fomod_plus/patchwizard/lib/PatchFinder.h49
-rw-r--r--libs/installer_fomod_plus/patchwizard/resources.qrc6
-rw-r--r--libs/installer_fomod_plus/patchwizard/resources/fomod_icon.pngbin1782 -> 0 bytes
-rw-r--r--libs/installer_fomod_plus/patchwizard/resources/infoscroll.pngbin3682 -> 0 bytes
-rw-r--r--libs/installer_fomod_plus/scanner/CMakeLists.txt54
-rw-r--r--libs/installer_fomod_plus/scanner/FomodPlusScanner.cpp128
-rw-r--r--libs/installer_fomod_plus/scanner/FomodPlusScanner.h64
-rw-r--r--libs/installer_fomod_plus/scanner/archiveparser.h97
-rw-r--r--libs/installer_fomod_plus/scanner/fomod_plus_scanner_de.ts57
-rw-r--r--libs/installer_fomod_plus/scanner/fomod_plus_scanner_en.ts57
-rw-r--r--libs/installer_fomod_plus/scanner/fomod_plus_scanner_zh_CN.ts57
-rw-r--r--libs/installer_fomod_plus/scanner/fomodplusscanner.json8
-rw-r--r--libs/installer_fomod_plus/scanner/resources.qrc6
-rw-r--r--libs/installer_fomod_plus/scanner/resources/fomod_icon.pngbin1782 -> 0 bytes
-rw-r--r--libs/installer_fomod_plus/scanner/resources/wizard.gifbin5263 -> 0 bytes
-rw-r--r--libs/installer_fomod_plus/share/FOMODData/ArchiveExtractor.h160
-rw-r--r--libs/installer_fomod_plus/share/FOMODData/FomodDB.h144
-rw-r--r--libs/installer_fomod_plus/share/FOMODData/FomodDBEntry.h129
-rw-r--r--libs/installer_fomod_plus/share/FOMODData/FomodRescan.h277
-rw-r--r--libs/installer_fomod_plus/share/FOMODData/PluginReader.h119
-rw-r--r--libs/installer_fomod_plus/share/stringutil.h150
-rw-r--r--libs/installer_fomod_plus/share/xml/FomodInfoFile.cpp44
-rw-r--r--libs/installer_fomod_plus/share/xml/FomodInfoFile.h25
-rw-r--r--libs/installer_fomod_plus/share/xml/ModuleConfiguration.cpp352
-rw-r--r--libs/installer_fomod_plus/share/xml/ModuleConfiguration.h305
-rw-r--r--libs/installer_fomod_plus/share/xml/XmlHelper.h17
-rw-r--r--libs/installer_fomod_plus/share/xml/XmlParseException.h10
-rw-r--r--libs/plugin_python/src/runner/pythonrunner.cpp89
81 files changed, 85 insertions, 7850 deletions
diff --git a/libs/game_bethesda/src/games/starfield/gamestarfield.cpp b/libs/game_bethesda/src/games/starfield/gamestarfield.cpp
index 06a6d79..1d3f0ca 100644
--- a/libs/game_bethesda/src/games/starfield/gamestarfield.cpp
+++ b/libs/game_bethesda/src/games/starfield/gamestarfield.cpp
@@ -235,11 +235,10 @@ QStringList GameStarfield::testFilePlugins() const
QStringList GameStarfield::primaryPlugins() const
{
- QStringList plugins = {"Starfield.esm", "Constellation.esm",
- "ShatteredSpace.esm", "OldMars.esm",
- "SFBGS003.esm", "SFBGS004.esm",
- "SFBGS006.esm", "SFBGS007.esm",
- "SFBGS008.esm", "BlueprintShips-Starfield.esm"};
+ QStringList plugins = {"Starfield.esm", "Constellation.esm", "OldMars.esm",
+ "ShatteredSpace.esm", "SFBGS00D.esm", "SFBGS050.esm",
+ "SFBGS003.esm", "SFBGS004.esm", "SFBGS006.esm",
+ "SFBGS007.esm", "SFBGS008.esm", "SFBGS047.esm"};
for (auto plugin : CCCPlugins()) {
if (!plugins.contains(plugin, Qt::CaseInsensitive)) {
@@ -290,7 +289,7 @@ bool GameStarfield::prepareIni(const QString& exec)
QStringList GameStarfield::DLCPlugins() const
{
- return {"Constellation.esm", "ShatteredSpace.esm"};
+ return {"Constellation.esm", "ShatteredSpace.esm", "SFBGS050.esm"};
}
QStringList GameStarfield::CCCPlugins() const
diff --git a/libs/installer_bsplugins/src/CMakeLists.txt b/libs/installer_bsplugins/src/CMakeLists.txt
index c3c67a1..ec75efe 100644
--- a/libs/installer_bsplugins/src/CMakeLists.txt
+++ b/libs/installer_bsplugins/src/CMakeLists.txt
@@ -103,7 +103,7 @@ target_link_libraries(bsplugins PRIVATE
ZLIB::ZLIB
)
-# Install into fluorine plugins dir (uses fomod_plus-style shim).
+# Install into the Fluorine plugins dir.
if(COMMAND mo2_install_plugin)
mo2_install_plugin(bsplugins)
elseif(COMMAND mo2_install_target)
diff --git a/libs/installer_fomod_plus/.clang-format b/libs/installer_fomod_plus/.clang-format
deleted file mode 100644
index 37702ff..0000000
--- a/libs/installer_fomod_plus/.clang-format
+++ /dev/null
@@ -1,4 +0,0 @@
-BasedOnStyle: WebKit
-AlignConsecutiveAssignments: Consecutive
-AccessModifierOffset: -2
-ColumnLimit: 120 \ No newline at end of file
diff --git a/libs/installer_fomod_plus/.clangd b/libs/installer_fomod_plus/.clangd
deleted file mode 100644
index f22e5c1..0000000
--- a/libs/installer_fomod_plus/.clangd
+++ /dev/null
@@ -1,16 +0,0 @@
-CompileFlags:
- Add:
- - "-ID:/var/mo2-fomod-plus/installer"
- - "-ID:/var/mo2-fomod-plus/scanner"
- - "-ID:/var/mo2-fomod-plus/share"
- - "-ID:/var/mo2-fomod-plus/vsbuild/_deps/pugixml-src/src"
- - "-ID:/var/mo2-fomod-plus/vsbuild/_deps/json-src/include"
- - "-ID:/var/vcpkg/packages/mo2-uibase_x64-windows/include"
- - "-ID:/var/vcpkg/packages/mo2-uibase_x64-windows/include/uibase"
- - "-ID:/var/vcpkg/packages/mo2-uibase_x64-windows/include/uibase/game_features"
- - "-IC:/Qt/6.7.3/msvc2022_64/include"
- - "-IC:/Qt/6.7.3/msvc2022_64/include/QtCore"
- - "-IC:/Qt/6.7.3/msvc2022_64/include/QtGui"
- - "-IC:/Qt/6.7.3/msvc2022_64/include/QtWidgets"
- - "-std=c++20"
-
diff --git a/libs/installer_fomod_plus/.editorconfig b/libs/installer_fomod_plus/.editorconfig
deleted file mode 100644
index a915e4f..0000000
--- a/libs/installer_fomod_plus/.editorconfig
+++ /dev/null
@@ -1,16 +0,0 @@
-root = true
-
-[*.cpp]
-indent_style = space
-indent_size = 2
-insert_final_newline = true
-
-[*.h]
-indent_style = space
-indent_size = 2
-insert_final_newline = true
-
-[*.ui]
-indent_style = space
-indent_size = 2
-insert_final_newline = true
diff --git a/libs/installer_fomod_plus/CMakeLists.txt b/libs/installer_fomod_plus/CMakeLists.txt
deleted file mode 100644
index 3367145..0000000
--- a/libs/installer_fomod_plus/CMakeLists.txt
+++ /dev/null
@@ -1,36 +0,0 @@
-# mo2-fomod-plus — vendored from github.com/aglowinthefield/mo2-fomod-plus
-# Commit: da6c07ed4bbe235759910c14c2450e1bfe8fe25e (2026-01-27)
-#
-# Pure C++20 FOMOD installer replacement (no .NET dependency).
-# 3 plugin targets: installer, scanner, patch wizard.
-
-include(FetchContent)
-
-# Force pugixml to build as a static library so plugins don't need libpugixml.so at runtime.
-# (The global BUILD_SHARED_LIBS is ON for MO2's own archive lib, which would make pugixml shared.)
-set(BUILD_SHARED_LIBS_SAVED ${BUILD_SHARED_LIBS})
-set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
-
-# Compatibility: sub-CMakeLists call mo2_install_target but Fluorine defines mo2_install_plugin
-if (NOT COMMAND mo2_install_target AND COMMAND mo2_install_plugin)
- function(mo2_install_target target)
- mo2_install_plugin(${target})
- endfunction()
-endif ()
-
-# Compute include dirs from existing targets for sub-CMakeLists that reference these variables
-get_target_property(MO2_UIBASE_INCLUDE_DIRS mo2::uibase INTERFACE_INCLUDE_DIRECTORIES)
-foreach(dir ${MO2_UIBASE_INCLUDE_DIRS})
- list(APPEND MO2_UIBASE_INCLUDE_DIRS "${dir}/uibase" "${dir}/uibase/game_features")
-endforeach()
-get_target_property(MO2_ARCHIVE_INCLUDE_DIRS mo2::archive INTERFACE_INCLUDE_DIRECTORIES)
-foreach(dir ${MO2_ARCHIVE_INCLUDE_DIRS})
- list(APPEND MO2_ARCHIVE_INCLUDE_DIRS "${dir}/archive")
-endforeach()
-
-add_subdirectory(installer)
-add_subdirectory(scanner)
-add_subdirectory(patchwizard)
-
-# Restore BUILD_SHARED_LIBS so subsequent targets aren't affected.
-set(BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS_SAVED} CACHE BOOL "" FORCE)
diff --git a/libs/installer_fomod_plus/cmake/patch_pugixml.cmake b/libs/installer_fomod_plus/cmake/patch_pugixml.cmake
deleted file mode 100644
index 01f1a68..0000000
--- a/libs/installer_fomod_plus/cmake/patch_pugixml.cmake
+++ /dev/null
@@ -1,20 +0,0 @@
-if(NOT DEFINED PUGIXML_CMAKELISTS)
- message(FATAL_ERROR "PUGIXML_CMAKELISTS not set; cannot patch pugixml.")
-endif()
-
-if(NOT EXISTS "${PUGIXML_CMAKELISTS}")
- message(FATAL_ERROR "Pugixml CMakeLists.txt not found at '${PUGIXML_CMAKELISTS}'.")
-endif()
-
-file(READ "${PUGIXML_CMAKELISTS}" _pugixml_content)
-string(REGEX REPLACE
- "cmake_minimum_required\\(VERSION [^)]+\\)"
- "cmake_minimum_required(VERSION 3.10)"
- _pugixml_patched "${_pugixml_content}")
-
-if(_pugixml_content STREQUAL _pugixml_patched)
- message(STATUS "pugixml CMakeLists already uses a modern cmake_minimum_required.")
-else()
- file(WRITE "${PUGIXML_CMAKELISTS}" "${_pugixml_patched}")
- message(STATUS "Updated pugixml cmake_minimum_required to 3.10 to silence deprecation warnings.")
-endif()
diff --git a/libs/installer_fomod_plus/installer/CMakeLists.txt b/libs/installer_fomod_plus/installer/CMakeLists.txt
deleted file mode 100644
index 07a670b..0000000
--- a/libs/installer_fomod_plus/installer/CMakeLists.txt
+++ /dev/null
@@ -1,53 +0,0 @@
-cmake_minimum_required(VERSION 3.16)
-include(FetchContent)
-
-file(GLOB_RECURSE INSTALLER_SOURCES CONFIGURE_DEPENDS
- ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp
- ${CMAKE_CURRENT_SOURCE_DIR}/*.h
- ${CMAKE_CURRENT_SOURCE_DIR}/*.ui
- ${CMAKE_CURRENT_SOURCE_DIR}/*.qrc
-)
-file(GLOB SHARE_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/../share/**/*.cpp")
-
-add_library(fomod_plus_installer SHARED ${INSTALLER_SOURCES} ${SHARE_SOURCES})
-
-target_include_directories(
- fomod_plus_installer
- PUBLIC
- ${CMAKE_CURRENT_SOURCE_DIR}/../share
- ${MO2_UIBASE_INCLUDE_DIRS}
- ${MO2_ARCHIVE_INCLUDE_DIRS}
- ${CMAKE_CURRENT_SOURCE_DIR}
-)
-
-FetchContent_Declare(
- pugixml
- GIT_REPOSITORY https://github.com/zeux/pugixml
- GIT_TAG v1.14
- PATCH_COMMAND ${CMAKE_COMMAND}
- -DPUGIXML_CMAKELISTS=<SOURCE_DIR>/CMakeLists.txt
- -P ${CMAKE_CURRENT_LIST_DIR}/../cmake/patch_pugixml.cmake
-)
-
-FetchContent_Declare(json URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz DOWNLOAD_EXTRACT_TIMESTAMP TRUE)
-FetchContent_MakeAvailable(pugixml json)
-
-set(project_type plugin)
-project(fomod_plus_installer)
-
-target_link_libraries(fomod_plus_installer PRIVATE mo2::uibase pugixml nlohmann_json::nlohmann_json)
-if (WIN32)
- target_link_libraries(fomod_plus_installer PRIVATE dbghelp)
-endif ()
-mo2_configure_plugin(fomod_plus_installer NO_SOURCES WARNINGS OFF PRIVATE_DEPENDS)
-
-if (MSVC)
- target_compile_options(fomod_plus_installer PRIVATE $<$<CONFIG:RelWithDebInfo>:/Zi>)
- target_link_options(fomod_plus_installer PRIVATE $<$<CONFIG:RelWithDebInfo>:/DEBUG>)
- install(FILES $<TARGET_PDB_FILE:fomod_plus_installer>
- DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO}
- CONFIGURATIONS RelWithDebInfo
- OPTIONAL)
-endif ()
-
-mo2_install_target(fomod_plus_installer)
diff --git a/libs/installer_fomod_plus/installer/FomodInstallerWindow.cpp b/libs/installer_fomod_plus/installer/FomodInstallerWindow.cpp
deleted file mode 100644
index bd1c0bc..0000000
--- a/libs/installer_fomod_plus/installer/FomodInstallerWindow.cpp
+++ /dev/null
@@ -1,842 +0,0 @@
-#include "FomodInstallerWindow.h"
-
-#include "ui/FomodImageViewer.h"
-
-#include "ui/ScaleLabel.h"
-#include "ui/UIHelper.h"
-#include "xml/ModuleConfiguration.h"
-
-#include <QButtonGroup>
-#include <QCheckBox>
-#include <QComboBox>
-#include <QCompleter>
-#include <QGroupBox>
-#include <QLabel>
-#include <QRadioButton>
-#include <QScrollArea>
-#include <QScrollBar>
-#include <QSettings>
-#include <QSizePolicy>
-#include <QStandardPaths>
-#include <QSplitter>
-#include <QVBoxLayout>
-#include <utility>
-
-#include "ui/FomodViewModel.h"
-
-#include <unordered_set>
-
-/**
- *
- * @param installer
- * @param modName
- * @param tree
- * @param fomodPath
- * @param viewModel
- * @param fomodJson
- * @param parent
- */
-FomodInstallerWindow::FomodInstallerWindow(
- FomodPlusInstaller* installer,
- GuessedValue<QString>& modName,
- const std::shared_ptr<IFileTree>& tree,
- QString fomodPath,
- const std::shared_ptr<FomodViewModel>& viewModel,
- const nlohmann::json& fomodJson,
- QWidget* parent): QDialog(parent),
- mInstaller(installer),
- mFomodPath(std::move(fomodPath)),
- mModName(modName),
- mTree(tree),
- mViewModel(viewModel),
- mFomodJson(fomodJson)
-{
- setupUi();
-
- mInstallStepStack = new QStackedWidget(this);
-
- // Handle legacy FOMODs with no steps
- if (mViewModel->getSteps().empty()) {
- // Create a simple "Install" widget for legacy FOMODs
- auto* legacyWidget = new QWidget(this);
- auto* layout = new QVBoxLayout(legacyWidget);
- auto* label = new QLabel("This mod will install all files automatically.", legacyWidget);
- layout->addWidget(label);
- mInstallStepStack->addWidget(legacyWidget);
- } else {
- updateInstallStepStack();
- stylePreviouslySelectedOptions();
- stylePreviouslyDeselectedOptions();
- }
-
- const auto containerLayout = createContainerLayout();
- setLayout(containerLayout);
-
- updateButtons();
- restoreGeometryAndState();
-
- if (!mViewModel->getSteps().empty()) {
- populatePluginMap();
- if (mInstaller->shouldAutoRestoreChoices()) {
- onSelectPreviousClicked();
- }
- } else {
- // For empty FOMODs, set default description
- mDescriptionBox->setText("This mod will install all files automatically.");
- }
-}
-
-void FomodInstallerWindow::closeEvent(QCloseEvent* event)
-{
- saveGeometryAndState();
- QDialog::closeEvent(event);
-}
-
-void FomodInstallerWindow::saveGeometryAndState() const
-{
- const auto configDir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
- QDir().mkpath(configDir);
- QSettings settings(configDir + "/fomod-plus-settings.ini", QSettings::IniFormat);
- settings.setValue("windowGeometry", saveGeometry());
- settings.setValue("centerSplitState", mCenterRow->saveState());
- settings.setValue("leftSplitState", mLeftPane->saveState());
-}
-
-void FomodInstallerWindow::restoreGeometryAndState()
-{
- const auto configDir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
- const QSettings settings(configDir + "/fomod-plus-settings.ini", QSettings::IniFormat);
- restoreGeometry(settings.value("windowGeometry").toByteArray());
- mCenterRow->restoreState(settings.value("centerSplitState").toByteArray());
- mLeftPane->restoreState(settings.value("leftSplitState").toByteArray());
-}
-
-void FomodInstallerWindow::populatePluginMap()
-{
- const auto checkboxes = findChildren<QCheckBox*>();
- const auto radioButtons = findChildren<QRadioButton*>();
-
- for (const auto& step : mViewModel->getSteps()) {
- for (const auto& group : step->getGroups()) {
- for (const auto& plugin : group->getPlugins()) {
-
- const auto name = createObjectName(plugin, group);
-
- for (auto* checkbox : checkboxes) {
- if (checkbox->objectName() == name) {
- mPluginMap.insert({ name, { plugin, checkbox } });
- }
- }
- for (auto* radioButton : radioButtons) {
- if (radioButton->objectName() == name) {
- mPluginMap.insert({ name, { plugin, radioButton } });
- }
- }
- }
- }
- }
-}
-
-void FomodInstallerWindow::onNextClicked()
-{
- // For legacy FOMODs with no steps, always install
- if (mViewModel->getSteps().empty()) {
- onInstallClicked();
- return;
- }
-
- if (!mViewModel->isLastVisibleStep()) {
- mViewModel->stepForward();
- mInstallStepStack->setCurrentIndex(mViewModel->getCurrentStepIndex());
- updateButtons();
- updateDisplayForActivePlugin();
- } else {
- onInstallClicked();
- }
-}
-
-void FomodInstallerWindow::updateCheckboxStates() const
-{
- // PluginMap presumed to be populated
- for (const auto& [objectName, pluginData] : mPluginMap) {
- if (pluginData.plugin->isSelected() != pluginData.uiElement->isChecked()) {
- const auto widgetType = pluginData.uiElement->metaObject()->className();
- if (objectName != nullptr) {
- logMessage(DEBUG,
- "Updating " + objectName.toStdString() + " to state: " + (pluginData.plugin->isSelected()
- ? "TRUE"
- : "FALSE") + " because " + widgetType +
- " selection state is " + (pluginData.uiElement->isChecked() ? "TRUE" : "FALSE"));
- }
- pluginData.uiElement->setChecked(pluginData.plugin->isSelected());
- }
-
- if (pluginData.plugin->isEnabled() != pluginData.uiElement->isEnabled()) {
- logMessage(DEBUG, "[WINDOW] Changing enabled state of element " + objectName.toStdString() + " to " +
- (pluginData.plugin->isEnabled() ? "TRUE" : "FALSE"));
- pluginData.uiElement->setEnabled(pluginData.plugin->isEnabled());
- }
- }
-}
-
-void FomodInstallerWindow::onPluginToggled(const bool selected, const std::shared_ptr<GroupViewModel>& group,
- const std::shared_ptr<PluginViewModel>& plugin) const
-{
- logMessage(INFO,
- "onPluginToggled called with " + plugin->getName() + " in " + group->getName() + ": " +
- std::to_string(selected));
- if (mViewModel->togglePlugin(group, plugin, selected)) {
- updateCheckboxStates();
- }
- if (mNextInstallButton != nullptr) {
- updateButtons();
- }
-}
-
-void FomodInstallerWindow::onPluginManuallyUnchecked(const std::shared_ptr<PluginViewModel>& plugin) const
-{
- logMessage(INFO, "onPluginManuallyUnchecked called with " + plugin->getName());
- mViewModel->markManuallySet(plugin);
-}
-
-void FomodInstallerWindow::onPluginHovered(const std::shared_ptr<PluginViewModel>& plugin) const
-{
- mViewModel->setActivePlugin(plugin);
- updateDisplayForActivePlugin();
-}
-
-void FomodInstallerWindow::onBackClicked() const
-{
- mViewModel->stepBack();
- mInstallStepStack->setCurrentIndex(mViewModel->getCurrentStepIndex());
- updateButtons();
- updateDisplayForActivePlugin();
-}
-
-void FomodInstallerWindow::onInstallClicked()
-{
- saveGeometryAndState();
-
- logMessage(DEBUG, "Installing mod: " + mModName->toStdString());
- mModName.update(mModNameInput->currentText(), GUESS_USER);
- mViewModel->preinstall(mTree, mFomodPath);
- // now the installer is available in the outer scope
- // the outer scope should call getFileInstaller() and install there.
- this->accept();
-}
-
-void FomodInstallerWindow::updateButtons() const
-{
- // For legacy FOMODs with no steps, always show Install
- if (mViewModel->getSteps().empty()) {
- mBackButton->setEnabled(false);
- mNextInstallButton->setText(tr("Install"));
- return;
- }
-
- if (mViewModel->isFirstVisibleStep()) {
- mBackButton->setEnabled(false);
- } else {
- mBackButton->setEnabled(true);
- }
-
- if (mViewModel->isLastVisibleStep()) {
- mNextInstallButton->setText(tr("Install"));
- } else {
- mNextInstallButton->setText(tr("Next"));
- }
-}
-
-void FomodInstallerWindow::setupUi()
-{
- setWindowIcon(QIcon(":/fomod/hat"));
- setWindowFlags(Qt::Window); // Allows OS-controlled resizing, including snapping
- setMinimumSize(UiConstants::WINDOW_MIN_WIDTH, UiConstants::WINDOW_MIN_HEIGHT);
- setWindowTitle(mModName);
- setWindowModality(Qt::NonModal); // To allow scrolling modlist without closing the window
-}
-
-// mInstallStepStack must be initialized before calling this
-void FomodInstallerWindow::updateInstallStepStack()
-{
- if (!mInstallStepStack) {
- logMessage(ERR, "updateInstallStepStack called with no initialized mInstallStepStack. tf?");
- return;
- }
- // Create the widgets for each step. Not sure if we need these as member variables. Try it like this for now.
- for (const auto& steps = mViewModel->getSteps(); const auto& installStep : steps) {
- mInstallStepStack->addWidget(createStepWidget(installStep));
- }
- mInstallStepStack->setCurrentIndex(mViewModel->getCurrentStepIndex());
-}
-
-/*
-+-------------------------------------------------------------------+
-| +----------------------------------------------------------------+|
-| | ||
-| | Metadata and Name Input ||
-| | ||
-| +----------------------------------------------------------------+|
-| +------------------------------++--------------------------------+|
-| | || ||
-| | || ||
-| | Description || ||
-| | || ||
-| | || Step/Group/Plugins ||
-| | || ||
-| +------------------------------+| ||
-| +------------------------------+| ||
-| | || ||
-| | || ||
-| | Image || ||
-| | || ||
-| | || ||
-| | || ||
-| +----------------------------------------------------------------+|
-| | ||
-| | Bottom Bar ||
-| +----------------------------------------------------------------+|
-+-------------------------------------------------------------------+
-*/
-QBoxLayout* FomodInstallerWindow::createContainerLayout()
-{
- const auto layout = new QVBoxLayout(this);
-
- mTopRow = createTopRow();
- mCenterRow = createCenterRow();
- mBottomRow = createBottomRow();
-
- layout->addWidget(mTopRow);
- layout->addWidget(mCenterRow, 1); // stretch 1 here so the others are static size
- layout->addWidget(mBottomRow);
-
- if (mInstaller->shouldShowNotifications()) {
- mNotificationsPanel = createNotificationPanel();
- layout->addWidget(mNotificationsPanel);
- // Set a default welcome message
- addNotification("FOMOD Plus notification panel initialized :)", INFO);
- }
- return layout;
-}
-
-QSplitter* FomodInstallerWindow::createCenterRow()
-{
- mLeftPane = createLeftPane(); // Instance var to persist the geometry
- const auto centerRow = new QSplitter(Qt::Horizontal, this);
- const auto rightPane = createRightPane();
- centerRow->addWidget(mLeftPane);
- centerRow->addWidget(rightPane);
- centerRow->setSizes({ width() / 2, width() / 2 });
- return centerRow;
-}
-
-QWidget* FomodInstallerWindow::createTopRow()
-{
- const auto topRow = new QWidget(this);
-
- auto* mainHLayout = new QHBoxLayout(topRow);
-
- // Holds the name (label), author, version, and website
- auto* metadataLayout = new QHBoxLayout();
-
- // left side metadata. just the titles of the metadata
- auto* labelsColumn = new QVBoxLayout();
- QLabel* nameLabel = UIHelper::createLabel(tr("Name:"), topRow);
- QLabel* authorLabel = UIHelper::createLabel(tr("Author:"), topRow);
- QLabel* versionLabel = UIHelper::createLabel(tr("Version:"), topRow);
- QLabel* websiteLabel = UIHelper::createLabel(tr("Website:"), topRow);
-
- labelsColumn->addWidget(nameLabel);
- labelsColumn->addWidget(authorLabel);
- labelsColumn->addWidget(versionLabel);
- labelsColumn->addWidget(websiteLabel);
-
- UIHelper::reduceLabelPadding(labelsColumn);
- UIHelper::setGlobalAlignment(labelsColumn, Qt::AlignTop);
-
- // the values of the metadata MINUS the search box
- auto* valuesColumn = new QVBoxLayout();
- QLabel* emptyLabel = UIHelper::createLabel("", topRow);
- QLabel* authorValueLabel = UIHelper::createLabel(mViewModel->getInfoViewModel()->getAuthor().c_str(), topRow);
- QLabel* versionValueLabel = UIHelper::createLabel(mViewModel->getInfoViewModel()->getVersion().c_str(), topRow);
- QLabel* websiteValueLabel = UIHelper::createHyperlink(mViewModel->getInfoViewModel()->getWebsite().c_str(), topRow);
-
- valuesColumn->addWidget(emptyLabel);
- valuesColumn->addWidget(authorValueLabel);
- valuesColumn->addWidget(versionValueLabel);
- valuesColumn->addWidget(websiteValueLabel);
-
- // We want these cleanup fns to be at the layout level directly containing the labels.
- // Since we aren't recursing down the UI forever we can't just call it for mainHLayout.
- UIHelper::reduceLabelPadding(valuesColumn);
- UIHelper::setGlobalAlignment(valuesColumn, Qt::AlignTop);
-
- metadataLayout->addLayout(labelsColumn);
- metadataLayout->addLayout(valuesColumn, 1); // To push the right column close to the edge of the left.
-
- mainHLayout->addLayout(metadataLayout, 1);
-
- // Now make the search bar layout
- auto* modNameComboBox = createModNameComboBox();
- mainHLayout->addWidget(modNameComboBox, 4);
- UIHelper::setGlobalAlignment(mainHLayout, Qt::AlignTop);
-
- // Extra stuff
- topRow->setLayout(mainHLayout);
- topRow->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
- return topRow;
-}
-
-QComboBox* FomodInstallerWindow::createModNameComboBox()
-{
- mModNameInput = new QComboBox(this);
- mModNameInput->setEditable(true);
-
- mModNameInput->addItem(mModName);
-
- for (const auto& variant : mModName.variants()) {
- if (variant.toStdString() != mModName->toStdString()) {
- mModNameInput->addItem(variant);
- }
- }
- mModNameInput->completer()->setCaseSensitivity(Qt::CaseSensitive);
- return mModNameInput;
-}
-
-QWidget* FomodInstallerWindow::createBottomRow()
-{
- // In vanilla FOMOD installer, left has the Manual button, right has back, next/install, and cancel buttons
- const auto bottomRow = new QWidget(this);
- auto* layout = new QHBoxLayout(bottomRow);
-
- // Manual on far left
- mManualButton = UIHelper::createButton(tr("Manual"), bottomRow);
- mSelectPreviousButton = UIHelper::createButton(tr("Restore Previous Choices"), bottomRow);
- mResetChoicesButton = UIHelper::createButton(tr("Reset Choices"), bottomRow);
-
- layout->addWidget(mManualButton);
- layout->addWidget(mSelectPreviousButton);
- layout->addWidget(mResetChoicesButton);
-
- // Space to push remaining buttons right
- layout->addStretch();
-
- mBackButton = UIHelper::createButton(tr("Back"), bottomRow);
- mNextInstallButton = UIHelper::createButton(tr("Next"), bottomRow);
- mCancelButton = UIHelper::createButton(tr("Cancel"), bottomRow);
-
- mNextInstallButton->setDefault(true);
- mNextInstallButton->setAutoDefault(true);
-
- connect(mManualButton, SIGNAL(clicked()), this, SLOT(onManualClicked()));
- connect(mNextInstallButton, SIGNAL(clicked()), this, SLOT(onNextClicked()));
- connect(mBackButton, SIGNAL(clicked()), this, SLOT(onBackClicked()));
- connect(mCancelButton, SIGNAL(clicked()), this, SLOT(onCancelClicked()));
- connect(mSelectPreviousButton, SIGNAL(clicked()), this, SLOT(onSelectPreviousClicked()));
- connect(mResetChoicesButton, SIGNAL(clicked()), this, SLOT(onResetChoicesClicked()));
- // connect(mHideImagesButton, SIGNAL(clicked()), this, SLOT(toggleImagesShown()));
-
- layout->addWidget(mBackButton);
- layout->addWidget(mNextInstallButton);
- layout->addWidget(mCancelButton);
-
- bottomRow->setLayout(layout);
- return bottomRow;
-}
-
-QSplitter* FomodInstallerWindow::createLeftPane()
-{
- const auto leftPane = new QSplitter(Qt::Vertical, this);
-
- // Add description box
- // Initialize with defaults (the first plugin's description (which defaults to the module image otherwise))
- auto* scrollArea = new QScrollArea(leftPane);
- scrollArea->setWidgetResizable(true);
-
- mDescriptionBox = new QLabel("", leftPane);
- mDescriptionBox->setTextFormat(Qt::RichText);
- mDescriptionBox->setTextInteractionFlags(Qt::TextBrowserInteraction);
- mDescriptionBox->setOpenExternalLinks(true);
- mDescriptionBox->setWordWrap(true);
- mDescriptionBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
- mDescriptionBox->setAlignment(Qt::AlignTop | Qt::AlignLeft);
-
- scrollArea->setWidget(mDescriptionBox);
- leftPane->addWidget(scrollArea);
-
- // Add image
- // Initialize with defaults (the first plugin's image)
- mImageLabel = new ScaleLabel(leftPane);
- mImageLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
-
- connect(mImageLabel, &ScaleLabel::clicked, this, [this] {
- if (!mImageLabel->hasResource()) {
- return;
- }
- const auto viewer = new FomodImageViewer(this, mFomodPath, mViewModel->getActiveStep(),
- mViewModel->getActivePlugin());
- viewer->showMaximized();
- });
-
- if (!mInstaller->shouldShowImages()) {
- mImageLabel->hide();
- }
-
- leftPane->addWidget(mImageLabel);
- leftPane->setSizes({ height() / 2, height() / 2 });
-
- updateDisplayForActivePlugin();
-
- return leftPane;
-}
-
-QWidget* FomodInstallerWindow::createRightPane()
-{
- const auto rightPane = new QWidget(this);
- auto* layout = new QVBoxLayout(rightPane);
-
- layout->addWidget(mInstallStepStack);
-
- return rightPane;
-}
-
-QTextEdit* FomodInstallerWindow::createNotificationPanel()
-{
- auto* panel = new QTextEdit(this);
- panel->setReadOnly(true);
- panel->setMaximumHeight(100); // Limit height
- panel->setStyleSheet("font-family: monospace; font-size: 9pt;");
-
- return panel;
-}
-
-QWidget* FomodInstallerWindow::createStepWidget(const std::shared_ptr<StepViewModel>& installStep)
-{
- const auto stepBox = new QGroupBox(QString::fromStdString(installStep->getName()), this);
- const auto stepBoxLayout = new QVBoxLayout(stepBox);
-
- auto* scrollArea = new QScrollArea(stepBox);
- scrollArea->setWidgetResizable(true);
-
- const auto scrollAreaContent = new QWidget(scrollArea);
- auto* scrollAreaLayout = new QVBoxLayout(scrollAreaContent);
-
- for (const auto& group : installStep->getGroups()) {
- const auto groupSection = renderGroup(group);
- scrollAreaLayout->addWidget(groupSection);
- }
-
- scrollAreaContent->setLayout(scrollAreaLayout);
- scrollArea->setWidget(scrollAreaContent);
-
- stepBoxLayout->addWidget(scrollArea);
- stepBox->setLayout(stepBoxLayout);
- return stepBox;
-}
-
-QWidget* FomodInstallerWindow::renderGroup(const std::shared_ptr<GroupViewModel>& group)
-{
- const auto groupBox = new QGroupBox(QString::fromStdString(group->getName()), this);
- const auto groupBoxLayout = new QVBoxLayout(groupBox);
-
- switch (group->getType()) {
- case SelectAtLeastOne:
- case SelectAny:
- case SelectAll:
- renderCheckboxGroup(groupBox, groupBoxLayout, group);
- break;
- case SelectExactlyOne:
- case SelectAtMostOne:
- renderSelectExactlyOne(groupBox, groupBoxLayout, group);
- break;
- default: ;
- }
-
- groupBox->setLayout(groupBoxLayout);
- return groupBox;
-}
-
-QString FomodInstallerWindow::createObjectName(const std::shared_ptr<PluginViewModel>& plugin,
- const std::shared_ptr<GroupViewModel>& group)
-{
- const std::string objectName = std::format("[{}:{}] {}-{}",
- group->getStepIndex(), group->getOwnIndex(),
- group->getName(), plugin->getName());
-
- return QString::fromStdString(objectName);
-}
-
-QRadioButton* FomodInstallerWindow::createPluginRadioButton(const std::shared_ptr<PluginViewModel>& plugin,
- const std::shared_ptr<GroupViewModel>& group,
- QWidget* parent)
-{
- auto* radioButton = new QRadioButton(QString::fromStdString(plugin->getName()), parent);
- radioButton->setObjectName(createObjectName(plugin, group));
- auto* hoverFilter = new HoverEventFilter(plugin, this);
- radioButton->installEventFilter(hoverFilter);
- connect(hoverFilter, &HoverEventFilter::hovered, this, &FomodInstallerWindow::onPluginHovered);
-
- connect(radioButton, &QRadioButton::toggled, this, [this, radioButton, group, plugin](const bool checked) {
- logMessage(INFO,
- "Received toggled signal for radio: " + plugin->getName() + ": " + (checked ? "TRUE" : "FALSE") +
- " Radio is now: " + (radioButton->isChecked() ? "TRUE" : "FALSE"));
- onPluginToggled(checked, group, plugin);
- });
-
- radioButton->setEnabled(plugin->isEnabled());
- radioButton->setChecked(plugin->isSelected());
- return radioButton;
-}
-
-QCheckBox* FomodInstallerWindow::createPluginCheckBox(const std::shared_ptr<PluginViewModel>& plugin,
- const std::shared_ptr<GroupViewModel>& group, QWidget* parent)
-{
- auto* checkBox = new QCheckBox(QString::fromStdString(plugin->getName()), parent);
- checkBox->setObjectName(createObjectName(plugin, group));
-
- // Make the hover stuff work
- auto* hoverFilter = new HoverEventFilter(plugin, this);
- checkBox->installEventFilter(hoverFilter);
- connect(hoverFilter, &HoverEventFilter::hovered, this, &FomodInstallerWindow::onPluginHovered);
-
- // Install Ctrl+click event filter
- auto* ctrlClickFilter = new CtrlClickEventFilter(plugin, group, this);
- checkBox->installEventFilter(ctrlClickFilter);
-
- checkBox->setEnabled(plugin->isEnabled());
- checkBox->setChecked(plugin->isSelected());
- connect(checkBox, &QCheckBox::clicked, this, [this, plugin](const bool checked) {
- // Send a message to viewModel saying the user deactivated the plugin manually.
- // This may get overridden later by automatic checking, but we'll reconcile that at JSON serialization time.
- if (!checked) {
- onPluginManuallyUnchecked(plugin);
- }
- });
- connect(checkBox, &QCheckBox::toggled, this, [this, checkBox, group, plugin](const bool checked) {
- logMessage(INFO,
- "Received toggled signal for checkbox: " + plugin->getName() + ": " + (checked ? "true" : "false") +
- " Checkbox was previously " + (checkBox->isChecked() ? "true" : "false"));
- onPluginToggled(checked, group, plugin);
- });
- return checkBox;
-}
-
-void FomodInstallerWindow::renderSelectExactlyOne(QWidget* parent, QLayout* parentLayout,
- const std::shared_ptr<GroupViewModel>& group)
-{
- // This is for parity with the legacy installer. Both styles are functionally equivalent
- // for a group size of 1, but they chose checkbox.
- if (group->getPlugins().size() == 1) {
- renderCheckboxGroup(parent, parentLayout, group);
- } else {
- renderRadioGroup(parent, parentLayout, group);
- }
-}
-
-void FomodInstallerWindow::renderCheckboxGroup(QWidget* parent, QLayout* parentLayout,
- const std::shared_ptr<GroupViewModel>& group)
-{
- for (const auto& plugin : group->getPlugins()) {
- auto* checkbox = createPluginCheckBox(plugin, group, parent);
- parentLayout->addWidget(checkbox);
- }
-}
-
-QButtonGroup* FomodInstallerWindow::renderRadioGroup(QWidget* parent, QLayout* parentLayout,
- const std::shared_ptr<GroupViewModel>& group)
-{
- auto* buttonGroup = new QButtonGroup(parent);
- buttonGroup->setExclusive(true); // Ensure only one button can be selected
-
- for (const auto& plugin : group->getPlugins()) {
- auto* radioButton = createPluginRadioButton(plugin, group, parent);
- buttonGroup->addButton(radioButton);
- parentLayout->addWidget(radioButton);
- }
- return buttonGroup;
-}
-
-void FomodInstallerWindow::addNotification(const QString& message, const LogLevel level) const
-{
- if (!mNotificationsPanel) {
- return;
- }
-
- const QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss");
- const QString formattedMsg = QString("<span>[%2] [%3] %4</span>")
- .arg(timestamp).arg(logLevelToString(level)).arg(message);
-
- mNotificationsPanel->append(formattedMsg);
-
- // Auto-scroll to bottom
- QScrollBar* scrollbar = mNotificationsPanel->verticalScrollBar();
- scrollbar->setValue(scrollbar->maximum());
-}
-
-[[deprecated]]
-void FomodInstallerWindow::toggleImagesShown() const
-{
- logMessage(DEBUG, "Toggling image visibility");
- mInstaller->toggleShouldShowImages();
- if (mInstaller->shouldShowImages()) {
- logMessage(DEBUG, "Turning images ON");
- mImageLabel->show();
- mHideImagesButton->setText(tr("Hide Images"));
- } else {
- logMessage(DEBUG, "Turning images OFF");
- mImageLabel->hide();
- mHideImagesButton->setText(tr("Show Images"));
- }
-}
-
-
-// Updates the image and description field for a given plugin. Also use this on initialization of those widgets.
-void FomodInstallerWindow::updateDisplayForActivePlugin() const
-{
- // Skip if no steps (legacy FOMOD)
- if (mViewModel->getSteps().empty()) {
- return;
- }
-
- auto plugin = mViewModel->getActivePlugin();
- if (!plugin) {
- const auto activeStep = mViewModel->getActiveStep();
- if (!activeStep || activeStep->getGroups().empty() || activeStep->getGroups().front()->getPlugins().empty()) {
- mDescriptionBox->setText(tr("Select a plugin to see its description."));
- mImageLabel->clear();
- return;
- }
- // Fall back to the first plugin in the active step when no active plugin is set.
- plugin = activeStep->getGroups().front()->getPlugins().front();
- mViewModel->setActivePlugin(plugin);
- }
-
- const QString description = formatPluginDescription(QString::fromStdString(plugin->getDescription()));
- mDescriptionBox->setText(description);
-
- const auto image = mViewModel->getDisplayImage();
- if (image.empty()) {
- mImageLabel->clear();
- return;
- }
-
- const auto imagePath = UIHelper::getFullImagePath(mFomodPath, QString::fromStdString(image));
- mImageLabel->setScalableResource(imagePath);
-}
-
-/**
- *
- * @param pluginSelector For now either 'plugins', or 'deselected'. The key of the member of 'groups' to iterate over.
- * @param fn The callback for each plugin in the chosen group member.
- */
-void FomodInstallerWindow::applyFnFromJson(const std::string& pluginSelector,
- const std::function<void(QAbstractButton*)>& fn)
-{
- if (mFomodJson.empty()) {
- return;
- }
-
- const auto jsonSteps = mFomodJson["steps"];
- // for each step in JSON, create a <group>-<plugin> string out of the { groups: [ { plugins... } ] } array
- vector<std::string> selectedPlugins;
-
- // TODO: Can groups have the same name within a step, or across steps? How do we account for that?
- for (int stepIndex = 0; stepIndex < jsonSteps.size(); ++stepIndex) {
- const auto& step = jsonSteps[stepIndex];
- for (int groupIndex = 0; groupIndex < step["groups"].size(); ++groupIndex) {
- const auto& group = step["groups"][groupIndex];
-
- if (!group.contains(pluginSelector)) {
- continue;
- }
-
- for (int pluginIndex = 0; pluginIndex < group[pluginSelector].size(); ++pluginIndex) {
- const auto& plugin = group[pluginSelector][pluginIndex];
-
- std::string name = std::format("[{}:{}] {}-{}",
- stepIndex, groupIndex, group["name"].get<std::string>(), plugin.get<std::string>());
- selectedPlugins.push_back(name);
- }
- }
- }
-
- const auto checkboxes = findChildren<QCheckBox*>();
- const auto radioButtons = findChildren<QRadioButton*>();
-
- for (auto* checkbox : checkboxes) {
- for (const auto& selectedPlugin : selectedPlugins) {
- if (checkbox->objectName().toStdString() == selectedPlugin) {
- fn(checkbox);
- }
- }
- }
- for (auto* radio : radioButtons) {
- for (const auto& selectedPlugin : selectedPlugins) {
- if (radio->objectName().toStdString() == selectedPlugin) {
- fn(radio);
- }
- }
- }
-}
-
-void FomodInstallerWindow::stylePreviouslySelectedOptions()
-{
- const auto stylesheet = getColorStyle(UiColors::ColorApplication::BACKGROUND);
-
- const auto tooltip = "You previously selected this plugin when installing this mod.";
-
- logMessage(INFO, "Styling previously selected choices with stylesheet " + stylesheet.toStdString(), true);
- applyFnFromJson("plugins", [stylesheet, tooltip](QAbstractButton* button) {
- button->setStyleSheet(stylesheet);
- button->setToolTip(tooltip);
- });
-}
-
-void FomodInstallerWindow::stylePreviouslyDeselectedOptions()
-{
- const auto stylesheet = getColorStyle(UiColors::ColorApplication::BORDER);
- const auto tooltip = "You previously unchecked this plugin when installing this mod.";
- applyFnFromJson("deselected", [stylesheet, tooltip](QAbstractButton* button) {
- button->setStyleSheet(stylesheet);
- button->setToolTip(tooltip);
- });
-}
-
-void FomodInstallerWindow::selectPreviouslySelectedOptions() const
-{
- logMessage(INFO, "Selecting previously selected choices", true);
- logMessage(INFO, "Existing JSON provided: " + mFomodJson.dump(4));
- if (mFomodJson.empty()) {
- return;
- }
- try {
- mViewModel->selectFromJson(mFomodJson);
- } catch (Exception& e) {
- logMessage(ERR, std::string("Error selecting previously selected options: ") + e.what(), true);
- }
- updateCheckboxStates();
-}
-
-void FomodInstallerWindow::onResetChoicesClicked()
-{
- logMessage(INFO, "Resetting choices to author defaults", true);
- try {
- mViewModel->resetToDefaults();
- } catch (Exception& e) {
- logMessage(ERR, std::string("Error resetting choices: ") + e.what(), true);
- return;
- }
-
- // Reset the UI to show the first step
- mInstallStepStack->setCurrentIndex(mViewModel->getCurrentStepIndex());
- updateCheckboxStates();
- updateButtons();
- updateDisplayForActivePlugin();
-}
-
-QString FomodInstallerWindow::getColorStyle(const UiColors::ColorApplication color_application) const
-{
- const auto selectedColor = mInstaller->getSelectedColor();
- logMessage(DEBUG, "Selected color: " + selectedColor.toStdString());
- return getStyle(selectedColor, color_application);
-}
diff --git a/libs/installer_fomod_plus/installer/FomodInstallerWindow.h b/libs/installer_fomod_plus/installer/FomodInstallerWindow.h
deleted file mode 100644
index bca2215..0000000
--- a/libs/installer_fomod_plus/installer/FomodInstallerWindow.h
+++ /dev/null
@@ -1,214 +0,0 @@
-#ifndef FOMODINSTALLERWINDOW_H
-#define FOMODINSTALLERWINDOW_H
-
-#include <qboxlayout.h>
-#include <qbuttongroup.h>
-#include <qcheckbox.h>
-#include <qcombobox.h>
-#include <qtextedit.h>
-
-#include "FomodPlusInstaller.h"
-#include "xml/ModuleConfiguration.h"
-
-#include <QDialog>
-#include <QStackedWidget>
-#include <qradiobutton.h>
-#include <ui/ScaleLabel.h>
-
-#include "FomodInstallerWindow.h"
-#include "lib/FileInstaller.h"
-#include "ui/FomodViewModel.h"
-#include "ui/Colors.h"
-
-#include <QSplitter>
-
-using namespace MOBase;
-
-struct PluginData {
- std::shared_ptr<PluginViewModel> plugin;
- QAbstractButton* uiElement;
-};
-
-
-class FomodPlusInstaller;
-/**
- * @class FomodInstallerWindow
- * @brief This class represents a window for the FOMOD installer.
- *
- * The FomodInstallerWindow class is designed to handle and manage the FOMOD installation
- * process. It integrates functionalities specific to FOMOD package installations.
- * By inheriting from QObject, it supports signal-slot mechanisms, enabling interaction
- * and communication with other components in the application.
- *
- * This class is primarily intended to provide a user interface and functionality
- * to process and run the FOMOD installer in a structured manner.
- */
-class FomodInstallerWindow final : public QDialog {
- Q_OBJECT
-
-public:
- FomodInstallerWindow(FomodPlusInstaller* installer,
- GuessedValue<QString>& modName,
- const std::shared_ptr<IFileTree>& tree,
- QString fomodPath,
- const std::shared_ptr<FomodViewModel>& viewModel,
- const nlohmann::json& fomodJson,
- QWidget* parent = nullptr);
-
- void closeEvent(QCloseEvent* event) override;
-
- void saveGeometryAndState() const;
-
- void restoreGeometryAndState();
-
- void populatePluginMap();
-
-
- // So FomodPlusInstaller can check if the user wants to manually install
- [[nodiscard]] bool isManualInstall() const
- {
- return mIsManualInstall;
- }
-
- [[nodiscard]] std::shared_ptr<FileInstaller> getFileInstaller() const { return mViewModel->getFileInstaller(); }
-
-private slots:
- void onNextClicked();
-
- void updateCheckboxStates() const;
-
- void onPluginToggled(bool selected, const std::shared_ptr<GroupViewModel>& group,
- const std::shared_ptr<PluginViewModel>& plugin) const;
-
- void onPluginManuallyUnchecked(const std::shared_ptr<PluginViewModel>& plugin) const;
-
- void onPluginHovered(const std::shared_ptr<PluginViewModel>& plugin) const;
-
- void onSelectPreviousClicked() const { this->selectPreviouslySelectedOptions(); }
-
- void onResetChoicesClicked();
-
- void onBackClicked() const;
-
- void onCancelClicked()
- {
- this->saveGeometryAndState();
- this->reject();
- }
-
- void onManualClicked()
- {
- mIsManualInstall = true;
- this->saveGeometryAndState();
- this->reject();
- }
-
- void onInstallClicked();
-
- [[deprecated]] void toggleImagesShown() const;
-
-private:
- Logger& log = Logger::getInstance();
- FomodPlusInstaller* mInstaller;
- QString mFomodPath;
- GuessedValue<QString>& mModName;
- std::shared_ptr<IFileTree> mTree;
- std::shared_ptr<FomodViewModel> mViewModel;
- bool mInitialized{ false };
- std::unordered_map<QString, PluginData> mPluginMap;
-
-
- // Meta
- bool mIsManualInstall{};
- nlohmann::json mFomodJson;
-
- // Buttons
- QPushButton* mNextInstallButton{};
- QPushButton* mBackButton{};
- QPushButton* mCancelButton{};
- QPushButton* mManualButton{};
- QPushButton* mSelectPreviousButton{};
- QPushButton* mResetChoicesButton{};
- QPushButton* mHideImagesButton{};
-
- // Widgets
- QComboBox* mModNameInput{};
- QLabel* mDescriptionBox{};
- QSplitter* mCenterRow{};
- QSplitter* mLeftPane{};
- QStackedWidget* mInstallStepStack{};
- QTextEdit* mNotificationsPanel{};
- QWidget* mBottomRow{};
- QWidget* mTopRow{};
- ScaleLabel* mImageLabel{};
-
- // Fn
- void setupUi();
-
- void updateButtons() const;
-
- void updateInstallStepStack();
-
- void updateDisplayForActivePlugin() const;
-
- void applyFnFromJson(const std::string& pluginSelector, const std::function<void(QAbstractButton*)> &fn);
-
- void stylePreviouslySelectedOptions();
-
- void stylePreviouslyDeselectedOptions();
-
- void selectPreviouslySelectedOptions() const;
-
- [[nodiscard]] QString getColorStyle(UiColors::ColorApplication color_application) const;
-
- [[nodiscard]] QBoxLayout* createContainerLayout();
-
- [[nodiscard]] QSplitter* createCenterRow();
-
- [[nodiscard]] QWidget* createTopRow();
-
- [[nodiscard]] QComboBox* createModNameComboBox();
-
- [[nodiscard]] QWidget* createBottomRow();
-
- [[nodiscard]] QSplitter* createLeftPane();
-
- [[nodiscard]] QWidget* createRightPane();
-
- [[nodiscard]] QTextEdit* createNotificationPanel();
-
- [[nodiscard]] QWidget* createStepWidget(const std::shared_ptr<StepViewModel>& installStep);
-
- [[nodiscard]] QWidget* renderGroup(const std::shared_ptr<GroupViewModel>& group);
-
- static QString createObjectName(const std::shared_ptr<PluginViewModel>& plugin,
- const std::shared_ptr<GroupViewModel>& group);
-
- QRadioButton* createPluginRadioButton(const std::shared_ptr<PluginViewModel>& plugin,
- const std::shared_ptr<GroupViewModel>& group, QWidget* parent);
-
- QCheckBox* createPluginCheckBox(const std::shared_ptr<PluginViewModel>& plugin,
- const std::shared_ptr<GroupViewModel>& group, QWidget* parent);
-
- void renderSelectExactlyOne(QWidget* parent, QLayout* parentLayout, const std::shared_ptr<GroupViewModel>& group);
-
- void renderCheckboxGroup(QWidget* parent, QLayout* parentLayout,
- const std::shared_ptr<GroupViewModel>& group);
-
- QButtonGroup* renderRadioGroup(QWidget* parent, QLayout* parentLayout,
- const std::shared_ptr<GroupViewModel>& group);
-
- void addNotification(const QString& message, LogLevel level) const;
-
- void logMessage(const LogLevel level, const std::string& message, const bool asNotification = true) const
- {
- log.logMessage(level, "[WINDOW] " + message);
- if (asNotification) {
- addNotification(QString::fromStdString(message), level);
- }
- }
-
-};
-
-
-#endif //FOMODINSTALLERWINDOW_H
diff --git a/libs/installer_fomod_plus/installer/FomodPlusInstaller.cpp b/libs/installer_fomod_plus/installer/FomodPlusInstaller.cpp
deleted file mode 100644
index c748495..0000000
--- a/libs/installer_fomod_plus/installer/FomodPlusInstaller.cpp
+++ /dev/null
@@ -1,510 +0,0 @@
-#include "FomodPlusInstaller.h"
-
-#include <igamefeatures.h>
-#include <iinstallationmanager.h>
-#include <iplugingame.h>
-#include <QEventLoop>
-#include <QTreeWidget>
-#include <xml/FomodInfoFile.h>
-#include <xml/ModuleConfiguration.h>
-#include <xml/XmlParseException.h>
-
-#include "FomodInstallerWindow.h"
-#include "stringutil.h"
-#include "integration/FomodDataContent.h"
-#include "ui/Colors.h"
-#include "ui/FomodViewModel.h"
-#include "lib/CrashHandler.h"
-
-#include <QMessageBox>
-#include <QSettings>
-
-using namespace Qt::Literals::StringLiterals;
-
-/*
---------------------------------------------------------------------------------
- Init
---------------------------------------------------------------------------------
-*/
-#pragma region Initialization
-
-bool FomodPlusInstaller::init(IOrganizer* organizer)
-{
- CrashHandler::initialize();
- mOrganizer = organizer;
- mFomodContent = make_shared<FomodDataContent>(organizer);
- log.setLogFilePath(QDir::currentPath().toStdString() + "/logs/fomodplus.log");
-
- // REMEMBER: This mFomodDB persists beyond the scope of an individual install. Do not do anything nasty to it.
- mFomodDb = std::make_unique<FomodDB>(mOrganizer->basePath().toStdString());
- setupUiInjection();
- return true;
-}
-
-void FomodPlusInstaller::setupUiInjection() const
-{
- if (shouldShowSidebarFilter()) {
- mOrganizer->gameFeatures()->registerFeature(mFomodContent, 9999, false);
- }
-
- mOrganizer->onPluginSettingChanged([this](const QString& pluginName, const QString& key, const QVariant& oldValue, const QVariant& newValue) {
- if (pluginName == name() && key == "show_fomod_filter") {
- toggleFeature(newValue.toBool());
- }
- });
-}
-
-void FomodPlusInstaller::toggleFeature(const bool enabled) const
-{
- if (enabled) {
- mOrganizer->gameFeatures()->registerFeature(mFomodContent, 9999, false);
- } else {
- mOrganizer->gameFeatures()->unregisterFeature(mFomodContent);
- }
- mOrganizer->refresh();
-}
-
-#pragma endregion
-
-/*
---------------------------------------------------------------------------------
- Settings
---------------------------------------------------------------------------------
-*/
-#pragma region Settings
-bool FomodPlusInstaller::shouldFallbackToLegacyInstaller() const
-{
- return mOrganizer->pluginSetting(name(), "fallback_to_legacy").value<bool>();
-}
-
-bool FomodPlusInstaller::shouldShowImages() const
-{
- return mOrganizer->pluginSetting(name(), "show_images").value<bool>();
-}
-
-bool FomodPlusInstaller::shouldShowNotifications() const
-{
- return mOrganizer->pluginSetting(name(), "show_notifications").value<bool>();
-}
-
-bool FomodPlusInstaller::shouldShowSidebarFilter() const
-{
- return mOrganizer->pluginSetting(name(), "show_fomod_filter").value<bool>();
-}
-
-bool FomodPlusInstaller::shouldAutoRestoreChoices() const
-{
- return mOrganizer->pluginSetting(name(), "always_restore_choices").value<bool>();
-}
-
-bool FomodPlusInstaller::isWizardIntegrated() const
-{
- return mOrganizer->pluginSetting(name(), "wizard_integration").value<bool>();
-}
-
-void FomodPlusInstaller::toggleShouldShowImages() const
-{
- const bool showImages = shouldShowImages();
- mOrganizer->setPluginSetting(name(), "show_images", !showImages);
-}
-
-QString FomodPlusInstaller::getSelectedColor() const
-{
- const auto colorName = mOrganizer->pluginSetting(name(), "color_theme").toString();
- const auto it = UiColors::colorStyles.find(colorName);
- return it != UiColors::colorStyles.end() ? it->first : "Blue";
-}
-
-std::vector<std::shared_ptr<const IPluginRequirement> > FomodPlusInstaller::requirements() const
-{
- return { Requirements::gameDependency(
- { u"Morrowind"_s, u"Oblivion"_s, u"Fallout 3"_s, u"New Vegas"_s, u"Skyrim"_s, u"Enderal"_s,
- u"Fallout 4"_s, u"Skyrim Special Edition"_s, u"Enderal Special Edition"_s,
- u"Skyrim VR"_s, u"Fallout 4 VR"_s, u"Starfield"_s }) };
-}
-
-QList<PluginSetting> FomodPlusInstaller::settings() const
-{
- return {
- { u"fallback_to_legacy"_s, u"When hitting cancel, fall back to the legacy FOMOD installer."_s, false },
- { u"always_restore_choices"_s, u"Restore previous choices without clicking the magic button"_s, false },
- { u"show_images"_s, u"Show image previews and the image carousel in installer windows."_s, true },
- { u"color_theme"_s, u"Select the color theme for the installer"_s, QString("Blue") }, // Default color name
- { u"show_notifications"_s, u"Show the notifications panel"_s, false }, //WIP
- { u"wizard_integration"_s, u"Integrate the installer with patch wizard."_s, true }, //WIP
- { u"show_fomod_filter"_s, u"Show the filter in the sidebar (may break other content filters)"_s, true }
- };
-}
-
-#pragma endregion
-
-bool FomodPlusInstaller::isArchiveSupported(std::shared_ptr<const IFileTree> tree) const
-{
- tree = findFomodDirectory(tree);
- if (tree != nullptr) {
- return tree->exists(StringConstants::FomodFiles::MODULE_CONFIG.data(), FileTreeEntry::FILE);
- }
- return false;
-}
-
-
-std::pair<nlohmann::json, IModInterface*> FomodPlusInstaller::getExistingFomodJson(const GuessedValue<QString>& modName,
- const int& nexusId,
- const int& stepsInCurrentFomod) const
-{
- logMessage(DEBUG, "FomodPlusInstaller::getExistingFomodJson - modName: " + modName->toStdString() +
- " nexusId: " + std::to_string(nexusId));
-
- // Need to have better mod matching based on presence of FOMOD plugin data.
- struct ModMatch {
- MOBase::IModInterface* mod;
- bool hasFomodData;
- int stepCount;
- nlohmann::json fomodJson;
- };
- std::vector<ModMatch> matches;
-
- auto parseStepCount = [](const QVariant& fomodData) -> std::pair<bool, int> {
- try {
- if (fomodData.isValid() && !fomodData.isNull()) {
- if (auto json = nlohmann::json::parse(fomodData.toString().toStdString()); json.contains("steps") && json["steps"].is_array()) {
- return { true, static_cast<int>(json["steps"].size()) };
- }
- }
- } catch (...) {
- // Ignore parsing errors
- }
- return { false, 0 };
- };
-
- // Check exact match first
- const auto modList = mOrganizer->modList();
- if (modList == nullptr) {
- return {};
- }
-
- if (const auto exactMod = modList->getMod(modName)) {
- const auto fomodData = exactMod->pluginSetting(name(), "fomod", 0);
- if (auto [valid, stepCount] = parseStepCount(fomodData); valid) {
- matches.push_back({
- exactMod,
- true,
- stepCount,
- nlohmann::json::parse(fomodData.toString().toStdString())
- });
- } else {
- matches.push_back({
- exactMod,
- false,
- 0,
- nlohmann::json()
- });
- }
- }
-
- // Check all variants
- for (const auto& variant : modName.variants()) {
- if (const auto variantMod = modList->getMod(variant)) {
- const auto fomodData = variantMod->pluginSetting(name(), "fomod", 0);
- if (auto [valid, stepCount] = parseStepCount(fomodData); valid) {
- matches.push_back({
- variantMod,
- true,
- stepCount,
- nlohmann::json::parse(fomodData.toString().toStdString())
- });
- } else {
- matches.push_back({
- variantMod,
- false,
- 0,
- nlohmann::json()
- });
- }
- }
- }
-
- // No matches found
- if (matches.empty()) {
- return {};
- }
-
- // First try to find exact step count match
- const auto exactMatch = ranges::find_if(matches,
- [stepsInCurrentFomod](const ModMatch& match) {
- return match.hasFomodData && match.stepCount == stepsInCurrentFomod;
- });
-
- if (exactMatch != matches.end()) {
- logMessage(DEBUG, "Found exact step count match in mod: " +
- exactMatch->mod->name().toStdString() +
- " with " + std::to_string(exactMatch->stepCount) + " steps");
- return std::make_pair(exactMatch->fomodJson, exactMatch->mod);
- }
-
- // Find the closest step count among mods with FOMOD data
- const auto closestMatch = ranges::min_element(matches,
- [stepsInCurrentFomod](const ModMatch& a, const ModMatch& b) {
- if (!a.hasFomodData || !b.hasFomodData)
- return false;
- // The min difference between the step counts
- return std::abs(a.stepCount - stepsInCurrentFomod) <
- std::abs(b.stepCount - stepsInCurrentFomod);
- });
-
- if (closestMatch != matches.end() && closestMatch->hasFomodData) {
- logMessage(DEBUG, "Using closest step count match from mod: " +
- closestMatch->mod->name().toStdString() +
- " with " + std::to_string(closestMatch->stepCount) + " steps");
- return std::make_pair(closestMatch->fomodJson, closestMatch->mod);
- }
-
- // Fallback to first mod with any FOMOD data
- const auto anyFomod = ranges::find_if(matches,
- [](const ModMatch& match) { return match.hasFomodData; });
-
- if (anyFomod != matches.end()) {
- logMessage(DEBUG, "Using first available FOMOD data from mod: " +
- anyFomod->mod->name().toStdString());
- return std::make_pair(anyFomod->fomodJson, anyFomod->mod);
- }
-
- logMessage(DEBUG, "No matching FOMOD data found");
- return {};
-}
-
-void FomodPlusInstaller::clearPriorInstallData()
-{
- mInstallerUsed = false;
- mFomodJson = nullptr;
- mFomodPath = "";
-}
-
-/**
- *
- * @param modName
- * @param tree
- * @param version
- * @param nexusID
- * @return
- */
-IPluginInstaller::EInstallResult FomodPlusInstaller::install(GuessedValue<QString>& modName,
- std::shared_ptr<IFileTree>& tree, QString& version,
- int& nexusID)
-{
- clearPriorInstallData();
-
- logMessage(INFO, std::format("FomodPlusInstaller::install - modName: {}, version: {}, nexusID: {}",
- modName->toStdString(),
- version.toStdString(),
- nexusID
- ));
- logMessage(INFO, std::format("FomodPlusInstaller::install - tree size: {}", tree->size()));
-
- auto [infoFile, moduleConfigFile, filePaths] = parseFomodFiles(tree);
-
- if (infoFile == nullptr || moduleConfigFile == nullptr) {
- return RESULT_FAILED;
- }
-
- std::vector<QString> pluginPaths = {};
- for (const auto& filePath : filePaths) {
- if (isPluginFile(filePath)) {
- pluginPaths.emplace_back(filePath);
- }
- }
-
- const auto dbEntry = mFomodDb->getEntryFromFomod(moduleConfigFile.get(), pluginPaths, nexusID);
-
- // create ui & pass xml classes to ui
- auto [json, matchMod] = getExistingFomodJson(modName, nexusID, static_cast<int>(moduleConfigFile->installSteps.installSteps.size()));
- if (matchMod != nullptr) {
- modName.update(matchMod->name(), GUESS_USER);
- }
- auto fomodViewModel = FomodViewModel::create(mOrganizer, std::move(moduleConfigFile), std::move(infoFile));
- const auto window = std::make_shared<FomodInstallerWindow>(this, modName, tree, mFomodPath, fomodViewModel, json);
-
- // ReSharper disable once CppTooWideScopeInitStatement
- const QDialog::DialogCode result = showInstallerWindow(window);
- if (result == QDialog::Accepted) {
- // modname was updated in window
- mInstallerUsed = true;
- const std::shared_ptr<IFileTree> installTree = window->getFileInstaller()->install();
- tree = installTree;
- mFomodJson = std::make_shared<nlohmann::json>(window->getFileInstaller()->generateFomodJson());
-
- try {
- mFomodDb->addEntry(dbEntry);
- mFomodDb->saveToFile();
- } catch ([[maybe_unused]] Exception& e) {
- logMessage(ERR, "Failed to add FomodDB entries.");
- logMessage(ERR, e.what());
- }
- return RESULT_SUCCESS;
- }
- if (window->isManualInstall()) {
- return RESULT_MANUALREQUESTED;
- }
- if (shouldFallbackToLegacyInstaller()) {
- return RESULT_NOTATTEMPTED;
- }
- return RESULT_CANCELED;
-}
-
-
-/**
- *
- * @param tree
- * @return
- */
-ParsedFilesTuple FomodPlusInstaller::parseFomodFiles(const std::shared_ptr<IFileTree>& tree)
-{
- const auto emptyResult = std::make_tuple(nullptr, nullptr, QStringList());
-
- const auto fomodDir = findFomodDirectory(tree);
- if (fomodDir == nullptr) {
- logMessage(ERR, "FomodPlusInstaller::install - fomod directory not found");
- return emptyResult;
- }
-
- // This is a strange place to set this value but okay for now.
- mFomodPath = fomodDir->parent()->path();
-
- const auto infoXML = fomodDir->find(
- StringConstants::FomodFiles::INFO_XML.data(),
- FileTreeEntry::FILE
- );
- const auto moduleConfig = fomodDir->find(
- StringConstants::FomodFiles::MODULE_CONFIG.data(),
- FileTreeEntry::FILE
- );
-
- // Extract files first.
- vector<std::shared_ptr<const FileTreeEntry> > toExtract = {};
- if (moduleConfig) {
- toExtract.push_back(moduleConfig);
- } else {
- logMessage(ERR, "FomodPlusInstaller::install - error parsing moduleConfig.xml: Not Present");
- return emptyResult;
- }
- if (infoXML) {
- toExtract.push_back(infoXML);
- }
- appendImageFiles(toExtract, tree);
- appendPluginFiles(toExtract, tree); // For patch wizard data collection
- auto paths = manager()->extractFiles(toExtract);
- // Normalize backslash separators from MO2's internal file tree to forward slashes for Linux
- for (auto& p : paths) {
- p.replace('\\', '/');
- }
-
- auto moduleConfiguration = std::make_unique<ModuleConfiguration>();
- try {
- moduleConfiguration->deserialize(paths.at(0));
- } catch (XmlParseException& e) {
- logMessage(ERR, std::format("FomodPlusInstaller::install - error parsing moduleConfig.xml: {}", e.what()));
- return emptyResult;
- }
-
- auto infoFile = std::make_unique<FomodInfoFile>();
- if (infoXML) {
- try {
- infoFile->deserialize(paths.at(1));
- } catch (XmlParseException& e) {
- logMessage(ERR, std::format("FomodPlusInstaller::install - error parsing info.xml: {}", e.what()));
- }
- }
-
- return std::make_tuple(std::move(infoFile), std::move(moduleConfiguration), paths);
-}
-
-// Taken from https://github.com/ModOrganizer2/modorganizer-installer_fomod/blob/master/src/installerfomod.cpp#L123
-void FomodPlusInstaller::appendImageFiles(vector<shared_ptr<const FileTreeEntry> >& entries,
- const shared_ptr<const IFileTree>& tree)
-{
- static std::set<QString, FileNameComparator> imageSuffixes{ "png", "jpg", "jpeg", "gif", "bmp" };
- for (auto entry : *tree) {
- if (entry->isDir()) {
- appendImageFiles(entries, entry->astree());
- } else if (imageSuffixes.contains(entry->suffix())) {
- entries.push_back(entry);
- }
- }
-}
-
-void FomodPlusInstaller::appendPluginFiles(vector<shared_ptr<const FileTreeEntry> >& entries,
- const shared_ptr<const IFileTree>& tree)
-{
- // Don't bother with this stuff if the user doesn't care about the wizard.
- // It may slightly bloat the temp file directory if not needed.
- if (isWizardIntegrated()) {
- for (auto entry : *tree) {
- if (entry->isDir()) {
- appendPluginFiles(entries, entry->astree());
- } else if (isPluginFile(entry->suffix())) {
- entries.push_back(entry);
- }
- }
- }
-}
-
-void FomodPlusInstaller::onInstallationStart(QString const& archive, const bool reinstallation,
- IModInterface* currentMod)
-{
- IPluginInstallerSimple::onInstallationStart(archive, reinstallation, currentMod);
-}
-
-void FomodPlusInstaller::onInstallationEnd(const EInstallResult result, IModInterface* newMod)
-{
- IPluginInstallerSimple::onInstallationEnd(result, newMod);
-
- // Update the meta.ini file with the fomod information
- if (mFomodJson != nullptr && result == RESULT_SUCCESS && newMod != nullptr && mInstallerUsed) {
- newMod->setPluginSetting(this->name(), "fomod", mFomodJson->dump().c_str());
- mOrganizer->refresh();
- }
- clearPriorInstallData();
-}
-
-// Borrowed from https://github.com/ModOrganizer2/modorganizer-installer_fomod/blob/master/src/installerfomod.cpp
-std::shared_ptr<const IFileTree> FomodPlusInstaller::findFomodDirectory(const std::shared_ptr<const IFileTree>& tree)
-{
- // ReSharper disable once CppTooWideScopeInitStatement
- const auto entry = tree->find(StringConstants::FomodFiles::FOMOD_DIR.data(), FileTreeEntry::DIRECTORY);
-
- if (entry != nullptr) {
- return entry->astree();
- }
-
- if (tree->size() == 1 && tree->at(0)->isDir()) {
- return findFomodDirectory(tree->at(0)->astree());
- }
- return nullptr;
-}
-
-QDialog::DialogCode FomodPlusInstaller::showInstallerWindow(const std::shared_ptr<FomodInstallerWindow>& window)
-{
- QEventLoop loop;
- connect(window.get(), SIGNAL(accepted()), &loop, SLOT(quit()));
- connect(window.get(), SIGNAL(rejected()), &loop, SLOT(quit()));
- window->show();
- loop.exec();
- return static_cast<QDialog::DialogCode>(window->result());
-}
-
-void showError(const Exception& e)
-{
- const QString errorText = "Mod Name: \n" "Nexus ID: " "\n" "Exception: " + QString(e.what()) + "\n";
-
- QMessageBox msgBox;
- msgBox.setWindowTitle("FOMOD Plus Error :(");
- msgBox.setTextFormat(Qt::RichText);
- msgBox.setText("Sorry this happened. Please copy the following error and report it to me on Nexus or GitHub.\n"
- "<pre style='background-color: #f0f0f0; padding: 10px;'>"
- + errorText +
- "</pre>"
- );
- msgBox.setIcon(QMessageBox::Critical);
- msgBox.exec();
-
-}
diff --git a/libs/installer_fomod_plus/installer/FomodPlusInstaller.h b/libs/installer_fomod_plus/installer/FomodPlusInstaller.h
deleted file mode 100644
index 494ad8e..0000000
--- a/libs/installer_fomod_plus/installer/FomodPlusInstaller.h
+++ /dev/null
@@ -1,115 +0,0 @@
-#pragma once
-
-#include "stringutil.h"
-
-#include <iplugin.h>
-#include <iplugininstaller.h>
-#include <iplugininstallersimple.h>
-
-#include <nlohmann/json.hpp>
-#include "FomodInstallerWindow.h"
-#include "lib/Logger.h"
-#include "xml/FomodInfoFile.h"
-#include "xml/ModuleConfiguration.h"
-
-#include <QDialog>
-#include <integration/FomodDataContent.h>
-#include <FOMODData/FomodDB.h>
-
-class FomodInstallerWindow;
-
-using namespace MOBase;
-using namespace std;
-
-using ParsedFilesTuple = std::tuple<std::unique_ptr<FomodInfoFile>, std::unique_ptr<ModuleConfiguration>, QStringList>;
-
-class FomodPlusInstaller final : public IPluginInstallerSimple {
- Q_OBJECT
- Q_INTERFACES(MOBase::IPlugin MOBase::IPluginInstaller)
- Q_PLUGIN_METADATA(IID "io.clearing.FomodPlus" FILE "fomodplus.json")
-
-public:
- bool init(IOrganizer* organizer) override;
-
- // constant values
- [[nodiscard]] QString name() const override { return StringConstants::Plugin::NAME.data(); }
- [[nodiscard]] QString author() const override { return StringConstants::Plugin::AUTHOR.data(); }
- [[nodiscard]] QString description() const override { return StringConstants::Plugin::DESCRIPTION.data(); }
- [[nodiscard]] VersionInfo version() const override { return { 1, 0, 0, VersionInfo::RELEASE_FINAL }; }
-
- [[nodiscard]] unsigned int priority() const override
- {
- return 999; /* Above installer_fomod's highest priority. */
- }
-
- [[nodiscard]] std::vector<std::shared_ptr<const IPluginRequirement> > requirements() const override;
-
- [[nodiscard]] bool isManualInstaller() const override { return false; }
-
- [[nodiscard]] bool isArchiveSupported(std::shared_ptr<const IFileTree> tree) const override;
-
- [[nodiscard]] QList<PluginSetting> settings() const override;
-
- std::pair<nlohmann::json, IModInterface*> getExistingFomodJson(const GuessedValue<QString>& modName,
- const int& nexusId, const int& stepsInCurrentFomod) const;
-
- void clearPriorInstallData();
-
- EInstallResult install(GuessedValue<QString>& modName, std::shared_ptr<IFileTree>& tree, QString& version,
- int& nexusID) override;
-
- void onInstallationStart(QString const& archive, bool reinstallation, IModInterface* currentMod) override;
-
- void onInstallationEnd(EInstallResult result, IModInterface* newMod) override;
-
- [[nodiscard]] bool shouldShowImages() const;
-
- [[nodiscard]] bool shouldShowNotifications() const;
- bool shouldShowSidebarFilter() const;
-
- [[nodiscard]] bool shouldAutoRestoreChoices() const;
-
- [[nodiscard]] bool isWizardIntegrated() const;
-
- void toggleShouldShowImages() const;
-
- QString getSelectedColor() const;
-
-private:
- Logger& log = Logger::getInstance();
- IOrganizer* mOrganizer = nullptr;
- QString mFomodPath{};
- std::shared_ptr<nlohmann::json> mFomodJson{ nullptr };
- bool mInstallerUsed{ false };
- std::shared_ptr<FomodDataContent> mFomodContent{ nullptr };
- std::unique_ptr<FomodDB> mFomodDb;
-
- /**
- * @brief Retrieve the tree entry corresponding to the fomod directory.
- *
- * @param tree Tree to look-up the directory in.
- *
- * @return the entry corresponding to the fomod directory in the tree, or a null
- * pointer if the entry was not found.
- */
- [[nodiscard]] static shared_ptr<const IFileTree> findFomodDirectory(const shared_ptr<const IFileTree>& tree);
-
- [[nodiscard]] static QDialog::DialogCode showInstallerWindow(const shared_ptr<FomodInstallerWindow>& window);
-
- [[nodiscard]] ParsedFilesTuple parseFomodFiles(const shared_ptr<IFileTree>& tree);
-
- static void appendImageFiles(vector<shared_ptr<const FileTreeEntry> >& entries,
- const shared_ptr<const IFileTree>& tree);
-
- void appendPluginFiles(vector<shared_ptr<const FileTreeEntry> >& entries, const shared_ptr<const IFileTree>& tree);
-
- void setupUiInjection() const;
- void toggleFeature(bool enabled) const;
-
- [[nodiscard]] bool shouldFallbackToLegacyInstaller() const;
-
- void logMessage(const LogLevel level, const std::string& message) const
- {
- log.logMessage(level, "[INSTALLER] " + message);
- }
-};
diff --git a/libs/installer_fomod_plus/installer/fomod_plus_de.ts b/libs/installer_fomod_plus/installer/fomod_plus_de.ts
deleted file mode 100644
index 1552582..0000000
--- a/libs/installer_fomod_plus/installer/fomod_plus_de.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE TS>
-<TS version="2.1" language="de_DE">
-</TS>
diff --git a/libs/installer_fomod_plus/installer/fomod_plus_en.ts b/libs/installer_fomod_plus/installer/fomod_plus_en.ts
deleted file mode 100644
index bc6d6e7..0000000
--- a/libs/installer_fomod_plus/installer/fomod_plus_en.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE TS>
-<TS version="2.1" language="en_US">
-</TS>
diff --git a/libs/installer_fomod_plus/installer/fomod_plus_installer_de.ts b/libs/installer_fomod_plus/installer/fomod_plus_installer_de.ts
deleted file mode 100644
index ad7bfed..0000000
--- a/libs/installer_fomod_plus/installer/fomod_plus_installer_de.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE TS>
-<TS version="2.1" language="de_DE">
-<context>
- <name>FomodInstallerWindow</name>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="194"/>
- <source>Installieren</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="196"/>
- <location filename="FomodInstallerWindow.cpp" line="370"/>
- <source>Weiter</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="288"/>
- <source>Name:</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="289"/>
- <source>Autor:</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="290"/>
- <source>Version:</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="291"/>
- <source>Webseite:</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="357"/>
- <source>Manuell</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="358"/>
- <source>Alle Vorherige Optionen Ausw&auml;hlen</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="359"/>
- <source>Reset Choices</source>
- <translation>Optionen Zur&uuml;cksetzen</translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="361"/>
- <location filename="FomodInstallerWindow.cpp" line="586"/>
- <source>Bilder Ausblenden</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="369"/>
- <source>Zur&uuml;ck</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="371"/>
- <source>Abbrechen</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="360"/>
- <location filename="FomodInstallerWindow.cpp" line="590"/>
- <source>Bilder Einblenden</source>
- <translation type="unfinished"></translation>
- </message>
-</context>
-</TS>
diff --git a/libs/installer_fomod_plus/installer/fomod_plus_installer_en.ts b/libs/installer_fomod_plus/installer/fomod_plus_installer_en.ts
deleted file mode 100644
index 5cbe0a7..0000000
--- a/libs/installer_fomod_plus/installer/fomod_plus_installer_en.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE TS>
-<TS version="2.1" language="en_US">
-<context>
- <name>FomodInstallerWindow</name>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="231"/>
- <location filename="FomodInstallerWindow.cpp" line="242"/>
- <source>Install</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="244"/>
- <location filename="FomodInstallerWindow.cpp" line="423"/>
- <source>Next</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="342"/>
- <source>Name:</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="343"/>
- <source>Author:</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="344"/>
- <source>Version:</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="345"/>
- <source>Website:</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="411"/>
- <source>Manual</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="412"/>
- <source>Restore Previous Choices</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="413"/>
- <source>Reset Choices</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="422"/>
- <source>Back</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="424"/>
- <source>Cancel</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="682"/>
- <source>Hide Images</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="686"/>
- <source>Show Images</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="703"/>
- <source>Select a plugin to see its description.</source>
- <translation type="unfinished"></translation>
- </message>
-</context>
-</TS>
diff --git a/libs/installer_fomod_plus/installer/fomod_plus_installer_zh_CN.ts b/libs/installer_fomod_plus/installer/fomod_plus_installer_zh_CN.ts
deleted file mode 100644
index f2769e6..0000000
--- a/libs/installer_fomod_plus/installer/fomod_plus_installer_zh_CN.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE TS>
-<TS version="2.1" language="zh_CN">
-<context>
- <name>FomodInstallerWindow</name>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="194"/>
- <source>安装</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="196"/>
- <location filename="FomodInstallerWindow.cpp" line="370"/>
- <source>下一步</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="288"/>
- <source>名称</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="289"/>
- <source>作者</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="290"/>
- <source>版本</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="291"/>
- <source>网址</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="357"/>
- <source>手动</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="358"/>
- <source>选中所有上次安装的选择</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="359"/>
- <source>Reset Choices</source>
- <translation>重置选择</translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="361"/>
- <location filename="FomodInstallerWindow.cpp" line="586"/>
- <source>隐藏图片</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="369"/>
- <source>上一步</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="371"/>
- <source>取消</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodInstallerWindow.cpp" line="360"/>
- <location filename="FomodInstallerWindow.cpp" line="590"/>
- <source>显示图片</source>
- <translation type="unfinished"></translation>
- </message>
-</context>
-</TS>
diff --git a/libs/installer_fomod_plus/installer/fomod_plus_zh_CN.ts b/libs/installer_fomod_plus/installer/fomod_plus_zh_CN.ts
deleted file mode 100644
index e5ca8aa..0000000
--- a/libs/installer_fomod_plus/installer/fomod_plus_zh_CN.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE TS>
-<TS version="2.1" language="zh_CN">
-</TS>
diff --git a/libs/installer_fomod_plus/installer/fomodplus.json b/libs/installer_fomod_plus/installer/fomodplus.json
deleted file mode 100644
index dde5a05..0000000
--- a/libs/installer_fomod_plus/installer/fomodplus.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "Id": "FOMODPlus",
- "Version" : "1.0.0"
-} \ No newline at end of file
diff --git a/libs/installer_fomod_plus/installer/integration/FomodDataContent.cpp b/libs/installer_fomod_plus/installer/integration/FomodDataContent.cpp
deleted file mode 100644
index ac4508d..0000000
--- a/libs/installer_fomod_plus/installer/integration/FomodDataContent.cpp
+++ /dev/null
@@ -1,50 +0,0 @@
-#include "FomodDataContent.h"
-
-#include <ifiletree.h>
-
-#include "stringutil.h"
-
-#include <iplugingame.h>
-
-FomodDataContent::FomodDataContent(MOBase::IOrganizer* organizer) : mOrganizer(organizer)
-{
-}
-
-std::vector<MOBase::ModDataContent::Content> FomodDataContent::getAllContents() const
-{
- static const std::vector<Content> contents = {
- {FomodDataContentConstants::FOMOD_CONTENT_ID, "FOMOD", ":/fomod/hat", false}
- };
-
- return contents;
-}
-
-// Confirmed working, no need to update
-std::vector<int> FomodDataContent::getContentsFor(const std::shared_ptr<const MOBase::IFileTree> fileTree) const
-{
- std::vector<int> contents;
- if (!mOrganizer || !fileTree) {
- return contents;
- }
-
- const auto modList = mOrganizer->modList();
- if (!modList) {
- return contents;
- }
-
- const auto mod = modList->getMod(fileTree->name());
- if (modHasFomodContent(mod)) {
- contents.emplace_back(FomodDataContentConstants::FOMOD_CONTENT_ID);
- }
- return contents;
-}
-
-bool FomodDataContent::modHasFomodContent(const MOBase::IModInterface* mod)
-{
- if (!mod) {
- return false;
- }
- const auto pluginName = QString::fromStdString(StringConstants::Plugin::NAME.data());
- const auto fomodMeta = mod->pluginSetting(pluginName, "fomod", 0);
- return fomodMeta != 0;
-}
diff --git a/libs/installer_fomod_plus/installer/integration/FomodDataContent.h b/libs/installer_fomod_plus/installer/integration/FomodDataContent.h
deleted file mode 100644
index 4ffa3d7..0000000
--- a/libs/installer_fomod_plus/installer/integration/FomodDataContent.h
+++ /dev/null
@@ -1,21 +0,0 @@
-#pragma once
-
-#include <imoinfo.h>
-#include <moddatacontent.h>
-
-namespace FomodDataContentConstants {
-constexpr int FOMOD_CONTENT_ID = 400400;
-}
-
-class FomodDataContent final : public MOBase::ModDataContent {
-public:
- explicit FomodDataContent(MOBase::IOrganizer* organizer);
-
- [[nodiscard]] std::vector<Content> getAllContents() const override;
-
- [[nodiscard]] std::vector<int> getContentsFor(std::shared_ptr<const MOBase::IFileTree> fileTree) const override;
-
-private:
- MOBase::IOrganizer* mOrganizer;
- static bool modHasFomodContent(const MOBase::IModInterface* mod);
-};
diff --git a/libs/installer_fomod_plus/installer/lib/ConditionTester.cpp b/libs/installer_fomod_plus/installer/lib/ConditionTester.cpp
deleted file mode 100644
index c35bf47..0000000
--- a/libs/installer_fomod_plus/installer/lib/ConditionTester.cpp
+++ /dev/null
@@ -1,176 +0,0 @@
-#include "ConditionTester.h"
-
-#include "ui/FomodViewModel.h"
-
-#include <iplugingame.h>
-#include <ipluginlist.h>
-
-std::string setToString(const std::set<int> &set)
-{
- std::string str;
- for (const auto& i : set) {
- str += std::to_string(i) + ", ";
- }
- return str;
-}
-
-bool ConditionTester::isStepVisible(const std::shared_ptr<FlagMap>& flags,
- const CompositeDependency& compositeDependency,
- const int stepIndex,
- const std::vector<std::shared_ptr<StepViewModel>>& steps) const
-{
-
- // first things first: is it visible?
- if (!testCompositeDependency(flags, compositeDependency)) {
- return false;
- }
-
- const auto flagDependencies = compositeDependency.flagDependencies;
- if (flagDependencies.empty()) {
- return true;
- }
-
- std::set<int> stepsThatSetThisFlag;
-
- for (const auto& flagDependency : flagDependencies) {
- // for this flag, find the plugins that set it
- for (int i = stepIndex - 1; i >= 0; --i) {
- for (const auto& group : steps[i]->getGroups()) {
- for (const auto& plugin : group->getPlugins()) {
- if (std::ranges::any_of(plugin->getPlugin()->conditionFlags.flags,
- [&flagDependency](const ConditionFlag& flag) {
- return flag.name == flagDependency.flag && flag.value == flagDependency.value;
- })) {
- stepsThatSetThisFlag.insert(i);
- }
- }
- }
- }
- }
- const auto anyVisible = std::ranges::any_of(stepsThatSetThisFlag, [this, &steps, &flags](const int index) {
- return isStepVisible(flags, steps[index]->getVisibilityConditions(), index, steps);
- });
- if (!anyVisible) {
- log.logMessage(DEBUG, "Step " + steps[stepIndex]->getName() + " has no dependent steps that are visible.");
- log.logMessage(DEBUG, "Steps that set this flag: " + setToString(stepsThatSetThisFlag));
- }
- return anyVisible;
-
-}
-
-bool ConditionTester::testCompositeDependency(const std::shared_ptr<FlagMap>& flags,
- const CompositeDependency& compositeDependency) const
-{
- const auto fileDependencies = compositeDependency.fileDependencies;
- const auto flagDependencies = compositeDependency.flagDependencies;
- const auto gameDependencies = compositeDependency.gameDependencies;
- const auto nestedDependencies = compositeDependency.nestedDependencies;
- const auto globalOperatorType = compositeDependency.operatorType;
-
- // For the globalOperatorType
- // Evaluate all conditions and store the results in a vector<bool>, then return based on operator.
- // These aren't expensive to calculate so rather than do some fancy logic to short-circuit, just calculate all of 'em.
- std::vector<bool> results;
- for (const auto& fileDependency : fileDependencies) {
- results.emplace_back(testFileDependency(fileDependency));
- }
- for (const auto& flagDependency : flagDependencies) {
- results.emplace_back(testFlagDependency(flags, flagDependency));
- }
- for (const auto& gameDependency : gameDependencies) {
- results.emplace_back(testGameDependency(gameDependency));
- }
- for (const auto& nestedDependency : nestedDependencies) {
- results.emplace_back(testCompositeDependency(flags, nestedDependency));
- }
-
- if (globalOperatorType == OperatorTypeEnum::AND) {
- return std::ranges::all_of(results, [](const bool result) { return result; });
- }
- return std::ranges::any_of(results, [](const bool result) { return result; });
-}
-
-
-bool ConditionTester::testFlagDependency(const std::shared_ptr<FlagMap>& flags, const FlagDependency& flagDependency)
-{
- // Every instance of this flag being set in the map.
- const auto flagList = flags->getFlagsByKey(flagDependency.flag);
-
- // Find the first instance of this flag being set (in the order specified by getFlagsByKey)
- if (flagList.empty()) {
- // If the dependency value is an empty string, it means this flag should be unset.
- // So if we don't have any value for this flag, the result is true.
- return flagDependency.value.empty();
- }
-
- return flagList.front().second == flagDependency.value;
-}
-
-bool ConditionTester::testFileDependency(const FileDependency& fileDependency) const
-{
- const std::string& pluginName = fileDependency.file;
- const auto pluginState = getFileDependencyStateForPlugin(pluginName);
- return pluginState == fileDependency.state;
-}
-
-bool ConditionTester::testGameDependency(const GameDependency& gameDependency) const
-{
- const auto gameVersion = mOrganizer->managedGame()->gameVersion().toStdString();
- log.logMessage(DEBUG, "Comparing condition version " + gameDependency.version + " against " + gameVersion);
- if ( gameDependency.version <= gameVersion) {
- log.logMessage(DEBUG, "Version matches!");
- }
- return gameDependency.version <= gameVersion;
-}
-
-FileDependencyTypeEnum ConditionTester::getFileDependencyStateForPlugin(const std::string& pluginName) const
-{
- if (const auto it = pluginStateCache.find(pluginName); it != pluginStateCache.end()) {
- return it->second;
- }
-
- const QFlags<MOBase::IPluginList::PluginState> pluginState = mOrganizer->pluginList()->state(
- QString::fromStdString(pluginName));
-
- FileDependencyTypeEnum state;
-
- if (pluginState == MOBase::IPluginList::STATE_MISSING) {
- state = FileDependencyTypeEnum::Missing;
- } else if (pluginState == MOBase::IPluginList::STATE_INACTIVE) {
- state = FileDependencyTypeEnum::Inactive;
- } else if (pluginState == MOBase::IPluginList::STATE_ACTIVE) {
- state = FileDependencyTypeEnum::Active;
- } else {
- state = FileDependencyTypeEnum::UNKNOWN_STATE;
- }
-
- pluginStateCache[pluginName] = state;
- return state;
-}
-
-PluginTypeEnum ConditionTester::getPluginTypeDescriptorState(const std::shared_ptr<Plugin>& plugin,
- const std::shared_ptr<FlagMap>& flags) const
-{
- // NOTE: A plugin's ConditionFlags aren't the same thing as a step visibility one.
- // A plugin's ConditionFlags are toggled based on the selection state of the plugin
- // We only evaluate the typeDescriptor here.
-
- // We will return the 'winning' type or the default. If multiple conditions are met,
- // ...well, I'm not sure.
- // ReSharper disable once CppTooWideScopeInitStatement
- const auto& dependencyType = plugin->typeDescriptor.dependencyType;
- for (const auto& pattern : dependencyType.patterns.patterns) {
- if (testCompositeDependency(flags, pattern.dependencies)) {
- return pattern.type;
- }
- }
-
- // Sometimes authors do this.
- if (plugin->typeDescriptor.type != PluginTypeEnum::Optional) {
- return plugin->typeDescriptor.type;
- }
- if (plugin->typeDescriptor.dependencyType.defaultType.has_value()) {
- return plugin->typeDescriptor.dependencyType.defaultType.value();
- }
- return PluginTypeEnum::Optional;
-} \ No newline at end of file
diff --git a/libs/installer_fomod_plus/installer/lib/ConditionTester.h b/libs/installer_fomod_plus/installer/lib/ConditionTester.h
deleted file mode 100644
index 09aa26e..0000000
--- a/libs/installer_fomod_plus/installer/lib/ConditionTester.h
+++ /dev/null
@@ -1,42 +0,0 @@
-#pragma once
-
-#include <imoinfo.h>
-
-#include "FlagMap.h"
-#include "Logger.h"
-#include "xml/ModuleConfiguration.h"
-
-class CompositeDependency;
-class StepViewModel;
-
-class ConditionTester {
-public:
- explicit ConditionTester(MOBase::IOrganizer* organizer) : mOrganizer(organizer) {}
-
- bool isStepVisible(const std::shared_ptr<FlagMap>& flags, const CompositeDependency& compositeDependency,
- int stepIndex,
- const std::vector<std::shared_ptr<StepViewModel>>& steps) const;
-
- bool testCompositeDependency(const std::shared_ptr<FlagMap>& flags,
- const CompositeDependency& compositeDependency) const;
-
- static bool testFlagDependency(const std::shared_ptr<FlagMap>& flags, const FlagDependency& flagDependency);
-
- [[nodiscard]] bool testFileDependency(const FileDependency& fileDependency) const;
-
- bool testGameDependency(const GameDependency& gameDependency) const;
-
-private:
- Logger& log = Logger::getInstance();
- MOBase::IOrganizer* mOrganizer;
-
- friend class FomodViewModel;
-
- [[nodiscard]] FileDependencyTypeEnum getFileDependencyStateForPlugin(const std::string& pluginName) const;
-
- PluginTypeEnum getPluginTypeDescriptorState(const std::shared_ptr<Plugin>& plugin,
- const std::shared_ptr<FlagMap>& flags) const;
-
- mutable std::unordered_map<std::string, FileDependencyTypeEnum> pluginStateCache;
-
-};
diff --git a/libs/installer_fomod_plus/installer/lib/CrashHandler.cpp b/libs/installer_fomod_plus/installer/lib/CrashHandler.cpp
deleted file mode 100644
index 98ed1f3..0000000
--- a/libs/installer_fomod_plus/installer/lib/CrashHandler.cpp
+++ /dev/null
@@ -1,122 +0,0 @@
-#include "CrashHandler.h"
-#include <iostream>
-
-#ifdef _WIN32
-#include <windows.h>
-#include <dbghelp.h>
-#include <sstream>
-#include <fstream>
-
-LPTOP_LEVEL_EXCEPTION_FILTER CrashHandler::previousFilter = nullptr;
-PVOID CrashHandler::vectoredHandler = nullptr;
-#endif
-
-void CrashHandler::initialize() {
-#ifdef _WIN32
- std::cout << "CrashHandler initializing..." << std::endl;
- vectoredHandler = AddVectoredExceptionHandler(1, vectoredExceptionHandler);
- previousFilter = SetUnhandledExceptionFilter(unhandledExceptionFilter);
- std::cout << "CrashHandler initialized successfully" << std::endl;
-#else
- // No-op on Linux — Windows crash handler not needed.
-#endif
-}
-
-void CrashHandler::cleanup() {
-#ifdef _WIN32
- if (vectoredHandler) {
- RemoveVectoredExceptionHandler(vectoredHandler);
- vectoredHandler = nullptr;
- }
- if (previousFilter) {
- SetUnhandledExceptionFilter(previousFilter);
- previousFilter = nullptr;
- }
-#endif
-}
-
-#ifdef _WIN32
-LONG WINAPI CrashHandler::vectoredExceptionHandler(EXCEPTION_POINTERS* exceptionInfo) {
- // Log all exceptions but only create dumps for serious ones
- std::cout << "Exception caught: 0x" << std::hex << exceptionInfo->ExceptionRecord->ExceptionCode << std::endl;
-
- // Only create dumps for serious crashes, not benign exceptions
- if (exceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION ||
- exceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_STACK_OVERFLOW ||
- exceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_ILLEGAL_INSTRUCTION) {
-
- // For stack overflow, we need to handle it on a different thread with its own stack
- if (exceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_STACK_OVERFLOW) {
- // Create a new thread to handle the crash dump since our stack is corrupted
- HANDLE dumpThread = CreateThread(nullptr, 0, [](LPVOID param) -> DWORD {
- auto* exceptionInfo = static_cast<EXCEPTION_POINTERS*>(param);
- logCrashInfo(exceptionInfo);
-
- HANDLE file = CreateFileA("fomod_plus_stack_overflow.dmp", GENERIC_WRITE, 0, nullptr,
- CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
-
- if (file != INVALID_HANDLE_VALUE) {
- MINIDUMP_EXCEPTION_INFORMATION dumpInfo = {0};
- dumpInfo.ThreadId = GetCurrentThreadId();
- dumpInfo.ExceptionPointers = exceptionInfo;
- dumpInfo.ClientPointers = FALSE;
-
- MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), file,
- MiniDumpNormal, &dumpInfo, nullptr, nullptr);
- CloseHandle(file);
-
- std::cout << "Stack overflow crash dump written to fomod_plus_stack_overflow.dmp" << std::endl;
- }
- return 0;
- }, exceptionInfo, 0, nullptr);
-
- if (dumpThread) {
- WaitForSingleObject(dumpThread, 5000); // Wait up to 5 seconds
- CloseHandle(dumpThread);
- }
- } else {
- logCrashInfo(exceptionInfo);
-
- // Generate minidump
- HANDLE file = CreateFileA("fomod_plus_crash.dmp", GENERIC_WRITE, 0, nullptr,
- CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
-
- if (file != INVALID_HANDLE_VALUE) {
- MINIDUMP_EXCEPTION_INFORMATION dumpInfo = {0};
- dumpInfo.ThreadId = GetCurrentThreadId();
- dumpInfo.ExceptionPointers = exceptionInfo;
- dumpInfo.ClientPointers = FALSE;
-
- MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), file,
- MiniDumpNormal, &dumpInfo, nullptr, nullptr);
- CloseHandle(file);
-
- std::cout << "Crash dump written to fomod_plus_crash.dmp" << std::endl;
- }
- }
- }
-
- return EXCEPTION_CONTINUE_SEARCH; // Let other handlers process it
-}
-
-LONG WINAPI CrashHandler::unhandledExceptionFilter(EXCEPTION_POINTERS* exceptionInfo) {
- logCrashInfo(exceptionInfo);
- return EXCEPTION_EXECUTE_HANDLER;
-}
-
-void CrashHandler::logCrashInfo(const EXCEPTION_POINTERS* exceptionInfo) {
- std::ostringstream oss;
- oss << "FOMOD PLUS CRASH DETECTED!" << std::endl;
- oss << "Exception Code: 0x" << std::hex << exceptionInfo->ExceptionRecord->ExceptionCode << std::endl;
- oss << "Exception Address: 0x" << std::hex << exceptionInfo->ExceptionRecord->ExceptionAddress << std::endl;
-
- // Write to stdout immediately
- std::cout << oss.str() << std::endl;
-
- // Also write to crash log file
- std::ofstream crashLog("fomod_plus_crash.log", std::ios::app);
- if (crashLog.is_open()) {
- crashLog << oss.str() << std::endl;
- }
-}
-#endif
diff --git a/libs/installer_fomod_plus/installer/lib/CrashHandler.h b/libs/installer_fomod_plus/installer/lib/CrashHandler.h
deleted file mode 100644
index 78c8552..0000000
--- a/libs/installer_fomod_plus/installer/lib/CrashHandler.h
+++ /dev/null
@@ -1,20 +0,0 @@
-#pragma once
-
-#ifdef _WIN32
-#include <windows.h>
-#endif
-
-class CrashHandler {
-public:
- static void initialize();
- static void cleanup();
-
-private:
-#ifdef _WIN32
- static LONG WINAPI unhandledExceptionFilter(EXCEPTION_POINTERS* exceptionInfo);
- static LONG WINAPI vectoredExceptionHandler(EXCEPTION_POINTERS* exceptionInfo);
- static void logCrashInfo(const EXCEPTION_POINTERS* exceptionInfo);
- static LPTOP_LEVEL_EXCEPTION_FILTER previousFilter;
- static PVOID vectoredHandler;
-#endif
-};
diff --git a/libs/installer_fomod_plus/installer/lib/FileInstaller.cpp b/libs/installer_fomod_plus/installer/lib/FileInstaller.cpp
deleted file mode 100644
index e47e088..0000000
--- a/libs/installer_fomod_plus/installer/lib/FileInstaller.cpp
+++ /dev/null
@@ -1,274 +0,0 @@
-#include "FileInstaller.h"
-
-#include <utility>
-
-#include "ui/FomodViewModel.h"
-
-using namespace MOBase;
-
-FileInstaller::FileInstaller(
- IOrganizer* organizer,
- QString fomodPath,
- const std::shared_ptr<IFileTree>& fileTree,
- std::unique_ptr<ModuleConfiguration> fomodFile,
- const std::shared_ptr<FlagMap>& flagMap,
- const std::vector<std::shared_ptr<StepViewModel> >& steps) : mOrganizer(organizer),
- mFomodPath(std::move(fomodPath)),
- mFileTree(fileTree),
- mFomodFile(std::move(fomodFile)),
- mFlagMap(flagMap),
- mConditionTester(organizer), mSteps(steps)
-{
- const auto requiredCount =
- (mFomodFile != nullptr) ? mFomodFile->requiredInstallFiles.files.size() : 0;
- const auto stepCount = mSteps.size();
- logMessage(DEBUG,
- "FileInstaller constructed. steps=" + std::to_string(stepCount) + ", required files=" +
- std::to_string(requiredCount) + ", fomodPath=" + mFomodPath.toStdString());
-}
-
-std::shared_ptr<IFileTree> FileInstaller::install() const
-{
- logMessage(DEBUG, "Starting FileInstaller::install()");
- const auto filesToInstall = collectFilesToInstall();
- logMessage(INFO, "Installing " + std::to_string(filesToInstall.size()) + " files");
- logMessage(INFO, "FlagMap");
- logMessage(INFO, mFlagMap->toString());
-
- // update the file tree with the new files
- const std::shared_ptr<IFileTree> installTree = mFileTree->createOrphanTree();
-
- for (const auto& file : filesToInstall) {
-
- logMessage(DEBUG,
- "Processing install entry source=" + file.source + ", destination=" +
- (file.destination.has_value() ? file.destination.value() : "<default>") +
- ", priority=" + std::to_string(file.priority));
- const auto sourcePath = getQualifiedFilePath(file.source);
- const auto sourceNode = mFileTree->find(QString::fromStdString(sourcePath));
- if (sourceNode == nullptr) {
- logMessage(ERR, "Could not find source: " + file.source);
- continue;
- }
- const auto targetPath = file.destination.has_value()
- ? QString::fromStdString(file.destination.value())
- : QString::fromStdString(sourcePath);
-
- // If it's a folder, copy the contents of the folder, not the folder itself.
- if (sourceNode->isDir()) {
- logMessage(DEBUG,
- "Copying directory '" + file.source + "' into target '" + targetPath.toStdString() +
- "'");
- // TODO: Check if target path is literally undefined/null
- const auto& tree = sourceNode->astree();
- for (auto it = tree->begin(); it != tree->end(); ++it) {
- const auto& entry = *it;
- const auto path = targetPath.isEmpty() ? entry->name() : targetPath + "/" + entry->name();
- logMessage(DEBUG,
- " Copying entry '" + entry->name().toStdString() + "' to '" +
- path.toStdString() + "'");
- installTree->copy(entry, path, IFileTree::InsertPolicy::MERGE);
- }
- } else {
- logMessage(DEBUG,
- "Copying file '" + file.source + "' to '" + targetPath.toStdString() + "'");
- installTree->copy(sourceNode, targetPath, IFileTree::InsertPolicy::MERGE);
- }
- }
-
- // This file will be written by the InstallationManager later.
- const auto jsonFilePath = "fomod.json";
- installTree->addFile(QString::fromStdString(jsonFilePath), true);
- logMessage(DEBUG, "Added fomod.json placeholder file to install tree.");
-
- logMessage(DEBUG, "FileInstaller::install completed.");
- return installTree;
-}
-
-nlohmann::json FileInstaller::generateFomodJson() const
-{
- nlohmann::json fomodJson;
- logMessage(DEBUG, "Generating fomod.json representation for " + std::to_string(mSteps.size()) +
- " steps.");
-
- fomodJson["steps"] = nlohmann::json::array();
- for (const auto& stepViewModel : mSteps) {
- auto stepJson = nlohmann::json::object();
- stepJson["name"] = stepViewModel->getName();
- stepJson["groups"] = nlohmann::json::array();
- logMessage(DEBUG, "Serializing step '" + stepViewModel->getName() + "'");
-
- for (const auto& groupViewModel : stepViewModel->getGroups()) {
- auto groupJson = nlohmann::json::object();
- groupJson["name"] = groupViewModel->getName();
- auto pluginArray = nlohmann::json::array();
- auto deselectedArray = nlohmann::json::array();
- logMessage(DEBUG,
- " Serializing group '" + groupViewModel->getName() +
- "' with " + std::to_string(groupViewModel->getPlugins().size()) + " plugins");
-
- for (const auto& pluginViewModel : groupViewModel->getPlugins()) {
- logMessage(DEBUG,
- " Plugin '" + pluginViewModel->getName() + "' selected=" +
- (pluginViewModel->isSelected() ? "true" : "false") + ", manually-set=" +
- (pluginViewModel->wasManuallySet() ? "true" : "false"));
- if (pluginViewModel->isSelected()) {
- pluginArray.emplace_back(pluginViewModel->getName());
- }
- // Add deselected plugins here. (TODO: This will be replaced with an embedded db.)
- if (!pluginViewModel->isSelected() && pluginViewModel->wasManuallySet()) {
- deselectedArray.emplace_back(pluginViewModel->getName());
- }
- }
- groupJson["plugins"] = pluginArray;
- groupJson["deselected"] = deselectedArray;
- stepJson["groups"].emplace_back(groupJson);
- }
- fomodJson["steps"].emplace_back(stepJson);
- }
- return fomodJson;
-}
-
-std::string FileInstaller::getQualifiedFilePath(const std::string& treePath) const
-{
- // We need to prepend the fomod path to whatever source we reference. Guess we're passing that path around.
- const auto qualifiedPath = mFomodPath.toStdString() + "/" + treePath;
- logMessage(DEBUG, "Qualifying tree path '" + treePath + "' to '" + qualifiedPath + "'");
- return qualifiedPath;
-}
-
-std::vector<std::string> FileInstaller::collectPositiveFileNamesFromDependencyPatterns(
- const std::vector<DependencyPattern>& patterns)
-{
- std::vector<std::string> usableFileDependencyPluginNames = {};
- logMessage(DEBUG,
- "Collecting positive file names from " + std::to_string(patterns.size()) + " patterns.");
-
- for (const auto& pattern : patterns) {
- if (pattern.type == PluginTypeEnum::NotUsable) {
- logMessage(DEBUG, "Skipping pattern marked as NotUsable.");
- continue;
- }
-
- if (pattern.dependencies.fileDependencies.empty() && pattern.dependencies.nestedDependencies.empty()) {
- logMessage(DEBUG, "Skipping pattern with no dependencies.");
- continue;
- }
-
- const auto fileDependencies = pattern.dependencies.fileDependencies;
- const auto nestedDependencies = pattern.dependencies.nestedDependencies;
-
- for (const auto& fileDependency : fileDependencies) {
- if (fileDependency.state != FileDependencyTypeEnum::Active) {
- continue;
- }
- usableFileDependencyPluginNames.emplace_back(fileDependency.file);
- logMessage(DEBUG, "Adding active file dependency: " + fileDependency.file);
- }
-
- for (const auto& nestedDependency : nestedDependencies) {
- for (const auto& fileDependency : nestedDependency.fileDependencies) {
- if (fileDependency.state != FileDependencyTypeEnum::Active) {
- continue;
- }
- usableFileDependencyPluginNames.emplace_back(fileDependency.file);
- logMessage(DEBUG, "Adding nested active file dependency: " + fileDependency.file);
- }
- }
- // Not handling twice-nested dependencies now. IDK if that's even feasible.
- }
-
- logMessage(DEBUG,
- "CollectPositiveFileNamesFromDependencyPatterns found " +
- std::to_string(usableFileDependencyPluginNames.size()) + " entries.");
- return usableFileDependencyPluginNames;
-}
-
-// Generic vector appender
-void FileInstaller::addFiles(std::vector<File>& main, std::vector<File> toAdd) const
-{
- for (const auto& add : toAdd) {
- logMessage(INFO, "Adding file with source: " + add.source);
- }
- main.insert(main.end(), toAdd.begin(), toAdd.end());
- logMessage(DEBUG,
- "addFiles merged " + std::to_string(toAdd.size()) + " files. Total is now " +
- std::to_string(main.size()));
-}
-
-// TODO: Unclear if we're copying. oh well.
-// TODO: Rebuild flagmap and step indeces before installing
-std::vector<File> FileInstaller::collectFilesToInstall() const
-{
- logMessage(DEBUG, "collectFilesToInstall started.");
- std::vector<File> allFiles;
-
- // Required files from FOMOD
- const FileList requiredInstallFiles = mFomodFile->requiredInstallFiles;
- logMessage(DEBUG,
- "Adding " + std::to_string(requiredInstallFiles.files.size()) +
- " required install files from fomod.");
- addFiles(allFiles, requiredInstallFiles.files);
-
- // Selected files from visible steps
- for (const auto& stepViewModel : mSteps) {
- if (!mConditionTester.testCompositeDependency(mFlagMap, stepViewModel->getVisibilityConditions())) {
- logMessage(DEBUG,
- "Skipping invisible step '" + stepViewModel->getName() + "'");
- continue;
- }
- logMessage(DEBUG,
- "Processing visible step '" + stepViewModel->getName() + "' with " +
- std::to_string(stepViewModel->getGroups().size()) + " groups.");
- for (const auto& groupViewModel : stepViewModel->getGroups()) {
- logMessage(DEBUG,
- " Processing group '" + groupViewModel->getName() + "' with " +
- std::to_string(groupViewModel->getPlugins().size()) + " plugins.");
- for (const auto& pluginViewModel : groupViewModel->getPlugins()) {
- if (pluginViewModel->isSelected()) {
- logMessage(DEBUG,
- " Adding selected plugin '" + pluginViewModel->getName() +
- "' with " +
- std::to_string(pluginViewModel->getPlugin()->files.files.size()) +
- " files.");
- addFiles(allFiles, pluginViewModel->getPlugin()->files.files);
- } else {
- logMessage(DEBUG,
- " Skipping plugin '" + pluginViewModel->getName() +
- "' (not selected).");
- }
- }
- }
- }
-
- // ConditionalInstall files
- for (const auto conditionals = mFomodFile->conditionalFileInstalls; const auto& pattern : conditionals.patterns) {
- //<folder source="CR\Dagi-Raht LL\VLrn_Custom Race - Dagi-Raht LL" destination="" priority="2" />
-
- if (mConditionTester.testCompositeDependency(mFlagMap, pattern.dependencies)) {
- // also check if the plugins setting these flags are visible. at least one
-
- addFiles(allFiles, pattern.files.files);
- logMessage(DEBUG,
- "Conditional install pattern matched; added " +
- std::to_string(pattern.files.files.size()) + " files.");
- } else {
- logMessage(DEBUG, "Conditional install pattern did not match.");
- }
- }
-
- // Files will all have a default priority of 0 if not specified, so the order should also be informed by the
- // order they appear within XML. That's why we put conditionalFileInstalls after.
- logMessage(DEBUG, "Sorting " + std::to_string(allFiles.size()) + " files by priority.");
- std::ranges::sort(allFiles, [](const auto& a, const auto& b) {
- return a.priority < b.priority;
- });
-
- for (const auto& toInstall : allFiles) {
- logMessage(DEBUG, "File to install: " + toInstall.source);
- }
- logMessage(DEBUG,
- "collectFilesToInstall completed with " + std::to_string(allFiles.size()) + " files.");
-
- return allFiles;
-}
diff --git a/libs/installer_fomod_plus/installer/lib/FileInstaller.h b/libs/installer_fomod_plus/installer/lib/FileInstaller.h
deleted file mode 100644
index 1c122a7..0000000
--- a/libs/installer_fomod_plus/installer/lib/FileInstaller.h
+++ /dev/null
@@ -1,94 +0,0 @@
-#pragma once
-
-#include <ifiletree.h>
-#include <nlohmann/json.hpp>
-
-#include "ConditionTester.h"
-#include "FlagMap.h"
-#include "Logger.h"
-#include "xml/ModuleConfiguration.h"
-
-
-/* This is what legacy fomodInstaller does:
- modName.update(dialog.getName(), GUESS_USER);
- return dialog.updateTree(tree);
-
- This is a link to the legacy updateTree method:
- https://github.com/ModOrganizer2/modorganizer-installer_fomod/blob/fc263f2d923c704b4853c11ed4f8b8cf3920f30d/src/fomodinstallerdialog.cpp#L546
-
-*/
-
-// Things this should do:
-// - Copy all requiredInstall files
-// - Copy all conditionalInstall files
-// - Copy all selected files from steps that were visible to the user at the point of install
-
-using namespace MOBase;
-
-using FileGlobalIndex = int;
-using FileDescriptor = std::pair<File, FileGlobalIndex>;
-
-class StepViewModel;
-
-class FileInstaller {
-public:
- FileInstaller(
- IOrganizer* organizer,
- QString fomodPath,
- const std::shared_ptr<IFileTree>& fileTree,
- std::unique_ptr<ModuleConfiguration> fomodFile,
- const std::shared_ptr<FlagMap>& flagMap,
- const std::vector<std::shared_ptr<StepViewModel> >& steps);
-
- std::shared_ptr<IFileTree> install() const;
-
- /**
- * @brief Create a 'fomod.json' file to add to the base of the installTree. Functionally similar to MO2's meta.ini.
- *
- * Until there's more utility in the JSON structure itself, it will simply be of this format:
- * @code
- * {
- * "steps": [
- * {
- * "name": "Step 1",
- * "groups": [
- * {
- * "name": "Group 1",
- * "plugins: [
- * "Plugin1.esp",
- * "Plugin2.esp"
- * ]
- * }
- * ]
- * }
- * ]
- * }
- * @endcode
- *
- * @return nhlohmann::json
- */
- nlohmann::json generateFomodJson() const;
-
- std::string getQualifiedFilePath(const std::string& treePath) const;
-
- std::vector<std::string> collectPositiveFileNamesFromDependencyPatterns(const std::vector<DependencyPattern> &patterns);
-
- void addFiles(std::vector<File>& main, std::vector<File> toAdd) const;
-
-private:
- IOrganizer* mOrganizer;
- Logger& log = Logger::getInstance();
- QString mFomodPath;
- std::shared_ptr<IFileTree> mFileTree;
- std::unique_ptr<ModuleConfiguration> mFomodFile;
- std::shared_ptr<FlagMap> mFlagMap;
- ConditionTester mConditionTester;
- std::vector<std::shared_ptr<StepViewModel> > mSteps; // TODO: Maybe this is nasty. Idk.
-
- std::vector<File> collectFilesToInstall() const;
-
- void logMessage(LogLevel level, const std::string& message) const
- {
- log.logMessage(level, "[INSTALLER] " + message);
- }
-};
diff --git a/libs/installer_fomod_plus/installer/lib/FlagMap.h b/libs/installer_fomod_plus/installer/lib/FlagMap.h
deleted file mode 100644
index 4959cec..0000000
--- a/libs/installer_fomod_plus/installer/lib/FlagMap.h
+++ /dev/null
@@ -1,113 +0,0 @@
-#pragma once
-
-#include "ViewModels.h"
-#include "stringutil.h"
-
-#include <ranges>
-#include <string>
-#include <unordered_map>
-
-using Flag = std::pair<std::string, std::string>;
-using FlagList = std::vector<Flag>;
-
-class FlagMap {
-public:
-
- [[nodiscard]] std::vector<std::shared_ptr<PluginViewModel>> getPluginsSettingFlag(const std::string& key, const std::string& value) const
- {
- std::vector<std::shared_ptr<PluginViewModel>> result;
- for (const auto& [plugin, theseFlags] : flags) {
- for (const auto& [fst, snd] : theseFlags) {
- if (toLower(fst) == toLower(key) && snd == value) {
- result.emplace_back(plugin);
- }
- }
- }
- return result;
- }
-
- /**
- *
- * @param key The flag key
- * @return A list of flags currently set in this map with the given key. The list is ordered by step descending, then plugin ascending.
- * So if steps 1, 2, and 3 set flag X in their first two plugins, it'll be ordered [3:1, 3:2, 2:1, 2:2, 1:2, 1:1]
- */
- [[nodiscard]] FlagList getFlagsByKey(const std::string& key) const
- {
- FlagList result;
- std::vector<std::pair<int, std::shared_ptr<PluginViewModel>>> orderedPlugins;
-
- // Collect all plugins with their stepIndex and ownIndex
- for (const auto& plugin : flags | std::views::keys) {
- orderedPlugins.emplace_back(plugin->getStepIndex(), plugin);
- }
-
- // Sort plugins by stepIndex and ownIndex, stepIndex descending and ownIndex ascending
- // TODO: Might need group sorting too. How can I just do the natural order??
- std::ranges::sort(orderedPlugins, [](const auto& a, const auto& b) {
- return a.first > b.first || (a.first == b.first && a.second->getOwnIndex() < b.second->getOwnIndex());
- });
-
- // Collect flags in the sorted order
- for (const auto& plugin : orderedPlugins | std::views::values) {
- for (const auto& flag : flags.at(plugin)) {
- if (toLower(flag.first) == toLower(key)) {
- result.emplace_back(flag);
- }
- }
- }
- return result;
- }
-
- void setFlagsForPlugin(PluginRef plugin)
- {
- // Don't clutter the map with empty key-vals
- if (plugin->getConditionFlags().empty()) {
- return;
- }
- unsetFlagsForPlugin(plugin);
-
- FlagList flagList;
- for (const auto& conditionFlag : plugin->getConditionFlags()) {
- flagList.emplace_back(toLower(conditionFlag.name), conditionFlag.value);
- }
- flags[plugin] = flagList;
- }
-
- void unsetFlagsForPlugin(PluginRef plugin)
- {
- if (flags.contains(plugin)) {
- flags.erase(plugin);
- }
- }
-
- std::string toString()
- {
- auto result = std::string();
- result += "FlagMap:\n";
-
- for (const auto& [plugin, theseFlags] : flags) {
- result += plugin->getName() + " [";
- for (const auto& [fst, snd] : theseFlags) {
- result += fst + ": " + snd + ", ";
- }
- result.erase(result.size() - 2);
- result += "]\n";
- }
- return result;
- }
-
- void clearAll()
- {
- flags.clear();
- }
-
- [[nodiscard]] size_t getFlagCount() const
- {
- return flags.size();
- }
-
-
-private:
- std::unordered_map<std::shared_ptr<PluginViewModel>, FlagList> flags;
-};
diff --git a/libs/installer_fomod_plus/installer/lib/Logger.h b/libs/installer_fomod_plus/installer/lib/Logger.h
deleted file mode 100644
index 53fb7d9..0000000
--- a/libs/installer_fomod_plus/installer/lib/Logger.h
+++ /dev/null
@@ -1,116 +0,0 @@
-#pragma once
-#include <fstream>
-#include <iostream>
-#include <mutex>
-#include <regex>
-
-
-// Log levels
-enum LogLevel {
- DEBUG = 0,
- INFO = 1,
- WARN = 2,
- ERR = 3
-};
-
-// Convert LogLevel to string
-inline const char* logLevelToString(const LogLevel level) {
- switch (level) {
- case DEBUG: return "DEBUG";
- case INFO: return "INFO";
- case WARN: return "WARN";
- case ERR: return "ERROR";
- default: return "UNKNOWN";
- }
-}
-
-
-class Logger {
-public:
- static Logger& getInstance()
- {
- static Logger instance;
- return instance;
- }
-
- void setLogFilePath(const std::string& filePath)
- {
- std::lock_guard lock(mMutex);
- if (mLogFile.is_open()) {
- mLogFile.close();
- }
- mLogFile.open(filePath, std::ios::out);
- // std::ios::app is an option for appending but dont wanna grow it forever.
- }
-
- void setDebugMode(bool debug)
- {
- std::lock_guard lock(mMutex);
- mDebugMode = debug;
- }
-
- void logMessage(const LogLevel level, const std::string& message)
- {
-
-#if defined(__GNUC__) || defined(__clang__)
- std::string functionName = __PRETTY_FUNCTION__;
-#elif defined(_MSC_VER)
- std::string functionName = __FUNCSIG__;
-#else
- std::string functionName = "UnknownFunction";
-#endif
-
- std::regex classNameRegex(R"((\w+)::\w+\()");
- std::smatch match;
- std::string className = "UnknownClass";
-
- if (std::regex_search(functionName, match, classNameRegex) && match.size() > 1) {
- className = match.str(1);
- }
-
- std::lock_guard lock(mMutex);
-
- auto writeLog = [&](std::ostream& stream) {
- switch (level) {
- case DEBUG:
- stream << "[DEBUG] " << message << std::endl;
- break;
- case INFO:
- stream << "[INFO] " << message << std::endl;
- break;
- case WARN:
- stream << "[WARN] " << message << std::endl;
- break;
- case ERR:
- stream << "[ERROR] " << message << std::endl;
- break;
- }
- };
-
- if (mLogFile.is_open()) {
- writeLog(mLogFile);
- }
- }
-
- Logger& operator=(const Logger&) = delete;
-
-private:
- Logger() = default;
-
- ~Logger()
- {
- if (mLogFile.is_open()) {
- mLogFile.close();
- }
- }
-
- Logger(const Logger&) = delete;
-
- std::ofstream mLogFile;
- std::mutex mMutex;
-#if !defined(NDEBUG) || defined(CMAKE_BUILD_TYPE_RELWITHDEBINFO)
- bool mDebugMode = true; // Auto-enable in debug/RelWithDebInfo builds
-#else
- bool mDebugMode = false; // Disable in release builds
-#endif
-}; \ No newline at end of file
diff --git a/libs/installer_fomod_plus/installer/lib/ViewModels.h b/libs/installer_fomod_plus/installer/lib/ViewModels.h
deleted file mode 100644
index 062445a..0000000
--- a/libs/installer_fomod_plus/installer/lib/ViewModels.h
+++ /dev/null
@@ -1,120 +0,0 @@
-#pragma once
-
-#include <memory>
-#include <vector>
-
-#include "xml/ModuleConfiguration.h"
-
-
-template <typename T>
-using shared_ptr_list = std::vector<std::shared_ptr<T> >;
-
-
-/*
---------------------------------------------------------------------------------
- Plugins
---------------------------------------------------------------------------------
-*/
-class PluginViewModel {
-public:
- PluginViewModel(const std::shared_ptr<Plugin>& plugin_, const bool selected, bool, const int index)
- : ownIndex(index), selected(selected), enabled(true), manuallySet(false), plugin(plugin_) {}
-
- void setSelected(const bool selected) { this->selected = selected; }
- void setEnabled(const bool enabled) { this->enabled = enabled; }
- [[nodiscard]] std::string getName() const { return plugin ? plugin->name : std::string(); }
- [[nodiscard]] std::string getDescription() const { return plugin ? plugin->description : std::string(); }
- [[nodiscard]] std::string getImagePath() const { return plugin ? plugin->image.path : std::string(); }
- [[nodiscard]] bool isSelected() const { return selected; }
- [[nodiscard]] bool isEnabled() const { return enabled; }
- [[nodiscard]] int getOwnIndex() const { return ownIndex; }
- [[nodiscard]] std::vector<ConditionFlag> getConditionFlags() const { return plugin->conditionFlags.flags; }
- [[nodiscard]] PluginTypeEnum getCurrentPluginType() const { return currentPluginType; }
- [[nodiscard]] bool wasManuallySet() const { return manuallySet; }
-
- void setCurrentPluginType(const PluginTypeEnum type) { currentPluginType = type; }
- void setStepIndex(const int stepIndex) { this->stepIndex = stepIndex; }
- void setGroupIndex(const int groupIndex) { this->groupIndex = groupIndex; }
-
- [[nodiscard]] int getStepIndex() const { return stepIndex; }
- [[nodiscard]] int getGroupIndex() const { return groupIndex; }
-
- friend class FomodViewModel;
- friend class FileInstaller;
- friend class ConditionTester;
-
-protected:
- [[nodiscard]] std::shared_ptr<Plugin> getPlugin() const { return plugin; }
-
-private:
- int ownIndex;
- bool selected;
- bool enabled;
- bool manuallySet;
- PluginTypeEnum currentPluginType = PluginTypeEnum::UNKNOWN;
- std::shared_ptr<Plugin> plugin;
-
- int stepIndex{ -1 };
- int groupIndex{ -1 };
-};
-
-/*
---------------------------------------------------------------------------------
- Groups
---------------------------------------------------------------------------------
-*/
-class GroupViewModel {
-public:
- GroupViewModel(const std::shared_ptr<Group>& group_, const shared_ptr_list<PluginViewModel>& plugins,
- const int index, const int stepIndex)
- : plugins(plugins), group(group_), ownIndex(index), stepIndex(stepIndex) {}
-
- void addPlugin(const std::shared_ptr<PluginViewModel>& plugin) { plugins.emplace_back(plugin); }
-
- [[nodiscard]] std::string getName() const { return group->name; }
- [[nodiscard]] GroupTypeEnum getType() const { return group->type; }
- [[nodiscard]] const shared_ptr_list<PluginViewModel>& getPlugins() const { return plugins; }
- [[nodiscard]] int getOwnIndex() const { return ownIndex; }
- [[nodiscard]] int getStepIndex() const { return stepIndex; }
-
-private:
- shared_ptr_list<PluginViewModel> plugins;
- std::shared_ptr<Group> group;
- int ownIndex;
- int stepIndex;
-};
-
-/*
---------------------------------------------------------------------------------
- Steps
---------------------------------------------------------------------------------
-*/
-class StepViewModel {
-public:
- StepViewModel(const std::shared_ptr<InstallStep>& installStep_, const shared_ptr_list<GroupViewModel>& groups,
- const int index)
- : installStep(installStep_), groups(groups), ownIndex(index) {}
-
- [[nodiscard]] CompositeDependency& getVisibilityConditions() const { return installStep->visible; }
- [[nodiscard]] std::string getName() const { return installStep->name; }
- [[nodiscard]] const shared_ptr_list<GroupViewModel>& getGroups() const { return groups; }
- [[nodiscard]] int getOwnIndex() const { return ownIndex; }
- [[nodiscard]] bool getHasVisited() const { return visited; }
- void setVisited(const bool visited) { this->visited = visited; }
-
-private:
- bool visited{ false };
- std::shared_ptr<InstallStep> installStep;
- shared_ptr_list<GroupViewModel> groups;
- int ownIndex;
-};
-
-
-/*
---------------------------------------------------------------------------------
- Outbound Types
---------------------------------------------------------------------------------
-*/
-using StepRef = const std::shared_ptr<StepViewModel>&;
-using GroupRef = const std::shared_ptr<GroupViewModel>&;
-using PluginRef = const std::shared_ptr<PluginViewModel>&;
diff --git a/libs/installer_fomod_plus/installer/resources.qrc b/libs/installer_fomod_plus/installer/resources.qrc
deleted file mode 100644
index 6c583a7..0000000
--- a/libs/installer_fomod_plus/installer/resources.qrc
+++ /dev/null
@@ -1,14 +0,0 @@
-<RCC>
- <qresource prefix="/fomod">
- <file alias="hat">resources/fomod_icon.png</file>
- </qresource>
- <qresource prefix="/fomod">
- <file alias="previous">resources/left-chevron.png</file>
- </qresource>
- <qresource prefix="/fomod">
- <file alias="next">resources/right-chevron.png</file>
- </qresource>
- <qresource prefix="/fomod">
- <file alias="close">resources/delete.png</file>
- </qresource>
-</RCC> \ No newline at end of file
diff --git a/libs/installer_fomod_plus/installer/resources/delete.png b/libs/installer_fomod_plus/installer/resources/delete.png
deleted file mode 100644
index f6502b1..0000000
--- a/libs/installer_fomod_plus/installer/resources/delete.png
+++ /dev/null
Binary files differ
diff --git a/libs/installer_fomod_plus/installer/resources/fomod_icon.png b/libs/installer_fomod_plus/installer/resources/fomod_icon.png
deleted file mode 100644
index e044052..0000000
--- a/libs/installer_fomod_plus/installer/resources/fomod_icon.png
+++ /dev/null
Binary files differ
diff --git a/libs/installer_fomod_plus/installer/resources/image_icon.png b/libs/installer_fomod_plus/installer/resources/image_icon.png
deleted file mode 100644
index b300751..0000000
--- a/libs/installer_fomod_plus/installer/resources/image_icon.png
+++ /dev/null
Binary files differ
diff --git a/libs/installer_fomod_plus/installer/resources/left-chevron.png b/libs/installer_fomod_plus/installer/resources/left-chevron.png
deleted file mode 100644
index 28222a1..0000000
--- a/libs/installer_fomod_plus/installer/resources/left-chevron.png
+++ /dev/null
Binary files differ
diff --git a/libs/installer_fomod_plus/installer/resources/right-chevron.png b/libs/installer_fomod_plus/installer/resources/right-chevron.png
deleted file mode 100644
index 1a0af49..0000000
--- a/libs/installer_fomod_plus/installer/resources/right-chevron.png
+++ /dev/null
Binary files differ
diff --git a/libs/installer_fomod_plus/installer/ui/ClickableWidget.h b/libs/installer_fomod_plus/installer/ui/ClickableWidget.h
deleted file mode 100644
index 881600f..0000000
--- a/libs/installer_fomod_plus/installer/ui/ClickableWidget.h
+++ /dev/null
@@ -1,24 +0,0 @@
-#pragma once
-
-#include <QMouseEvent>
-#include <QWidget>
-
-class ClickableWidget final : public QWidget {
- Q_OBJECT
-
-public:
- explicit ClickableWidget(QWidget* parent = nullptr) : QWidget(parent) {
- setCursor(Qt::PointingHandCursor);
- }
-
- signals:
- void clicked();
-
-protected:
- void mousePressEvent(QMouseEvent* event) override {
- if (event->button() == Qt::LeftButton) {
- emit clicked();
- }
- QWidget::mousePressEvent(event);
- }
-};
diff --git a/libs/installer_fomod_plus/installer/ui/Colors.h b/libs/installer_fomod_plus/installer/ui/Colors.h
deleted file mode 100644
index c3262eb..0000000
--- a/libs/installer_fomod_plus/installer/ui/Colors.h
+++ /dev/null
@@ -1,155 +0,0 @@
-#pragma once
-
-#include <QString>
-#include <map>
-
-namespace UiColors {
-
-enum class ColorApplication {
- BACKGROUND,
- BORDER,
- TEXT,
- ALL
-};
-
-// Color values
-namespace Colors {
- // Light
- const QString Light0 = "251, 241, 199";
- const QString Light1 = "235, 219, 178";
- const QString Light2 = "213, 196, 161";
- const QString Light3 = "189, 174, 147";
-
- // Dark
- const QString Dark0 = "40, 40, 40";
- const QString Dark1 = "60, 56, 54";
- const QString Dark2 = "80, 73, 69";
- const QString Dark3 = "102, 92, 84";
-
- // Red
- const QString Red = "204, 36, 29";
- const QString RedBright = "251, 73, 52";
-
- // Green
- const QString Green = "152, 151, 26";
- const QString GreenBright = "184, 187, 38";
-
- // Yellow
- const QString Yellow = "215, 153, 33";
- const QString YellowBright = "250, 189, 47";
-
- // Blue
- const QString Blue = "69, 133, 136";
- const QString BlueBright = "131, 165, 152";
-
- // Purple
- const QString Purple = "177, 98, 134";
- const QString PurpleBright = "211, 134, 155";
-
- // Aqua
- const QString Aqua = "104, 157, 106";
- const QString AquaBright = "142, 192, 124";
-
- // Orange
- const QString Orange = "214, 93, 14";
- const QString OrangeBright = "254, 128, 25";
-}
-
-// Helper function to generate style strings based on color and application
-inline QString generateStyle(const QString& color, const ColorApplication application, const float opacity = 0.4,
- const int borderWidth = 1)
-{
- QString style;
-
- switch (application) {
- case ColorApplication::BACKGROUND:
- style = QString("QCheckBox { background-color: rgba(%1, %2); } "
- "QRadioButton { background-color: rgba(%1, %2); }")
- .arg(color).arg(opacity);
- break;
-
- case ColorApplication::BORDER:
- style = QString("QCheckBox { border: %1px dashed rgb(%2); } "
- "QRadioButton { border: %1px dashed rgb(%2); }")
- .arg(borderWidth).arg(color);
- break;
-
- case ColorApplication::TEXT:
- style = QString("QCheckBox { color: rgb(%1); } "
- "QRadioButton { color: rgb(%1); }")
- .arg(color);
- break;
-
- case ColorApplication::ALL:
- style = QString("QCheckBox { background-color: rgba(%1, %2); border: %3px solid rgb(%1); color: rgb(%1); } "
- "QRadioButton { background-color: rgba(%1, %2); border: %3px solid rgb(%1); color: rgb(%1); }")
- .arg(color).arg(opacity).arg(borderWidth);
- break;
- }
-
- return style;
-}
-
-// Main function to get style for a color name and application
-inline QString getStyle(const QString& colorName, const ColorApplication application = ColorApplication::BACKGROUND,
- const float opacity = 0.4, const int borderWidth = 1)
-{
- static const std::map<QString, QString> colorValues = {
- { "Light0", Colors::Light0 },
- { "Light1", Colors::Light1 },
- { "Light2", Colors::Light2 },
- { "Light3", Colors::Light3 },
- { "Dark0", Colors::Dark0 },
- { "Dark1", Colors::Dark1 },
- { "Dark2", Colors::Dark2 },
- { "Dark3", Colors::Dark3 },
- { "Red", Colors::Red },
- { "Red Bright", Colors::RedBright },
- { "Green", Colors::Green },
- { "Green Bright", Colors::GreenBright },
- { "Yellow", Colors::Yellow },
- { "Yellow Bright", Colors::YellowBright },
- { "Blue", Colors::Blue },
- { "Blue Bright", Colors::BlueBright },
- { "Purple", Colors::Purple },
- { "Purple Bright", Colors::PurpleBright },
- { "Aqua", Colors::Aqua },
- { "Aqua Bright", Colors::AquaBright },
- { "Orange", Colors::Orange },
- { "Orange Bright", Colors::OrangeBright }
- };
-
- if (const auto it = colorValues.find(colorName); it != colorValues.end()) {
- return generateStyle(it->second, application, opacity, borderWidth);
- }
-
- return {};
-}
-
-// For backward compatibility
-const static std::map<QString, QString> colorStyles = {
- { "Light0", getStyle("Light0", ColorApplication::BACKGROUND) },
- { "Light1", getStyle("Light1", ColorApplication::BACKGROUND) },
- { "Light2", getStyle("Light2", ColorApplication::BACKGROUND) },
- { "Light3", getStyle("Light3", ColorApplication::BACKGROUND) },
- { "Dark0", getStyle("Dark0", ColorApplication::BACKGROUND) },
- { "Dark1", getStyle("Dark1", ColorApplication::BACKGROUND) },
- { "Dark2", getStyle("Dark2", ColorApplication::BACKGROUND) },
- { "Dark3", getStyle("Dark3", ColorApplication::BACKGROUND) },
- { "Red", getStyle("Red", ColorApplication::BACKGROUND) },
- { "Red Bright", getStyle("Red Bright", ColorApplication::BACKGROUND) },
- { "Green", getStyle("Green", ColorApplication::BACKGROUND) },
- { "Green Bright", getStyle("Green Bright", ColorApplication::BACKGROUND) },
- { "Yellow", getStyle("Yellow", ColorApplication::BACKGROUND) },
- { "Yellow Bright", getStyle("Yellow Bright", ColorApplication::BACKGROUND) },
- { "Blue", getStyle("Blue", ColorApplication::BACKGROUND) },
- { "Blue Bright", getStyle("Blue Bright", ColorApplication::BACKGROUND) },
- { "Purple", getStyle("Purple", ColorApplication::BACKGROUND) },
- { "Purple Bright", getStyle("Purple Bright", ColorApplication::BACKGROUND) },
- { "Aqua", getStyle("Aqua", ColorApplication::BACKGROUND) },
- { "Aqua Bright", getStyle("Aqua Bright", ColorApplication::BACKGROUND) },
- { "Orange", getStyle("Orange", ColorApplication::BACKGROUND) },
- { "Orange Bright", getStyle("Orange Bright", ColorApplication::BACKGROUND) }
-};
-
-}
diff --git a/libs/installer_fomod_plus/installer/ui/FomodImageViewer.cpp b/libs/installer_fomod_plus/installer/ui/FomodImageViewer.cpp
deleted file mode 100644
index cb99d27..0000000
--- a/libs/installer_fomod_plus/installer/ui/FomodImageViewer.cpp
+++ /dev/null
@@ -1,284 +0,0 @@
-#include "FomodImageViewer.h"
-
-#include "ScaleLabel.h"
-#include "UIHelper.h"
-
-#include <QLabel>
-#include <QScrollArea>
-#include <QtConcurrent/QtConcurrent>
-
-constexpr int PREVIEW_IMAGE_WIDTH = 160;
-constexpr int PREVIEW_IMAGE_HEIGHT = 90;
-
-/*
-+----------------------------------------------------------+
-|n/N X |
-+---+--------------------------------------------------+---+
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| < | Image | > |
-| | | |
-| | | |
-| | | |
-| | | |
-| | label | |
-+------+------+------+---------------------------------+---+
-| | | | ...previews |
-| | | | |
-+------+------+------+-------------------------------------+
-*/
-constexpr auto BUTTON_STYLE =
- "font-size: 16px; font-weight: bold; color: white; background-color: black; padding: 5px; border-radius: 1px solid black;";
-
-FomodImageViewer::FomodImageViewer(QWidget* parent,
- const QString& fomodPath,
- const std::shared_ptr<StepViewModel>& activeStep,
- const std::shared_ptr<PluginViewModel>& activePlugin) : QDialog(parent), mFomodPath(fomodPath),
- mActiveStep(activeStep),
- mActivePlugin(activePlugin)
-{
-
- setWindowFlags(Qt::FramelessWindowHint | Qt::Window);
- setAttribute(Qt::WA_TranslucentBackground);
- setStyleSheet("background-color: rgba(0, 0, 0, 150);");
-
- const QScreen* screen = this->screen();
- const QRect availableGeometry = screen->availableGeometry();
- setFixedSize(availableGeometry.width(), availableGeometry.height());
- move(availableGeometry.x(), availableGeometry.y());
-
- collectImages();
- mMainImageWrapper = createSinglePhotoPane(this);
- mTopBar = createTopBar(this);
- mPreviewImages = createPreviewImages(this);
- mCenterRow = createCenterRow(this);
-
- const auto layout = new QVBoxLayout(this);
- layout->setContentsMargins(0, 0, 0, 0); // Remove margins
- layout->setSpacing(0); // Remove spacing between widgets
- layout->addWidget(mTopBar);
- layout->addWidget(mCenterRow, 1);
- layout->addWidget(mPreviewImages);
- setLayout(layout);
-
- select(mCurrentIndex);
-
- setFocusPolicy(Qt::StrongFocus); // so we can receive key events
-}
-
-void FomodImageViewer::collectImages()
-{
- mLabelsAndImages.clear();
- for (const auto& groupViewModel : mActiveStep->getGroups()) {
- for (const auto& pluginViewModel : groupViewModel->getPlugins()) {
- if (pluginViewModel->getImagePath().empty()) {
- continue;
- }
- QString imagePath = UIHelper::getFullImagePath(mFomodPath,
- QString::fromStdString(pluginViewModel->getImagePath()));
- mLabelsAndImages.emplace_back(QString::fromStdString(pluginViewModel->getName()), imagePath);
-
- if (pluginViewModel == mActivePlugin) {
- mCurrentIndex = static_cast<int>(mLabelsAndImages.size()) - 1;
- }
- }
- }
-}
-
-QWidget* FomodImageViewer::createCenterRow(QWidget* parent)
-{
- const auto centerRow = new QWidget(parent);
- const auto layout = new QHBoxLayout(centerRow);
-
- mBackButton = createBackButton(centerRow);
- mForwardButton = createForwardButton(centerRow);
-
- mBackButton->setFocusPolicy(Qt::NoFocus);
- mForwardButton->setFocusPolicy(Qt::NoFocus);
-
- layout->addWidget(mBackButton);
- layout->addWidget(mMainImageWrapper, 1);
- layout->addWidget(mForwardButton);
-
- return centerRow;
-}
-
-// ReSharper disable once CppMemberFunctionMayBeStatic
-QWidget* FomodImageViewer::createSinglePhotoPane(QWidget* parent)
-{
- const auto singlePhotoPane = new QWidget(parent);
- const auto layout = new QVBoxLayout(singlePhotoPane);
-
- // const auto [labelText, imagePath] = pair;
-
- mMainImage = new ScaleLabel(singlePhotoPane);
- mMainImage->setAlignment(Qt::AlignCenter);
- layout->addWidget(mMainImage, 1);
-
- mLabel = new QLabel(singlePhotoPane);
- // mLabel->setText(labelText);
- mLabel->setAlignment(Qt::AlignCenter);
- mLabel->setStyleSheet("color: white; font-size: 20px;");
- layout->addWidget(mLabel);
-
- return singlePhotoPane;
-}
-
-QScrollArea* FomodImageViewer::createPreviewImages(QWidget* parent)
-{
- mImagePanes.clear();
- const auto previewImages = new QScrollArea(parent);
- const auto widget = new QWidget(previewImages);
- const auto layout = new QHBoxLayout(previewImages);
-
- layout->setContentsMargins(0, 0, 0, 0);
- layout->setSpacing(0);
-
- for (int i = 0; i < mLabelsAndImages.size(); i++) {
- const auto imageLabel = new ScaleLabel(previewImages);
- imageLabel->setAlignment(Qt::AlignCenter);
- imageLabel->setFixedSize(PREVIEW_IMAGE_WIDTH, PREVIEW_IMAGE_HEIGHT);
- imageLabel->setFocusPolicy(Qt::NoFocus);
- connect(imageLabel, &ScaleLabel::clicked, this, [this, i] {
- select(i);
- });
- layout->addWidget(imageLabel);
- mImagePanes.emplace_back(imageLabel);
-
- // imageLabel->setScalableResource(mLabelsAndImages[i].second);
- const auto imagePath = mLabelsAndImages[i].second;
-
- QThreadPool::globalInstance()->start([imageLabel, imagePath]() {
- QMetaObject::invokeMethod(imageLabel, [imageLabel, imagePath]() {
- imageLabel->setScalableResource(imagePath);
- }, Qt::QueuedConnection);
- });
- }
-
- widget->setLayout(layout);
- previewImages->setFixedHeight(PREVIEW_IMAGE_HEIGHT + 10);
- previewImages->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
- previewImages->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
- previewImages->setFocusPolicy(Qt::NoFocus);
-
- previewImages->setWidget(widget);
- previewImages->setStyleSheet("QScrollArea { border: none; }");
-
- return previewImages;
-}
-
-QPushButton* FomodImageViewer::createBackButton(QWidget* parent) const
-{
- const auto backButton = new QPushButton(parent);
- backButton->setText("<");
- backButton->setStyleSheet(BUTTON_STYLE);
- connect(backButton, &QPushButton::clicked, this, &FomodImageViewer::goBack);
- return backButton;
-}
-
-QPushButton* FomodImageViewer::createForwardButton(QWidget* parent) const
-{
- const auto forwardButton = new QPushButton(parent);
- forwardButton->setText(">");
- forwardButton->
- setStyleSheet(BUTTON_STYLE);
- connect(forwardButton, &QPushButton::clicked, this, &FomodImageViewer::goForward);
- return forwardButton;
-}
-
-QWidget* FomodImageViewer::createTopBar(QWidget* parent)
-{
- const auto topBar = new QWidget(parent);
- const auto layout = new QHBoxLayout(topBar);
-
- // counter, spacer, close button
- mCounter = new QLabel(topBar);
- mCounter->setStyleSheet(BUTTON_STYLE);
- layout->addWidget(mCounter);
-
- layout->addStretch();
-
- mCloseButton = createCloseButton(topBar);
- layout->addWidget(mCloseButton);
- return topBar;
-}
-
-QPushButton* FomodImageViewer::createCloseButton(QWidget* parent)
-{
- const auto closeButton = new QPushButton(parent);
- // const QIcon icon(":/fomod/close");
- // closeButton->setIcon(icon);
- closeButton->setText("X");
- closeButton->setStyleSheet("color: white; background-color: black; padding: 5px; border-radius: 1px solid black;");
- connect(closeButton, &QPushButton::clicked, this, &FomodImageViewer::close);
- return closeButton;
-}
-
-void FomodImageViewer::updateCounterText() const
-{
- mCounter->setText(QString::number(mCurrentIndex + 1) + "/" + QString::number(mLabelsAndImages.size()));
-}
-
-void FomodImageViewer::goBack()
-{
- if (mCurrentIndex == 0) {
- return;
- }
- select(--mCurrentIndex);
-}
-
-void FomodImageViewer::goForward()
-{
- if (mCurrentIndex == mLabelsAndImages.size() - 1) {
- return;
- }
- select(++mCurrentIndex);
-}
-
-void FomodImageViewer::select(const int index)
-{
- if (index < 0 || index >= mLabelsAndImages.size()) {
- return;
- }
-
- // Remove border from previously selected image
- mCurrentIndex = index; // check for bounds?
- updateCounterText();
-
- for (int i = 0; i < mImagePanes.size(); i++) {
- if (i == mCurrentIndex) {
- mImagePanes[i]->setStyleSheet("border: 2px solid white;");
- } else {
- mImagePanes[i]->setStyleSheet("");
- }
- }
-
- const auto& imagePath = mLabelsAndImages[index].second;
- const auto& labelText = mLabelsAndImages[index].first;
- mLabel->setText(labelText);
- mMainImage->setScalableResource(imagePath);
-}
-
-void FomodImageViewer::keyPressEvent(QKeyEvent* event)
-{
- switch (event->key()) {
- case Qt::Key_Left:
- goBack();
- break;
- case Qt::Key_Right:
- goForward();
- break;
- default:
- QDialog::keyPressEvent(event);
- }
-}
-
-void FomodImageViewer::showEvent(QShowEvent* event)
-{
- QDialog::showEvent(event);
- setFocus();
-} \ No newline at end of file
diff --git a/libs/installer_fomod_plus/installer/ui/FomodImageViewer.h b/libs/installer_fomod_plus/installer/ui/FomodImageViewer.h
deleted file mode 100644
index 73a06f7..0000000
--- a/libs/installer_fomod_plus/installer/ui/FomodImageViewer.h
+++ /dev/null
@@ -1,92 +0,0 @@
-#pragma once
-
-#include "FomodViewModel.h"
-
-#include <QDialog>
-#include <QKeyEvent>
-#include <QLabel>
-#include <QScrollArea>
-
-class ScaleLabel;
-using LabelImagePair = std::pair<QString, QString>;
-
-
-/*
-+----------------------------------------------------------+
-|n/N X |
-+---+--------------------------------------------------+---+
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| < | Image | > |
-| | | |
-| | | |
-| | | |
-| | | |
-| | label | |
-+------+------+------+---------------------------------+---+
-| | | | ...previews |
-| | | | |
-+------+------+------+-------------------------------------+
-*/
-
-class FomodImageViewer final : public QDialog {
- Q_OBJECT
-
-public:
- explicit FomodImageViewer(QWidget* parent,
- const QString& fomodPath,
- const std::shared_ptr<StepViewModel>& activeStep,
- const std::shared_ptr<PluginViewModel>& activePlugin);
-
-private:
- void collectImages();
-
- QWidget* createCenterRow(QWidget* parent);
-
- QWidget* createSinglePhotoPane(QWidget* parent);
-
- QScrollArea* createPreviewImages(QWidget* parent);
-
- QPushButton* createBackButton(QWidget* parent) const;
-
- QPushButton* createForwardButton(QWidget* parent) const;
-
- QWidget* createTopBar(QWidget* parent);
-
- QPushButton* createCloseButton(QWidget* parent);
-
- void updateCounterText() const;
-
- void goBack();
-
- void goForward();
-
- void select(int index);
-
- void keyPressEvent(QKeyEvent* event) override;
-
- void showEvent(QShowEvent* event) override;
-
- std::vector<LabelImagePair> mLabelsAndImages;
- std::vector<QWidget*> mImagePanes{};
- int mCurrentIndex{ 0 };
-
- QString mFomodPath;
- const std::shared_ptr<StepViewModel>& mActiveStep;
- const std::shared_ptr<PluginViewModel>& mActivePlugin;
-
- QWidget* mCenterRow{ nullptr };
- QWidget* mTopBar{ nullptr };
- QWidget* mCloseButton{ nullptr };
- QPushButton* mBackButton{ nullptr };
- QPushButton* mForwardButton{ nullptr };
- QLabel* mCounter{ nullptr };
- QWidget* mMainImageWrapper{ nullptr };
- ScaleLabel* mMainImage{ nullptr };
- QLabel* mLabel{ nullptr };
- QScrollArea* mPreviewImages{ nullptr };
-}; \ No newline at end of file
diff --git a/libs/installer_fomod_plus/installer/ui/FomodViewModel.cpp b/libs/installer_fomod_plus/installer/ui/FomodViewModel.cpp
deleted file mode 100644
index b11674c..0000000
--- a/libs/installer_fomod_plus/installer/ui/FomodViewModel.cpp
+++ /dev/null
@@ -1,746 +0,0 @@
-#include "FomodViewModel.h"
-#include "xml/ModuleConfiguration.h"
-#include "lib/Logger.h"
-
-using GroupCallback = std::function<void(GroupRef)>;
-using PluginCallback = std::function<void(GroupRef, PluginRef)>;
-
-
-/*
---------------------------------------------------------------------------------
- Helpers
---------------------------------------------------------------------------------
-*/
-#pragma region Helpers
-
-bool isRadioLike(GroupRef group)
-{
- return group->getType() == SelectExactlyOne
- || (group->getType() == SelectAtMostOne && group->getPlugins().size() > 1);
-}
-
-bool moreThanOneSelected(GroupRef group)
-{
- auto selectedPlugins = group->getPlugins() | std::views::filter([](const auto& plugin) {
- return plugin->isSelected();
- });
- return std::ranges::distance(selectedPlugins) > 1;
-}
-
-bool anySelected(GroupRef group)
-{
- return std::ranges::any_of(group->getPlugins(), [](const auto& plugin) { return plugin->isSelected(); });
-}
-
-std::string pluginTypeEnumToString(const PluginTypeEnum type)
-{
- switch (type) {
- case PluginTypeEnum::Recommended:
- return "Recommended";
- case PluginTypeEnum::Required:
- return "Required";
- case PluginTypeEnum::Optional:
- return "Optional";
- case PluginTypeEnum::NotUsable:
- return "NotUsable";
- case PluginTypeEnum::CouldBeUsable:
- return "CouldBeUsable";
- default:
- return "Unknown";
- }
-}
-
-#pragma endregion
-
-/*
---------------------------------------------------------------------------------
- Lifecycle
---------------------------------------------------------------------------------
-*/
-#pragma region ViewModel Lifecycle
-
-/**
- * @brief FomodViewModel constructor
- *
- * @note DO NOT USE DIRECTLY. We should only use FomodViewModel::create() to create a new instance.
- * @see FomodViewModel::create
- *
- * @param organizer The organizer instance passed from the IInstaller
- * @param fomodFile The ModuleConfiguration instance created from the raw ModuleConfiguration.xml file
- * @param infoFile The FomodInfoFile instance created from the raw info.xml file
- *
- * @return A FomodViewModel instance
- */
-FomodViewModel::FomodViewModel(MOBase::IOrganizer* organizer,
- std::unique_ptr<ModuleConfiguration> fomodFile,
- std::unique_ptr<FomodInfoFile> infoFile)
- : mOrganizer(organizer), mFomodFile(std::move(fomodFile)), mInfoFile(std::move(infoFile)),
- mConditionTester(organizer),
- mInfoViewModel(std::make_shared<InfoViewModel>(mInfoFile))
-{
- mFlags = std::make_shared<FlagMap>();
-}
-
-/**
- *
- * @param organizer The organizer instance passed from the IInstaller
- * @param fomodFile The ModuleConfiguration instance created from the raw ModuleConfiguration.xml file
- * @param infoFile The FomodInfoFile instance created from the raw info.xml file
- * @return A shared pointer to the FomodViewModel instance
- */
-std::shared_ptr<FomodViewModel> FomodViewModel::create(MOBase::IOrganizer* organizer,
- std::unique_ptr<ModuleConfiguration> fomodFile,
- std::unique_ptr<FomodInfoFile> infoFile)
-{
- auto viewModel = std::make_shared<FomodViewModel>(organizer, std::move(fomodFile), std::move(infoFile));
- if (viewModel->mFlags == nullptr) {
- viewModel->mFlags = std::make_shared<FlagMap>();
- }
- viewModel->createStepViewModels();
-
- // Handle FOMODs with no steps
- if (viewModel->mSteps.empty()) {
- viewModel->mInitialized = true;
- viewModel->logMessage(INFO, "FOMOD with no steps - initialization complete");
- return viewModel;
- }
-
- viewModel->processPluginConditions(-1); // please dont judge me. ill fix this someday.
- viewModel->enforceGroupConstraints();
- viewModel->updateVisibleSteps();
- viewModel->mInitialized = true;
- viewModel->mCurrentStepIndex = viewModel->mVisibleStepIndices.front();
- viewModel->mActiveStep = viewModel->mSteps.at(viewModel->mVisibleStepIndices.front());
- viewModel->mActivePlugin = viewModel->getFirstPluginForActiveStep();
- viewModel->getActiveStep()->setVisited(true);
- viewModel->logMessage(DEBUG, "VIEWMODEL INITIALIZED");
- viewModel->logMessage(DEBUG, viewModel->toString());
- return viewModel;
-}
-#pragma endregion
-
-/*
---------------------------------------------------------------------------------
- Traversal Functions
---------------------------------------------------------------------------------
-*/
-#pragma region Traversal Functions
-
-void FomodViewModel::forEachGroup(const GroupCallback& callback) const
-{
- for (const auto& stepViewModel : mSteps) {
- for (const auto& groupViewModel : stepViewModel->getGroups()) {
- callback(groupViewModel);
- }
- }
-}
-
-void FomodViewModel::forEachPlugin(const PluginCallback& callback) const
-{
- for (const auto& stepViewModel : mSteps) {
- for (const auto& groupViewModel : stepViewModel->getGroups()) {
- for (const auto& pluginViewModel : groupViewModel->getPlugins()) {
- callback(groupViewModel, pluginViewModel);
- }
- }
- }
-}
-
-void FomodViewModel::forEachFuturePlugin(const int fromStepIndex, const PluginCallback& callback) const
-{
- for (int i = fromStepIndex + 1; i < mSteps.size(); ++i) {
- for (const auto& groupViewModel : mSteps[i]->getGroups()) {
- for (const auto& pluginViewModel : groupViewModel->getPlugins()) {
- callback(groupViewModel, pluginViewModel);
- }
- }
- }
-}
-#pragma endregion
-
-/*
---------------------------------------------------------------------------------
- Initialization
---------------------------------------------------------------------------------
-*/
-#pragma region Initializers
-void FomodViewModel::createStepViewModels()
-{
- shared_ptr_list<StepViewModel> stepViewModels;
-
- // Handle legacy FOMODs with no install steps
- if (mFomodFile->installSteps.installSteps.empty()) {
- logMessage(INFO, "No install steps found - creating default step for legacy FOMOD");
- return;
- }
-
- for (int stepIndex = 0; stepIndex < mFomodFile->installSteps.installSteps.size(); ++stepIndex) {
- const auto& installStep = mFomodFile->installSteps.installSteps[stepIndex];
- shared_ptr_list<GroupViewModel> groupViewModels;
-
- for (int groupIndex = 0; groupIndex < installStep.optionalFileGroups.groups.size(); ++groupIndex) {
- const auto& group = installStep.optionalFileGroups.groups[groupIndex];
- shared_ptr_list<PluginViewModel> pluginViewModels;
-
- for (int pluginIndex = 0; pluginIndex < group.plugins.plugins.size(); ++pluginIndex) {
- const auto& plugin = group.plugins.plugins[pluginIndex];
- auto pluginViewModel = std::make_shared<PluginViewModel>(std::make_shared<Plugin>(plugin), false, true,
- pluginIndex);
-
- pluginViewModel->setStepIndex(stepIndex);
- pluginViewModel->setGroupIndex(groupIndex);
- pluginViewModels.emplace_back(pluginViewModel); // Assuming default values for selected and enabled
- }
- auto groupViewModel = std::make_shared<GroupViewModel>(std::make_shared<Group>(group), pluginViewModels,
- groupIndex, stepIndex);
- if (groupViewModel->getType() == SelectAtMostOne && groupViewModel->getPlugins().size() > 1) {
- createNonePluginForGroup(groupViewModel);
- }
- groupViewModels.emplace_back(groupViewModel);
- }
- auto stepViewModel = std::make_shared<StepViewModel>(std::make_shared<InstallStep>(installStep),
- std::move(groupViewModels), stepIndex);
- stepViewModels.emplace_back(stepViewModel);
-
- }
- mSteps = std::move(stepViewModels);
-}
-
-void FomodViewModel::createNonePluginForGroup(GroupRef group)
-{
- const auto nonePlugin = std::make_shared<Plugin>();
- nonePlugin->name = "None";
- nonePlugin->typeDescriptor.type = PluginTypeEnum::Optional;
- const int newIndex = static_cast<int>(group->getPlugins().size());
- const auto nonePluginViewModel = std::make_shared<PluginViewModel>(nonePlugin, true, true, newIndex);
- group->addPlugin(nonePluginViewModel);
-}
-#pragma endregion
-
-/*
---------------------------------------------------------------------------------
- Group Constraints
---------------------------------------------------------------------------------
-*/
-#pragma region Group Constraints
-
-void FomodViewModel::enforceRadioGroupConstraints(GroupRef group) const
-{
- if (!isRadioLike(group)) {
- return;
- }
-
- logMessage(INFO, "Enforcing group constraints for group " + group->getName());
-
- if (group->getType() == SelectExactlyOne && group->getPlugins().size() == 1) {
- logMessage(INFO,
- "Disabling " + group->getPlugins().at(0)->getName() + " because it's the only plugin.");
- group->getPlugins().at(0)->setEnabled(false);
- }
-
- if (moreThanOneSelected(group)) {
- logMessage(ERR, "More than one plugin is selected in a SelectExactlyOne group. Deselecting all.");
- for (const auto& plugin : group->getPlugins()) {
- plugin->setSelected(false); // don't call toggle here, that'll do the radio stuff.
- }
- }
-
- if (anySelected(group)) {
- logMessage(INFO, "At least one plugin is selected. Nothing to enforce.");
- return;
- }
-
- // First, try to select the first Recommended plugin
- for (const auto& plugin : group->getPlugins()) {
- if (mConditionTester.getPluginTypeDescriptorState(plugin->getPlugin(), mFlags) == PluginTypeEnum::Recommended) {
- logMessage(INFO, "Selecting " + plugin->getName() + " because it's the first recommended plugin.");
- togglePlugin(group, plugin, true);
- return;
- }
- }
-
- // If no Recommended plugin is found, select the first one that isn't NotUsable
- for (const auto& plugin : group->getPlugins()) {
- if (mConditionTester.getPluginTypeDescriptorState(plugin->getPlugin(), mFlags) != PluginTypeEnum::NotUsable) {
- logMessage(INFO, "Selecting " + plugin->getName() + " because it's the first usable plugin.");
- togglePlugin(group, plugin, true);
- return;
- }
- }
-}
-
-void FomodViewModel::enforceSelectAllConstraint(GroupRef groupViewModel) const
-{
- if (groupViewModel->getType() != SelectAll) {
- return;
- }
-
- for (const auto& pluginViewModel : groupViewModel->getPlugins()) {
- togglePlugin(groupViewModel, pluginViewModel, true);
- pluginViewModel->setEnabled(false);
- }
-
-}
-
-void FomodViewModel::enforceSelectAtLeastOneConstraint(GroupRef group) const
-{
- if (group->getType() != SelectAtLeastOne || group->getPlugins().size() != 1) {
- return;
- }
-
- const auto plugin = group->getPlugins().front();
- if (mConditionTester.getPluginTypeDescriptorState(plugin->getPlugin(), mFlags) != PluginTypeEnum::NotUsable) {
- logMessage(DEBUG, "Selecting " + plugin->getName() + " because it's the only plugin in a SelectAtLeastOne.");
- togglePlugin(group, plugin, true);
- plugin->setEnabled(false);
- }
-}
-
-void FomodViewModel::enforceGroupConstraints() const
-{
- forEachGroup([this](const auto& groupViewModel) {
- enforceRadioGroupConstraints(groupViewModel);
- enforceSelectAllConstraint(groupViewModel);
- enforceSelectAtLeastOneConstraint(groupViewModel);
- });
-}
-
-#pragma endregion
-
-/*
---------------------------------------------------------------------------------
- Plugin Constraints
---------------------------------------------------------------------------------
-*/
-#pragma region Plugin Constraints
-
-void FomodViewModel::processPlugin(GroupRef group, PluginRef plugin) const
-{
- if (group->getType() == SelectAll) {
- return;
- }
- const auto typeDescriptor = mConditionTester.getPluginTypeDescriptorState(plugin->plugin, mFlags);
-
- if (typeDescriptor == plugin->getCurrentPluginType()) {
- return;
- }
-
- logMessage(DEBUG,
- "Plugin " + plugin->getName() + " in group " + std::to_string(group->getOwnIndex()) + " has changed type from "
- + pluginTypeEnumToString(plugin->getCurrentPluginType()) + " to " + pluginTypeEnumToString(typeDescriptor));
-
- plugin->setCurrentPluginType(typeDescriptor);
-
- const bool isOnlyPlugin = group->getPlugins().size() == 1
- && (group->getType() == SelectExactlyOne || group->getType() == SelectAtLeastOne);
-
- // check if step hasVisited, if it hasn't been, set it to unchecked if it's optional.
- const auto stepNotVisitedYet = !mSteps[group->getStepIndex()]->getHasVisited();
-
- switch (typeDescriptor) {
- case PluginTypeEnum::Recommended:
- plugin->setEnabled(true);
- if (!plugin->isSelected()) {
- togglePlugin(group, plugin, true);
- }
- break;
- case PluginTypeEnum::Required:
- plugin->setEnabled(false);
- if (!plugin->isSelected()) {
- togglePlugin(group, plugin, true);
- }
- break;
- case PluginTypeEnum::Optional:
- if (!isOnlyPlugin) {
- plugin->setEnabled(true);
- }
- // In the case where we're changing flags to make something optional from Recommended, set it back to unchecked.
- if (plugin->isSelected() & stepNotVisitedYet && group->getType() == SelectAny) {
- togglePlugin(group, plugin, false);
- }
- break;
- case PluginTypeEnum::NotUsable:
- plugin->setEnabled(false);
- if (plugin->isSelected()) {
- togglePlugin(group, plugin, false);
- }
- break;
- case PluginTypeEnum::CouldBeUsable:
- plugin->setEnabled(true);
- break;
- default: ;
- }
-}
-
-void FomodViewModel::processPluginConditions(const int fromStepIndex) const
-{
- // We only want to update plugins that haven't been seen yet. Otherwise, we could undo manual selections by the user.
- if (fromStepIndex >= 0) {
- logMessage(DEBUG, "Processing plugins from step " + std::to_string(fromStepIndex));
- forEachFuturePlugin(fromStepIndex, [this](const auto& groupViewModel, const auto& pluginViewModel) {
- processPlugin(groupViewModel, pluginViewModel);
- });
- } else {
- forEachPlugin([this](const auto& groupViewModel, const auto& pluginViewModel) {
- processPlugin(groupViewModel, pluginViewModel);
- });
- }
-}
-
-void FomodViewModel::setFlagForPluginState(PluginRef plugin) const
-{
- if (plugin->isSelected()) {
- mFlags->setFlagsForPlugin(plugin);
- } else {
- mFlags->unsetFlagsForPlugin(plugin);
- }
-}
-
-/*
- * In an exclusive group, this gets called for the deselected plugin and then the selected plugin.
- * So if we're unselecting modB to select modA, we will get calls like
- * togglePlugin(group, modB, false)
- * togglePlugin(group, modA, true)
- */
-bool FomodViewModel::togglePlugin(GroupRef group, PluginRef plugin, const bool selected) const
-{
- if (plugin->isSelected() == selected) {
- logMessage(DEBUG, "Plugin " + plugin->getName() + " is already " + (selected ? "selected" : "deselected"));
- return false;
- }
-
- // Disable other radio options first.
- if (selected && isRadioLike(group)) {
- for (const auto& otherPlugin : group->getPlugins()) {
- if (otherPlugin != plugin && plugin->isSelected()) {
- logMessage(DEBUG,
- "Deselecting " + otherPlugin->getName() + " because " + plugin->getName() + " was selected.");
- otherPlugin->setSelected(false);
- setFlagForPluginState(otherPlugin);
- }
- }
- }
-
- const auto stepIndex = group->getStepIndex();
-
- logMessage(INFO, "Toggling " + plugin->getName() + " to " + (selected ? "true" : "false"));
- plugin->setSelected(selected);
- setFlagForPluginState(plugin);
-
- if (mInitialized) {
- mActivePlugin = plugin;
- }
- processPluginConditions(stepIndex);
- updateVisibleSteps();
- return true;
-}
-
-void FomodViewModel::markManuallySet(PluginRef plugin)
-{
- plugin->manuallySet = true;
-}
-
-#pragma endregion
-
-/*
---------------------------------------------------------------------------------
- Step Constraints
---------------------------------------------------------------------------------
-*/
-#pragma region Step Constraints
-
-void FomodViewModel::updateVisibleSteps() const
-{
- mVisibleStepIndices.clear();
- mFlags->clearAll();
-
- for (int i = 0; i < mSteps.size(); ++i) {
- if (i == 0) {
- rebuildConditionFlagsForStep(i);
- }
-
- // This also depends on previous flags that may have set this particular flag.
- if (mConditionTester.isStepVisible(mFlags, mSteps[i]->getVisibilityConditions(), i, mSteps)) {
- mVisibleStepIndices.push_back(i);
- rebuildConditionFlagsForStep(i);
- }
- }
- if (mFlags->getFlagCount() > 0) {
- logMessage(DEBUG, mFlags->toString());
- }
-}
-
-void FomodViewModel::rebuildConditionFlagsForStep(const int stepIndex) const
-{
- for (const auto& group : mSteps[stepIndex]->getGroups()) {
- for (const auto& plugin : group->getPlugins()) {
- setFlagForPluginState(plugin);
- }
- }
-}
-#pragma endregion
-
-/*
---------------------------------------------------------------------------------
- Navigation/UI
---------------------------------------------------------------------------------
-*/
-#pragma region Navigation/UI
-
-void FomodViewModel::stepBack()
-{
- if (mSteps.empty()) {
- return; // No steps to move back to
- }
-
- logMessage(DEBUG, "Stepping back from step " + std::to_string(mCurrentStepIndex));
- const auto it = std::ranges::find(mVisibleStepIndices, mCurrentStepIndex);
- if (it != mVisibleStepIndices.end() && it != mVisibleStepIndices.begin()) {
- mCurrentStepIndex = *std::prev(it);
- mActiveStep = mSteps[mCurrentStepIndex];
- mActivePlugin = getFirstPluginForActiveStep();
- }
- logMessage(DEBUG, "Stepped back to step " + std::to_string(mCurrentStepIndex));
-}
-
-void FomodViewModel::stepForward()
-{
- if (mSteps.empty()) {
- return; // No steps to move forward to
- }
-
- logMessage(DEBUG, "Stepping forward from step " + std::to_string(mCurrentStepIndex));
- const auto it = std::ranges::find(mVisibleStepIndices, mCurrentStepIndex);
- if (it != mVisibleStepIndices.end() && std::next(it) != mVisibleStepIndices.end()) {
- mCurrentStepIndex = *std::next(it);
- mActiveStep = mSteps[mCurrentStepIndex];
- mActivePlugin = getFirstPluginForActiveStep();
- }
- mActiveStep->setVisited(true);
- logMessage(DEBUG, "Stepped forward to step " + std::to_string(mCurrentStepIndex));
-}
-
-bool FomodViewModel::isLastVisibleStep() const
-{
- if (mSteps.empty()) {
- return true; // Legacy FOMODs are always "last step"
- }
- return !mVisibleStepIndices.empty() && mCurrentStepIndex == mVisibleStepIndices.back();
-}
-
-bool FomodViewModel::isFirstVisibleStep() const
-{
- if (mSteps.empty()) {
- return true; // Legacy FOMODs are always "first step"
- }
- return !mVisibleStepIndices.empty() && mCurrentStepIndex == mVisibleStepIndices.front();
-}
-
-void FomodViewModel::preinstall(const std::shared_ptr<MOBase::IFileTree>& tree, const QString& fomodPath)
-{
- mFileInstaller = std::make_shared<
- FileInstaller>(mOrganizer, fomodPath, tree, std::move(mFomodFile), mFlags, mSteps);
-}
-
-
-std::string FomodViewModel::getDisplayImage() const
-{
- // if the active plugin has an image, return it
- if (mActivePlugin && !mActivePlugin->getImagePath().empty()) {
- return mActivePlugin->getImagePath();
- }
- return mCurrentStepIndex == 0 ? mFomodFile->moduleImage.path : "";
-}
-
-std::shared_ptr<PluginViewModel> FomodViewModel::getFirstPluginForActiveStep() const
-{
- if (!mActiveStep) {
- return nullptr;
- }
-
- const auto& groups = mActiveStep->getGroups();
- if (groups.empty()) {
- return nullptr;
- }
-
- const auto& plugins = groups.front()->getPlugins();
- if (plugins.empty()) {
- return nullptr;
- }
-
- return plugins.front();
-}
-#pragma endregion
-
-/*
---------------------------------------------------------------------------------
- Utility
---------------------------------------------------------------------------------
-*/
-#pragma region Utility
-std::string FomodViewModel::toString() const
-{
- std::string viewModel = "\n";
- for (const auto& step : mSteps) {
-
- const auto isVisible = std::ranges::find(mVisibleStepIndices, step->getOwnIndex()) != mVisibleStepIndices.end();
- viewModel += "Step " + std::to_string(step->getOwnIndex()) + ": " + step->getName() + "[Visible: " +
- std::to_string(isVisible) + "]\n";
-
- for (const auto& group : step->getGroups()) {
-
- viewModel += "\tGroup " + std::to_string(group->getOwnIndex()) + ": " + group->getName() + "\n";
-
- for (const auto& plugin : group->getPlugins()) {
- viewModel += "\t\tPlugin: " + plugin->getName() + "[Selected: " + (plugin->isSelected()
- ? "TRUE"
- : "FALSE") + "]\n";
- }
- }
- }
- viewModel += "\n";
- std::ostringstream oss;
- std::ranges::transform(mVisibleStepIndices,
- std::ostream_iterator<std::string>(oss, ", "),
- [](const int i) { return std::to_string(i); });
-
- std::string stepList = oss.str();
- stepList.erase(stepList.length() - 2);
-
- viewModel += "Visible Steps: [" + stepList + "]\n";
- viewModel += mFlags->toString();
- return viewModel;
-}
-
-
-void FomodViewModel::resetToDefaults()
-{
- logMessage(INFO, "Resetting all choices to author defaults");
-
- // Clear all flags first
- mFlags->clearAll();
-
- // Reset all plugins to deselected and clear visited states
- for (const auto& step : mSteps) {
- step->setVisited(false);
- for (const auto& group : step->getGroups()) {
- for (const auto& plugin : group->getPlugins()) {
- plugin->setSelected(false);
- plugin->setEnabled(true);
- plugin->manuallySet = false;
- plugin->setCurrentPluginType(PluginTypeEnum::UNKNOWN);
- }
- }
- }
-
- // Re-run the initial constraint enforcement to restore author defaults
- processPluginConditions(-1);
- enforceGroupConstraints();
- updateVisibleSteps();
-
- // Reset to first step
- mCurrentStepIndex = mVisibleStepIndices.empty() ? 0 : mVisibleStepIndices.front();
- mActiveStep = mSteps.empty() ? nullptr : mSteps.at(mCurrentStepIndex);
- mActivePlugin = getFirstPluginForActiveStep();
- if (mActiveStep) {
- mActiveStep->setVisited(true);
- }
-
- logMessage(DEBUG, "Reset complete. Current state:\n" + toString());
-}
-
-void FomodViewModel::selectFromJson(nlohmann::json json) const
-{
- const auto jsonSteps = json["steps"];
- const auto stepCount = jsonSteps.size();
-
- for (int stepIndex = 0; stepIndex < stepCount; ++stepIndex) {
-
- if (stepIndex > mSteps.size() - 1) {
- logMessage(ERR, "Step index " + std::to_string(stepIndex) + " is out of bounds.");
- continue;
- }
-
- const auto currentStep = mSteps[stepIndex];
- const auto step = jsonSteps[stepIndex];
- const auto groupCount = step["groups"].size();
-
- logMessage(DEBUG, "Selecting plugins for step " + std::to_string(stepIndex));
- logMessage(DEBUG, "There are " + std::to_string(groupCount) + " groups.");
-
- for (int groupIndex = 0; groupIndex < groupCount; ++groupIndex) {
- if (groupIndex > currentStep->getGroups().size() - 1) {
- logMessage(ERR, "Group index " + std::to_string(groupIndex) + " is out of bounds.");
- continue;
- }
-
- const auto group = step["groups"][groupIndex];
- const auto currentGroup = currentStep->getGroups()[groupIndex];
-
- for (const auto jsonPlugin : group["plugins"]) {
-
- const auto& allPlugins = currentGroup->getPlugins();
- const auto searchName = jsonPlugin.get<std::string>();
-
- logMessage(DEBUG, "Looking for plugin " + searchName);
-
- const auto currentPlugin = std::ranges::find_if(allPlugins,
- [searchName](PluginRef p) {
- return p->getName() == searchName;
- });
-
- if (currentPlugin == allPlugins.end()) {
- logMessage(DEBUG, "Plugin " + searchName + " not found in group " + currentGroup->getName());
- continue;
- }
-
- if ((*currentPlugin)->isSelected()) {
- logMessage(DEBUG, "Plugin " + searchName + " is already selected.");
- continue;
- }
- logMessage(DEBUG, "Toggle plugin " + searchName + " to selected.");
- if (!(*currentPlugin)->isEnabled()) {
- logMessage(DEBUG, "Plugin " + searchName + " is not enabled.");
- continue;
- }
- togglePlugin(currentGroup, *currentPlugin, true);
- }
-
- if (!group.contains("deselected")) {
- continue;
- }
-
- // Do the opposite of the above. For unchecked plugins, disable them.
- for (const auto jsonPlugin : group["deselected"]) {
-
- const auto& allPlugins = currentGroup->getPlugins();
- const auto searchName = jsonPlugin.get<std::string>();
-
- logMessage(DEBUG, "Looking for plugin to disable: " + searchName);
-
- const auto currentPlugin = std::ranges::find_if(allPlugins,
- [searchName](PluginRef p) {
- return p->getName() == searchName;
- });
-
- if (currentPlugin == allPlugins.end()) {
- logMessage(DEBUG, "Plugin " + searchName + " not found in group " + currentGroup->getName());
- continue;
- }
-
- if (!(*currentPlugin)->isSelected()) {
- logMessage(DEBUG, "Plugin " + searchName + " is already deselected.");
- continue;
- }
- logMessage(DEBUG, "Toggle plugin " + searchName + " to deselected.");
- if (!(*currentPlugin)->isEnabled()) {
- logMessage(DEBUG, "Plugin " + searchName + " is not enabled.");
- continue;
- }
- togglePlugin(currentGroup, *currentPlugin, false);
- (*currentPlugin)->manuallySet = true; // To preserve this state when serializing JSON.
- }
- }
- }
-}
-#pragma endregion
diff --git a/libs/installer_fomod_plus/installer/ui/FomodViewModel.h b/libs/installer_fomod_plus/installer/ui/FomodViewModel.h
deleted file mode 100644
index f0ce070..0000000
--- a/libs/installer_fomod_plus/installer/ui/FomodViewModel.h
+++ /dev/null
@@ -1,154 +0,0 @@
-#pragma once
-
-#include <imoinfo.h>
-#include <string>
-
-#include "lib/ConditionTester.h"
-#include "lib/FlagMap.h"
-#include "lib/FileInstaller.h"
-#include "lib/ViewModels.h"
-#include "xml/FomodInfoFile.h"
-
-/*
---------------------------------------------------------------------------------
- Info
---------------------------------------------------------------------------------
-*/
-class InfoViewModel {
-public:
- explicit InfoViewModel(const std::unique_ptr<FomodInfoFile>& infoFile)
- {
- if (infoFile) {
- // Copy the necessary members from FomodInfoFile to InfoViewModel
- mName = infoFile->getName();
- mVersion = infoFile->getVersion();
- mAuthor = infoFile->getAuthor();
- mWebsite = infoFile->getWebsite();
- }
- }
-
- // Accessor methods
- [[nodiscard]] std::string getName() const { return mName; }
- [[nodiscard]] std::string getVersion() const { return mVersion; }
- [[nodiscard]] std::string getAuthor() const { return mAuthor; }
- [[nodiscard]] std::string getWebsite() const { return mWebsite; }
-
-private:
- std::string mName;
- std::string mVersion;
- std::string mAuthor;
- std::string mWebsite;
-};
-
-/*
---------------------------------------------------------------------------------
- View Model
---------------------------------------------------------------------------------
-*/
-class FomodViewModel {
-public:
- FomodViewModel(
- MOBase::IOrganizer* organizer,
- std::unique_ptr<ModuleConfiguration> fomodFile,
- std::unique_ptr<FomodInfoFile> infoFile);
-
- static std::shared_ptr<FomodViewModel> create(
- MOBase::IOrganizer* organizer,
- std::unique_ptr<ModuleConfiguration> fomodFile,
- std::unique_ptr<FomodInfoFile> infoFile);
-
- void forEachGroup(const std::function<void(GroupRef)>& callback) const;
-
- void forEachPlugin(const std::function<void(GroupRef, PluginRef)>& callback) const;
-
- void forEachFuturePlugin(int fromStepIndex, const std::function<void(GroupRef, PluginRef)>& callback) const;
-
- void selectFromJson(nlohmann::json json) const;
-
- void resetToDefaults();
-
- [[nodiscard]] std::shared_ptr<PluginViewModel> getFirstPluginForActiveStep() const;
-
- // Steps
- [[nodiscard]] shared_ptr_list<StepViewModel> getSteps() const { return mSteps; }
- [[nodiscard]] StepRef getActiveStep() const { return mActiveStep; }
- [[nodiscard]] int getCurrentStepIndex() const { return mCurrentStepIndex; }
- [[deprecated]] void setCurrentStepIndex(const int index) { mCurrentStepIndex = index; }
-
- void updateVisibleSteps() const;
-
- void rebuildConditionFlagsForStep(int stepIndex) const;
-
- void preinstall(const std::shared_ptr<MOBase::IFileTree>& tree, const QString& fomodPath);
-
- std::shared_ptr<FileInstaller> getFileInstaller() { return mFileInstaller; }
-
- std::string getDisplayImage() const;
-
- // Plugins
- [[nodiscard]] PluginRef getActivePlugin() const { return mActivePlugin; }
-
- // Info
- [[nodiscard]] std::shared_ptr<InfoViewModel> getInfoViewModel() const { return mInfoViewModel; }
-
- // Interactions
- void stepBack();
-
- void stepForward();
-
- bool isLastVisibleStep() const;
-
- bool isFirstVisibleStep() const;
-
- bool togglePlugin(const GroupRef, const PluginRef, bool selected) const;
-
- bool ctrlTogglePlugin(const GroupRef, const PluginRef, bool selected) const;
-
- void setActivePlugin(const PluginRef plugin) const { mActivePlugin = plugin; }
-
- static void markManuallySet(PluginRef plugin);
-
-private:
- Logger& log = Logger::getInstance();
- MOBase::IOrganizer* mOrganizer = nullptr;
- std::unique_ptr<ModuleConfiguration> mFomodFile;
- std::unique_ptr<FomodInfoFile> mInfoFile;
- std::shared_ptr<FlagMap> mFlags{ nullptr };
- ConditionTester mConditionTester;
- std::shared_ptr<InfoViewModel> mInfoViewModel;
- std::vector<std::shared_ptr<StepViewModel> > mSteps;
- mutable std::shared_ptr<PluginViewModel> mActivePlugin{ nullptr };
- mutable std::shared_ptr<StepViewModel> mActiveStep{ nullptr };
- mutable std::vector<int> mVisibleStepIndices;
- std::shared_ptr<FileInstaller> mFileInstaller{ nullptr };
- bool mInitialized{ false };
-
- void createStepViewModels();
-
- void setFlagForPluginState(const std::shared_ptr<PluginViewModel>& plugin) const;
-
- static void createNonePluginForGroup(const std::shared_ptr<GroupViewModel>& group);
-
- void processPlugin(const std::shared_ptr<GroupViewModel>& group,
- const std::shared_ptr<PluginViewModel>& plugin) const;
-
- void enforceRadioGroupConstraints(const std::shared_ptr<GroupViewModel>& group) const;
-
- void enforceSelectAllConstraint(const std::shared_ptr<GroupViewModel>& groupViewModel) const;
-
- void enforceSelectAtLeastOneConstraint(const std::shared_ptr<GroupViewModel>& group) const;
-
- void enforceGroupConstraints() const;
-
- void processPluginConditions(int fromStepIndex) const;
-
- // Indices
- int mCurrentStepIndex{ 0 };
-
- void logMessage(const LogLevel level, const std::string& message) const
- {
- log.logMessage(level, "[VIEWMODEL] " + message);
- }
-
- std::string toString() const;
-};
diff --git a/libs/installer_fomod_plus/installer/ui/ScaleLabel.cpp b/libs/installer_fomod_plus/installer/ui/ScaleLabel.cpp
deleted file mode 100644
index 3a33640..0000000
--- a/libs/installer_fomod_plus/installer/ui/ScaleLabel.cpp
+++ /dev/null
@@ -1,120 +0,0 @@
-#include "ScaleLabel.h"
-#include <QResizeEvent>
-#include <iostream>
-
-// Taken from https://github.com/ModOrganizer2/modorganizer-installer_fomod/blob/master/src/scalelabel.h
-static bool isResourceMovie(const QString& path)
-{
- const auto formats = QMovie::supportedFormats();
- return std::ranges::any_of(formats, [&path](const QByteArray& format) {
- return path.endsWith("." + QString::fromUtf8(format));
- });
-}
-
-void ScaleLabel::setScalableResource(const QString& path)
-{
- if (const auto m = movie()) {
- setMovie(nullptr);
- delete m;
- mOriginalMovieSize = QSize();
- }
- if (!pixmap().isNull()) {
- setPixmap(QPixmap());
- mUnscaledImage = QImage();
- }
-
- if (path.isEmpty()) {
- return;
- }
-
- if (isResourceMovie(path)) {
- setScalableMovie(path);
- } else {
- setScalableImage(path);
- }
-}
-
-void ScaleLabel::setStatic(const bool isStatic)
-{
- misStatic = isStatic;
-
- if (const auto m = movie()) {
- if (isStatic) {
- m->stop();
- } else {
- m->start();
- }
- }
-}
-
-void ScaleLabel::setScalableMovie(const QString& path)
-{
- const auto m = new QMovie(path);
- if (!m->isValid()) {
- qWarning(">%s< is an invalid movie. Reason: %s", qUtf8Printable(path),
- m->lastErrorString().toStdString().c_str());
- delete m;
- return;
- }
-
- m->setParent(this);
- setMovie(m);
- m->start();
- m->stop();
- mOriginalMovieSize = m->currentImage().size();
-
- m->setScaledSize(mOriginalMovieSize.scaled(size(), Qt::KeepAspectRatio));
- if (!misStatic) {
- m->start();
- }
- mHasResource = true;
-}
-
-void ScaleLabel::setScalableImage(const QString& path)
-{
- if (const QImage image(path); image.isNull()) {
- qWarning(">%s< is a null image", qUtf8Printable(path));
- } else {
- mUnscaledImage = image;
- setPixmap(QPixmap::fromImage(image).scaled(size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
- mHasResource = true;
- }
-}
-
-void ScaleLabel::resizeEvent(QResizeEvent* event)
-{
- if (const auto m = movie()) {
- m->stop();
- m->setScaledSize(mOriginalMovieSize.scaled(event->size(), Qt::KeepAspectRatio));
- m->start();
-
- // We can't just skip the start() above since that is what triggers the label to
- // resize the movie The only way to resize the movie but keep it paused is to start
- // and then re-stop it
- if (misStatic) {
- m->stop();
- }
- }
- if (const auto p = pixmap(); !p.isNull()) {
- setPixmap(
- QPixmap::fromImage(mUnscaledImage).scaled(event->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
- }
-}
-
-void ScaleLabel::showEvent(QShowEvent* event)
-{
- QLabel::showEvent(event);
-
- if (const auto m = movie()) {
- m->stop();
- m->setScaledSize(mOriginalMovieSize.scaled(size(), Qt::KeepAspectRatio));
- m->start();
-
- if (misStatic) {
- m->stop();
- }
- }
- if (const auto p = pixmap(); !p.isNull()) {
- setPixmap(QPixmap::fromImage(mUnscaledImage).scaled(size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
- }
-} \ No newline at end of file
diff --git a/libs/installer_fomod_plus/installer/ui/ScaleLabel.h b/libs/installer_fomod_plus/installer/ui/ScaleLabel.h
deleted file mode 100644
index f4fb5f2..0000000
--- a/libs/installer_fomod_plus/installer/ui/ScaleLabel.h
+++ /dev/null
@@ -1,45 +0,0 @@
-#pragma once
-
-// Taken from https://github.com/ModOrganizer2/modorganizer-installer_fomod/blob/master/src/scalelabel.h
-#include <QLabel>
-#include <QMouseEvent>
-#include <QMovie>
-
-
-class ScaleLabel final : public QLabel {
- Q_OBJECT
-
-public:
- explicit ScaleLabel(QWidget* parent = nullptr) : QLabel(parent)
- {
- setCursor(Qt::PointingHandCursor);
- }
-
- void setScalableResource(const QString& path);
- void setStatic(bool isStatic);
- [[nodiscard]] bool hasResource() const { return mHasResource; }
-
-signals:
- void clicked();
-
-protected:
- void mousePressEvent(QMouseEvent* event) override
- {
- if (event->button() == Qt::LeftButton) {
- emit clicked();
- }
- QLabel::mousePressEvent(event);
- }
-
- void resizeEvent(QResizeEvent* event) override;
- void showEvent(QShowEvent* event) override;
-
-private:
- void setScalableMovie(const QString& path);
- void setScalableImage(const QString& path);
-
- QImage mUnscaledImage;
- QSize mOriginalMovieSize;
- bool mHasResource = false;
- bool misStatic = false;
-}; \ No newline at end of file
diff --git a/libs/installer_fomod_plus/installer/ui/UIHelper.cpp b/libs/installer_fomod_plus/installer/ui/UIHelper.cpp
deleted file mode 100644
index 5f34e5d..0000000
--- a/libs/installer_fomod_plus/installer/ui/UIHelper.cpp
+++ /dev/null
@@ -1,97 +0,0 @@
-#include "UIHelper.h"
-
-#include <qcoreevent.h>
-#include <QDir>
-#include <QMouseEvent>
-
-HoverEventFilter::HoverEventFilter(const std::shared_ptr<PluginViewModel>& plugin, QObject* parent)
- : QObject(parent), mPlugin(plugin) {}
-
-bool HoverEventFilter::eventFilter(QObject* obj, QEvent* event)
-{
- if (event->type() == QEvent::HoverEnter) {
- emit hovered(mPlugin);
- return true;
- }
- return QObject::eventFilter(obj, event);
-}
-
-CtrlClickEventFilter::CtrlClickEventFilter(const std::shared_ptr<PluginViewModel>& plugin,
- const std::shared_ptr<GroupViewModel>& group, QObject* parent)
- : QObject(parent), mPlugin(plugin), mGroup(group) {}
-
-bool CtrlClickEventFilter::eventFilter(QObject* obj, QEvent* event)
-{
- if (event->type() == QEvent::MouseButtonPress) {
- const QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
- if (mouseEvent->button() == Qt::LeftButton &&
- mouseEvent->modifiers() & Qt::ControlModifier) {
- // TODO: Add Ctrl+click handler logic here
- // For now, just fall through to default behavior
- }
- }
- return QObject::eventFilter(obj, event);
-}
-
-QPushButton* UIHelper::createButton(const QString& text, QWidget* parent = nullptr)
-{
- const auto button = new QPushButton(text, parent);
- return button;
-}
-
-QLabel* UIHelper::createLabel(const QString& text, QWidget* parent = nullptr)
-{
- const auto label = new QLabel(text, parent);
- return label;
-}
-
-QLabel* UIHelper::createHyperlink(const QString& url, QWidget* parent = nullptr)
-{
- if (url.isEmpty() || !QUrl(url).isValid()) {
- return createLabel(url, parent);
- }
- const auto label = new QLabel(url, parent);
- const QString hyperlink = QString("<a href=\"%1\">%2</a>").arg(url, "Link");
- label->setText(hyperlink);
- label->setOpenExternalLinks(true);
- label->setTextFormat(Qt::RichText);
- return label;
-}
-
-QString UIHelper::getFullImagePath(const QString& fomodPath, const QString& imagePath)
-{
- // Fomod XMLs may use Windows backslashes in image paths (e.g. "fomod\MCM.png")
- QString normalized = imagePath;
- normalized.replace('\\', '/');
- return QDir::tempPath() + "/" + fomodPath + "/" + normalized;
-}
-
-void UIHelper::setGlobalAlignment(QBoxLayout* layout, const Qt::Alignment alignment)
-{
- for (int i = 0; i < layout->count(); ++i) {
- if (const QLayoutItem* item = layout->itemAt(i); item->widget()) {
- layout->setAlignment(item->widget(), alignment);
- }
- }
-}
-
-void UIHelper::setDebugBorders(QWidget* widget)
-{
- widget->setStyleSheet("border: 1px solid red;");
- for (auto* child : widget->findChildren<QWidget*>()) {
- child->setStyleSheet("border: 1px solid red;");
- }
-}
-
-void UIHelper::reduceLabelPadding(const QLayout* layout)
-{
- for (int i = 0; i < layout->count(); ++i) {
- const QLayoutItem* item = layout->itemAt(i);
- if (QWidget* widget = item->widget()) {
- if (const auto label = qobject_cast<QLabel*>(widget)) {
- label->setContentsMargins(0, 0, 0, 0);
- label->setStyleSheet("padding: 0px; margin: 0px;");
- }
- }
- }
-}
diff --git a/libs/installer_fomod_plus/installer/ui/UIHelper.h b/libs/installer_fomod_plus/installer/ui/UIHelper.h
deleted file mode 100644
index 1007e7c..0000000
--- a/libs/installer_fomod_plus/installer/ui/UIHelper.h
+++ /dev/null
@@ -1,80 +0,0 @@
-#pragma once
-
-#include <QPushButton>
-#include <QVBoxLayout>
-#include <QLabel>
-
-#include "FomodViewModel.h"
-
-class HoverEventFilter final : public QObject {
- Q_OBJECT
-
-public:
- explicit HoverEventFilter(const std::shared_ptr<PluginViewModel>& plugin, QObject* parent = nullptr);
-
-signals:
- void hovered(const std::shared_ptr<PluginViewModel>& plugin);
-
-protected:
- bool eventFilter(QObject* obj, QEvent* event) override;
-
-private:
- std::shared_ptr<PluginViewModel> mPlugin;
-};
-
-class CtrlClickEventFilter final : public QObject {
- Q_OBJECT
-
-public:
- explicit CtrlClickEventFilter(const std::shared_ptr<PluginViewModel>& plugin,
- const std::shared_ptr<GroupViewModel>& group, QObject* parent = nullptr);
-
-signals:
- void ctrlClicked(bool selected, const std::shared_ptr<GroupViewModel>& group,
- const std::shared_ptr<PluginViewModel>& plugin);
-
-protected:
- bool eventFilter(QObject* obj, QEvent* event) override;
-
-private:
- std::shared_ptr<PluginViewModel> mPlugin;
- std::shared_ptr<GroupViewModel> mGroup;
-};
-
-
-namespace UiConstants {
-constexpr int WINDOW_MIN_WIDTH = 900;
-constexpr int WINDOW_MIN_HEIGHT = 600;
-}
-
-class UIHelper {
-public:
- /*
- --------------------------------------------------------------------------------
- Widgets & Events
- --------------------------------------------------------------------------------
- */
- static QPushButton* createButton(const QString& text, QWidget* parent);
-
- static QLabel* createLabel(const QString& text, QWidget* parent);
-
- static QLabel* createHyperlink(const QString& url, QWidget* parent);
-
- /*
- --------------------------------------------------------------------------------
- Helpers
- --------------------------------------------------------------------------------
- */
- static QString getFullImagePath(const QString& fomodPath, const QString& imagePath);
-
- static void setGlobalAlignment(QBoxLayout* layout, Qt::Alignment alignment);
-
- static void reduceLabelPadding(const QLayout* layout);
-
- /*
- --------------------------------------------------------------------------------
- Development
- --------------------------------------------------------------------------------
- */
- static void setDebugBorders(QWidget* widget);
-}; \ No newline at end of file
diff --git a/libs/installer_fomod_plus/patchwizard/CMakeLists.txt b/libs/installer_fomod_plus/patchwizard/CMakeLists.txt
deleted file mode 100644
index 2cb8ebb..0000000
--- a/libs/installer_fomod_plus/patchwizard/CMakeLists.txt
+++ /dev/null
@@ -1,45 +0,0 @@
-cmake_minimum_required(VERSION 3.16)
-project(fomod_plus_patch_wizard)
-
-include(FetchContent)
-set(project_type plugin)
-
-file(GLOB_RECURSE PATCHWIZARD_SOURCES CONFIGURE_DEPENDS
- ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp
- ${CMAKE_CURRENT_SOURCE_DIR}/*.h
- ${CMAKE_CURRENT_SOURCE_DIR}/*.ui
- ${CMAKE_CURRENT_SOURCE_DIR}/*.qrc
-)
-file(GLOB SHARE_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/../share/**/*.cpp")
-
-add_library(fomod_plus_patch_wizard SHARED ${PATCHWIZARD_SOURCES} ${SHARE_SOURCES})
-
-FetchContent_Declare(json URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz DOWNLOAD_EXTRACT_TIMESTAMP TRUE)
-FetchContent_Declare(pugixml GIT_REPOSITORY https://github.com/zeux/pugixml GIT_TAG v1.14)
-FetchContent_MakeAvailable(pugixml json)
-
-target_include_directories(
- fomod_plus_patch_wizard
- PUBLIC
- ${CMAKE_CURRENT_SOURCE_DIR}
- ${CMAKE_CURRENT_SOURCE_DIR}/../share
- ${CMAKE_CURRENT_SOURCE_DIR}/../share/FOMODData
- ${CMAKE_CURRENT_SOURCE_DIR}/../share/xml
- ${MO2_ARCHIVE_INCLUDE_DIRS}
-)
-
-if (MSVC)
- target_compile_options(
- fomod_plus_patch_wizard
- PRIVATE
- /bigobj
- /W4
- /WX
- /wd4201
- /wd4458
- )
-endif ()
-
-target_link_libraries(fomod_plus_patch_wizard PRIVATE mo2::uibase pugixml nlohmann_json::nlohmann_json)
-mo2_configure_plugin(fomod_plus_patch_wizard NO_SOURCES WARNINGS OFF PRIVATE_DEPENDS archive)
-mo2_install_target(fomod_plus_patch_wizard)
diff --git a/libs/installer_fomod_plus/patchwizard/FomodPlusPatchWizard.cpp b/libs/installer_fomod_plus/patchwizard/FomodPlusPatchWizard.cpp
deleted file mode 100644
index 00be8d5..0000000
--- a/libs/installer_fomod_plus/patchwizard/FomodPlusPatchWizard.cpp
+++ /dev/null
@@ -1,181 +0,0 @@
-#include "FomodPlusPatchWizard.h"
-
-#include <QApplication>
-#include <QDialog>
-#include <QHBoxLayout>
-#include <QLabel>
-#include <QMessageBox>
-#include <QProgressDialog>
-#include <QPushButton>
-#include <QVBoxLayout>
-
-#include "lib/PatchFinder.h"
-#include <FomodRescan.h>
-
-bool FomodPlusPatchWizard::init(IOrganizer* organizer)
-{
- mOrganizer = organizer;
- mDialog = new QDialog();
- mDialog->setWindowTitle(tr("Patch Wizard"));
- mDialog->setMinimumSize(400, 200);
- log.setLogFilePath(QDir::currentPath().toStdString() + "/logs/fomodplus-patchwizard.log");
-
- mOrganizer->onUserInterfaceInitialized([this](QMainWindow*) {
- logMessage(DEBUG, "patches populated.");
- mPatchFinder = std::make_unique<PatchFinder>(mOrganizer);
- mPatchFinder->populateInstalledPlugins();
- mAvailablePatches = mPatchFinder->getAvailablePatchesForModList();
- logMessage(DEBUG, "Available Patches: " + std::to_string(mAvailablePatches.size()));
- });
-
- return true;
-}
-
-void FomodPlusPatchWizard::display() const
-{
- // Clear any existing layout
- if (mDialog->layout() != nullptr) {
- QLayoutItem* item;
- while ((item = mDialog->layout()->takeAt(0)) != nullptr) {
- delete item->widget();
- delete item;
- }
- delete mDialog->layout();
- }
-
- if (mAvailablePatches.empty()) {
- setupEmptyState();
- } else {
- setupPatchList();
- }
-
- mDialog->exec();
-}
-
-void FomodPlusPatchWizard::setupEmptyState() const
-{
- auto* mainLayout = new QVBoxLayout(mDialog);
- mainLayout->setAlignment(Qt::AlignCenter);
-
- auto* contentWidget = new QWidget(mDialog);
- auto* contentLayout = new QHBoxLayout(contentWidget);
- contentLayout->setAlignment(Qt::AlignCenter);
- contentLayout->setSpacing(16);
-
- auto* imageLabel = new QLabel(contentWidget);
- imageLabel->setPixmap(QPixmap(":/fomod/infoscroll").scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
-
- auto* textLabel = new QLabel(
- tr("Nothing of interest yet. The wizard gets wiser as you\ninstall FOMODs, so check back later!"),
- contentWidget
- );
- textLabel->setAlignment(Qt::AlignVCenter | Qt::AlignLeft);
-
- contentLayout->addWidget(imageLabel);
- contentLayout->addWidget(textLabel);
-
- mainLayout->addWidget(contentWidget);
-
- auto* rescanButton = new QPushButton(tr("Rescan Load Order"), mDialog);
- // Use const_cast since setupEmptyState is const but onRescanClicked modifies state
- connect(rescanButton, &QPushButton::clicked, const_cast<FomodPlusPatchWizard*>(this),
- &FomodPlusPatchWizard::onRescanClicked);
- mainLayout->addWidget(rescanButton, 0, Qt::AlignCenter);
-}
-
-void FomodPlusPatchWizard::onRescanClicked()
-{
- const auto confirmResult = QMessageBox::question(
- mDialog,
- tr("Rescan Load Order"),
- tr("Rescanning will populate as many existing choices and options as we can, "
- "but if some downloads are deleted it may be missing things! "
- "It may take a few minutes depending on the size of your load order and all that."),
- QMessageBox::Ok | QMessageBox::Cancel,
- QMessageBox::Cancel
- );
-
- if (confirmResult != QMessageBox::Ok) {
- return;
- }
-
- logMessage(DEBUG, "Rescan requested by user");
-
- // Create progress dialog
- QProgressDialog progress(tr("Scanning mods..."), tr("Cancel"), 0, 100, mDialog);
- progress.setWindowModality(Qt::WindowModal);
- progress.setMinimumDuration(0);
- progress.setValue(0);
-
- bool cancelled = false;
-
- // Perform the rescan
- FomodRescan rescan(mOrganizer, mPatchFinder->mFomodDb.get());
- auto result = rescan.scanAllModsWithChoices([&](int current, int total, const QString& modName) {
- if (progress.wasCanceled()) {
- cancelled = true;
- return;
- }
- const int percent = total > 0 ? (current * 100 / total) : 0;
- progress.setValue(percent);
- progress.setLabelText(tr("Scanning: %1 (%2/%3)").arg(modName).arg(current).arg(total));
- QApplication::processEvents();
- });
-
- progress.setValue(100);
-
- if (cancelled) {
- QMessageBox::information(
- mDialog,
- tr("Rescan Cancelled"),
- tr("The rescan was cancelled. Partial results may have been saved.")
- );
- logMessage(INFO, "Rescan cancelled by user");
- } else {
- // Show result summary
- QString summary = tr("Rescan complete!\n\n"
- "Mods processed: %1\n"
- "Successfully scanned: %2\n"
- "Missing archives: %3\n"
- "Parse errors: %4")
- .arg(result.totalModsProcessed)
- .arg(result.successfullyScanned)
- .arg(result.missingArchives)
- .arg(result.parseErrors);
-
- if (!result.failedMods.empty() && result.failedMods.size() <= 10) {
- summary += tr("\n\nFailed mods:");
- for (const auto& mod : result.failedMods) {
- summary += QString("\n- %1").arg(QString::fromStdString(mod));
- }
- } else if (result.failedMods.size() > 10) {
- summary += tr("\n\n%1 mods failed (see log for details)").arg(result.failedMods.size());
- for (const auto& mod : result.failedMods) {
- logMessage(INFO, "Failed mod: " + mod);
- }
- }
-
- QMessageBox::information(mDialog, tr("Rescan Complete"), summary);
-
- logMessage(INFO, "Rescan complete: " + std::to_string(result.successfullyScanned) +
- "/" + std::to_string(result.totalModsProcessed) + " successful");
- }
-
- // Refresh available patches
- mPatchFinder->populateInstalledPlugins();
- mAvailablePatches = mPatchFinder->getAvailablePatchesForModList();
- logMessage(DEBUG, "Available Patches after rescan: " + std::to_string(mAvailablePatches.size()));
-
- // Refresh the UI
- display();
-}
-
-void FomodPlusPatchWizard::setupPatchList() const
-{
- auto* mainLayout = new QVBoxLayout(mDialog);
-
- auto* label = new QLabel(tr("Available patches: %1").arg(mAvailablePatches.size()), mDialog);
- mainLayout->addWidget(label);
-
- // TODO: Implement actual patch list UI
-} \ No newline at end of file
diff --git a/libs/installer_fomod_plus/patchwizard/FomodPlusPatchWizard.h b/libs/installer_fomod_plus/patchwizard/FomodPlusPatchWizard.h
deleted file mode 100644
index 2324fb0..0000000
--- a/libs/installer_fomod_plus/patchwizard/FomodPlusPatchWizard.h
+++ /dev/null
@@ -1,54 +0,0 @@
-#pragma once
-#include "../installer/lib/Logger.h"
-#include "lib/PatchFinder.h"
-
-#include <iplugintool.h>
-#include <qtmetamacros.h>
-
-using namespace MOBase;
-
-class FomodPlusPatchWizard final : public IPluginTool {
- Q_OBJECT
- Q_INTERFACES(MOBase::IPlugin MOBase::IPluginTool)
-#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
- Q_PLUGIN_METADATA(IID "io.clearing.FomodPlusPatchWizard" FILE "fomodpluspatchwizard.json")
-#endif
-
-public:
- bool init(IOrganizer* organizer) override;
-
- [[nodiscard]] QString name() const override { return tr("Patch Wizard"); };
-
- [[nodiscard]] QString author() const override { return "clearing"; };
-
- [[nodiscard]] QString description() const override { return tr("Find missing patches from FOMODs in your load order."); };
-
- [[nodiscard]] VersionInfo version() const override { return { 1, 0, 0, VersionInfo::RELEASE_BETA }; };
-
- [[nodiscard]] QList<PluginSetting> settings() const override { return {}; };
-
- [[nodiscard]] QString displayName() const override { return tr("Patch Wizard"); };
-
- [[nodiscard]] QString tooltip() const override { return tr("Find missing patches from FOMODs in your load order."); };
-
- [[nodiscard]] QIcon icon() const override { return QIcon(":/fomod/hat"); }
-
- void display() const override;
-
-private:
- Logger& log = Logger::getInstance();
- QDialog* mDialog{ nullptr };
- IOrganizer* mOrganizer{ nullptr };
- std::unique_ptr<PatchFinder> mPatchFinder{ nullptr };
- std::vector<AvailablePatch> mAvailablePatches;
-
- void setupEmptyState() const;
- void setupPatchList() const;
- void onRescanClicked();
-
- void logMessage(const LogLevel level, const std::string& message) const
- {
- log.logMessage(level, "[PATCHFINDER] " + message);
- }
-
-}; \ No newline at end of file
diff --git a/libs/installer_fomod_plus/patchwizard/fomod_plus_patch_wizard_en.ts b/libs/installer_fomod_plus/patchwizard/fomod_plus_patch_wizard_en.ts
deleted file mode 100644
index 7da04f1..0000000
--- a/libs/installer_fomod_plus/patchwizard/fomod_plus_patch_wizard_en.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE TS>
-<TS version="2.1" language="en_US">
-<context>
- <name>FomodPlusPatchWizard</name>
- <message>
- <location filename="FomodPlusPatchWizard.cpp" line="19"/>
- <location filename="FomodPlusPatchWizard.h" line="20"/>
- <location filename="FomodPlusPatchWizard.h" line="30"/>
- <source>Patch Wizard</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusPatchWizard.cpp" line="69"/>
- <source>Nothing of interest yet. The wizard gets wiser as you
-install FOMODs, so check back later!</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusPatchWizard.cpp" line="79"/>
- <location filename="FomodPlusPatchWizard.cpp" line="90"/>
- <source>Rescan Load Order</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusPatchWizard.cpp" line="91"/>
- <source>Rescanning will populate as many existing choices and options as we can, but if some downloads are deleted it may be missing things! It may take a few minutes depending on the size of your load order and all that.</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusPatchWizard.cpp" line="105"/>
- <source>Scanning mods...</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusPatchWizard.cpp" line="105"/>
- <source>Cancel</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusPatchWizard.cpp" line="121"/>
- <source>Scanning: %1 (%2/%3)</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusPatchWizard.cpp" line="130"/>
- <source>Rescan Cancelled</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusPatchWizard.cpp" line="131"/>
- <source>The rescan was cancelled. Partial results may have been saved.</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusPatchWizard.cpp" line="136"/>
- <source>Rescan complete!
-
-Mods processed: %1
-Successfully scanned: %2
-Missing archives: %3
-Parse errors: %4</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusPatchWizard.cpp" line="147"/>
- <source>
-
-Failed mods:</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusPatchWizard.cpp" line="152"/>
- <source>
-
-%1 mods failed (see log for details)</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusPatchWizard.cpp" line="158"/>
- <source>Rescan Complete</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusPatchWizard.cpp" line="177"/>
- <source>Available patches: %1</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusPatchWizard.h" line="24"/>
- <location filename="FomodPlusPatchWizard.h" line="32"/>
- <source>Find missing patches from FOMODs in your load order.</source>
- <translation type="unfinished"></translation>
- </message>
-</context>
-</TS>
diff --git a/libs/installer_fomod_plus/patchwizard/fomod_plus_patchwizard_de.ts b/libs/installer_fomod_plus/patchwizard/fomod_plus_patchwizard_de.ts
deleted file mode 100644
index aaa26e2..0000000
--- a/libs/installer_fomod_plus/patchwizard/fomod_plus_patchwizard_de.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE TS>
-<TS version="2.1" language="de_DE">
-<context>
- <name>FomodPlusPatchWizard</name>
- <message>
- <location filename="FomodPlusPatchWizard.cpp" line="26"/>
- <location filename="FomodPlusPatchWizard.cpp" line="51"/>
- <location filename="FomodPlusPatchWizard.cpp" line="74"/>
- <source>Patch Wizard</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusPatchWizard.cpp" line="36"/>
- <source>Finde Patches, die du aus den FOMODs in deiner Modliste installieren m&ouml;chtest.</source>
- <translation type="unfinished"></translation>
- </message>
-</context>
-</TS>
diff --git a/libs/installer_fomod_plus/patchwizard/fomodpluspatchwizard.json b/libs/installer_fomod_plus/patchwizard/fomodpluspatchwizard.json
deleted file mode 100644
index 2d6025f..0000000
--- a/libs/installer_fomod_plus/patchwizard/fomodpluspatchwizard.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "author": "clearing",
- "date": "2025/02/06",
- "name": "FOMOD Plus - Patch Wizard",
- "version": "1.0.0",
- "des": "Find missing patches in your modlist",
- "dependencies": []
-}
diff --git a/libs/installer_fomod_plus/patchwizard/lib/PatchFinder.cpp b/libs/installer_fomod_plus/patchwizard/lib/PatchFinder.cpp
deleted file mode 100644
index 855fe2b..0000000
--- a/libs/installer_fomod_plus/patchwizard/lib/PatchFinder.cpp
+++ /dev/null
@@ -1,85 +0,0 @@
-#include "PatchFinder.h"
-
-std::vector<AvailablePatch> PatchFinder::getAvailablePatchesForMod(const MOBase::IModInterface* mod)
-{
- std::vector<AvailablePatch> available_patches = {};
-
- // Verify that the mod has plugins in some shape or form.
- if (const auto it = m_installedPlugins.find(mod); it == m_installedPlugins.end()) {
- return available_patches;
- }
-
- // Look through the database
- for (const auto& fomodDbEntries = mFomodDb->getEntries(); const auto& entry : fomodDbEntries) {
- for (const auto& option : entry->getOptions()) {
- // Skip options that are already selected/installed
- if (option.selectionState == SelectionState::Selected) {
- continue;
- }
-
- // Extract just the filename from the path (handle both / and \)
- const auto fileName = option.fileName.substr(option.fileName.find_last_of("/\\") + 1);
-
- // Skip if already installed in the modlist
- if (m_installedPluginsCacheSet.contains(fileName)) {
- continue;
- }
-
- // Technically without this check, we're doing a lookup for the entire modlist, which actually
- // kinda works, but isn't the intention of this function. Might want to consider reworking
- // it to map to mod names with one pass of the DB instead of a DB pass per mod.
- if (!std::ranges::any_of(option.masters, [&mod](const std::string& master) {
- return master == mod->name().toStdString();
- })) {
- continue;
- }
-
- // If we have all of this patch's masters in our modlist, and it's not installed, add it to the results.
- if (std::ranges::all_of(option.masters, [this](const std::string& master) {
- return m_installedPluginsCacheSet.contains(master);
- })) {
- AvailablePatch patch{
- option,
- entry->getDisplayName(),
- mod->name().toStdString(),
- false, // not installed
- false, // not hidden
- option.selectionState == SelectionState::Deselected // userDeselected
- };
- available_patches.push_back(patch);
- }
- }
- }
-
- return available_patches;
-}
-
-std::vector<AvailablePatch> PatchFinder::getAvailablePatchesForModList()
-{
- std::vector<AvailablePatch> available_patches = {};
- for (const auto& modName : m_organizer->modList()->allMods()) {
- const auto mod = m_organizer->modList()->getMod(modName);
- if (mod == nullptr) {
- continue;
- }
- for (const auto& available_patch : getAvailablePatchesForMod(mod)) {
- available_patches.emplace_back(available_patch);
- }
- }
-
- return available_patches;
-}
-
-void PatchFinder::populateInstalledPlugins()
-{
- for (const auto& modName : m_organizer->modList()->allMods()) {
- const auto mod = m_organizer->modList()->getMod(modName);
- const auto mod_tree = mod->fileTree();
- for (auto it = mod_tree->begin(); it != mod_tree->end(); ++it) {
- if ((*it)->isFile() && isPluginFile((*it)->name())) {
- m_installedPlugins[mod].emplace_back((*it)->name().toStdString());
- m_installedPluginsCacheSet.insert((*it)->name().toStdString());
- }
- }
- }
-} \ No newline at end of file
diff --git a/libs/installer_fomod_plus/patchwizard/lib/PatchFinder.h b/libs/installer_fomod_plus/patchwizard/lib/PatchFinder.h
deleted file mode 100644
index 02327e3..0000000
--- a/libs/installer_fomod_plus/patchwizard/lib/PatchFinder.h
+++ /dev/null
@@ -1,49 +0,0 @@
-#pragma once
-
-#include "../../installer/lib/Logger.h"
-
-#include <FomodDB.h>
-#include <imodinterface.h>
-#include <imoinfo.h>
-#include <ifiletree.h>
-
-struct AvailablePatch {
- FomodOption fomod_option;
- std::string installer_name;
- std::string patch_for_mod;
- bool installed = false;
- bool hidden = false;
- bool userDeselected = false; // True if user explicitly chose not to install
-};
-
-class PatchFinder {
-friend class FomodPlusPatchWizard;
-
-public:
- explicit PatchFinder(MOBase::IOrganizer* m_organizer) : m_organizer(m_organizer)
- {
- mFomodDb = std::make_unique<FomodDB>(m_organizer->basePath().toStdString());
- logMessage(DEBUG, "mFomodDb loaded.");
- }
-
- std::vector<AvailablePatch> getAvailablePatchesForMod(const MOBase::IModInterface* mod);
- std::vector<AvailablePatch> getAvailablePatchesForModList();
-
-protected:
- void populateInstalledPlugins();
-
-private:
- Logger& log = Logger::getInstance();
- MOBase::IOrganizer* m_organizer;
- std::unique_ptr<FomodDB> mFomodDb;
-
- // Map of { pluginPtr: [1.esp, 2.esp, 3.esp] }
- std::unordered_map<const MOBase::IModInterface*, std::vector<std::string> > m_installedPlugins;
- std::unordered_set<std::string> m_installedPluginsCacheSet;
-
- void logMessage(const LogLevel level, const std::string& message) const
- {
- log.logMessage(level, "[PATCHFINDER] " + message);
- }
-
-}; \ No newline at end of file
diff --git a/libs/installer_fomod_plus/patchwizard/resources.qrc b/libs/installer_fomod_plus/patchwizard/resources.qrc
deleted file mode 100644
index fe6971e..0000000
--- a/libs/installer_fomod_plus/patchwizard/resources.qrc
+++ /dev/null
@@ -1,6 +0,0 @@
-<RCC>
- <qresource prefix="/fomod">
- <file alias="hat">resources/fomod_icon.png</file>
- <file alias="infoscroll">resources/infoscroll.png</file>
- </qresource>
-</RCC>
diff --git a/libs/installer_fomod_plus/patchwizard/resources/fomod_icon.png b/libs/installer_fomod_plus/patchwizard/resources/fomod_icon.png
deleted file mode 100644
index e044052..0000000
--- a/libs/installer_fomod_plus/patchwizard/resources/fomod_icon.png
+++ /dev/null
Binary files differ
diff --git a/libs/installer_fomod_plus/patchwizard/resources/infoscroll.png b/libs/installer_fomod_plus/patchwizard/resources/infoscroll.png
deleted file mode 100644
index 0c31777..0000000
--- a/libs/installer_fomod_plus/patchwizard/resources/infoscroll.png
+++ /dev/null
Binary files differ
diff --git a/libs/installer_fomod_plus/scanner/CMakeLists.txt b/libs/installer_fomod_plus/scanner/CMakeLists.txt
deleted file mode 100644
index f50dea2..0000000
--- a/libs/installer_fomod_plus/scanner/CMakeLists.txt
+++ /dev/null
@@ -1,54 +0,0 @@
-cmake_minimum_required(VERSION 3.16)
-project(fomod_plus_scanner)
-
-include(FetchContent)
-set(project_type plugin)
-
-file(GLOB_RECURSE SCANNER_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(fomod_plus_scanner SHARED ${SCANNER_SOURCES})
-
-FetchContent_Declare(json URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz DOWNLOAD_EXTRACT_TIMESTAMP TRUE)
-FetchContent_Declare(pugixml GIT_REPOSITORY https://github.com/zeux/pugixml GIT_TAG v1.14)
-FetchContent_MakeAvailable(pugixml json)
-
-target_include_directories(
- fomod_plus_scanner
- PUBLIC
- ${CMAKE_CURRENT_SOURCE_DIR}
- ${CMAKE_CURRENT_SOURCE_DIR}/../share
- ${CMAKE_CURRENT_SOURCE_DIR}/../share/FOMODData
- ${MO2_UIBASE_INCLUDE_DIRS}
- ${MO2_ARCHIVE_INCLUDE_DIRS}
-)
-
-if (MSVC)
- target_compile_options(
- fomod_plus_scanner
- PRIVATE
- /bigobj
- /W4
- /WX
- /wd4201
- /wd4458
- )
-endif ()
-
-target_link_libraries(fomod_plus_scanner PRIVATE mo2::uibase mo2::archive pugixml nlohmann_json::nlohmann_json)
-mo2_configure_plugin(fomod_plus_scanner NO_SOURCES WARNINGS OFF PRIVATE_DEPENDS archive)
-
-if (MSVC)
- target_compile_options(fomod_plus_scanner PRIVATE $<$<CONFIG:RelWithDebInfo>:/Zi>)
- target_link_options(fomod_plus_scanner PRIVATE $<$<CONFIG:RelWithDebInfo>:/DEBUG>)
- install(FILES $<TARGET_PDB_FILE:fomod_plus_scanner>
- DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO}
- CONFIGURATIONS RelWithDebInfo
- OPTIONAL)
-endif ()
-
-mo2_install_target(fomod_plus_scanner)
diff --git a/libs/installer_fomod_plus/scanner/FomodPlusScanner.cpp b/libs/installer_fomod_plus/scanner/FomodPlusScanner.cpp
deleted file mode 100644
index 4770178..0000000
--- a/libs/installer_fomod_plus/scanner/FomodPlusScanner.cpp
+++ /dev/null
@@ -1,128 +0,0 @@
-#include "FomodPlusScanner.h"
-
-#include "archiveparser.h"
-#include "FomodDBEntry.h"
-
-#include <QDialog>
-#include <QPushButton>
-#include <QProgressBar>
-#include <QVBoxLayout>
-#include <QMessageBox>
-#include <archive.h>
-
-#include <QLabel>
-#include <QMovie>
-#include <iostream>
-
-#include "stringutil.h"
-
-using ScanCallbackFn = std::function<bool(IModInterface*, ScanResult result)>;
-
-bool FomodPlusScanner::init(IOrganizer* organizer)
-{
- mOrganizer = organizer;
-
- mDialog = new QDialog();
- mDialog->setWindowTitle(tr("FOMOD Scanner"));
-
- const auto layout = new QVBoxLayout(mDialog);
-
- const QString description = tr("Greetings, traveler.\n"
- "This tool will scan your load order for mods installed via FOMOD.\n"
- "It will also fix up any erroneous FOMOD flags from previous versions of FOMOD Plus :) \n\n"
- "Safe travels, and may your load order be free of conflicts.");
-
- const auto descriptionLabel = new QLabel(description, mDialog);
- descriptionLabel->setWordWrap(true); // Enable word wrap for large text
- descriptionLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
- descriptionLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); // Align text to the top-left corner
-
- const auto gifLabel = new QLabel(mDialog);
- const auto movie = new QMovie(":/fomod/wizard");
- gifLabel->setMovie(movie);
- movie->start();
-
- mProgressBar = new QProgressBar(mDialog);
- mProgressBar->setRange(0, mOrganizer->modList()->allMods().size());
- mProgressBar->setVisible(false);
-
- const auto scanButton = new QPushButton(tr("Scan"), mDialog);
- const auto cancelButton = new QPushButton(tr("Cancel"), mDialog);
-
- layout->addWidget(descriptionLabel, 1);
- layout->addWidget(gifLabel, 1);
- layout->addWidget(mProgressBar, 1);
- layout->addWidget(scanButton, 1);
- layout->addWidget(cancelButton, 1);
-
- connect(cancelButton, &QPushButton::clicked, mDialog, &QDialog::reject);
- connect(scanButton, &QPushButton::clicked, this, &FomodPlusScanner::onScanClicked);
- connect(mDialog, &QDialog::finished, this, &FomodPlusScanner::cleanup);
-
- mDialog->setLayout(layout);
- mDialog->setMinimumSize(400, 300);
- mDialog->adjustSize();
- descriptionLabel->adjustSize();
- return true;
-}
-
-void FomodPlusScanner::onScanClicked() const
-{
- mProgressBar->setVisible(true);
- const int added = scanLoadOrder(setFomodInfoForMod);
- mDialog->accept();
- QMessageBox::information(mDialog, tr("Scan Complete"),
- tr("The load order scan is complete. Updated filter info for ") + QString::number(added) + tr(" mods."));
- mOrganizer->refresh();
-}
-
-void FomodPlusScanner::cleanup() const
-{
- mProgressBar->reset();
- mProgressBar->setVisible(false);
-}
-
-void FomodPlusScanner::display() const
-{
- mDialog->exec();
-}
-
-int FomodPlusScanner::scanLoadOrder(const ScanCallbackFn& callback) const
-{
- int progress = 0;
- int modified = 0;
- for (const auto& modName : mOrganizer->modList()->allMods()) {
- const auto mod = mOrganizer->modList()->getMod(modName);
- if (const ScanResult result = openInstallationArchive(mod); callback(mod, result)) {
- modified++;
- }
- mProgressBar->setValue(++progress);
- }
- return modified;
-}
-
-ScanResult FomodPlusScanner::openInstallationArchive(const IModInterface* mod) const
-{
- const auto downloadsDir = mOrganizer->downloadsPath();
- const auto installationFilePath = mod->installationFile();
- return ArchiveParser::scanForFomodFiles(downloadsDir, installationFilePath, mod->name());
-}
-
-bool FomodPlusScanner::setFomodInfoForMod(IModInterface* mod, const ScanResult result)
-{
- const auto pluginName = "FOMOD Plus";
- const auto setting = mod->pluginSetting(pluginName, "fomod", 0);
- if (setting == 0 && ScanResult::HAS_FOMOD == result) {
- return mod->setPluginSetting(pluginName, "fomod", "{}");
- }
- if (setting != 0 && ScanResult::NO_FOMOD == result) {
- return mod->setPluginSetting(pluginName, "fomod", 0);
- }
- return false;
-}
-
-bool FomodPlusScanner::removeFomodInfoFromMod(IModInterface* mod, ScanResult)
-{
- const auto pluginName = QString::fromStdString("FOMOD Plus");
- return mod->setPluginSetting(pluginName, "fomod", 0);
-}
diff --git a/libs/installer_fomod_plus/scanner/FomodPlusScanner.h b/libs/installer_fomod_plus/scanner/FomodPlusScanner.h
deleted file mode 100644
index b7893f7..0000000
--- a/libs/installer_fomod_plus/scanner/FomodPlusScanner.h
+++ /dev/null
@@ -1,64 +0,0 @@
-#ifndef FOMODPLUSSCANNER_H
-#define FOMODPLUSSCANNER_H
-
-#include "archiveparser.h"
-
-#include <iplugin.h>
-#include <iplugintool.h>
-
-#include <QDialog>
-#include <QProgressBar>
-
-using namespace MOBase;
-
-class FomodPlusScanner final : public IPluginTool {
- Q_OBJECT
- Q_INTERFACES(MOBase::IPlugin MOBase::IPluginTool)
-#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
- Q_PLUGIN_METADATA(IID "io.clearing.FomodPlusScanner" FILE "fomodplusscanner.json")
-#endif
-
-public:
- ~FomodPlusScanner() override
- {
- delete mDialog;
- mDialog = nullptr;
- mProgressBar = nullptr;
- }
-
- bool init(IOrganizer* organizer) override;
-
- void onScanClicked() const;
-
- void cleanup() const;
-
- [[nodiscard]] QString name() const override { return "FOMOD Scanner"; } // This should not be translated
- [[nodiscard]] QString author() const override { return "clearing"; }
- [[nodiscard]] QString description() const override { return tr("Scans modlist for files installed via FOMOD"); }
- [[nodiscard]] VersionInfo version() const override { return {1, 0, 0, VersionInfo::RELEASE_FINAL}; }
-
- [[nodiscard]] QList<PluginSetting> settings() const override { return {}; }
-
- [[nodiscard]] QString displayName() const override { return tr("FOMOD Scanner"); }
-
- [[nodiscard]] QString tooltip() const override { return tr("Scan modlist for files installed via FOMOD"); }
-
- [[nodiscard]] QIcon icon() const override { return QIcon(":/fomod/hat"); }
-
- void display() const override;
-
- int scanLoadOrder(const std::function<bool(IModInterface*, ScanResult result)> &callback) const;
-
- ScanResult openInstallationArchive(const IModInterface* mod) const;
-
- static bool setFomodInfoForMod(IModInterface *mod, ScanResult result);
-
- static bool removeFomodInfoFromMod(IModInterface *mod, ScanResult);
-
-private:
- QDialog* mDialog{ nullptr };
- QProgressBar* mProgressBar{ nullptr };
- IOrganizer* mOrganizer{ nullptr };
-};
-
-#endif // FOMODPLUSSCANNER_H \ No newline at end of file
diff --git a/libs/installer_fomod_plus/scanner/archiveparser.h b/libs/installer_fomod_plus/scanner/archiveparser.h
deleted file mode 100644
index 8819a84..0000000
--- a/libs/installer_fomod_plus/scanner/archiveparser.h
+++ /dev/null
@@ -1,97 +0,0 @@
-#pragma once
-#include "stringutil.h"
-
-#include <QDir>
-#include <QString>
-#include <archive.h>
-#include <iostream>
-#include <ostream>
-
-inline std::ostream& operator<<(std::ostream& os, const Archive::Error& error)
-{
- switch (error) {
- case Archive::Error::ERROR_NONE:
- os << "No error";
- break;
- case Archive::Error::ERROR_ARCHIVE_NOT_FOUND:
- os << "File not found";
- break;
- case Archive::Error::ERROR_FAILED_TO_OPEN_ARCHIVE:
- os << "Failed to open file";
- break;
- case Archive::Error::ERROR_INVALID_ARCHIVE_FORMAT:
- os << "Invalid archive format";
- break;
- default:
- os << "Unknown error??";
- }
- return os;
-}
-
-inline bool hasFomodFiles(const std::vector<FileData*>& files)
-{
- bool hasModuleXml = false;
- bool hasInfoXml = false;
-
- for (const auto* file : files) {
- if (endsWithCaseInsensitive(file->getArchiveFilePath(), StringConstants::FomodFiles::W_MODULE_CONFIG.data())) {
- hasModuleXml = true;
- }
- if (endsWithCaseInsensitive(file->getArchiveFilePath(), StringConstants::FomodFiles::W_INFO_XML.data())) {
- hasInfoXml = true;
- }
- }
- return hasModuleXml && hasInfoXml;
-}
-
-/* This class can do the following:
- * 1.) Detects if an archive has FOMOD files in it (without extracting)
- * - This is used by the FOMOD scanner to set content flags
- *
- * 2.) It may support extracting ESPs and FOMODs, but this might be better served
- * by a separate class. Maybe it could return a ModuleConfiguration to use like
- * the installer.
- */
-
-enum class ScanResult {
- HAS_FOMOD,
- NO_FOMOD,
- NO_ARCHIVE
-};
-
-class ArchiveParser {
-public:
- static ScanResult scanForFomodFiles(const QString& downloadsPath, const QString& installationFilePath,
- const QString& modName)
- {
- if (installationFilePath.isEmpty()) {
- return ScanResult::NO_ARCHIVE;
- }
- const auto qualifiedInstallerPath = QDir(installationFilePath).isAbsolute()
- ? installationFilePath
- : downloadsPath + "/" + installationFilePath;
-
- const auto archive = CreateArchive();
-
- if (!archive->isValid()) {
- logErrorForMod(modName, "Failed to load the archive module ", archive);
- return ScanResult::NO_ARCHIVE;
- }
- if (!archive->open(qualifiedInstallerPath.toStdWString(), nullptr)) {
- logErrorForMod(modName, "Failed to open archive [" + qualifiedInstallerPath + "]", archive);
- return ScanResult::NO_ARCHIVE;
- }
-
- if (hasFomodFiles(archive->getFileList())) {
- return ScanResult::HAS_FOMOD;
- }
- return ScanResult::NO_FOMOD;
- }
-
-private:
- static void logErrorForMod(const QString& modName, const QString& message, const std::unique_ptr<Archive>& archive)
- {
- std::cerr << "[" << modName.toStdString() << "] " << message.toStdString() << " (" << archive->getLastError() <<
- ")" << std::endl;
- }
-}; \ No newline at end of file
diff --git a/libs/installer_fomod_plus/scanner/fomod_plus_scanner_de.ts b/libs/installer_fomod_plus/scanner/fomod_plus_scanner_de.ts
deleted file mode 100644
index 759824f..0000000
--- a/libs/installer_fomod_plus/scanner/fomod_plus_scanner_de.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE TS>
-<TS version="2.1" language="de_DE">
-<context>
- <name>FomodPlusScanner</name>
- <message>
- <location filename="FomodPlusScanner.cpp" line="23"/>
- <location filename="FomodPlusScanner.h" line="47"/>
- <source>FOMOD Scanner</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.cpp" line="27"/>
- <source>Sei gegrüßt, Reisender.
-Dieses Tool scannt deine Ladereihenfolge nach Mods, die über einen FOMOD installiert wurden.
-Es wird außerdem fehlerhafte FOMOD-Selektierungen aus früheren Versionen von FOMOD Plus beheben. :)
-
-Gute Reise, und möge deine Ladereihenfolge frei von Konflikten sein.</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.cpp" line="46"/>
- <source>Scannen</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.cpp" line="47"/>
- <source>Abbrechen</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.cpp" line="71"/>
- <source>Scan Abgeschlossen</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.cpp" line="72"/>
- <source>Der Scan der Ladereihenfolge ist abgeschlossen. Aktualisierte Filterinformationen für </source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.cpp" line="72"/>
- <source> Mods.</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.h" line="42"/>
- <source>Scannt die Modliste nach Dateien, die über einen FOMOD installiert wurden.</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.h" line="49"/>
- <source>Modliste nach Dateien durchsuchen, die über einen FOMOD installiert wurden.</source>
- <translation type="unfinished"></translation>
- </message>
-</context>
-</TS>
diff --git a/libs/installer_fomod_plus/scanner/fomod_plus_scanner_en.ts b/libs/installer_fomod_plus/scanner/fomod_plus_scanner_en.ts
deleted file mode 100644
index 83a310b..0000000
--- a/libs/installer_fomod_plus/scanner/fomod_plus_scanner_en.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE TS>
-<TS version="2.1" language="en_US">
-<context>
- <name>FomodPlusScanner</name>
- <message>
- <location filename="FomodPlusScanner.cpp" line="26"/>
- <location filename="FomodPlusScanner.h" line="42"/>
- <source>FOMOD Scanner</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.cpp" line="30"/>
- <source>Greetings, traveler.
-This tool will scan your load order for mods installed via FOMOD.
-It will also fix up any erroneous FOMOD flags from previous versions of FOMOD Plus :)
-
-Safe travels, and may your load order be free of conflicts.</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.cpp" line="49"/>
- <source>Scan</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.cpp" line="50"/>
- <source>Cancel</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.cpp" line="74"/>
- <source>Scan Complete</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.cpp" line="75"/>
- <source>The load order scan is complete. Updated filter info for </source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.cpp" line="75"/>
- <source> mods.</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.h" line="37"/>
- <source>Scans modlist for files installed via FOMOD</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.h" line="44"/>
- <source>Scan modlist for files installed via FOMOD</source>
- <translation type="unfinished"></translation>
- </message>
-</context>
-</TS>
diff --git a/libs/installer_fomod_plus/scanner/fomod_plus_scanner_zh_CN.ts b/libs/installer_fomod_plus/scanner/fomod_plus_scanner_zh_CN.ts
deleted file mode 100644
index 01f84c8..0000000
--- a/libs/installer_fomod_plus/scanner/fomod_plus_scanner_zh_CN.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE TS>
-<TS version="2.1" language="zh_CN">
-<context>
- <name>FomodPlusScanner</name>
- <message>
- <location filename="FomodPlusScanner.cpp" line="23"/>
- <location filename="FomodPlusScanner.h" line="47"/>
- <source>FOMOD Scanner</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.cpp" line="27"/>
- <source>你好,旅行者。
-该工具将扫描模组列表来查找通过 FOMOD 安装的模组,
-并修正旧版本错误的 FOMOD 标志(需保留安装包才能被检测到):)
-
-一路平安,祝您的 load order 没有冲突。</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.cpp" line="46"/>
- <source>扫描</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.cpp" line="47"/>
- <source>取消</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.cpp" line="71"/>
- <source>扫描完成</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.cpp" line="72"/>
- <source>模组扫描已完成。更新了 </source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.cpp" line="72"/>
- <source> 个模组筛选框信息。</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.h" line="42"/>
- <source>扫描模组列表查找通过 FOMOD 安装的文件</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="FomodPlusScanner.h" line="49"/>
- <source>扫描模组列表查找通过 FOMOD 安装的文件</source>
- <translation type="unfinished"></translation>
- </message>
-</context>
-</TS>
diff --git a/libs/installer_fomod_plus/scanner/fomodplusscanner.json b/libs/installer_fomod_plus/scanner/fomodplusscanner.json
deleted file mode 100644
index f6643ea..0000000
--- a/libs/installer_fomod_plus/scanner/fomodplusscanner.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "author" : "clearing",
- "date" : "2025/02/06",
- "name" : "FOMOD Plus - Scanner",
- "version" : "1.0.0",
- "des" : "Scans load order for FOMOD installations",
- "dependencies" : []
-}
diff --git a/libs/installer_fomod_plus/scanner/resources.qrc b/libs/installer_fomod_plus/scanner/resources.qrc
deleted file mode 100644
index df7c48c..0000000
--- a/libs/installer_fomod_plus/scanner/resources.qrc
+++ /dev/null
@@ -1,6 +0,0 @@
-<RCC>
- <qresource prefix="/fomod">
- <file alias="hat">resources/fomod_icon.png</file>
- <file alias="wizard">resources/wizard.gif</file>
- </qresource>
-</RCC> \ No newline at end of file
diff --git a/libs/installer_fomod_plus/scanner/resources/fomod_icon.png b/libs/installer_fomod_plus/scanner/resources/fomod_icon.png
deleted file mode 100644
index e044052..0000000
--- a/libs/installer_fomod_plus/scanner/resources/fomod_icon.png
+++ /dev/null
Binary files differ
diff --git a/libs/installer_fomod_plus/scanner/resources/wizard.gif b/libs/installer_fomod_plus/scanner/resources/wizard.gif
deleted file mode 100644
index c1f75d3..0000000
--- a/libs/installer_fomod_plus/scanner/resources/wizard.gif
+++ /dev/null
Binary files differ
diff --git a/libs/installer_fomod_plus/share/FOMODData/ArchiveExtractor.h b/libs/installer_fomod_plus/share/FOMODData/ArchiveExtractor.h
deleted file mode 100644
index 65cd0ae..0000000
--- a/libs/installer_fomod_plus/share/FOMODData/ArchiveExtractor.h
+++ /dev/null
@@ -1,160 +0,0 @@
-#pragma once
-
-#include "stringutil.h"
-
-#include <QDir>
-#include <QFileInfo>
-#include <QString>
-#include <QTemporaryDir>
-#include <archive.h>
-#include <filesystem>
-#include <functional>
-#include <iostream>
-#include <memory>
-#include <optional>
-
-struct ExtractionResult {
- bool success = false;
- QString moduleConfigPath;
- std::vector<QString> pluginPaths;
- QString errorMessage;
- std::unique_ptr<QTemporaryDir> tempDir; // Owns the temp directory lifetime
-};
-
-/**
- * Utility class to extract FOMOD data from archives without being in an installer context.
- * Used by the Patch Wizard's rescan functionality.
- */
-class ArchiveExtractor {
-public:
- using ProgressCallback = std::function<void(const QString& fileName)>;
-
- /**
- * Extract ModuleConfig.xml and plugin files from an archive.
- * @param archiveFilePath Full path to the archive file
- * @param progressCallback Optional callback for progress updates
- * @return ExtractionResult containing paths to extracted files
- */
- static ExtractionResult extractFomodData(
- const QString& archiveFilePath,
- const ProgressCallback& progressCallback = nullptr)
- {
- ExtractionResult result;
- result.tempDir = std::make_unique<QTemporaryDir>();
-
- if (!result.tempDir->isValid()) {
- result.errorMessage = "Failed to create temporary directory";
- return result;
- }
-
- const auto archive = CreateArchive();
- if (!archive->isValid()) {
- result.errorMessage = "Failed to load archive module";
- return result;
- }
-
- if (!archive->open(archiveFilePath.toStdWString(), nullptr)) {
- result.errorMessage = QString("Failed to open archive (error %1)")
- .arg(static_cast<int>(archive->getLastError()));
- return result;
- }
-
- // Get file list and mark files for extraction
- const auto& fileList = archive->getFileList();
- QString moduleConfigInArchive;
-
- for (auto* fileData : fileList) {
- const auto entryPath = QString::fromStdWString(fileData->getArchiveFilePath());
-
- // Check for ModuleConfig.xml
- if (entryPath.toLower().endsWith("fomod/moduleconfig.xml") ||
- entryPath.toLower().endsWith("fomod\\moduleconfig.xml")) {
- moduleConfigInArchive = entryPath;
- // Set output path relative to output directory for extract()
- fileData->addOutputFilePath(L"ModuleConfig.xml");
- result.moduleConfigPath = result.tempDir->filePath("ModuleConfig.xml");
- }
- // Check for plugin files
- else if (isPluginFile(entryPath)) {
- const auto fileName = QFileInfo(entryPath).fileName();
- const auto relativePath = QString("plugins/") + fileName;
- fileData->addOutputFilePath(relativePath.toStdWString());
- result.pluginPaths.push_back(result.tempDir->filePath(relativePath));
- }
- }
-
- if (moduleConfigInArchive.isEmpty()) {
- result.errorMessage = "No ModuleConfig.xml found in archive";
- return result;
- }
-
- // Create plugins subdirectory
- QDir(result.tempDir->path()).mkpath("plugins");
-
- // Extract the files
- Archive::FileChangeCallback fileChangeCallback = [&progressCallback](
- Archive::FileChangeType, const std::wstring& fileName) {
- if (progressCallback) {
- progressCallback(QString::fromStdWString(fileName));
- }
- };
-
- Archive::ErrorCallback errorCallback = [&result](const std::wstring& error) {
- result.errorMessage = QString::fromStdWString(error);
- };
-
- const bool extractSuccess = archive->extract(
- result.tempDir->path().toStdWString(),
- Archive::ProgressCallback{}, // progress callback
- fileChangeCallback,
- errorCallback
- );
-
- if (!extractSuccess) {
- if (result.errorMessage.isEmpty()) {
- result.errorMessage = "Extraction failed";
- }
- return result;
- }
-
- // Verify ModuleConfig.xml was extracted
- if (!QFile::exists(result.moduleConfigPath)) {
- result.errorMessage = "ModuleConfig.xml extraction failed";
- return result;
- }
-
- // Filter to only existing plugin files
- std::vector<QString> existingPlugins;
- for (const auto& path : result.pluginPaths) {
- if (QFile::exists(path)) {
- existingPlugins.push_back(path);
- }
- }
- result.pluginPaths = std::move(existingPlugins);
-
- result.success = true;
- return result;
- }
-
- /**
- * Check if an archive contains FOMOD files without extracting.
- * @param archiveFilePath Full path to the archive file
- * @return true if the archive contains fomod/ModuleConfig.xml
- */
- static bool hasFomodFiles(const QString& archiveFilePath)
- {
- const auto archive = CreateArchive();
- if (!archive->isValid() || !archive->open(archiveFilePath.toStdWString(), nullptr)) {
- return false;
- }
-
- for (const auto* fileData : archive->getFileList()) {
- const auto path = QString::fromStdWString(fileData->getArchiveFilePath());
- if (path.toLower().endsWith("fomod/moduleconfig.xml") ||
- path.toLower().endsWith("fomod\\moduleconfig.xml")) {
- return true;
- }
- }
- return false;
- }
-};
diff --git a/libs/installer_fomod_plus/share/FOMODData/FomodDB.h b/libs/installer_fomod_plus/share/FOMODData/FomodDB.h
deleted file mode 100644
index f5ea6f5..0000000
--- a/libs/installer_fomod_plus/share/FOMODData/FomodDB.h
+++ /dev/null
@@ -1,144 +0,0 @@
-#pragma once
-
-#include <fstream>
-#include <stringutil.h>
-
-#include "FomodDBEntry.h"
-
-#include <xml/ModuleConfiguration.h>
-
-#include "PluginReader.h"
-
-using FOMODDBEntries = std::vector<std::shared_ptr<FomodDbEntry> >;
-
-constexpr std::string FOMOD_DB_FILE = "fomod.db";
-
-class FomodDB {
-public:
- /**
- *
- * @param moBasePath The organizer instance's basePath() value
- * @param dbName The filename of the db. Only settable for testing purposes.
- */
- explicit FomodDB(const std::string &moBasePath, const std::string &dbName = FOMOD_DB_FILE) {
- dbFilePath = (std::filesystem::path(moBasePath) / dbName).string();
- loadFromFile();
- }
-
- // TODO: Also pull from non install steps (requiredInstallFiles or whatever, and optional);
- static std::shared_ptr<FomodDbEntry> getEntryFromFomod(ModuleConfiguration *fomod, std::vector<QString> pluginPaths,
- int modId) {
- std::vector<FomodOption> options;
- for (const auto &installStep: fomod->installSteps.installSteps) {
- for (const auto &group: installStep.optionalFileGroups.groups) {
- for (const auto &plugin: group.plugins.plugins) {
- // Create a DB entry for the given plugin if it has an ESP
- for (auto file: plugin.files.files) {
- if (file.isFolder || !isPluginFile(file.source)) {
- continue;
- }
-
- // Find the path in pluginPaths that ends with this path
- // PluginPaths is gathered from the archive contents.
- auto it = std::ranges::find_if(pluginPaths, [&file](const QString &path) {
- return path.endsWith(file.source.c_str());
- });
- if (it == pluginPaths.end()) {
- continue;
- }
- const auto &pluginPath = *it;
- const auto masters = PluginReader::readMasters(pluginPath.toStdString(), true);
- options.emplace_back(
- plugin.name,
- file.source,
- masters,
- installStep.name,
- group.name
- );
- }
- }
- }
- }
- return std::make_shared<FomodDbEntry>(modId, fomod->moduleName, options);
- }
-
- void addEntry(const std::shared_ptr<FomodDbEntry> &entry, const bool upsert = true) {
- // TODO: Test this upsert.
- if (upsert) {
- const auto it = std::ranges::find_if(entries, [&entry](const std::shared_ptr<FomodDbEntry> &e) {
- return e->getModId() == entry->getModId();
- });
- if (it != entries.end()) {
- *it = entry;
- } else {
- entries.emplace_back(entry);
- }
- } else {
- entries.emplace_back(entry);
- }
- }
-
- [[nodiscard]] const FOMODDBEntries &getEntries() { return entries; }
-
- void saveToFile() const {
- try {
- std::ofstream file(dbFilePath);
- if (!file.is_open()) {
- return;
- }
-
- file << toJson().dump(2); // Pretty-print with 2-space indentation
- file.close();
- } catch ([[maybe_unused]] const std::exception &e) {
- // Handle saving errors
- }
- }
-
- [[nodiscard]] nlohmann::json toJson() const {
- nlohmann::json jsonArray = nlohmann::json::array();
-
- for (const auto &entry: entries) {
- jsonArray.push_back(entry->toJson());
- }
-
- return jsonArray;
- }
-
-private:
- FOMODDBEntries entries;
- std::string dbFilePath;
-
- void loadFromFile() {
- entries.clear();
-
- // Create empty file if it doesn't exist
- if (!std::filesystem::exists(dbFilePath)) {
- std::ofstream file(dbFilePath);
- file << "[]"; // Empty JSON array
- file.close();
- return; // No entries to load
- }
-
- try {
- // Read and parse the JSON file
- std::ifstream file(dbFilePath);
- if (!file.is_open()) {
- return;
- }
-
- nlohmann::json jsonArray = nlohmann::json::parse(file);
-
- // Ensure it's an array
- if (!jsonArray.is_array()) {
- return;
- }
-
- // Process each entry in the array
- for (const auto &entryJson: jsonArray) {
- entries.push_back(std::make_unique<FomodDbEntry>(entryJson));
- }
- } catch ([[maybe_unused]] const std::exception &e) {
- // Handle parsing errors (leave entries empty)
- }
- }
-};
diff --git a/libs/installer_fomod_plus/share/FOMODData/FomodDBEntry.h b/libs/installer_fomod_plus/share/FOMODData/FomodDBEntry.h
deleted file mode 100644
index 5d39f95..0000000
--- a/libs/installer_fomod_plus/share/FOMODData/FomodDBEntry.h
+++ /dev/null
@@ -1,129 +0,0 @@
-#pragma once
-
-#include <string>
-#include <utility>
-#include <vector>
-#include <nlohmann/json.hpp>
-
-/*
-The following JSON will be part of an array of similar objects in the root level "JSON DB" for FOMOD Plus.
-It contains information to resolve the identity of a given mod (names can change), and then the options
-in the FOMOD with their respective masters.
-{
- modId: 12345,
- displayName: "Lux (Patch Hub)",
- options: [
- {
- "name" "JK's The Hag's Cure",
- "fileName": "Lux - JK's The Hag's Cure patch.esp",
- "masters": [
- "Skyrim.esm",
- "JK's The Hag's Cure.esp",
- "Lux - Resources.esp",
- "Lux.esp"
- ],
- "step": "Page One",
- "group": "Group One",
- "selectionState": "Available"
- }
- ]
-}
-*/
-
-enum class SelectionState {
- Unknown, // Not yet matched to choices
- Selected, // User selected this option
- Deselected, // User manually deselected
- Available // Present but user didn't interact (or choices not recorded)
-};
-
-inline std::string selectionStateToString(SelectionState state) {
- switch (state) {
- case SelectionState::Unknown: return "Unknown";
- case SelectionState::Selected: return "Selected";
- case SelectionState::Deselected: return "Deselected";
- case SelectionState::Available: return "Available";
- default: return "Unknown";
- }
-}
-
-inline SelectionState stringToSelectionState(const std::string& str) {
- if (str == "Selected") return SelectionState::Selected;
- if (str == "Deselected") return SelectionState::Deselected;
- if (str == "Available") return SelectionState::Available;
- return SelectionState::Unknown;
-}
-
-struct FomodOption {
- std::string name;
- std::string fileName;
- std::vector<std::string> masters;
- std::string step;
- std::string group;
- SelectionState selectionState = SelectionState::Unknown;
-
- FomodOption(std::string n, std::string fn, std::vector<std::string> m, std::string s, std::string g,
- SelectionState state = SelectionState::Unknown)
- : name(std::move(n)), fileName(std::move(fn)), masters(std::move(m)), step(std::move(s)), group(std::move(g)),
- selectionState(state) {}
-};
-
-class FomodDbEntry {
-public:
- explicit FomodDbEntry(nlohmann::json json) {
- modId = json["modId"];
- displayName = json["displayName"];
- for (auto &option: json["options"]) {
- // create an option from this object
- SelectionState state = SelectionState::Unknown;
- if (option.contains("selectionState")) {
- state = stringToSelectionState(option["selectionState"]);
- }
- FomodOption fomodOption(
- option["name"],
- option["fileName"],
- option["masters"],
- option["step"],
- option["group"],
- state
- );
- options.push_back(fomodOption);
- }
- }
-
- explicit FomodDbEntry(const int modId, std::string displayName, const std::vector<FomodOption> &options)
- : modId(modId), displayName(std::move(displayName)), options(options) {
- }
-
-
- [[nodiscard]] int getModId() const { return modId; }
- [[nodiscard]] std::string getDisplayName() const { return displayName; }
- [[nodiscard]] const std::vector<FomodOption>& getOptions() const { return options; }
- [[nodiscard]] std::vector<FomodOption>& getOptionsMutable() { return options; }
-
- [[nodiscard]] nlohmann::json toJson() const {
- nlohmann::json result;
- result["modId"] = modId;
- result["displayName"] = displayName;
-
- nlohmann::json optionsArray = nlohmann::json::array();
- for (const auto &[name, fileName, masters, step, group, selectionState]: options) {
- nlohmann::json optionJson;
- optionJson["name"] = name;
- optionJson["fileName"] = fileName;
- optionJson["masters"] = masters;
- optionJson["step"] = step;
- optionJson["group"] = group;
- optionJson["selectionState"] = selectionStateToString(selectionState);
- optionsArray.push_back(optionJson);
- }
-
- result["options"] = optionsArray;
- return result;
- }
-
-private:
- int modId;
- std::string displayName;
- std::vector<FomodOption> options;
-};
diff --git a/libs/installer_fomod_plus/share/FOMODData/FomodRescan.h b/libs/installer_fomod_plus/share/FOMODData/FomodRescan.h
deleted file mode 100644
index c7d53a2..0000000
--- a/libs/installer_fomod_plus/share/FOMODData/FomodRescan.h
+++ /dev/null
@@ -1,277 +0,0 @@
-#pragma once
-
-#include "ArchiveExtractor.h"
-#include "FomodDB.h"
-#include "stringutil.h"
-#include "xml/ModuleConfiguration.h"
-
-#include <QDir>
-#include <QString>
-#include <functional>
-#include <imodinterface.h>
-#include <imoinfo.h>
-#include <nlohmann/json.hpp>
-
-struct RescanResult {
- int totalModsProcessed = 0;
- int successfullyScanned = 0;
- int missingArchives = 0;
- int parseErrors = 0;
- std::vector<std::string> failedMods;
-};
-
-/**
- * Orchestrates rescanning of all mods with stored FOMOD choices to repopulate the database.
- * Used when fomod.db is missing or needs to be regenerated from existing installations.
- */
-class FomodRescan {
-public:
- using ProgressCallback = std::function<void(int current, int total, const QString& modName)>;
-
- FomodRescan(MOBase::IOrganizer* organizer, FomodDB* db)
- : mOrganizer(organizer), mFomodDb(db) {}
-
- /**
- * Scan all mods that have stored FOMOD Plus choices and repopulate the database.
- * @param progressCallback Optional callback for progress updates
- * @return RescanResult with statistics about the scan
- */
- RescanResult scanAllModsWithChoices(const ProgressCallback& progressCallback = nullptr)
- {
- RescanResult result;
-
- const auto modList = mOrganizer->modList();
- if (!modList) {
- return result;
- }
-
- // First pass: gather all mods with stored choices
- std::vector<MOBase::IModInterface*> modsWithChoices;
- for (const auto& modName : modList->allMods()) {
- auto* mod = modList->getMod(modName);
- if (mod && hasStoredChoices(mod)) {
- modsWithChoices.push_back(mod);
- }
- }
-
- result.totalModsProcessed = static_cast<int>(modsWithChoices.size());
-
- // Second pass: process each mod
- int current = 0;
- for (auto* mod : modsWithChoices) {
- current++;
- if (progressCallback) {
- progressCallback(current, result.totalModsProcessed, mod->name());
- }
-
- const auto scanResult = processMod(mod);
- switch (scanResult) {
- case ScanOutcome::Success:
- result.successfullyScanned++;
- break;
- case ScanOutcome::MissingArchive:
- result.missingArchives++;
- result.failedMods.push_back(mod->name().toStdString() + " (missing archive)");
- break;
- case ScanOutcome::ParseError:
- result.parseErrors++;
- result.failedMods.push_back(mod->name().toStdString() + " (parse error)");
- break;
- case ScanOutcome::NoFomod:
- result.failedMods.push_back(mod->name().toStdString() + " (no FOMOD)");
- break;
- }
- }
-
- // Save the database
- mFomodDb->saveToFile();
-
- return result;
- }
-
-private:
- MOBase::IOrganizer* mOrganizer;
- FomodDB* mFomodDb;
-
- enum class ScanOutcome {
- Success,
- MissingArchive,
- ParseError,
- NoFomod
- };
-
- /**
- * Check if a mod has stored FOMOD Plus choices (non-zero pluginSetting).
- */
- bool hasStoredChoices(MOBase::IModInterface* mod) const
- {
- const auto fomodData = mod->pluginSetting(
- StringConstants::Plugin::NAME.data(), "fomod", 0);
-
- if (!fomodData.isValid() || fomodData.isNull()) {
- return false;
- }
-
- // Check if it's actually valid JSON with steps
- try {
- const auto json = nlohmann::json::parse(fomodData.toString().toStdString());
- return json.contains("steps") && json["steps"].is_array() && !json["steps"].empty();
- } catch (...) {
- return false;
- }
- }
-
- /**
- * Get the stored choices JSON from a mod's pluginSetting.
- */
- nlohmann::json getStoredChoices(MOBase::IModInterface* mod) const
- {
- const auto fomodData = mod->pluginSetting(
- StringConstants::Plugin::NAME.data(), "fomod", 0);
-
- try {
- return nlohmann::json::parse(fomodData.toString().toStdString());
- } catch (...) {
- return nlohmann::json();
- }
- }
-
- /**
- * Process a single mod: extract archive, parse FOMOD, create DB entry with selection states.
- */
- ScanOutcome processMod(MOBase::IModInterface* mod)
- {
- // Get the archive path
- const auto installationFile = mod->installationFile();
- if (installationFile.isEmpty()) {
- return ScanOutcome::MissingArchive;
- }
-
- const auto downloadsPath = mOrganizer->downloadsPath();
- const auto archivePath = QDir(installationFile).isAbsolute()
- ? installationFile
- : downloadsPath + "/" + installationFile;
-
- if (!QFile::exists(archivePath)) {
- return ScanOutcome::MissingArchive;
- }
-
- // Extract FOMOD data from archive
- auto extractionResult = ArchiveExtractor::extractFomodData(archivePath);
- if (!extractionResult.success) {
- return ScanOutcome::ParseError;
- }
-
- // Parse ModuleConfiguration
- auto moduleConfig = std::make_unique<ModuleConfiguration>();
- try {
- if (!moduleConfig->deserialize(extractionResult.moduleConfigPath)) {
- return ScanOutcome::ParseError;
- }
- } catch (...) {
- return ScanOutcome::ParseError;
- }
-
- // Get the mod's Nexus ID
- const int modId = mod->nexusId();
-
- // Create FomodDbEntry using existing logic
- auto entry = FomodDB::getEntryFromFomod(
- moduleConfig.get(),
- extractionResult.pluginPaths,
- modId
- );
-
- if (!entry || entry->getOptions().empty()) {
- return ScanOutcome::NoFomod;
- }
-
- // Apply selection states from stored choices
- const auto choices = getStoredChoices(mod);
- applySelectionsToEntry(*entry, choices);
-
- // Add to database (upsert)
- mFomodDb->addEntry(entry, true);
-
- return ScanOutcome::Success;
- }
-
- /**
- * Apply user selection states to a FomodDbEntry based on stored choices JSON.
- *
- * Choices JSON format:
- * {
- * "steps": [{
- * "name": "Step Name",
- * "groups": [{
- * "name": "Group Name",
- * "plugins": ["Selected Plugin 1"],
- * "deselected": ["Manually Deselected Plugin"]
- * }]
- * }]
- * }
- */
- void applySelectionsToEntry(FomodDbEntry& entry, const nlohmann::json& choices)
- {
- if (!choices.contains("steps") || !choices["steps"].is_array()) {
- // No choices data - mark all as Available
- for (auto& option : entry.getOptionsMutable()) {
- option.selectionState = SelectionState::Available;
- }
- return;
- }
-
- // Build a lookup map for quick matching: stepName/groupName/pluginName -> state
- struct PluginState {
- bool selected = false;
- bool deselected = false;
- };
- std::map<std::string, PluginState> stateMap;
-
- for (const auto& step : choices["steps"]) {
- if (!step.contains("name") || !step.contains("groups")) continue;
- const std::string stepName = step["name"];
-
- for (const auto& group : step["groups"]) {
- if (!group.contains("name")) continue;
- const std::string groupName = group["name"];
-
- // Process selected plugins
- if (group.contains("plugins") && group["plugins"].is_array()) {
- for (const auto& plugin : group["plugins"]) {
- const std::string pluginName = plugin;
- const auto key = stepName + "/" + groupName + "/" + pluginName;
- stateMap[key].selected = true;
- }
- }
-
- // Process deselected plugins
- if (group.contains("deselected") && group["deselected"].is_array()) {
- for (const auto& plugin : group["deselected"]) {
- const std::string pluginName = plugin;
- const auto key = stepName + "/" + groupName + "/" + pluginName;
- stateMap[key].deselected = true;
- }
- }
- }
- }
-
- // Apply states to options
- for (auto& option : entry.getOptionsMutable()) {
- const auto key = option.step + "/" + option.group + "/" + option.name;
-
- if (auto it = stateMap.find(key); it != stateMap.end()) {
- if (it->second.selected) {
- option.selectionState = SelectionState::Selected;
- } else if (it->second.deselected) {
- option.selectionState = SelectionState::Deselected;
- } else {
- option.selectionState = SelectionState::Available;
- }
- } else {
- // Plugin not found in choices - mark as Available
- option.selectionState = SelectionState::Available;
- }
- }
- }
-};
diff --git a/libs/installer_fomod_plus/share/FOMODData/PluginReader.h b/libs/installer_fomod_plus/share/FOMODData/PluginReader.h
deleted file mode 100644
index dade7e1..0000000
--- a/libs/installer_fomod_plus/share/FOMODData/PluginReader.h
+++ /dev/null
@@ -1,119 +0,0 @@
-#pragma once
-
-#include <fstream>
-#include <string>
-#include <vector>
-#include <cstdint>
-#include <unordered_set>
-
-static const std::unordered_set<std::string> VANILLA_MASTERS = {
- "Skyrim.esm",
- "Update.esm",
- "Dawnguard.esm",
- "HearthFires.esm",
- "Dragonborn.esm"
-};
-
-class PluginReader {
-public:
-
- /**
- * Reads the master files from a Bethesda plugin file (ESP/ESM/ESL)
- * @param filePath Path to the plugin file
- * @param trimVanilla Exclude the vanilla game masters or not. Mostly to save DB space.
- * @return Vector of master filenames
- */
- static std::vector<std::string> readMasters(const std::string& filePath, const bool trimVanilla = false)
- {
- std::vector<std::string> masters;
- std::ifstream file(filePath, std::ios::binary);
-
- if (!file) {
- return masters;
- }
-
- // Check TES4 record signature
- char signature[4];
- file.read(signature, 4);
- if (strncmp(signature, "TES4", 4) != 0) {
- return masters;
- }
-
- // Read record size
- uint32_t recordSize;
- file.read(reinterpret_cast<char*>(&recordSize), 4);
-
- constexpr uint32_t skipSize = sizeof(uint32_t) // flags
- + sizeof(uint32_t) // formId
- + sizeof(uint16_t) // timestamp
- + sizeof(uint16_t) // version control
- + sizeof(uint16_t) // internal version
- + sizeof(uint16_t); // unknown
-
- // Skip header flags, formID, etc. (total 8 bytes)
- file.seekg(skipSize, std::ios::cur);
-
- // Calculate where the TES4 record ends
- std::streampos recordEnd = file.tellg() + static_cast<std::streampos>(recordSize);
-
- // Read subrecords until we reach the end of the TES4 record
- while (file && file.tellg() < recordEnd) {
- char subRecordType[4];
- uint16_t subRecordSize;
-
- // Read subrecord type and size
- file.read(subRecordType, 4);
- file.read(reinterpret_cast<char*>(&subRecordSize), 2);
-
- if (strncmp(subRecordType, "MAST", 4) == 0) {
- // Read master filename (null-terminated string)
- std::string masterName;
- masterName.resize(subRecordSize);
- file.read(masterName.data(), subRecordSize);
-
- // Remove null terminator if present
- if (!masterName.empty() && masterName.back() == '\0') {
- masterName.pop_back();
- }
-
- // Only add if it's not a vanilla master or if we're not trimming
- if (!trimVanilla || !VANILLA_MASTERS.contains(masterName)) {
- masters.push_back(masterName);
- }
-
- // Each MAST is followed by a DATA subrecord
- char dataType[4];
- uint16_t dataSize;
- file.read(dataType, 4);
- file.read(reinterpret_cast<char*>(&dataSize), 2);
-
- // Skip DATA content (usually an 8-byte value)
- file.seekg(dataSize, std::ios::cur);
- } else {
- // Skip other subrecord types
- file.seekg(subRecordSize, std::ios::cur);
- }
- }
-
- return masters;
- }
-
- /**
- * Checks if a file is a valid Bethesda plugin (ESP/ESM/ESL)
- * @param filePath Path to the file
- * @return True if the file is a valid plugin
- */
- static bool isValidPlugin(const std::string& filePath)
- {
- std::ifstream file(filePath, std::ios::binary);
-
- if (!file) {
- return false;
- }
-
- char signature[4];
- file.read(signature, 4);
-
- return strncmp(signature, "TES4", 4) == 0;
- }
-}; \ No newline at end of file
diff --git a/libs/installer_fomod_plus/share/stringutil.h b/libs/installer_fomod_plus/share/stringutil.h
deleted file mode 100644
index c5d2fb8..0000000
--- a/libs/installer_fomod_plus/share/stringutil.h
+++ /dev/null
@@ -1,150 +0,0 @@
-#ifndef STRINGCONSTANTS_H
-#define STRINGCONSTANTS_H
-#include <algorithm>
-#include <regex>
-#include <string>
-#include <vector>
-#include <QString>
-
-
-namespace StringConstants
-{
- namespace Plugin
- {
- constexpr std::string_view NAME = "FOMOD Plus";
- constexpr std::string_view AUTHOR = "clearing";
- constexpr std::string_view DESCRIPTION =
- "Extends the capabilities of the FOMOD installer for advanced users.\n\n"
- "Available colors (enter exactly): \n"
- "'Light0'\t'Light1'\t'Light2'\t'Light3'\n"
- "'Dark0'\t'Dark1'\t'Dark2'\t'Dark3'\n"
- "'Red'\t'Red Bright'\n"
- "'Green'\t'Green Bright'\n"
- "'Yellow'\t'Yellow Bright'\n"
- "'Blue'\t'Blue Bright'\n"
- "'Purple'\t'Purple Bright'\n"
- "'Aqua'\t'Aqua Bright'\n"
- "'Orange'\t'Orange Bright'\n";
- constexpr std::wstring_view W_NAME = L"FOMOD Plus";
- constexpr std::wstring_view W_AUTHOR = L"clearing";
- constexpr std::wstring_view W_DESCRIPTION =
- L"Extends the capabilities of the FOMOD installer for advanced users.";
- }
-
- namespace FomodFiles
- {
- constexpr std::string_view FOMOD_DIR = "fomod";
- constexpr std::string_view INFO_XML = "info.xml";
- constexpr std::string_view MODULE_CONFIG = "ModuleConfig.xml";
-
- // Wide string versions for archive API
- constexpr std::wstring_view W_FOMOD_DIR = L"fomod";
- constexpr std::wstring_view W_INFO_XML = L"fomod/info.xml";
- constexpr std::wstring_view W_MODULE_CONFIG = L"fomod/ModuleConfig.xml";
-
- constexpr std::string_view TYPE_REQUIRED = "Required";
- constexpr std::string_view TYPE_OPTIONAL = "Optional";
- constexpr std::string_view TYPE_RECOMMENDED = "Recommended";
- constexpr std::string_view TYPE_NOT_USABLE = "NotUsable";
- constexpr std::string_view TYPE_COULD_BE_USABLE = "CouldBeUsable";
- }
-}
-
-// Convert narrow string_view to wstring (for ASCII strings only)
-inline std::wstring toWide(std::string_view sv)
-{
- return std::wstring(sv.begin(), sv.end());
-}
-
-// trim from start (in place)
-inline void ltrim(std::string& s)
-{
- s.erase(s.begin(), std::ranges::find_if(s, [](const unsigned char ch)
- {
- return !std::isspace(ch);
- }));
-}
-
-// trim from end (in place)
-inline void rtrim(std::string& s)
-{
- s.erase(std::find_if(s.rbegin(), s.rend(), [](const unsigned char ch)
- {
- return !std::isspace(ch);
- }).base(), s.end());
-}
-
-// trim from both ends (in place)
-inline std::string& trim(std::string& s)
-{
- ltrim(s);
- rtrim(s);
- return s;
-}
-
-inline void trim(const std::vector<std::string>& strings)
-{
- for (auto s : strings) { trim(s); }
-}
-
-inline std::wstring toLower(const std::wstring& str)
-{
- std::wstring lowerStr = str;
- std::ranges::transform(lowerStr, lowerStr.begin(), towlower);
- return lowerStr;
-}
-
-inline std::string toLower(const std::string& str)
-{
- std::string lowerStr = str;
- std::ranges::transform(lowerStr, lowerStr.begin(), tolower);
- return lowerStr;
-}
-
-inline bool endsWithCaseInsensitive(const std::wstring& str, const std::wstring& suffix)
-{
- const std::wstring lowerStr = toLower(str);
- if (const std::wstring lowerSuffix = toLower(suffix); lowerStr.length() >= lowerSuffix.length())
- {
- return 0 == lowerStr.compare(lowerStr.length() - lowerSuffix.length(), lowerSuffix.length(), lowerSuffix);
- }
- return false;
-}
-
-inline QString formatPluginDescription(const QString& text)
-{
- std::string formattedText = text.toStdString();
- // Replace URLs with <a href> tags
- const std::regex
- urlRegex(R"((http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-]))");
- formattedText = std::regex_replace(formattedText, urlRegex, R"(<a href="$&">$&</a>)");
-
- // Replace line breaks
- formattedText = std::regex_replace(formattedText, std::regex("&#13;&#10;"), "<br>");
- formattedText = std::regex_replace(formattedText, std::regex("\\r\\n"), "<br>");
- formattedText = std::regex_replace(formattedText, std::regex("\\r"), "<br>");
- formattedText = std::regex_replace(formattedText, std::regex("\\n"), "<br>");
-
- return QString::fromStdString(formattedText);
-}
-
-// NOTE: This isn't perfect. Sometimes we have whole filenames, sometimes we're just passing
-// the suffix. It should be fine as long as no one names a file like..."Testesl". Idk what that
-// would do anyway.
-inline bool isPluginFile(const QString& file)
-{
- return file.toLower().endsWith("esl")
- || file.toLower().endsWith("esp")
- || file.toLower().endsWith("esm");
-}
-
-inline bool isPluginFile(const std::string& file)
-{
- const auto lower = toLower(file);
- return lower.ends_with("esl")
- || lower.ends_with("esp")
- || lower.ends_with("esm");
-}
-
-
-#endif
diff --git a/libs/installer_fomod_plus/share/xml/FomodInfoFile.cpp b/libs/installer_fomod_plus/share/xml/FomodInfoFile.cpp
deleted file mode 100644
index d4fed69..0000000
--- a/libs/installer_fomod_plus/share/xml/FomodInfoFile.cpp
+++ /dev/null
@@ -1,44 +0,0 @@
-#include "FomodInfoFile.h"
-#include "XmlParseException.h"
-#include <format>
-#include <pugixml.hpp>
-
-#include "stringutil.h"
-
-#include <QFile>
-#include <QString>
-
-bool FomodInfoFile::deserialize(const QString& filePath)
-{
- QFile file(filePath);
- if (!file.open(QIODevice::ReadOnly)) {
- throw XmlParseException(std::format("Failed to open file: {}", filePath.toStdString()));
- }
- const QByteArray content = file.readAll();
- file.close();
-
- pugi::xml_document doc;
- if (const pugi::xml_parse_result result = doc.load_buffer(content.constData(), content.size()); !result) {
- throw XmlParseException(std::format("XML parsed with errors: {}", result.description()));
- }
-
- const pugi::xml_node fomodNode = doc.child("fomod");
- if (!fomodNode) {
- throw XmlParseException("No <config> node found");
- }
-
- name = fomodNode.child("Name").text().as_string();
- author = fomodNode.child("Author").text().as_string();
- version = fomodNode.child("Version").text().as_string();
- website = fomodNode.child("Website").text().as_string();
- description = fomodNode.child("Description").text().as_string();
-
- trim({ name, author, version, website, description });
-
- for (pugi::xml_node groupNode : fomodNode.child("Groups").children("element")) {
- groups.emplace_back(groupNode.text().as_string());
- }
-
- return true;
-
-} \ No newline at end of file
diff --git a/libs/installer_fomod_plus/share/xml/FomodInfoFile.h b/libs/installer_fomod_plus/share/xml/FomodInfoFile.h
deleted file mode 100644
index d2c2a23..0000000
--- a/libs/installer_fomod_plus/share/xml/FomodInfoFile.h
+++ /dev/null
@@ -1,25 +0,0 @@
-#pragma once
-
-#include <qstring.h>
-#include <string>
-#include <vector>
-
-class FomodInfoFile {
-public:
- bool deserialize(const QString &filePath);
-
- [[nodiscard]] const std::string& getName() const { return name; }
- [[nodiscard]] const std::string& getAuthor() const { return author; }
- [[nodiscard]] const std::string& getVersion() const { return version; }
- [[nodiscard]] const std::string& getWebsite() const { return website; }
- [[nodiscard]] const std::string& getDescription() const { return description; }
- [[nodiscard]] const std::vector<std::string>& getGroups() const { return groups; }
-
-private:
- std::string name;
- std::string author;
- std::string version;
- std::string website;
- std::string description;
- std::vector<std::string> groups;
-}; \ No newline at end of file
diff --git a/libs/installer_fomod_plus/share/xml/ModuleConfiguration.cpp b/libs/installer_fomod_plus/share/xml/ModuleConfiguration.cpp
deleted file mode 100644
index 64d6b0b..0000000
--- a/libs/installer_fomod_plus/share/xml/ModuleConfiguration.cpp
+++ /dev/null
@@ -1,352 +0,0 @@
-#include "ModuleConfiguration.h"
-
-#include <format>
-
-#include "XmlHelper.h"
-#include "XmlParseException.h"
-#include "stringutil.h"
-
-#include <QFile>
-#include <QString>
-
-using namespace StringConstants::FomodFiles;
-
-static GroupTypeEnum groupTypeFromString(const std::string& groupType)
-{
- if (groupType == "SelectAny")
- return SelectAny;
- if (groupType == "SelectAll")
- return SelectAll;
- if (groupType == "SelectExactlyOne")
- return SelectExactlyOne;
- if (groupType == "SelectAtMostOne")
- return SelectAtMostOne;
- if (groupType == "SelectAtLeastOne")
- return SelectAtLeastOne;
- return SelectAny; // is this a sane default? probably
-}
-
-PluginTypeEnum pluginTypeFromString(const std::string& typeStr)
-{
- if (typeStr == TYPE_REQUIRED)
- return PluginTypeEnum::Required;
- if (typeStr == TYPE_OPTIONAL)
- return PluginTypeEnum::Optional;
- if (typeStr == TYPE_RECOMMENDED)
- return PluginTypeEnum::Recommended;
- if (typeStr == TYPE_NOT_USABLE)
- return PluginTypeEnum::NotUsable;
- if (typeStr == TYPE_COULD_BE_USABLE)
- return PluginTypeEnum::CouldBeUsable;
- return PluginTypeEnum::Optional;
-}
-
-template <typename T>
-bool deserializeList(pugi::xml_node& node, const char* childName, std::vector<T>& list)
-{
- for (pugi::xml_node childNode : node.children(childName)) {
- T item;
- item.deserialize(childNode);
- list.push_back(item);
- }
- return true;
-}
-
-/*
- * NOTE: We call 'trim()' on all error-prone fields that rely on user-input. I'm assuming these FOMODs are created with
- * the FOMOD creation tool, so I won't trim things like flags that the tool sets for the user.
- */
-
-bool FileDependency::deserialize(pugi::xml_node& node)
-{
- file = node.attribute("file").as_string();
- trim(file);
- // ReSharper disable once CppTooWideScopeInitStatement
- // Do not use 'auto' for these. it will break equality checks
- const std::string stateStr = node.attribute("state").as_string();
-
- if (stateStr == "Missing")
- state = FileDependencyTypeEnum::Missing;
- else if (stateStr == "Inactive")
- state = FileDependencyTypeEnum::Inactive;
- else if (stateStr == "Active")
- state = FileDependencyTypeEnum::Active;
- return true;
-}
-
-bool FlagDependency::deserialize(pugi::xml_node& node)
-{
- flag = node.attribute("flag").as_string();
- value = node.attribute("value").as_string();
- trim({ flag, value });
- return true;
-}
-
-bool GameDependency::deserialize(pugi::xml_node& node)
-{
- version = node.attribute("version").as_string();
- trim(version);
- return true;
-}
-
-bool CompositeDependency::deserialize(pugi::xml_node& node)
-{
-
- // this could EITHER have a dependencies child or the dependencies are here.
- // turns out they could have both.
- pugi::xml_node possibleNode = node;
-
- // If the dependencies are all right inside, just use the root node as the dependency base.
- // This looks hacky but accommodates both _nested_ dependencies for plugins, and extremely simple ones for step visibility.
- if (node.child("dependencies") && !node.child("fileDependency") && !node.child("flagDependency") &&!node.child("gameDependency")) {
- possibleNode = node.child("dependencies");
- }
-
- deserializeList(possibleNode, "fileDependency", fileDependencies);
- deserializeList(possibleNode, "flagDependency", flagDependencies);
- deserializeList(possibleNode, "gameDependency", gameDependencies);
- deserializeList(possibleNode, "dependencies", nestedDependencies);
-
- operatorType = OperatorTypeEnum::AND; // safest default.
-
- if (const std::string operatorStr = possibleNode.attribute("operator").as_string(); operatorStr == "Or") {
- operatorType = OperatorTypeEnum::OR;
- }
-
- return true;
-}
-
-bool DependencyPattern::deserialize(pugi::xml_node& node)
-{
- if (!node)
- return false;
- pugi::xml_node dependenciesNode = node.child("dependencies");
- dependencies.deserialize(dependenciesNode);
-
- const pugi::xml_node typeNode = node.child("type");
- type = pluginTypeFromString(typeNode.attribute("name").as_string());
- return true;
-}
-
-bool DependencyPatternList::deserialize(pugi::xml_node& node)
-{
- return deserializeList(node, "pattern", patterns);
-}
-
-bool DependencyPluginType::deserialize(pugi::xml_node& node)
-{
- pugi::xml_node patternsNode = node.child("patterns");
- const pugi::xml_node defaultTypeNode = node.child("defaultType");
- defaultType = pluginTypeFromString(defaultTypeNode.attribute("name").as_string());
- patterns.deserialize(patternsNode);
- return true;
-}
-
-bool TypeDescriptor::deserialize(pugi::xml_node& node)
-{
- pugi::xml_node dependencyTypeNode = node.child("dependencyType");
- dependencyType.deserialize(dependencyTypeNode);
- const pugi::xml_node typeNode = node.child("type");
- type = pluginTypeFromString(typeNode.attribute("name").as_string());
- return true;
-}
-
-bool Image::deserialize(pugi::xml_node& node)
-{
- path = node.attribute("path").as_string();
- return true;
-}
-
-bool HeaderImage::deserialize(pugi::xml_node& node)
-{
- path = node.attribute("path").as_string();
- showImage = node.attribute("showImage").as_bool();
- showFade = node.attribute("showFade").as_bool();
- height = node.attribute("height").as_int();
- return true;
-}
-
-bool FileList::deserialize(pugi::xml_node& node)
-{
- for (pugi::xml_node childNode : node.children()) {
- if (std::string(childNode.name()) == "folder"
- || std::string(childNode.name()) == "file") {
- File file;
- file.deserialize(childNode);
- files.emplace_back(file);
- }
- }
- return true;
-}
-
-bool ConditionalFileInstallPattern::deserialize(pugi::xml_node& node)
-{
- pugi::xml_node dependenciesNode = node.child("dependencies");
- pugi::xml_node filesNode = node.child("files");
-
- dependencies.deserialize(dependenciesNode);
- files.deserialize(filesNode);
-
- return true;
-}
-
-// <flag name="2">On</flag>
-bool ConditionFlag::deserialize(pugi::xml_node& node)
-{
- name = node.attribute("name").as_string();
- value = node.child_value(); //
- return true;
-}
-
-bool ConditionFlagList::deserialize(pugi::xml_node& node)
-{
- return deserializeList(node, "flag", flags);
-}
-
-bool File::deserialize(pugi::xml_node& node)
-{
- source = node.attribute("source").as_string();
- // destination = node.attribute("destination").as_string();
- priority = node.attribute("priority").as_int();
- isFolder = strcmp(node.name(), "folder") == 0;
- if (auto attr = node.attribute("destination"); attr) {
- destination = attr.as_string();
- } else {
- destination = std::nullopt;
- }
- return true;
-}
-
-
-bool Plugin::deserialize(pugi::xml_node& node)
-{
- pugi::xml_node imageNode = node.child("image");
- pugi::xml_node typeDescriptorNode = node.child("typeDescriptor");
- pugi::xml_node conditionFlagsNode = node.child("conditionFlags");
- pugi::xml_node filesNode = node.child("files");
-
- // Description is optional in the schema; guard against null C strings from pugixml.
- if (const pugi::xml_node descNode = node.child("description")) {
- if (const char* rawDesc = descNode.text().as_string()) {
- description = rawDesc;
- }
- }
- description = trim(description); // Find a better way to do this eventually.
- image.deserialize(imageNode);
- typeDescriptor.deserialize(typeDescriptorNode);
- name = node.attribute("name").as_string();
- name = trim(name);
- conditionFlags.deserialize(conditionFlagsNode);
- files.deserialize(filesNode);
- return true;
-}
-
-bool PluginList::deserialize(pugi::xml_node& node)
-{
- deserializeList(node, "plugin", plugins);
- order = XmlHelper::getOrderType(node.attribute("order").as_string(), OrderTypeEnum::Ascending);
-
- // Sort the plugins based on the specified order
- std::ranges::sort(plugins, [this](const Plugin& a, const Plugin& b) {
- if (order == OrderTypeEnum::Ascending) {
- return a.name < b.name;
- }
- if (order == OrderTypeEnum::Descending) {
- return a.name > b.name;
- }
- return false; // Default case, no sorting
- });
-
- return true;
-}
-
-bool Group::deserialize(pugi::xml_node& node)
-{
- pugi::xml_node pluginsNode = node.child("plugins");
- plugins.deserialize(pluginsNode);
- name = node.attribute("name").as_string();
- type = groupTypeFromString(node.attribute("type").as_string());
- return true;
-}
-
-bool GroupList::deserialize(pugi::xml_node& node)
-{
- deserializeList(node, "group", groups);
- order = XmlHelper::getOrderType(node.attribute("order").as_string());
-
- // Sort the groups based on the specified order
- std::ranges::sort(groups, [this](const Group& a, const Group& b) {
- if (order == OrderTypeEnum::Ascending) {
- return a.name < b.name;
- }
- if (order == OrderTypeEnum::Descending) {
- return a.name > b.name;
- }
- return false; // Default case, no sorting
- });
- return true;
-}
-
-bool InstallStep::deserialize(pugi::xml_node& node)
-{
- pugi::xml_node visibleNode = node.child("visible");
- pugi::xml_node optionalFileGroupsNode = node.child("optionalFileGroups");
- visible.deserialize(visibleNode);
- optionalFileGroups.deserialize(optionalFileGroupsNode);
- name = node.attribute("name").as_string();
- return true;
-}
-
-bool ConditionalFileInstall::deserialize(pugi::xml_node& node)
-{
- pugi::xml_node patternsNode = node.child("patterns");
- deserializeList(patternsNode, "pattern", patterns);
- return true;
-}
-
-bool StepList::deserialize(pugi::xml_node& node)
-{
- deserializeList(node, "installStep", installSteps);
- order = XmlHelper::getOrderType(node.attribute("order").as_string());
- return true;
-}
-
-bool ModuleConfiguration::deserialize(const QString& filePath)
-{
- QFile file(filePath);
- if (!file.open(QIODevice::ReadOnly)) {
- throw XmlParseException(std::format("Failed to open file: {}", filePath.toStdString()));
- }
- const QByteArray content = file.readAll();
- file.close();
-
- pugi::xml_document doc;
- if (const pugi::xml_parse_result result = doc.load_buffer(content.constData(), content.size()); !result) {
- throw XmlParseException(std::format("XML parsed with errors: {}", result.description()));
- }
-
- const pugi::xml_node configNode = doc.child("config");
- if (!configNode) {
- throw XmlParseException("No <config> node found");
- }
-
- moduleName = configNode.child("moduleName").text().as_string();
-
- moduleImage = HeaderImage();
- pugi::xml_node moduleImageNode = configNode.child("moduleImage");
- moduleImage.deserialize(moduleImageNode);
-
- pugi::xml_node moduleDependenciesNode = configNode.child("moduleDependencies");
- moduleDependencies.deserialize(moduleDependenciesNode);
-
- pugi::xml_node requiredInstallFilesNode = configNode.child("requiredInstallFiles");
- requiredInstallFiles.deserialize(requiredInstallFilesNode);
-
- pugi::xml_node installStepsNode = configNode.child("installSteps");
- installSteps.deserialize(installStepsNode);
-
- pugi::xml_node conditionalFileInstallsNode = configNode.child("conditionalFileInstalls");
- conditionalFileInstalls.deserialize(conditionalFileInstallsNode);
-
- return true;
-}
diff --git a/libs/installer_fomod_plus/share/xml/ModuleConfiguration.h b/libs/installer_fomod_plus/share/xml/ModuleConfiguration.h
deleted file mode 100644
index a0a017b..0000000
--- a/libs/installer_fomod_plus/share/xml/ModuleConfiguration.h
+++ /dev/null
@@ -1,305 +0,0 @@
-#pragma once
-
-#include <iostream>
-#include <optional>
-#include <pugixml.hpp>
-#include <qstring.h>
-#include <string>
-#include <vector>
-
-class XmlDeserializable {
-public:
- virtual ~XmlDeserializable() = default;
-
- virtual bool deserialize(pugi::xml_node& node) = 0;
-
-protected:
- XmlDeserializable() = default;
-};
-
-enum GroupTypeEnum {
- SelectAny,
- SelectAll,
- SelectExactlyOne,
- SelectAtMostOne,
- SelectAtLeastOne
-};
-
-enum class OperatorTypeEnum {
- AND,
- OR
-};
-
-enum class OrderTypeEnum {
- Explicit,
- Ascending,
- Descending
-};
-
-enum class FileDependencyTypeEnum {
- Missing,
- Inactive,
- Active,
- UNKNOWN_STATE
-};
-
-template <typename T>
-class OrderedContents {
-public:
- OrderTypeEnum order;
-
- OrderedContents() : order(OrderTypeEnum::Ascending) {}
- explicit OrderedContents(const OrderTypeEnum orderType): order(orderType) {}
-
- template <typename Accessor>
- bool compare(const T& a, const T& b, Accessor accessor) const
- {
- switch (order) {
- case OrderTypeEnum::Ascending:
- return accessor(a) < accessor(b);
- case OrderTypeEnum::Descending:
- return accessor(a) > accessor(b);
- case OrderTypeEnum::Explicit:
- default:
- return false; // No sorting for explicit order
- }
- }
-};
-
-enum class PluginTypeEnum {
- Recommended,
- Required,
- Optional,
- NotUsable,
- CouldBeUsable,
- UNKNOWN
-};
-
-
-inline std::ostream& operator<<(std::ostream& os, const PluginTypeEnum& type)
-{
- switch (type) {
- case PluginTypeEnum::Recommended:
- os << "Recommended";
- break;
- case PluginTypeEnum::Required:
- os << "Required";
- break;
- case PluginTypeEnum::Optional:
- os << "Optional";
- break;
- case PluginTypeEnum::NotUsable:
- os << "NotUsable";
- break;
- case PluginTypeEnum::CouldBeUsable:
- os << "CouldBeUsable";
- break;
- default: ;
- }
- return os;
-}
-
-class PluginType final : public XmlDeserializable {
-public:
- PluginTypeEnum name = PluginTypeEnum::Optional; // sane default
-
- bool deserialize(pugi::xml_node& node) override;
-};
-
-class FileDependency final : public XmlDeserializable {
-public:
- std::string file;
- FileDependencyTypeEnum state = FileDependencyTypeEnum::UNKNOWN_STATE;
-
- bool deserialize(pugi::xml_node& node) override;
-};
-
-class FlagDependency final : public XmlDeserializable {
-public:
- std::string flag;
- std::string value;
-
- bool deserialize(pugi::xml_node& node) override;
-};
-
-class GameDependency final : public XmlDeserializable {
-public:
- std::string version;
-
- bool deserialize(pugi::xml_node& node) override;
-};
-
-class CompositeDependency final : public XmlDeserializable {
-public:
- std::vector<FileDependency> fileDependencies;
- std::vector<FlagDependency> flagDependencies;
- std::vector<GameDependency> gameDependencies;
- std::vector<CompositeDependency> nestedDependencies;
- OperatorTypeEnum operatorType = OperatorTypeEnum::AND; // safest default.
-
- bool deserialize(pugi::xml_node& node) override;
-};
-
-class DependencyPattern final : public XmlDeserializable {
-public:
- CompositeDependency dependencies;
- PluginTypeEnum type;
-
- bool deserialize(pugi::xml_node& node) override;
-};
-
-
-class DependencyPatternList final : public XmlDeserializable {
-public:
- std::vector<DependencyPattern> patterns;
-
- bool deserialize(pugi::xml_node& node) override;
-};
-
-class DependencyPluginType final : public XmlDeserializable {
-public:
- std::optional<PluginTypeEnum> defaultType;
- DependencyPatternList patterns;
-
- bool deserialize(pugi::xml_node& node) override;
-};
-
-class TypeDescriptor final : public XmlDeserializable {
-public:
- DependencyPluginType dependencyType;
- PluginTypeEnum type;
-
- bool deserialize(pugi::xml_node& node) override;
-};
-
-class Image final : public XmlDeserializable {
-public:
- std::string path;
-
- bool deserialize(pugi::xml_node& node) override;
-};
-
-class HeaderImage final : public XmlDeserializable {
-public:
- std::string path;
- bool showImage;
- bool showFade;
- int height;
-
- bool deserialize(pugi::xml_node& node) override;
-};
-
-class File final : public XmlDeserializable {
-public:
- std::string source;
- std::optional<std::string> destination;
- int priority{ 0 };
- bool isFolder;
-
- bool deserialize(pugi::xml_node& node) override;
-};
-
-
-class FileList final : public XmlDeserializable {
-public:
- std::vector<File> files;
-
- bool deserialize(pugi::xml_node& node) override;
-};
-
-class ConditionalFileInstallPattern final : public XmlDeserializable {
-public:
- CompositeDependency dependencies;
- FileList files;
-
- bool deserialize(pugi::xml_node& node) override;
-};
-
-// <flag name="2">On</flag>
-class ConditionFlag final : public XmlDeserializable {
-public:
- std::string name;
- std::string value;
-
- bool deserialize(pugi::xml_node& node) override;
-};
-
-class ConditionFlagList final : public XmlDeserializable {
-public:
- std::vector<ConditionFlag> flags;
-
- bool deserialize(pugi::xml_node& node) override;
-};
-
-class Plugin final : public XmlDeserializable {
-public:
- std::string description;
- Image image;
- TypeDescriptor typeDescriptor;
- std::string name;
- ConditionFlagList conditionFlags;
- FileList files;
-
- bool deserialize(pugi::xml_node& node) override;
-};
-
-class PluginList final : public XmlDeserializable, public OrderedContents<Plugin> {
-public:
- std::vector<Plugin> plugins;
- OrderTypeEnum order;
-
- bool deserialize(pugi::xml_node& node) override;
-};
-
-class Group final : public XmlDeserializable {
-public:
- PluginList plugins;
- std::string name;
- GroupTypeEnum type;
-
- bool deserialize(pugi::xml_node& node) override;
-};
-
-class GroupList final : public XmlDeserializable, public OrderedContents<Group> {
-public:
- std::vector<Group> groups;
- OrderTypeEnum order;
-
- bool deserialize(pugi::xml_node& node) override;
-};
-
-class InstallStep final : public XmlDeserializable {
-public:
- CompositeDependency visible;
- GroupList optionalFileGroups;
- std::string name;
-
- bool deserialize(pugi::xml_node& node) override;
-};
-
-class ConditionalFileInstall final : public XmlDeserializable {
-public:
- std::vector<ConditionalFileInstallPattern> patterns;
-
- bool deserialize(pugi::xml_node& node) override;
-};
-
-class StepList final : public XmlDeserializable, public OrderedContents<InstallStep> {
-public:
- std::vector<InstallStep> installSteps;
- OrderTypeEnum order;
-
- bool deserialize(pugi::xml_node& node) override;
-};
-
-class ModuleConfiguration {
-public:
- std::string moduleName;
- HeaderImage moduleImage;
- CompositeDependency moduleDependencies;
- FileList requiredInstallFiles;
- StepList installSteps;
- ConditionalFileInstall conditionalFileInstalls;
-
- bool deserialize(const QString& filePath);
-}; \ No newline at end of file
diff --git a/libs/installer_fomod_plus/share/xml/XmlHelper.h b/libs/installer_fomod_plus/share/xml/XmlHelper.h
deleted file mode 100644
index 6934a86..0000000
--- a/libs/installer_fomod_plus/share/xml/XmlHelper.h
+++ /dev/null
@@ -1,17 +0,0 @@
-#pragma once
-
-#include "ModuleConfiguration.h"
-
-class XmlHelper {
-public:
- static OrderTypeEnum getOrderType(const std::string& orderType, OrderTypeEnum defaultOrder = OrderTypeEnum::Explicit)
- {
- if (orderType == "Explicit")
- return OrderTypeEnum::Explicit;
- if (orderType == "Ascending")
- return OrderTypeEnum::Ascending;
- if (orderType == "Descending")
- return OrderTypeEnum::Descending;
- return defaultOrder; // Ascending for plugins, Explicit for groups
- }
-}; \ No newline at end of file
diff --git a/libs/installer_fomod_plus/share/xml/XmlParseException.h b/libs/installer_fomod_plus/share/xml/XmlParseException.h
deleted file mode 100644
index 6406f3b..0000000
--- a/libs/installer_fomod_plus/share/xml/XmlParseException.h
+++ /dev/null
@@ -1,10 +0,0 @@
-#pragma once
-
-#include <stdexcept>
-#include <string>
-
-class XmlParseException final : public std::runtime_error {
-public:
- explicit XmlParseException(const std::string& message)
- : std::runtime_error(message) {}
-};
diff --git a/libs/plugin_python/src/runner/pythonrunner.cpp b/libs/plugin_python/src/runner/pythonrunner.cpp
index d74db4c..25451ff 100644
--- a/libs/plugin_python/src/runner/pythonrunner.cpp
+++ b/libs/plugin_python/src/runner/pythonrunner.cpp
@@ -460,19 +460,88 @@ namespace mo2::python {
py::object sys = py::module_::import("sys");
py::dict modules = sys.attr("modules");
py::list keys = modules.attr("keys")();
+
+ auto pathBelongsToPlugin = [&folder](const QString& path) {
+ if (path.isEmpty()) {
+ return false;
+ }
+
+ const QString relative = folder.relativeFilePath(path);
+ return relative == "." || (!relative.startsWith("..") &&
+ !QDir::isAbsolutePath(relative));
+ };
+
+ auto tryCastPath = [](const py::object& object) -> std::optional<QString> {
+ if (object.is_none() || !py::isinstance<py::str>(object)) {
+ return {};
+ }
+ return object.cast<QString>();
+ };
+
+ QStringList modulesToRemove;
for (std::size_t i = 0; i < py::len(keys); ++i) {
- py::object mod = modules[keys[i]];
- if (PyObject_HasAttrString(mod.ptr(), "__path__")) {
- QString mpath =
- mod.attr("__path__")[py::int_(0)].cast<QString>();
+ try {
+ py::object key = keys[i];
+ if (PyDict_Contains(modules.ptr(), key.ptr()) != 1) {
+ continue;
+ }
+
+ py::object mod = modules[key];
+ bool remove = false;
+ QString removePath;
- if (!folder.relativeFilePath(mpath).startsWith("..")) {
- // If the path is under identifier, we need to unload
- // it.
- log::debug("Unloading module {} from {} for {}.",
- keys[i].cast<std::string>(), mpath, identifier);
+ if (PyObject_HasAttrString(mod.ptr(), "__file__")) {
+ const auto path = tryCastPath(mod.attr("__file__"));
+ if (path && pathBelongsToPlugin(*path)) {
+ remove = true;
+ removePath = *path;
+ }
+ }
+
+ if (!remove && PyObject_HasAttrString(mod.ptr(), "__path__")) {
+ py::object paths = mod.attr("__path__");
+ for (std::size_t j = 0; j < py::len(paths); ++j) {
+ const auto path = tryCastPath(paths[py::int_(j)]);
+ if (path && pathBelongsToPlugin(*path)) {
+ remove = true;
+ removePath = *path;
+ break;
+ }
+ }
+ }
- PyDict_DelItem(modules.ptr(), keys[i].ptr());
+ if (remove) {
+ const QString moduleName = key.cast<QString>();
+ log::debug("Queueing module {} from {} for unload of {}.",
+ moduleName, removePath, identifier);
+ modulesToRemove.append(moduleName);
+ }
+ } catch (const py::error_already_set& ex) {
+ MOBase::log::warn("failed to inspect python module during "
+ "unload of {}: {}",
+ identifier, ex.what());
+ } catch (const std::exception& ex) {
+ MOBase::log::warn("failed to inspect python module during "
+ "unload of {}: {}",
+ identifier, ex.what());
+ }
+ }
+
+ std::sort(modulesToRemove.begin(), modulesToRemove.end(),
+ [](const QString& lhs, const QString& rhs) {
+ return lhs.count('.') > rhs.count('.');
+ });
+
+ for (const auto& moduleName : modulesToRemove) {
+ py::str key(moduleName.toStdString());
+ if (PyDict_Contains(modules.ptr(), key.ptr()) == 1) {
+ log::debug("Unloading module {} for {}.", moduleName,
+ identifier);
+ if (PyDict_DelItem(modules.ptr(), key.ptr()) != 0) {
+ PyErr_Clear();
+ log::warn("failed to remove python module {} during "
+ "unload of {}",
+ moduleName, identifier);
}
}
}