diff options
114 files changed, 701 insertions, 8530 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index e2f8759..d5fcc5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ follows SemVer (MAJOR.MINOR.PATCH). Two distribution channels: ### Changed - Made the virtual filesystem faster and more reliable for large mod lists, file moves, and frequent file changes. -- Updated the bundled FUSE support and added an optional `io_uring` setting. +- Updated the bundled FUSE support. - Made Proton launches and prefix setup use the expected Steam Linux Runtime environment more consistently. diff --git a/CMakeLists.txt b/CMakeLists.txt index 4da42fd..21c3d77 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -254,7 +254,6 @@ endif() add_subdirectory(libs/game_features) add_subdirectory(libs/game_bethesda) add_subdirectory(libs/installer_fomod) -add_subdirectory(libs/installer_fomod_plus) add_subdirectory(libs/installer_bsplugins) add_subdirectory(libs/installer_bain) add_subdirectory(libs/installer_bundle) diff --git a/docker/Dockerfile b/docker/Dockerfile index c721021..376f711 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -18,8 +18,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libtinyxml2-dev \ libfontconfig1-dev \ libspdlog-dev \ - libfuse3-dev fuse3 libcap-dev \ - liburing-dev libnuma-dev \ + libfuse3-dev fuse3 libcap-dev libnuma-dev \ liblz4-dev zlib1g-dev libzstd-dev libbz2-dev liblzma-dev \ libssl-dev libcurl4-openssl-dev \ libtomlplusplus-dev \ @@ -40,9 +39,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ clang-tidy \ && rm -rf /var/lib/apt/lists/* -# Ubuntu 25.10 currently carries libfuse 3.17.x, which cannot mount with -# the stable FUSE-over-io_uring option. Build the pinned upstream release -# so Fluorine compiles and bundles matching 3.18.x headers/runtime. +# Build a pinned upstream libfuse so Fluorine compiles and bundles matching +# headers/runtime across supported build images. ARG LIBFUSE_VERSION=3.18.2 RUN curl -L --fail \ "https://github.com/libfuse/libfuse/releases/download/fuse-${LIBFUSE_VERSION}/fuse-${LIBFUSE_VERSION}.tar.gz" \ @@ -54,8 +52,7 @@ RUN curl -L --fail \ -Dexamples=false \ -Dutils=true \ -Dtests=false \ - -Duseroot=false \ - -Denable-io-uring=true && \ + -Duseroot=false && \ meson compile -C /tmp/libfuse-build && \ meson install -C /tmp/libfuse-build && \ ldconfig && \ diff --git a/docker/build-inner.sh b/docker/build-inner.sh index 3cc5559..135ffff 100755 --- a/docker/build-inner.sh +++ b/docker/build-inner.sh @@ -68,7 +68,6 @@ cp -f "${RUNDIR}/ModOrganizer" "${OUT_DIR}/ModOrganizer-core" find build/libs -type f \( \ -name "libgame_*.so" -o \ -name "libinstaller_*.so" -o \ - -name "libfomod_plus_*.so" -o \ -name "libpreview_*.so" -o \ -name "libdiagnose_*.so" -o \ -name "libcheck_*.so" -o \ @@ -482,9 +481,8 @@ export FLUORINE_ORIG_PATH="${PATH}" export FLUORINE_ORIG_XDG_DATA_DIRS="${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" export FLUORINE_ORIG_QT_PLUGIN_PATH="${QT_PLUGIN_PATH:-}" -# Steam injects 32-bit gameoverlayrenderer.so via LD_PRELOAD which causes -# "wrong ELF class" errors for 64-bit Qt6 apps (see PrismLauncher #3421). -# Clear it for our process; game launches restore via FLUORINE_ORIG_LD_PRELOAD. +# Clear any injected preload for the bundled Qt6 process. Game launches restore +# the original value via FLUORINE_ORIG_LD_PRELOAD. unset LD_PRELOAD # Suppress Qt debug logging by default. Some plugins (e.g. BG3 file mapper) 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ählen</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="FomodInstallerWindow.cpp" line="359"/> - <source>Reset Choices</source> - <translation>Optionen Zurü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ü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 Binary files differdeleted file mode 100644 index f6502b1..0000000 --- a/libs/installer_fomod_plus/installer/resources/delete.png +++ /dev/null diff --git a/libs/installer_fomod_plus/installer/resources/fomod_icon.png b/libs/installer_fomod_plus/installer/resources/fomod_icon.png Binary files differdeleted file mode 100644 index e044052..0000000 --- a/libs/installer_fomod_plus/installer/resources/fomod_icon.png +++ /dev/null diff --git a/libs/installer_fomod_plus/installer/resources/image_icon.png b/libs/installer_fomod_plus/installer/resources/image_icon.png Binary files differdeleted file mode 100644 index b300751..0000000 --- a/libs/installer_fomod_plus/installer/resources/image_icon.png +++ /dev/null diff --git a/libs/installer_fomod_plus/installer/resources/left-chevron.png b/libs/installer_fomod_plus/installer/resources/left-chevron.png Binary files differdeleted file mode 100644 index 28222a1..0000000 --- a/libs/installer_fomod_plus/installer/resources/left-chevron.png +++ /dev/null diff --git a/libs/installer_fomod_plus/installer/resources/right-chevron.png b/libs/installer_fomod_plus/installer/resources/right-chevron.png Binary files differdeleted file mode 100644 index 1a0af49..0000000 --- a/libs/installer_fomod_plus/installer/resources/right-chevron.png +++ /dev/null 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ö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 Binary files differdeleted file mode 100644 index e044052..0000000 --- a/libs/installer_fomod_plus/patchwizard/resources/fomod_icon.png +++ /dev/null diff --git a/libs/installer_fomod_plus/patchwizard/resources/infoscroll.png b/libs/installer_fomod_plus/patchwizard/resources/infoscroll.png Binary files differdeleted file mode 100644 index 0c31777..0000000 --- a/libs/installer_fomod_plus/patchwizard/resources/infoscroll.png +++ /dev/null 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 Binary files differdeleted file mode 100644 index e044052..0000000 --- a/libs/installer_fomod_plus/scanner/resources/fomod_icon.png +++ /dev/null diff --git a/libs/installer_fomod_plus/scanner/resources/wizard.gif b/libs/installer_fomod_plus/scanner/resources/wizard.gif Binary files differdeleted file mode 100644 index c1f75d3..0000000 --- a/libs/installer_fomod_plus/scanner/resources/wizard.gif +++ /dev/null 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(" "), "<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); } } } diff --git a/src/src/envshortcut.cpp b/src/src/envshortcut.cpp index 60786d2..2baeeac 100644 --- a/src/src/envshortcut.cpp +++ b/src/src/envshortcut.cpp @@ -46,6 +46,13 @@ static QString sanitizeDesktopName(const QString& s) return result; } +static QString shellQuote(const QString& s) +{ + QString result = s; + result.replace("'", "'\"'\"'"); + return "'" + result + "'"; +} + // --------------------------------------------------------------------------- // PE icon extractor — reads the largest icon embedded in a Windows .exe // and returns it as a QImage. Returns a null QImage on failure. @@ -443,6 +450,33 @@ bool Shortcut::add(Locations loc) QDir().mkpath(QFileInfo(path).absolutePath()); + const QString script = scriptPath(loc); + if (script.isEmpty()) { + return false; + } + + QFile scriptFile(script); + if (!scriptFile.open(QIODevice::WriteOnly | QIODevice::Text)) { + return false; + } + + QTextStream scriptOut(&scriptFile); + scriptOut << "#!/usr/bin/env bash\n"; + scriptOut << "set -euo pipefail\n"; + if (!m_workingDirectory.isEmpty()) { + scriptOut << "cd " << shellQuote(m_workingDirectory) << "\n"; + } + scriptOut << "exec " << shellQuote(m_target); + if (!m_arguments.isEmpty()) { + scriptOut << " " << m_arguments; + } + scriptOut << "\n"; + scriptFile.close(); + scriptFile.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner | + QFileDevice::ExeOwner | QFileDevice::ReadGroup | + QFileDevice::ExeGroup | QFileDevice::ReadOther | + QFileDevice::ExeOther); + QFile file(path); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { return false; @@ -463,12 +497,7 @@ bool Shortcut::add(Locations loc) if (!m_description.isEmpty()) { out << "Comment=" << m_description << "\n"; } - // .desktop Exec values require quoting paths that contain spaces - out << "Exec=\"" << m_target << "\""; - if (!m_arguments.isEmpty()) { - out << " " << m_arguments; - } - out << "\n"; + out << "Exec=\"" << script << "\"\n"; if (!m_workingDirectory.isEmpty()) { out << "Path=" << m_workingDirectory << "\n"; } @@ -481,7 +510,10 @@ bool Shortcut::add(Locations loc) file.close(); // Make it executable (required by some desktop environments) - file.setPermissions(file.permissions() | QFileDevice::ExeUser); + file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner | + QFileDevice::ExeOwner | QFileDevice::ReadGroup | + QFileDevice::ExeGroup | QFileDevice::ReadOther | + QFileDevice::ExeOther); return true; } @@ -492,7 +524,12 @@ bool Shortcut::remove(Locations loc) if (path.isEmpty()) { return false; } - return QFile::remove(path); + const bool removedShortcut = QFile::remove(path); + const QString script = scriptPath(loc); + if (!script.isEmpty()) { + QFile::remove(script); + } + return removedShortcut; } QString Shortcut::shortcutPath(Locations loc) const @@ -504,6 +541,15 @@ QString Shortcut::shortcutPath(Locations loc) const return dir + "/" + shortcutFilename(); } +QString Shortcut::scriptPath(Locations loc) const +{ + const QString dir = shortcutDirectory(loc); + if (dir.isEmpty()) { + return {}; + } + return dir + "/" + scriptFilename(); +} + QString Shortcut::shortcutDirectory(Locations loc) { if (loc == Desktop) { @@ -535,6 +581,19 @@ QString Shortcut::shortcutFilename() const return base + ".desktop"; } +QString Shortcut::scriptFilename() const +{ + if (m_name.isEmpty()) { + return "fluorine-launch.sh"; + } + + QString base = m_name; + if (!m_instanceName.isEmpty()) { + base = sanitizeDesktopName(m_instanceName) + "-" + base; + } + return sanitizeDesktopName(base) + ".sh"; +} + QString toString(Shortcut::Locations loc) { switch (loc) { diff --git a/src/src/envshortcut.h b/src/src/envshortcut.h index 4c9675b..66cfdda 100644 --- a/src/src/envshortcut.h +++ b/src/src/envshortcut.h @@ -89,18 +89,20 @@ private: int m_iconIndex;
QString m_workingDirectory;
- // returns the path where the shortcut file should be saved
- //
- QString shortcutPath(Locations loc) const;
-
- // returns the directory where the shortcut file should be saved
- //
- static QString shortcutDirectory(Locations loc) ;
-
- // returns the filename of the shortcut file that should be used when saving
- //
- QString shortcutFilename() const;
-};
+ // returns the path where the shortcut file should be saved + // + QString shortcutPath(Locations loc) const; + QString scriptPath(Locations loc) const; + + // returns the directory where the shortcut file should be saved + // + static QString shortcutDirectory(Locations loc); + + // returns the filename of the shortcut file that should be used when saving + // + QString shortcutFilename() const; + QString scriptFilename() const; +}; // returns a string representation of the given location
//
diff --git a/src/src/fuseconnector.cpp b/src/src/fuseconnector.cpp index fb2a518..c5b5718 100644 --- a/src/src/fuseconnector.cpp +++ b/src/src/fuseconnector.cpp @@ -56,69 +56,6 @@ namespace { namespace fs = std::filesystem; -bool parseVersionTriplet(const char* text, int* major, int* minor, int* patch) -{ - if (text == nullptr || major == nullptr || minor == nullptr || patch == nullptr) { - return false; - } - - char* end = nullptr; - const long ma = std::strtol(text, &end, 10); - if (end == text || *end != '.') { - return false; - } - const long mi = std::strtol(end + 1, &end, 10); - if (*end != '.') { - return false; - } - const long pa = std::strtol(end + 1, &end, 10); - if (ma < 0 || mi < 0 || pa < 0) { - return false; - } - - *major = static_cast<int>(ma); - *minor = static_cast<int>(mi); - *patch = static_cast<int>(pa); - return true; -} - -bool runtimeLibfuseHasStableIoUring() -{ -#if defined(FUSE_CAP_OVER_IO_URING) && FUSE_VERSION >= FUSE_MAKE_VERSION(3, 18) - int major = 0; - int minor = 0; - int patch = 0; - if (!parseVersionTriplet(fuse_pkgversion(), &major, &minor, &patch)) { - return false; - } - return major > 3 || (major == 3 && (minor > 18 || (minor == 18 && patch >= 2))); -#else - return false; -#endif -} - -void recordRequestTransport(fuse_req_t req) noexcept -{ - if (req == nullptr) { - return; - } - - auto* ctx = static_cast<Mo2FsContext*>(fuse_req_userdata(req)); - if (ctx == nullptr) { - return; - } - -#if defined(FUSE_CAP_OVER_IO_URING) && FUSE_VERSION >= FUSE_MAKE_VERSION(3, 18) - if (fuse_req_is_uring(req)) { - ctx->uring_request_count.fetch_add(1, std::memory_order_relaxed); - } else { - ctx->legacy_request_count.fetch_add(1, std::memory_order_relaxed); - } -#else - ctx->legacy_request_count.fetch_add(1, std::memory_order_relaxed); -#endif -} - std::string decodeProcMountField(const std::string& in) { std::string out; @@ -272,21 +209,18 @@ void wrap_init(void* userdata, struct fuse_conn_info* conn) noexcept void wrap_lookup(fuse_req_t req, fuse_ino_t parent, const char* name) noexcept { - recordRequestTransport(req); try { mo2_lookup(req, parent, name); } MO2_TRY_REPLY(req, "lookup", parent, EIO) } void wrap_getattr(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexcept { - recordRequestTransport(req); try { mo2_getattr(req, ino, fi); } MO2_TRY_REPLY(req, "getattr", ino, EIO) } void wrap_opendir(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexcept { - recordRequestTransport(req); try { mo2_opendir(req, ino, fi); } MO2_TRY_REPLY(req, "opendir", ino, EIO) } @@ -294,7 +228,6 @@ void wrap_opendir(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noe void wrap_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, struct fuse_file_info* fi) noexcept { - recordRequestTransport(req); try { mo2_readdir(req, ino, size, off, fi); } MO2_TRY_REPLY(req, "readdir", ino, EIO) } @@ -302,14 +235,12 @@ void wrap_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, void wrap_readdirplus(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, struct fuse_file_info* fi) noexcept { - recordRequestTransport(req); try { mo2_readdirplus(req, ino, size, off, fi); } MO2_TRY_REPLY(req, "readdirplus", ino, EIO) } void wrap_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexcept { - recordRequestTransport(req); try { mo2_open(req, ino, fi); } MO2_TRY_REPLY(req, "open", ino, EIO) } @@ -317,7 +248,6 @@ void wrap_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexce void wrap_read(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, struct fuse_file_info* fi) noexcept { - recordRequestTransport(req); try { mo2_read(req, ino, size, off, fi); } MO2_TRY_REPLY(req, "read", ino, EIO) } @@ -325,7 +255,6 @@ void wrap_read(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, void wrap_write(fuse_req_t req, fuse_ino_t ino, const char* buf, size_t size, off_t off, struct fuse_file_info* fi) noexcept { - recordRequestTransport(req); try { mo2_write(req, ino, buf, size, off, fi); } MO2_TRY_REPLY(req, "write", ino, EIO) } @@ -333,7 +262,6 @@ void wrap_write(fuse_req_t req, fuse_ino_t ino, const char* buf, size_t size, void wrap_create(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t mode, struct fuse_file_info* fi) noexcept { - recordRequestTransport(req); try { mo2_create(req, parent, name, mode, fi); } MO2_TRY_REPLY(req, "create", parent, EIO) } @@ -341,7 +269,6 @@ void wrap_create(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t mod void wrap_rename(fuse_req_t req, fuse_ino_t parent, const char* name, fuse_ino_t newparent, const char* newname, unsigned int flags) noexcept { - recordRequestTransport(req); try { mo2_rename(req, parent, name, newparent, newname, flags); } MO2_TRY_REPLY(req, "rename", parent, EIO) } @@ -349,56 +276,48 @@ void wrap_rename(fuse_req_t req, fuse_ino_t parent, const char* name, void wrap_setattr(fuse_req_t req, fuse_ino_t ino, struct stat* attr, int to_set, struct fuse_file_info* fi) noexcept { - recordRequestTransport(req); try { mo2_setattr(req, ino, attr, to_set, fi); } MO2_TRY_REPLY(req, "setattr", ino, EIO) } void wrap_unlink(fuse_req_t req, fuse_ino_t parent, const char* name) noexcept { - recordRequestTransport(req); try { mo2_unlink(req, parent, name); } MO2_TRY_REPLY(req, "unlink", parent, EIO) } void wrap_mkdir(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t mode) noexcept { - recordRequestTransport(req); try { mo2_mkdir(req, parent, name, mode); } MO2_TRY_REPLY(req, "mkdir", parent, EIO) } void wrap_rmdir(fuse_req_t req, fuse_ino_t parent, const char* name) noexcept { - recordRequestTransport(req); try { mo2_rmdir(req, parent, name); } MO2_TRY_REPLY(req, "rmdir", parent, EIO) } void wrap_release(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexcept { - recordRequestTransport(req); try { mo2_release(req, ino, fi); } MO2_TRY_REPLY(req, "release", ino, EIO) } void wrap_releasedir(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexcept { - recordRequestTransport(req); try { mo2_releasedir(req, ino, fi); } MO2_TRY_REPLY(req, "releasedir", ino, EIO) } void wrap_forget(fuse_req_t req, fuse_ino_t ino, uint64_t nlookup) noexcept { - recordRequestTransport(req); try { mo2_forget(req, ino, nlookup); } catch (...) { fuse_reply_none(req); } } void wrap_flush(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexcept { - recordRequestTransport(req); try { mo2_flush(req, ino, fi); } MO2_TRY_REPLY(req, "flush", ino, EIO) } @@ -406,7 +325,6 @@ void wrap_flush(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) noexc void wrap_fsync(fuse_req_t req, fuse_ino_t ino, int datasync, struct fuse_file_info* fi) noexcept { - recordRequestTransport(req); try { mo2_fsync(req, ino, datasync, fi); } MO2_TRY_REPLY(req, "fsync", ino, EIO) } @@ -640,15 +558,8 @@ bool FuseConnector::mount( std::vector<std::string> argvStorage = { "mo2fuse", "-o", "fsname=mo2linux", "-o", "noatime", "-o", "default_permissions", "-o", "max_read=1048576"}; - const bool requestIoUring = runtimeLibfuseHasStableIoUring(); - if (requestIoUring) { - argvStorage.push_back("-o"); - argvStorage.push_back("io_uring"); - } - std::fprintf(stderr, - "[VFS] libfuse=%s headers=%d.%d io_uring_mount_option=%d\n", - fuse_pkgversion(), FUSE_MAJOR_VERSION, FUSE_MINOR_VERSION, - requestIoUring ? 1 : 0); + std::fprintf(stderr, "[VFS] libfuse=%s headers=%d.%d\n", + fuse_pkgversion(), FUSE_MAJOR_VERSION, FUSE_MINOR_VERSION); std::vector<char*> argv; argv.reserve(argvStorage.size()); diff --git a/src/src/instancemanager.cpp b/src/src/instancemanager.cpp index 066c0bd..1b95b5e 100644 --- a/src/src/instancemanager.cpp +++ b/src/src/instancemanager.cpp @@ -228,7 +228,12 @@ void Instance::updateIni() // updating the settings since some of these values might have been missing s.game().setName(m_gameName); - s.game().setDirectory(m_gameDir); + if (!m_gameDirAutoDetectedFromInvalidIni) { + s.game().setDirectory(m_gameDir); + } else { + log::warn("preserving existing gamePath in ini {} after auto-detecting {}", + iniPath(), m_gameDir); + } s.game().setSelectedProfileName(m_profile); if (!m_gameVariant.isEmpty()) { @@ -241,6 +246,7 @@ void Instance::setGame(const QString& name, const QString& dir) { m_gameName = name; m_gameDir = dir; + m_gameDirAutoDetectedFromInvalidIni = false; } void Instance::setVariant(const QString& name) @@ -276,6 +282,7 @@ Instance::SetupResults Instance::getGamePlugin(PluginContainer& plugins) game->gameDirectory().absolutePath()); m_gameDir = game->gameDirectory().absolutePath(); + m_gameDirAutoDetectedFromInvalidIni = true; } else { // game seems to be gone completely log::warn("game plugin {} found no game installation at all", diff --git a/src/src/instancemanager.h b/src/src/instancemanager.h index 0d3becd..0208e3e 100644 --- a/src/src/instancemanager.h +++ b/src/src/instancemanager.h @@ -190,11 +190,12 @@ public: std::vector<Object> objectsForDeletion() const;
private:
- QString m_dir;
- bool m_portable;
- QString m_gameName, m_gameDir, m_gameVariant, m_baseDir;
- MOBase::IPluginGame* m_plugin{nullptr};
- QString m_profile;
+ QString m_dir; + bool m_portable; + QString m_gameName, m_gameDir, m_gameVariant, m_baseDir; + bool m_gameDirAutoDetectedFromInvalidIni{false}; + MOBase::IPluginGame* m_plugin{nullptr}; + QString m_profile; // figures out the game plugin for this instance
//
diff --git a/src/src/instancemanagerdialog.cpp b/src/src/instancemanagerdialog.cpp index 06522dd..567026f 100644 --- a/src/src/instancemanagerdialog.cpp +++ b/src/src/instancemanagerdialog.cpp @@ -240,15 +240,6 @@ InstanceManagerDialog::InstanceManagerDialog(PluginContainer& pc, QWidget* paren s.setValue("fluorine/vfs_root_builder", checked); }); - connect(ui->steamOverlayCheckBox, &QCheckBox::toggled, [&](bool checked) { - const auto* inst = singleSelection(); - if (!inst) return; - const QString ini = inst->iniPath(); - if (ini.isEmpty()) return; - QSettings s(ini, QSettings::IniFormat); - s.setValue("fluorine/steam_overlay", checked); - }); - connect(ui->switchToInstance, &QPushButton::clicked, [&] { openSelectedInstance(); }); @@ -835,9 +826,6 @@ void InstanceManagerDialog::fillData(const Instance& ii) ui->vfsRootBuilderCheckBox->setChecked(s.value("fluorine/vfs_root_builder", true).toBool()); ui->vfsRootBuilderCheckBox->blockSignals(false); - ui->steamOverlayCheckBox->blockSignals(true); - ui->steamOverlayCheckBox->setChecked(s.value("fluorine/steam_overlay", false).toBool()); - ui->steamOverlayCheckBox->blockSignals(false); } else { ui->prefixPath->clear(); ui->protonVersion->clear(); @@ -850,9 +838,6 @@ void InstanceManagerDialog::fillData(const Instance& ii) ui->vfsRootBuilderCheckBox->blockSignals(true); ui->vfsRootBuilderCheckBox->setChecked(false); ui->vfsRootBuilderCheckBox->blockSignals(false); - ui->steamOverlayCheckBox->blockSignals(true); - ui->steamOverlayCheckBox->setChecked(false); - ui->steamOverlayCheckBox->blockSignals(false); } } diff --git a/src/src/instancemanagerdialog.ui b/src/src/instancemanagerdialog.ui index 6a4e5ee..64928f5 100644 --- a/src/src/instancemanagerdialog.ui +++ b/src/src/instancemanagerdialog.ui @@ -353,16 +353,6 @@ </property> </widget> </item> - <item row="13" column="0" colspan="2"> - <widget class="QCheckBox" name="steamOverlayCheckBox"> - <property name="text"> - <string>Enable Steam Overlay</string> - </property> - <property name="toolTip"> - <string>Inject the Steam overlay (Shift+Tab) into the game. Requires Steam running, the game's Steam App ID set, and that you own the game on Steam. Combines LD_PRELOAD of gameoverlayrenderer.so with the Steam Vulkan overlay layer (needed for DXVK-rendered games like Skyrim/Fallout).</string> - </property> - </widget> - </item> </layout> </widget> </item> diff --git a/src/src/mainwindow.cpp b/src/src/mainwindow.cpp index ca92499..c70772b 100644 --- a/src/src/mainwindow.cpp +++ b/src/src/mainwindow.cpp @@ -673,6 +673,8 @@ void MainWindow::resetButtonIcons() MainWindow::~MainWindow() { try { + m_ArchiveListWriter.writeImmediately(true); + m_OrganizerCore.pluginsWriter().writeImmediately(true); cleanup(); m_OrganizerCore.setUserInterface(nullptr); @@ -2245,7 +2247,8 @@ void MainWindow::readSettings() s.geometry().restoreToolbars(this); s.geometry().restoreState(ui->splitter); s.geometry().restoreState(ui->categoriesSplitter); - s.geometry().restoreVisibility(ui->menuBar); + ui->menuBar->show(); + ui->toolBar->show(); s.geometry().restoreVisibility(ui->statusBar); FilterWidget::setOptions(s.interface().filterOptions()); @@ -2341,7 +2344,6 @@ void MainWindow::storeSettings() s.geometry().saveGeometry(this); s.geometry().saveDocks(this); - s.geometry().saveVisibility(ui->menuBar); s.geometry().saveVisibility(ui->statusBar); s.geometry().saveToolbars(this); s.geometry().saveState(ui->splitter); diff --git a/src/src/moapplication.cpp b/src/src/moapplication.cpp index 94e5b02..16ffb62 100644 --- a/src/src/moapplication.cpp +++ b/src/src/moapplication.cpp @@ -43,9 +43,11 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "tutorialmanager.h" #include <QDebug> #include <QDesktopServices> +#include <QEvent> #include <QFile> #include <QPainter> #include <QProxyStyle> +#include <QRegularExpression> #include <QSslSocket> #include <QStringList> #include <QStyleFactory> @@ -498,6 +500,16 @@ int MOApplication::run(MOMultiProcess& multiProcess) // main window is about to be destroyed m_nexus->getAccessManager()->setTopLevelWidget(nullptr); + + try { + if (m_core != nullptr) { + m_core->saveCurrentProfileForShutdown(); + } + } catch (const std::exception& e) { + log::error("failed to save current profile during shutdown: {}", e.what()); + } catch (...) { + log::error("failed to save current profile during shutdown: unknown exception"); + } } // reset geometry if the flag was set from the settings dialog @@ -650,11 +662,19 @@ void MOApplication::resetForRestart() // clear instance and profile overrides InstanceManager::singleton().clearOverrides(); - m_core = {}; + if (m_core != nullptr) { + m_core->saveCurrentProfileForShutdown(); + } + m_plugins = {}; + QCoreApplication::removePostedEvents(nullptr); + + m_core = {}; m_nexus = {}; m_settings = {}; m_instance = {}; + + QCoreApplication::removePostedEvents(nullptr); } bool MOApplication::setStyleFile(const QString& styleName) @@ -772,6 +792,78 @@ bool MOApplication::notify(QObject* receiver, QEvent* event) namespace { +QString styleSheetFontSizeOverride(int fontSize) +{ + if (fontSize <= 0) { + return {}; + } + + return QStringLiteral("\n\n/* Fluorine QSS font size override */\n" + "QWidget { font-size: %1px; }\n") + .arg(fontSize); +} + +QString resolveStyleSheetUrl(const QString& url, const QString& baseDir) +{ + const QString trimmed = url.trimmed(); + if (trimmed.isEmpty() || trimmed.startsWith(':') || trimmed.startsWith('/') || + trimmed.contains("://")) { + return trimmed; + } + + return QUrl::fromLocalFile(QDir(baseDir).absoluteFilePath(trimmed)).toString(); +} + +QString resolveRelativeStyleSheetUrls(const QString& stylesheet, + const QString& baseDir) +{ + static const QRegularExpression urlRe( + QStringLiteral(R"(url\(\s*(['"]?)([^'")]+)\1\s*\))"), + QRegularExpression::CaseInsensitiveOption); + + QString result; + qsizetype last = 0; + auto matches = urlRe.globalMatch(stylesheet); + while (matches.hasNext()) { + const auto match = matches.next(); + result += stylesheet.mid(last, match.capturedStart() - last); + result += QStringLiteral("url(%1)") + .arg(resolveStyleSheetUrl(match.captured(2), baseDir)); + last = match.capturedEnd(); + } + result += stylesheet.mid(last); + return result; +} + +QString applyQssFontSize(const QString& stylesheet, int fontSize) +{ + if (fontSize <= 0) { + return stylesheet; + } + + static const QRegularExpression fontSizeRe( + QStringLiteral(R"(font-size\s*:\s*[^;{}]+;)"), + QRegularExpression::CaseInsensitiveOption); + + QString result = stylesheet; + result.replace(fontSizeRe, QStringLiteral("font-size: %1px;").arg(fontSize)); + result += styleSheetFontSizeOverride(fontSize); + return result; +} + +std::optional<QString> loadStyleSheet(const QString& fileName, int fontSize) +{ + QFile stylesheet(fileName); + if (!stylesheet.open(QFile::ReadOnly | QFile::Text)) { + log::error("failed to open stylesheet file {}", fileName); + return {}; + } + + QString content = QString::fromUtf8(stylesheet.readAll()); + content = resolveRelativeStyleSheetUrls(content, QFileInfo(fileName).absolutePath()); + return applyQssFontSize(content, fontSize); +} + QStringList extractTopStyleSheetComments(QFile& stylesheet) { if (!stylesheet.open(QFile::ReadOnly)) { @@ -903,6 +995,9 @@ static void createStylesheetCaseShims(const QString& dirPath) void MOApplication::updateStyle(const QString& fileName) { + const int qssFontSize = + m_settings != nullptr ? m_settings->interface().qssFontSize() : 0; + if (QStyleFactory::keys().contains(fileName)) { setStyleSheet(""); setStyle(new ProxyStyle(QStyleFactory::create(fileName))); @@ -914,7 +1009,9 @@ void MOApplication::updateStyle(const QString& fileName) createStylesheetCaseShims(QFileInfo(fileName).absolutePath()); setStyle(new ProxyStyle(QStyleFactory::create( extractBaseStyleFromStyleSheet(stylesheet, m_defaultStyle)))); - setStyleSheet(QString("file:///%1").arg(fileName)); + if (auto content = loadStyleSheet(fileName, qssFontSize)) { + setStyleSheet(*content); + } } else { log::warn("invalid stylesheet: {}", fileName); } diff --git a/src/src/modlistview.cpp b/src/src/modlistview.cpp index 89e0faa..36bee5f 100644 --- a/src/src/modlistview.cpp +++ b/src/src/modlistview.cpp @@ -702,6 +702,66 @@ void ModListView::updateGroupByProxy() } } +void ModListView::applyDefaultHeaderState() +{ + // hide these columns by default + header()->setSectionHidden(ModList::COL_CONTENT, true); + header()->setSectionHidden(ModList::COL_MODID, true); + header()->setSectionHidden(ModList::COL_UPLOADER, true); + header()->setSectionHidden(ModList::COL_GAME, true); + header()->setSectionHidden(ModList::COL_INSTALLTIME, true); + header()->setSectionHidden(ModList::COL_NOTES, true); + + // resize mod list to fit content + for (int i = 0; i < header()->count(); ++i) { + header()->setSectionResizeMode(i, QHeaderView::ResizeToContents); + } + + header()->setSectionResizeMode(ModList::COL_NAME, QHeaderView::Stretch); +} + +void ModListView::forceHeaderVisibilityRefresh() +{ + // restoreState() does not reliably emit resize/visibility signals for each + // section, so force our proxy's visible-column bookkeeping to catch up. + for (int column = 0; column <= ModList::COL_LASTCOLUMN; ++column) { + const int sectionSize = header()->sectionSize(column); + header()->resizeSection(column, sectionSize + 1); + header()->resizeSection(column, sectionSize); + } +} + +bool ModListView::headerStateLooksBroken() const +{ + int visibleCount = 0; + int tinyCount = 0; + + for (int column = 0; column <= ModList::COL_LASTCOLUMN; ++column) { + if (header()->isSectionHidden(column)) { + continue; + } + + ++visibleCount; + if (header()->sectionSize(column) < 50) { + ++tinyCount; + } + } + + return visibleCount >= 8 && tinyCount >= visibleCount - 1; +} + +void ModListView::syncColumnVisibilityFromHeader() +{ + if (m_sortProxy == nullptr) { + return; + } + + for (int column = 0; column <= ModList::COL_LASTCOLUMN; ++column) { + m_sortProxy->setColumnVisible( + column, !header()->isSectionHidden(column) && header()->sectionSize(column) > 0); + } +} + void ModListView::setup(OrganizerCore& core, CategoryFactory& factory, MainWindow* mw, Ui::MainWindow* mwui) { @@ -798,6 +858,9 @@ void ModListView::setup(OrganizerCore& core, CategoryFactory& factory, MainWindo [=, this](int logicalIndex, int oldSize, int newSize) { m_sortProxy->setColumnVisible(logicalIndex, newSize != 0); }); + connect(header(), &QHeaderView::geometriesChanged, [=, this] { + syncColumnVisibilityFromHeader(); + }); setItemDelegateForColumn(ModList::COL_FLAGS, new ModFlagIconDelegate(this, ModList::COL_FLAGS, 120)); @@ -809,38 +872,6 @@ void ModListView::setup(OrganizerCore& core, CategoryFactory& factory, MainWindo setItemDelegateForColumn(ModList::COL_VERSION, new ModListVersionDelegate(this, core.settings())); - m_restoringHeaderState = true; - const bool headerRestored = m_core->settings().geometry().restoreState(header()); - m_restoringHeaderState = false; - - if (headerRestored) { - // hack: force the resize-signal to be triggered because restoreState doesn't seem - // to do that - for (int column = 0; column <= ModList::COL_LASTCOLUMN; ++column) { - int sectionSize = header()->sectionSize(column); - header()->resizeSection(column, sectionSize + 1); - header()->resizeSection(column, sectionSize); - } - } else { - // hide these columns by default - header()->setSectionHidden(ModList::COL_CONTENT, true); - header()->setSectionHidden(ModList::COL_MODID, true); - header()->setSectionHidden(ModList::COL_UPLOADER, true); - header()->setSectionHidden(ModList::COL_GAME, true); - header()->setSectionHidden(ModList::COL_INSTALLTIME, true); - header()->setSectionHidden(ModList::COL_NOTES, true); - - // resize mod list to fit content - for (int i = 0; i < header()->count(); ++i) { - header()->setSectionResizeMode(i, QHeaderView::ResizeToContents); - } - - header()->setSectionResizeMode(ModList::COL_NAME, QHeaderView::Stretch); - } - - // prevent the name-column from being hidden - header()->setSectionHidden(ModList::COL_NAME, false); - // we need QueuedConnection for the download/archive dropped otherwise the // installation starts within the drop-event and it's not possible to drag&drop // in the manual installer @@ -901,8 +932,20 @@ void ModListView::restoreState(const Settings& s) s.widgets().restoreIndex(ui.groupBy); m_restoringHeaderState = true; - s.geometry().restoreState(header()); + const bool headerRestored = s.geometry().restoreState(header()); m_restoringHeaderState = false; + if (headerRestored && !headerStateLooksBroken()) { + forceHeaderVisibilityRefresh(); + } else { + if (headerRestored) { + log::warn("discarding broken mod list header state"); + } + applyDefaultHeaderState(); + } + + // prevent the name-column from being hidden + header()->setSectionHidden(ModList::COL_NAME, false); + syncColumnVisibilityFromHeader(); s.widgets().restoreTreeExpandState(this); diff --git a/src/src/modlistview.h b/src/src/modlistview.h index a5c079c..839e777 100644 --- a/src/src/modlistview.h +++ b/src/src/modlistview.h @@ -314,12 +314,16 @@ private: // private functions //
void refreshExpandedItems();
- // refresh the group-by proxy, if the index is -1 will refresh the
- // current one (e.g. when changing the sort column)
- //
- void updateGroupByProxy();
-
-public: // member variables
+ // refresh the group-by proxy, if the index is -1 will refresh the + // current one (e.g. when changing the sort column) + // + void updateGroupByProxy(); + void applyDefaultHeaderState(); + void forceHeaderVisibilityRefresh(); + bool headerStateLooksBroken() const; + void syncColumnVisibilityFromHeader(); + +public: // member variables OrganizerCore* m_core{nullptr};
std::unique_ptr<FilterList> m_filters;
CategoryFactory* m_categories;
diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp index 242ae1c..64772fa 100644 --- a/src/src/organizercore.cpp +++ b/src/src/organizercore.cpp @@ -340,23 +340,44 @@ OrganizerCore::OrganizerCore(Settings& settings) OrganizerCore::~OrganizerCore() { - m_RefresherThread.exit(); - m_RefresherThread.wait(); - - if (m_StructureDeleter.joinable()) { - m_StructureDeleter.join(); + try { + saveCurrentProfileForShutdown(); + } catch (const std::exception& e) { + log::error("failed to save current profile during OrganizerCore shutdown: {}", + e.what()); + } catch (...) { + log::error( + "failed to save current profile during OrganizerCore shutdown: unknown exception"); } - saveCurrentProfile(); + try { + m_RefresherThread.exit(); + m_RefresherThread.wait(); + + if (m_StructureDeleter.joinable()) { + m_StructureDeleter.join(); + } + } catch (const std::exception& e) { + log::error("failed while stopping OrganizerCore worker threads: {}", e.what()); + } catch (...) { + log::error("failed while stopping OrganizerCore worker threads: unknown exception"); + } - // profile has to be cleaned up before the modinfo-buffer is cleared - m_CurrentProfile.reset(); + try { + // profile has to be cleaned up before the modinfo-buffer is cleared + m_CurrentProfile.reset(); - ModInfo::clear(); - m_ModList.setProfile(nullptr); - // NexusInterface::instance()->cleanup(); + ModInfo::clear(); + m_ModList.setProfile(nullptr); + // NexusInterface::instance()->cleanup(); - delete m_DirectoryStructure; + delete m_DirectoryStructure; + m_DirectoryStructure = nullptr; + } catch (const std::exception& e) { + log::error("failed while clearing OrganizerCore state: {}", e.what()); + } catch (...) { + log::error("failed while clearing OrganizerCore state: unknown exception"); + } } void OrganizerCore::storeSettings() @@ -2462,6 +2483,51 @@ void OrganizerCore::saveCurrentProfile() storeSettings(); } +void OrganizerCore::saveCurrentProfileForShutdown() +{ + if (m_CurrentProfileSavedForShutdown) { + return; + } + + try { + saveCurrentProfile(); + } catch (const std::exception& e) { + log::error("failed to save current profile during shutdown: {}", e.what()); + } catch (...) { + log::error("failed to save current profile during shutdown: unknown exception"); + } + + try { + if (m_CurrentProfile != nullptr) { + m_CurrentProfile->writeModlistNow(true); + } + } catch (const std::exception& e) { + log::error("failed to flush mod list during shutdown: {}", e.what()); + } catch (...) { + log::error("failed to flush mod list during shutdown: unknown exception"); + } + + try { + m_PluginListsWriter.writeImmediately(true); + } catch (const std::exception& e) { + log::error("failed to flush plugin list during shutdown: {}", e.what()); + } catch (...) { + log::error("failed to flush plugin list during shutdown: unknown exception"); + } + + try { + if (m_UserInterface != nullptr) { + m_UserInterface->archivesWriter().writeImmediately(true); + } + } catch (const std::exception& e) { + log::error("failed to flush archive list during shutdown: {}", e.what()); + } catch (...) { + log::error("failed to flush archive list during shutdown: unknown exception"); + } + + m_CurrentProfileSavedForShutdown = true; +} + ProcessRunner OrganizerCore::processRunner() { return ProcessRunner(*this, m_UserInterface); diff --git a/src/src/organizercore.h b/src/src/organizercore.h index 973ee64..f5aad22 100644 --- a/src/src/organizercore.h +++ b/src/src/organizercore.h @@ -442,12 +442,14 @@ public: // - group to add the function to
// - if immediateIfReady is true, the function will be called immediately if no
// directory update is running
- boost::signals2::connection onNextRefresh(std::function<void()> const& func,
- RefreshCallbackGroup group,
- RefreshCallbackMode mode);
-
-public: // IPluginDiagnose interface
- std::vector<unsigned int> activeProblems() const override;
+ boost::signals2::connection onNextRefresh(std::function<void()> const& func, + RefreshCallbackGroup group, + RefreshCallbackMode mode); + + void saveCurrentProfileForShutdown(); + +public: // IPluginDiagnose interface + std::vector<unsigned int> activeProblems() const override; QString shortDescription(unsigned int key) const override;
QString fullDescription(unsigned int key) const override;
bool hasGuidedFix(unsigned int key) const override;
@@ -559,10 +561,11 @@ private: QString m_GameName;
MOBase::IPluginGame* m_GamePlugin{nullptr};
ModDataContentHolder m_Contents;
-
- std::shared_ptr<Profile> m_CurrentProfile;
-
- Settings& m_Settings;
+ + std::shared_ptr<Profile> m_CurrentProfile; + bool m_CurrentProfileSavedForShutdown = false; + + Settings& m_Settings; SelfUpdater m_Updater;
class FluorineUpdater* m_FluorineUpdater = nullptr;
diff --git a/src/src/plugincontainer.cpp b/src/src/plugincontainer.cpp index e2ca421..277b1d9 100644 --- a/src/src/plugincontainer.cpp +++ b/src/src/plugincontainer.cpp @@ -8,9 +8,11 @@ #include <QCoreApplication> #include <QDirIterator> #include <QMessageBox> +#include <QSet> #include <QToolButton> #include <QSettings> #include <cstdio> +#include <utility> #include <boost/fusion/algorithm/iteration/for_each.hpp> #include <boost/fusion/include/at_key.hpp> #include <boost/fusion/include/for_each.hpp> @@ -333,8 +335,14 @@ PluginContainer::PluginContainer(OrganizerCore* organizer) PluginContainer::~PluginContainer() { + try { + unloadPlugins(); + } catch (const std::exception& e) { + log::error("failed to unload plugins during shutdown: {}", e.what()); + } catch (...) { + log::error("failed to unload plugins during shutdown: unknown exception"); + } m_Organizer = nullptr; - unloadPlugins(); } void PluginContainer::startPlugins(IUserInterface* userInterface) @@ -1140,6 +1148,48 @@ void PluginContainer::unloadPlugins() m_Organizer->settings().plugins().clearPlugins(); } + QSet<QString> seenProxiedPaths; + std::vector<std::pair<IPluginProxy*, QString>> proxiedPaths; + const auto objects = bf::at_key<QObject>(m_Plugins); + for (QObject* object : objects) { + auto* plugin = qobject_cast<IPlugin*>(object); + if (!plugin) { + continue; + } + + const auto req = m_Requirements.find(plugin); + if (req == m_Requirements.end()) { + continue; + } + + if (auto* proxy = req->second.m_Organizer) { + proxy->disconnectSignals(); + } + + if (auto* game = qobject_cast<IPluginGame*>(object)) { + unregisterGame(game); + } + + auto* pluginProxy = req->second.proxy(); + if (pluginProxy) { + const QString path = filepath(plugin); + if (!seenProxiedPaths.contains(path)) { + proxiedPaths.emplace_back(pluginProxy, path); + seenProxiedPaths.insert(path); + } + } + } + + for (const auto& [proxy, path] : proxiedPaths) { + try { + proxy->unload(path); + } catch (const std::exception& e) { + log::error("failed to unload proxied plugin '{}': {}", path, e.what()); + } catch (...) { + log::error("failed to unload proxied plugin '{}': unknown exception", path); + } + } + bf::for_each(m_Plugins, [](auto& t) { t.second.clear(); }); @@ -1151,8 +1201,17 @@ void PluginContainer::unloadPlugins() while (!m_PluginLoaders.empty()) { QPluginLoader* loader = m_PluginLoaders.back(); m_PluginLoaders.pop_back(); - if ((loader != nullptr) && !loader->unload()) { - log::debug("failed to unload {}: {}", loader->fileName(), loader->errorString()); + if (loader != nullptr) { + try { + if (!loader->unload()) { + log::debug("failed to unload {}: {}", loader->fileName(), + loader->errorString()); + } + } catch (const std::exception& e) { + log::error("failed to unload {}: {}", loader->fileName(), e.what()); + } catch (...) { + log::error("failed to unload {}: unknown exception", loader->fileName()); + } } delete loader; } diff --git a/src/src/processrunner.cpp b/src/src/processrunner.cpp index a0deae2..36108b8 100644 --- a/src/src/processrunner.cpp +++ b/src/src/processrunner.cpp @@ -1,10 +1,8 @@ #include "processrunner.h" #include "env.h" #include "envmodule.h" -#include "fluorineconfig.h" #include "instancemanager.h" #include "iuserinterface.h" -#include "knowngames.h" #include "organizercore.h" #include <iplugingame.h> @@ -19,7 +17,6 @@ #include <QMetaObject> #include <QPointer> #include <QProcess> -#include <QSettings> #include <QThread> #include <cerrno> @@ -34,31 +31,6 @@ using namespace MOBase; -static QString steamAppIdFromFluorineConfig() -{ - if (auto cfg = FluorineConfig::load(); cfg.has_value() && cfg->app_id != 0) { - return QString::number(cfg->app_id); - } - - return {}; -} - -static QString steamAppIdFromKnownGame(const QString& gameName) -{ - if (const auto* knownGame = findKnownGameByTitle(gameName); - knownGame != nullptr && knownGame->steam_app_id != nullptr) { - return QString::fromLatin1(knownGame->steam_app_id); - } - - return {}; -} - -static bool steamOverlayEnabled(const Settings& settings) -{ - const QSettings instanceIni(settings.filename(), QSettings::IniFormat); - return instanceIni.value("fluorine/steam_overlay", false).toBool(); -} - void adjustForVirtualized(const IPluginGame* game, spawn::SpawnParameters& sp, const Settings& settings) { @@ -1212,27 +1184,6 @@ std::optional<ProcessRunner::Results> ProcessRunner::runBinary() } } - const bool resolveOverlaySteamAppId = m_sp.useProton && steamOverlayEnabled(settings); - - if (resolveOverlaySteamAppId && m_sp.steamAppID.trimmed().isEmpty()) { - const QString knownSteamId = steamAppIdFromKnownGame(game->gameName()); - if (!knownSteamId.isEmpty()) { - m_sp.steamAppID = knownSteamId; - log::debug("process runner: using known-game steam app id '{}' for '{}'", - m_sp.steamAppID, game->gameName()); - } - } - - if (resolveOverlaySteamAppId && m_sp.steamAppID.trimmed().isEmpty()) { - const QString configSteamId = steamAppIdFromFluorineConfig(); - if (!configSteamId.isEmpty()) { - m_sp.steamAppID = configSteamId; - log::debug("process runner: using Fluorine config steam app id '{}' " - "for launch", - m_sp.steamAppID); - } - } - if (!checkSteam(parent, m_sp, game->gameDirectory(), m_sp.steamAppID, settings)) { return Error; } diff --git a/src/src/profile.cpp b/src/src/profile.cpp index d7dc83e..212da8d 100644 --- a/src/src/profile.cpp +++ b/src/src/profile.cpp @@ -151,8 +151,15 @@ Profile::Profile(const Profile& reference) Profile::~Profile() { + try { + m_ModListWriter.writeImmediately(true); + } catch (const std::exception& e) { + log::error("failed to flush mod list during profile shutdown: {}", e.what()); + } catch (...) { + log::error("failed to flush mod list during profile shutdown: unknown exception"); + } + delete m_Settings; - m_ModListWriter.writeImmediately(true); } void Profile::findProfileSettings() diff --git a/src/src/protonlauncher.cpp b/src/src/protonlauncher.cpp index e7da03a..0c876ec 100644 --- a/src/src/protonlauncher.cpp +++ b/src/src/protonlauncher.cpp @@ -633,12 +633,6 @@ ProtonLauncher& ProtonLauncher::setSteamDrm(bool useSteamDrm) return *this; } -ProtonLauncher& ProtonLauncher::setSteamOverlay(bool useSteamOverlay) -{ - m_useSteamOverlay = useSteamOverlay; - return *this; -} - ProtonLauncher& ProtonLauncher::setUseSLR(bool useSLR) { m_useSLR = useSLR; @@ -714,7 +708,7 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const return false; } - if (m_useSteamDrm || m_useSteamOverlay) { + if (m_useSteamDrm) { ensureSteamRunning(); } @@ -766,32 +760,26 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const pressureVesselImportantPaths << protonDir; } - // Steam overlay needs gameoverlayrenderer.so visible inside the - // pressure-vessel container. Steam dirs are usually mapped already, - // but bind them explicitly to be safe — pressure-vessel's defaults - // change between SLR versions. - if (m_useSteamOverlay) { - const QString steamPath = detectSteamPath(); - if (!steamPath.isEmpty()) { - slrArgs << QStringLiteral("--filesystem=%1/ubuntu12_32").arg(steamPath) - << QStringLiteral("--filesystem=%1/ubuntu12_64").arg(steamPath); - } - } - // Expose dedicated xrandr dir so container sees our injected xrandr // (steamrt4 ships without it, required by Proton-GE protonfixes). // Pressure-vessel forces PATH=/usr/bin:/bin inside the container and // ignores host PATH, so we prepend via `env PATH=...` as the command. QStringList containerCmd; + containerCmd << QStringLiteral("/usr/bin/env"); { const QString xrandrDir = QDir::homePath() + "/.local/share/fluorine/steamrt/xrandr-bin"; if (QDir(xrandrDir).exists()) { slrArgs << QStringLiteral("--filesystem=%1").arg(xrandrDir); - containerCmd << QStringLiteral("/usr/bin/env") - << QStringLiteral("PATH=%1:/usr/bin:/bin").arg(xrandrDir); + containerCmd << QStringLiteral("PATH=%1:/usr/bin:/bin").arg(xrandrDir); } } + if (m_steamAppId != 0) { + const QString appId = QString::number(m_steamAppId); + containerCmd << QStringLiteral("STEAM_COMPAT_APP_ID=%1").arg(appId) + << QStringLiteral("SteamAppId=%1").arg(appId) + << QStringLiteral("SteamGameId=%1").arg(appId); + } containerCmd << protonScript << protonArgs; slrArgs << "--"; @@ -836,64 +824,11 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const if (m_steamAppId != 0) { const QString appId = QString::number(m_steamAppId); + env.insert("STEAM_COMPAT_APP_ID", appId); env.insert("SteamAppId", appId); env.insert("SteamGameId", appId); } - // Steam overlay injection. Requires (a) Steam client running (handled - // above), (b) the game owned on the running Steam account, (c) the env - // triplet SteamAppId/SteamGameId/SteamOverlayGameId pointing at the real - // Steam app id so the overlay's IPC handshake matches an installed app, - // (d) gameoverlayrenderer.so preloaded for legacy GL/X11 hooks, and (e) - // the Steam Vulkan overlay layer enabled for DXVK-rendered games (most - // modern titles via Proton — Bethesda games included). We don't add any - // wrapper process (no reaper) so Steam's "in-game" indicator may stay - // blank, but the overlay itself still attaches because the .so/layer - // hook the running process directly. - if (m_useSteamOverlay && m_steamAppId != 0 && !steamPath.isEmpty()) { - const QString appId = QString::number(m_steamAppId); - env.insert("SteamOverlayGameId", appId); - - const QString preload32 = - steamPath + "/ubuntu12_32/gameoverlayrenderer.so"; - const QString preload64 = - steamPath + "/ubuntu12_64/gameoverlayrenderer.so"; - - QStringList preloads; - if (QFileInfo::exists(preload32)) preloads << preload32; - if (QFileInfo::exists(preload64)) preloads << preload64; - - if (preloads.isEmpty()) { - MOBase::log::warn( - "Steam overlay enabled but gameoverlayrenderer.so not found under " - "'{}/ubuntu12_{{32,64}}' — skipping overlay env", - steamPath); - } else { - const QString existing = env.value("LD_PRELOAD"); - const QString joined = preloads.join(":"); - env.insert("LD_PRELOAD", - existing.isEmpty() ? joined : existing + ":" + joined); - - // Force-enable the Vulkan overlay implicit layer. Steam ships - // VkLayer_VALVE_steam_overlay as an *implicit* Vulkan layer that is - // normally auto-loaded for Steam-launched processes; pressure-vessel - // and some loader configurations skip it unless explicitly enabled. - env.insert("ENABLE_VK_LAYER_VALVE_steam_overlay_1", "1"); - // Also clear the disable-flag in case a Proton wrapper script set it. - env.remove("DISABLE_VK_LAYER_VALVE_steam_overlay_1"); - - MOBase::log::info( - "Steam overlay enabled (appid={}, LD_PRELOAD+={}, " - "ENABLE_VK_LAYER_VALVE_steam_overlay_1=1)", - appId, joined); - } - } else if (m_useSteamOverlay && m_steamAppId == 0) { - MOBase::log::warn( - "Steam overlay requested but no Steam App ID is set for this " - "executable — overlay needs an appid for Steam to recognise the " - "game; skipping"); - } - // When Steam DRM is disabled (e.g. GOG games), set UMU_ID so that // Proton-GE skips the built-in steam.exe bridge. Without this, Proton // tries to initialise the Steam client which causes an assertion failure diff --git a/src/src/protonlauncher.h b/src/src/protonlauncher.h index f0ce09b..7ea41eb 100644 --- a/src/src/protonlauncher.h +++ b/src/src/protonlauncher.h @@ -22,7 +22,6 @@ public: ProtonLauncher& setSteamAppId(uint32_t id); ProtonLauncher& setWrapper(const QString& wrapperCmd); ProtonLauncher& setSteamDrm(bool useSteamDrm); - ProtonLauncher& setSteamOverlay(bool useSteamOverlay); ProtonLauncher& setUseSLR(bool useSLR); ProtonLauncher& setStoreVariant(const QString& variant); ProtonLauncher& addEnvVar(const QString& key, const QString& value); @@ -57,7 +56,6 @@ private: uint32_t m_steamAppId{0}; QStringList m_wrapperCommands; bool m_useSteamDrm{true}; - bool m_useSteamOverlay = false; bool m_useSLR = true; QString m_storeVariant; // "GOG", "Epic", or empty for Steam QMap<QString, QString> m_envVars; diff --git a/src/src/settings.cpp b/src/src/settings.cpp index 7f8a447..df4275d 100644 --- a/src/src/settings.cpp +++ b/src/src/settings.cpp @@ -2120,6 +2120,28 @@ void InterfaceSettings::setStyleName(const QString& name) set(m_Settings, "Settings", "style", name); } +int InterfaceSettings::qssFontSize() const +{ + const int size = get<int>(m_Settings, "Settings", "qss_font_size", 0); + if (size < 0) { + return 0; + } + if (size > 48) { + return 48; + } + return size; +} + +void InterfaceSettings::setQssFontSize(int size) +{ + if (size < 0) { + size = 0; + } else if (size > 48) { + size = 48; + } + set(m_Settings, "Settings", "qss_font_size", size); +} + bool InterfaceSettings::collapsibleSeparators(Qt::SortOrder order) const { return get<bool>(m_Settings, "Settings", diff --git a/src/src/settings.h b/src/src/settings.h index 9c4023e..68a668e 100644 --- a/src/src/settings.h +++ b/src/src/settings.h @@ -590,6 +590,11 @@ public: std::optional<QString> styleName() const; void setStyleName(const QString& name); + // QSS font size override in pixels; 0 means use the stylesheet defaults + // + int qssFontSize() const; + void setQssFontSize(int size); + // whether to use collapsible separators when possible // bool collapsibleSeparators(Qt::SortOrder order) const; diff --git a/src/src/settingsdialog.ui b/src/src/settingsdialog.ui index fe57b49..833a23e 100644 --- a/src/src/settingsdialog.ui +++ b/src/src/settingsdialog.ui @@ -441,6 +441,32 @@ </widget> </item> <item> + <widget class="QLabel" name="qssFontSizeLabel"> + <property name="text"> + <string>Font size</string> + </property> + <property name="buddy"> + <cstring>qssFontSizeSpinBox</cstring> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="qssFontSizeSpinBox"> + <property name="toolTip"> + <string>Overrides font-size values in QSS themes. Default uses the theme value.</string> + </property> + <property name="specialValueText"> + <string>Default</string> + </property> + <property name="suffix"> + <string> px</string> + </property> + <property name="maximum"> + <number>48</number> + </property> + </widget> + </item> + <item> <spacer name="horizontalSpacer_5"> <property name="orientation"> <enum>Qt::Horizontal</enum> @@ -1843,16 +1869,6 @@ If you disable this feature, MO will only display official DLCs this way. Please </property> </widget> </item> - <item row="5" column="0" colspan="4"> - <widget class="QCheckBox" name="fuseIoUringCheckBox"> - <property name="toolTip"> - <string>Enable the kernel FUSE io_uring transport for new VFS mounts. Requires administrator authentication and a VFS remount; Fluorine falls back automatically when unsupported.</string> - </property> - <property name="text"> - <string>Enable FUSE io_uring</string> - </property> - </widget> - </item> <item row="6" column="0"> <widget class="QLabel" name="label_64"> <property name="text"> diff --git a/src/src/settingsdialogproton.cpp b/src/src/settingsdialogproton.cpp index fdb8cbd..f4815c2 100644 --- a/src/src/settingsdialogproton.cpp +++ b/src/src/settingsdialogproton.cpp @@ -11,7 +11,6 @@ #include "steamdetection.h" #include "slrmanager.h" #include <atomic> -#include <QCheckBox> #include <QComboBox> #include <QCoreApplication> #include <QDateTime> @@ -32,115 +31,11 @@ #include <QSettings> #include <QScopeGuard> #include <QStandardPaths> -#include <QSignalBlocker> #include <QVBoxLayout> namespace { std::atomic<ProtonSettingsTab*> g_activeInstallTab = nullptr; - -constexpr const char* kFuseIoUringParameter = - "/sys/module/fuse/parameters/enable_uring"; - -enum class FuseIoUringState -{ - Unsupported, - Disabled, - Enabled, - Unknown -}; - -FuseIoUringState readFuseIoUringState() -{ - QFile parameter(QString::fromUtf8(kFuseIoUringParameter)); - if (!parameter.exists()) { - return FuseIoUringState::Unsupported; - } - - if (!parameter.open(QIODevice::ReadOnly | QIODevice::Text)) { - return FuseIoUringState::Unknown; - } - - const QByteArray value = parameter.readAll().trimmed().toUpper(); - if (value == "Y" || value == "1") { - return FuseIoUringState::Enabled; - } - if (value == "N" || value == "0") { - return FuseIoUringState::Disabled; - } - - return FuseIoUringState::Unknown; -} - -QString findPrivilegeHelper(QStringList* arguments) -{ - const QString command = - QStringLiteral("printf Y > %1").arg(QString::fromUtf8(kFuseIoUringParameter)); - - const QString pkexec = QStandardPaths::findExecutable(QStringLiteral("pkexec")); - if (!pkexec.isEmpty()) { - *arguments = {QStringLiteral("/bin/sh"), QStringLiteral("-c"), command}; - return pkexec; - } - - const QString sudo = QStandardPaths::findExecutable(QStringLiteral("sudo")); - if (!sudo.isEmpty()) { - const QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); - if (!env.value(QStringLiteral("SUDO_ASKPASS")).isEmpty()) { - *arguments = {QStringLiteral("-A"), QStringLiteral("/bin/sh"), - QStringLiteral("-c"), command}; - return sudo; - } - - const QFileInfo stdinInfo(QStringLiteral("/proc/self/fd/0")); - const QString stdinTarget = stdinInfo.symLinkTarget(); - if (stdinInfo.exists() && - (stdinTarget.startsWith(QStringLiteral("/dev/pts/")) || - stdinTarget == QStringLiteral("/dev/tty"))) { - *arguments = {QStringLiteral("/bin/sh"), QStringLiteral("-c"), command}; - return sudo; - } - } - - return {}; -} - -QString enableFuseIoUring() -{ - QStringList arguments; - const QString helper = findPrivilegeHelper(&arguments); - if (helper.isEmpty()) { - return QObject::tr("Install pkexec/polkit, or configure sudo askpass, then try " - "again."); - } - - QProcess process; - process.start(helper, arguments); - if (!process.waitForStarted(5000)) { - return QObject::tr("Failed to start %1.").arg(helper); - } - - if (!process.waitForFinished(120000)) { - process.kill(); - process.waitForFinished(3000); - return QObject::tr("Timed out waiting for administrator authentication."); - } - - if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) { - const QString stderrText = QString::fromLocal8Bit(process.readAllStandardError()) - .trimmed(); - if (!stderrText.isEmpty()) { - return stderrText; - } - return QObject::tr("Administrator authentication was cancelled or failed."); - } - - if (readFuseIoUringState() != FuseIoUringState::Enabled) { - return QObject::tr("The kernel did not accept the FUSE io_uring setting."); - } - - return {}; -} } ProtonSettingsTab::ProtonSettingsTab(Settings& s, SettingsDialog& d) @@ -203,8 +98,6 @@ ProtonSettingsTab::ProtonSettingsTab(Settings& s, SettingsDialog& d) &ProtonSettingsTab::onBrowsePrefixLocation); QObject::connect(ui->downloadSLRButton, &QPushButton::clicked, this, &ProtonSettingsTab::onDownloadSLR); - QObject::connect(ui->fuseIoUringCheckBox, &QCheckBox::clicked, this, - &ProtonSettingsTab::onFuseIoUringToggled); QObject::connect(&m_installWatcher, &QFutureWatcher<InstallResult>::finished, this, &ProtonSettingsTab::onInstallFinished); @@ -291,8 +184,6 @@ void ProtonSettingsTab::refreshState() ui->openPrefixFolderButton->setEnabled(!m_busy && active); ui->winetricksButton->setEnabled(!m_busy && active); ui->protonVersionCombo->setEnabled(!m_busy); - - refreshFuseIoUringState(); } void ProtonSettingsTab::setBusy(bool busy) @@ -442,74 +333,6 @@ void ProtonSettingsTab::onDownloadSLR() progress->show(); } -void ProtonSettingsTab::refreshFuseIoUringState() -{ - QSignalBlocker blocker(ui->fuseIoUringCheckBox); - Q_UNUSED(blocker); - - const FuseIoUringState state = readFuseIoUringState(); - ui->fuseIoUringCheckBox->setVisible(true); - - switch (state) { - case FuseIoUringState::Enabled: - ui->fuseIoUringCheckBox->setChecked(true); - ui->fuseIoUringCheckBox->setEnabled(false); - ui->fuseIoUringCheckBox->setToolTip( - tr("FUSE io_uring is enabled in the kernel. It applies to new VFS " - "mounts after relaunch/remount.")); - break; - case FuseIoUringState::Disabled: - ui->fuseIoUringCheckBox->setChecked(false); - ui->fuseIoUringCheckBox->setEnabled(!m_busy); - ui->fuseIoUringCheckBox->setToolTip( - tr("Enable the kernel FUSE io_uring transport for new VFS mounts. " - "Requires administrator authentication and a VFS remount; Fluorine " - "falls back automatically when unsupported.")); - break; - case FuseIoUringState::Unsupported: - ui->fuseIoUringCheckBox->setChecked(false); - ui->fuseIoUringCheckBox->setEnabled(false); - ui->fuseIoUringCheckBox->setToolTip( - tr("This kernel does not expose %1. FUSE io_uring requires kernel " - "support for CONFIG_FUSE_IO_URING.") - .arg(QString::fromUtf8(kFuseIoUringParameter))); - break; - case FuseIoUringState::Unknown: - ui->fuseIoUringCheckBox->setChecked(false); - ui->fuseIoUringCheckBox->setEnabled(!m_busy); - ui->fuseIoUringCheckBox->setToolTip( - tr("Fluorine could not read the kernel FUSE io_uring setting. Click " - "to try enabling it with administrator authentication.")); - break; - } -} - -void ProtonSettingsTab::onFuseIoUringToggled(bool checked) -{ - if (!checked) { - refreshFuseIoUringState(); - return; - } - - MOBase::log::info("[VFS] user requested enabling FUSE io_uring via settings UI"); - - const QString error = enableFuseIoUring(); - if (!error.isEmpty()) { - MOBase::log::warn("[VFS] failed to enable FUSE io_uring: {}", error); - QMessageBox::warning(parentWidget(), tr("FUSE io_uring"), - tr("Could not enable FUSE io_uring:\n%1").arg(error)); - refreshFuseIoUringState(); - return; - } - - MOBase::log::info("[VFS] kernel FUSE io_uring parameter is now enabled"); - QMessageBox::information( - parentWidget(), tr("FUSE io_uring"), - tr("FUSE io_uring is enabled. Restart the game/VFS mount before testing; " - "existing mounts keep their current transport.")); - refreshFuseIoUringState(); -} - void ProtonSettingsTab::onBrowsePrefixLocation() { const QString dir = QFileDialog::getExistingDirectory( diff --git a/src/src/settingsdialogproton.h b/src/src/settingsdialogproton.h index 321e5a2..5eee3bd 100644 --- a/src/src/settingsdialogproton.h +++ b/src/src/settingsdialogproton.h @@ -33,11 +33,9 @@ private: void onWinetricks(); void onBrowsePrefixLocation(); void onDownloadSLR(); - void onFuseIoUringToggled(bool checked); static QString ensureWinetricks(); static QString findProtonWine(const QString& protonPath); - void refreshFuseIoUringState(); void runPrefixSetupDialog(uint32_t appId, const QString& prefixPath, const QString& protonName, const QString& protonPath); diff --git a/src/src/settingsdialogtheme.cpp b/src/src/settingsdialogtheme.cpp index a88396b..205789a 100644 --- a/src/src/settingsdialogtheme.cpp +++ b/src/src/settingsdialogtheme.cpp @@ -14,9 +14,10 @@ using namespace MOBase; ThemeSettingsTab::ThemeSettingsTab(Settings& s, SettingsDialog& d) : SettingsTab(s, d)
{
- // style
- addStyles();
- selectStyle();
+ // style + addStyles(); + selectStyle(); + selectQssFontSize(); // colors
ui->colorTable->load(s);
@@ -33,14 +34,23 @@ ThemeSettingsTab::ThemeSettingsTab(Settings& s, SettingsDialog& d) : SettingsTab void ThemeSettingsTab::update()
{
// style
- const QString oldStyle = settings().interface().styleName().value_or("");
- const QString newStyle =
- ui->styleBox->itemData(ui->styleBox->currentIndex()).toString();
-
- if (oldStyle != newStyle) {
- settings().interface().setStyleName(newStyle);
- emit settings().styleChanged(newStyle);
- }
+ const QString oldStyle = settings().interface().styleName().value_or(""); + const QString newStyle = + ui->styleBox->itemData(ui->styleBox->currentIndex()).toString(); + const int oldQssFontSize = settings().interface().qssFontSize(); + const int newQssFontSize = ui->qssFontSizeSpinBox->value(); + + if (oldStyle != newStyle) { + settings().interface().setStyleName(newStyle); + } + + if (oldQssFontSize != newQssFontSize) { + settings().interface().setQssFontSize(newQssFontSize); + } + + if (oldStyle != newStyle || oldQssFontSize != newQssFontSize) { + emit settings().styleChanged(newStyle); + } // colors
ui->colorTable->commitColors();
@@ -90,13 +100,18 @@ void ThemeSettingsTab::selectStyle() const int currentID =
ui->styleBox->findData(settings().interface().styleName().value_or(""));
- if (currentID != -1) {
- ui->styleBox->setCurrentIndex(currentID);
- }
-}
-
-void ThemeSettingsTab::onExploreStyles()
-{
+ if (currentID != -1) { + ui->styleBox->setCurrentIndex(currentID); + } +} + +void ThemeSettingsTab::selectQssFontSize() +{ + ui->qssFontSizeSpinBox->setValue(settings().interface().qssFontSize()); +} + +void ThemeSettingsTab::onExploreStyles() +{ // Open the instance's stylesheets directory (where custom themes from
// modlists live), or the user data dir as fallback.
QString ssPath;
diff --git a/src/src/settingsdialogtheme.h b/src/src/settingsdialogtheme.h index 0d60f5c..2e22b0b 100644 --- a/src/src/settingsdialogtheme.h +++ b/src/src/settingsdialogtheme.h @@ -13,10 +13,11 @@ public: void update() override;
-private:
- void addStyles();
- void selectStyle();
- static void onExploreStyles();
-};
+private: + void addStyles(); + void selectStyle(); + void selectQssFontSize(); + static void onExploreStyles(); +}; #endif // SETTINGSDIALOGGENERAL_H
diff --git a/src/src/spawn.cpp b/src/src/spawn.cpp index 701304b..5deaa1f 100644 --- a/src/src/spawn.cpp +++ b/src/src/spawn.cpp @@ -22,7 +22,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "env.h" #include "envmodule.h" #include "fluorineconfig.h" -#include "knowngames.h" #include "protonlauncher.h" #include "settings.h" #include "shared/appconfig.h" @@ -193,38 +192,6 @@ uint32_t parseSteamAppId(const QString& steamAppId) return (ok ? n : 0u); } -static uint32_t steamAppIdFromFluorineConfig() -{ - if (auto cfg = FluorineConfig::load(); cfg.has_value()) { - return cfg->app_id; - } - - return 0; -} - -static uint32_t steamAppIdFromKnownGame(const QString& gameName) -{ - if (const auto* knownGame = findKnownGameByTitle(gameName); - knownGame != nullptr && knownGame->steam_app_id != nullptr) { - bool ok = false; - const auto n = QString::fromLatin1(knownGame->steam_app_id).toUInt(&ok); - return (ok ? n : 0u); - } - - return 0; -} - -static bool steamOverlayEnabledForCurrentInstance() -{ - const Settings* instanceForLaunch = Settings::maybeInstance(); - if (!instanceForLaunch) { - return false; - } - - const QSettings instanceIni(instanceForLaunch->filename(), QSettings::IniFormat); - return instanceIni.value("fluorine/steam_overlay", false).toBool(); -} - QString firstExistingSetting(const QSettings& settings, const QStringList& keys) { for (const QString& key : keys) { @@ -440,31 +407,6 @@ int spawn(const SpawnParameters& sp, pid_t& processId) logSpawning(sp, bin + " " + sp.arguments); uint32_t steamAppId = parseSteamAppId(sp.steamAppID); - const bool resolveOverlaySteamAppId = - sp.useProton && steamOverlayEnabledForCurrentInstance(); - if (resolveOverlaySteamAppId && steamAppId == 0) { - const Settings* instanceForLaunch = Settings::maybeInstance(); - if (instanceForLaunch) { - const auto gameName = instanceForLaunch->game().name(); - if (gameName && !gameName->trimmed().isEmpty()) { - steamAppId = steamAppIdFromKnownGame(*gameName); - if (steamAppId != 0) { - MOBase::log::debug("spawn: using known-game steam app id '{}' " - "for '{}'", - steamAppId, *gameName); - } - } - } - } - if (resolveOverlaySteamAppId && steamAppId == 0) { - steamAppId = steamAppIdFromFluorineConfig(); - if (steamAppId != 0) { - MOBase::log::debug("spawn: using Fluorine config steam app id '{}' " - "for Proton launch", - steamAppId); - } - } - ProtonLauncher launcher; launcher.setBinary(bin) .setArguments(argList) @@ -476,17 +418,14 @@ int spawn(const SpawnParameters& sp, pid_t& processId) // Read per-instance settings from the instance INI (not the global QSettings). const Settings* instanceForLaunch = Settings::maybeInstance(); bool useSteamDrm = true; - bool useSteamOverlay = false; QString storeVariant; if (instanceForLaunch) { const QSettings instanceIni(instanceForLaunch->filename(), QSettings::IniFormat); useSteamDrm = instanceIni.value("fluorine/steam_drm", true).toBool(); - useSteamOverlay = instanceIni.value("fluorine/steam_overlay", false).toBool(); storeVariant = instanceIni.value("game_edition").toString().trimmed(); } launcher.setSteamDrm(useSteamDrm) - .setSteamOverlay(useSteamOverlay) .setStoreVariant(storeVariant) .setUseSLR(true); diff --git a/src/src/steamdetection.cpp b/src/src/steamdetection.cpp index 512b1fa..c1c3f7f 100644 --- a/src/src/steamdetection.cpp +++ b/src/src/steamdetection.cpp @@ -4,6 +4,7 @@ #include <QDirIterator> #include <QFile> #include <QFileInfo> +#include <QSet> #include <QStandardPaths> #include <uibase/log.h> @@ -69,10 +70,22 @@ QStringList findSteamLibraryPaths() auto addLibrary = [&](const QString& path) { const QString cleanPath = QDir::cleanPath(path); - if (cleanPath.isEmpty() || libraries.contains(cleanPath)) + if (cleanPath.isEmpty()) { return; - if (QFileInfo::exists(QDir(cleanPath).filePath(QStringLiteral("steamapps")))) - libraries.append(cleanPath); + } + + const QFileInfo info(cleanPath); + const QString canonicalPath = info.canonicalFilePath(); + const QString libraryPath = + canonicalPath.isEmpty() ? info.absoluteFilePath() : canonicalPath; + const QString normalizedLibraryPath = QDir::cleanPath(libraryPath); + + if (normalizedLibraryPath.isEmpty() || libraries.contains(normalizedLibraryPath)) { + return; + } + if (QFileInfo::exists(QDir(normalizedLibraryPath).filePath(QStringLiteral("steamapps")))) { + libraries.append(normalizedLibraryPath); + } }; addLibrary(steamPath); @@ -225,6 +238,36 @@ QVector<SteamProtonInfo> findSteamProtons() }), protons.end()); + QSet<QString> seenPaths; + QSet<QString> seenNames; + QVector<SteamProtonInfo> uniqueProtons; + uniqueProtons.reserve(protons.size()); + + for (SteamProtonInfo& proton : protons) { + const QFileInfo pathInfo(proton.path); + const QString canonicalPath = pathInfo.canonicalFilePath(); + const QString pathKey = + QDir::cleanPath(canonicalPath.isEmpty() ? proton.path : canonicalPath); + const QString nameKey = proton.name.trimmed().toCaseFolded(); + + if ((!pathKey.isEmpty() && seenPaths.contains(pathKey)) || + (!nameKey.isEmpty() && seenNames.contains(nameKey))) { + MOBase::log::debug("Skipping duplicate Proton '{}' at '{}'", + proton.name, proton.path); + continue; + } + + if (!pathKey.isEmpty()) { + seenPaths.insert(pathKey); + } + if (!nameKey.isEmpty()) { + seenNames.insert(nameKey); + } + uniqueProtons.append(std::move(proton)); + } + + protons = std::move(uniqueProtons); + // Sort: Experimental first, then by name descending (newest first). std::sort(protons.begin(), protons.end(), [](const SteamProtonInfo& a, const SteamProtonInfo& b) { diff --git a/src/src/vfs/mo2filesystem.cpp b/src/src/vfs/mo2filesystem.cpp index 7b15d04..9a65017 100644 --- a/src/src/vfs/mo2filesystem.cpp +++ b/src/src/vfs/mo2filesystem.cpp @@ -93,57 +93,40 @@ struct OpTimer OpTimer& operator=(const OpTimer&) = delete; }; -bool fuseHasFeature(const struct fuse_conn_info* conn, uint64_t flag) +bool fuseHasFeature(const struct fuse_conn_info* conn, uint32_t flag) { if (conn == nullptr) { return false; } -#ifdef FUSE_CAP_OVER_IO_URING - return (conn->capable_ext & flag) != 0 || - (flag <= UINT32_MAX && (conn->capable & static_cast<uint32_t>(flag)) != 0); -#else - return (conn->capable & static_cast<uint32_t>(flag)) != 0; -#endif + return (conn->capable & flag) != 0; } -bool fuseWantsFeature(const struct fuse_conn_info* conn, uint64_t flag) +bool fuseWantsFeature(const struct fuse_conn_info* conn, uint32_t flag) { if (conn == nullptr) { return false; } -#if defined(FUSE_CAP_OVER_IO_URING) && FUSE_VERSION >= FUSE_MAKE_VERSION(3, 17) - return fuse_get_feature_flag(const_cast<struct fuse_conn_info*>(conn), flag); -#else - return (conn->want & static_cast<uint32_t>(flag)) != 0; -#endif + return (conn->want & flag) != 0; } -bool fuseRequestFeature(struct fuse_conn_info* conn, uint64_t flag) +bool fuseRequestFeature(struct fuse_conn_info* conn, uint32_t flag) { if (conn == nullptr) { return false; } -#if defined(FUSE_CAP_OVER_IO_URING) && FUSE_VERSION >= FUSE_MAKE_VERSION(3, 17) - return fuse_set_feature_flag(conn, flag); -#else - if ((conn->capable & static_cast<uint32_t>(flag)) == 0) { + if ((conn->capable & flag) == 0) { return false; } - conn->want |= static_cast<uint32_t>(flag); + conn->want |= flag; return true; -#endif } -void fuseDropFeature(struct fuse_conn_info* conn, uint64_t flag) +void fuseDropFeature(struct fuse_conn_info* conn, uint32_t flag) { if (conn == nullptr) { return; } -#if defined(FUSE_CAP_OVER_IO_URING) && FUSE_VERSION >= FUSE_MAKE_VERSION(3, 17) - fuse_unset_feature_flag(conn, flag); -#else - conn->want &= ~static_cast<uint32_t>(flag); -#endif + conn->want &= ~flag; } void maybeLogCounters(Mo2FsContext* ctx) @@ -222,18 +205,13 @@ void maybeLogCounters(Mo2FsContext* ctx) static_cast<unsigned long long>( ctx->retained_ro_fd_evictions.load(std::memory_order_relaxed))); std::fprintf(stderr, - "[VFS] io bytes_read=%llu bytes_written=%llu cow_writes=%llu " - "transport_uring=%llu transport_legacy=%llu\n", + "[VFS] io bytes_read=%llu bytes_written=%llu cow_writes=%llu\n", static_cast<unsigned long long>( ctx->read_bytes.load(std::memory_order_relaxed)), static_cast<unsigned long long>( ctx->write_bytes.load(std::memory_order_relaxed)), static_cast<unsigned long long>( - ctx->cow_write_count.load(std::memory_order_relaxed)), - static_cast<unsigned long long>( - ctx->uring_request_count.load(std::memory_order_relaxed)), - static_cast<unsigned long long>( - ctx->legacy_request_count.load(std::memory_order_relaxed))); + ctx->cow_write_count.load(std::memory_order_relaxed))); { size_t lookupSize = 0; size_t attrSize = 0; @@ -1331,15 +1309,6 @@ void mo2_init(void* userdata, struct fuse_conn_info* conn) fuseRequestFeature(conn, FUSE_CAP_EXPIRE_ONLY); } - bool uringCapable = false; - bool uringWanted = false; -#ifdef FUSE_CAP_OVER_IO_URING - uringCapable = fuseHasFeature(conn, FUSE_CAP_OVER_IO_URING); - if (uringCapable) { - uringWanted = fuseRequestFeature(conn, FUSE_CAP_OVER_IO_URING); - } -#endif - // Maximize async I/O slots (default is 12). The kernel will still only // dispatch as many as there are actual concurrent requests, so higher // values just raise the ceiling without wasting memory. @@ -1366,8 +1335,7 @@ void mo2_init(void* userdata, struct fuse_conn_info* conn) std::fprintf(stderr, "[VFS] init: auto_inval=%d explicit_inval=%d readdirplus=%d " - "no_opendir=%d uring_capable=%d uring_wanted=%d " - "max_bg=%u max_readahead=%u\n", + "no_opendir=%d max_bg=%u max_readahead=%u\n", fuseWantsFeature(conn, FUSE_CAP_AUTO_INVAL_DATA) ? 1 : 0, fuseWantsFeature(conn, FUSE_CAP_EXPLICIT_INVAL_DATA) ? 1 : 0, fuseWantsFeature(conn, FUSE_CAP_READDIRPLUS) ? 1 : 0, @@ -1376,19 +1344,13 @@ void mo2_init(void* userdata, struct fuse_conn_info* conn) #else 0, #endif - uringCapable ? 1 : 0, - uringWanted ? 1 : 0, conn->max_background, conn->max_readahead); std::fprintf(stderr, "[VFS] init_caps: libfuse=%s headers=%d.%d proto=%u.%u " - "capable=0x%08x capable_ext=0x%016llx want=0x%08x " - "want_ext=0x%016llx max_write=%u max_read=%u\n", + "capable=0x%08x want=0x%08x max_write=%u max_read=%u\n", fuse_pkgversion(), FUSE_MAJOR_VERSION, FUSE_MINOR_VERSION, conn->proto_major, conn->proto_minor, conn->capable, - static_cast<unsigned long long>(conn->capable_ext), - conn->want, - static_cast<unsigned long long>(conn->want_ext), - conn->max_write, conn->max_read); + conn->want, conn->max_write, conn->max_read); } void mo2_lookup(fuse_req_t req, fuse_ino_t parent, const char* name) diff --git a/src/src/vfs/mo2filesystem.h b/src/src/vfs/mo2filesystem.h index 111b1b9..f8d5330 100644 --- a/src/src/vfs/mo2filesystem.h +++ b/src/src/vfs/mo2filesystem.h @@ -153,8 +153,6 @@ struct Mo2FsContext std::atomic<uint64_t> read_bytes{0}; std::atomic<uint64_t> write_bytes{0}; std::atomic<uint64_t> cow_write_count{0}; - std::atomic<uint64_t> uring_request_count{0}; - std::atomic<uint64_t> legacy_request_count{0}; // CPU snapshot from previous stats tick (microseconds, from getrusage). // Used to compute per-tick CPU delta so we can distinguish disk-bound vs // CPU-bound slowness in the VFS layer. |
