From b54e479a3f9e973a083c643905f21c59fadd63bb Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Sun, 12 Apr 2026 03:00:11 -0500 Subject: Remove LOOT integration entirely The sort button was already hidden on Linux (setVisible(false)) and the LOOT feedback path (LootDialog -> addLootReport) has always been Windows-only, meaning the dirty/incompatibilities/missingMasters/messages tooltip bullets and the dirty-plugin icon could never fire on Linux. MO2 shipped lootcli + libloot for a load-order sort mechanism that was unreachable from the UI. Users who want LOOT can download it from https://github.com/loot/loot and run it against their instance directly. - Drop libs/lootcli/, libloot Dockerfile build step, and lootcli staging/patchelf in build-inner.sh - Drop src/src/loot.{cpp,h}, src/src/lootdialog.{cpp,h,ui} - Extract MarkdownDocument / MarkdownPage (used by UpdateDialog for its changelog view) into src/src/markdowndocument.{h,cpp} - Strip addLootReport, makeLootTooltip, Loot::Plugin field, LOOT icon checks, and LOOT tooltip/isProblematic/hasInfo paths from pluginlist - Delete sortButton widget, updateSortButton(), onSortButtonClicked(), and all m_didUpdateMasterList wiring - Delete lootLogLevel setting, combo box, and "Integrated LOOT" group from the diagnostics settings tab --- CMakeLists.txt | 1 - docker/Dockerfile | 29 - docker/build-inner.sh | 18 - libs/lootcli/.clang-format | 41 - libs/lootcli/.git-blame-ignore-revs | 2 - libs/lootcli/.gitattributes | 7 - libs/lootcli/.github/workflows/build.yml | 18 - libs/lootcli/.github/workflows/linting.yml | 15 - libs/lootcli/.gitignore | 5 - libs/lootcli/.pre-commit-config.yaml | 20 - libs/lootcli/CMakeLists.txt | 32 - libs/lootcli/CMakePresets.json | 52 - libs/lootcli/README.md | 3 - libs/lootcli/cmake/config.cmake.in | 3 - libs/lootcli/include/lootcli/lootcli.h | 116 -- libs/lootcli/src/CMakeLists.txt | 138 --- libs/lootcli/src/game_settings.cpp | 435 -------- libs/lootcli/src/game_settings.h | 101 -- libs/lootcli/src/lootthread.cpp | 1197 -------------------- libs/lootcli/src/lootthread.h | 107 -- libs/lootcli/src/main.cpp | 102 -- libs/lootcli/src/pch.cpp | 1 - libs/lootcli/src/pch.h | 84 -- libs/lootcli/src/version.h | 4 - libs/lootcli/src/version.rc | 35 - libs/lootcli/vcpkg.json | 30 - src/src/CMakeLists.txt | 1 - src/src/loot.cpp | 1674 ---------------------------- src/src/loot.h | 149 --- src/src/lootdialog.cpp | 350 ------ src/src/lootdialog.h | 82 -- src/src/lootdialog.ui | 230 ---- src/src/mainwindow.cpp | 33 - src/src/mainwindow.h | 10 +- src/src/mainwindow.ui | 17 - src/src/markdowndocument.cpp | 33 + src/src/markdowndocument.h | 40 + src/src/pluginlist.cpp | 113 +- src/src/pluginlist.h | 10 +- src/src/pluginlistview.cpp | 54 +- src/src/pluginlistview.h | 3 - src/src/settings.cpp | 11 - src/src/settings.h | 7 +- src/src/settingsdialog.ui | 22 - src/src/settingsdialogdiagnostics.cpp | 30 - src/src/settingsdialogdiagnostics.h | 1 - src/src/updatedialog.cpp | 1 - src/src/updatedialog.h | 2 +- 48 files changed, 89 insertions(+), 5380 deletions(-) delete mode 100644 libs/lootcli/.clang-format delete mode 100644 libs/lootcli/.git-blame-ignore-revs delete mode 100644 libs/lootcli/.gitattributes delete mode 100644 libs/lootcli/.github/workflows/build.yml delete mode 100644 libs/lootcli/.github/workflows/linting.yml delete mode 100644 libs/lootcli/.gitignore delete mode 100644 libs/lootcli/.pre-commit-config.yaml delete mode 100644 libs/lootcli/CMakeLists.txt delete mode 100644 libs/lootcli/CMakePresets.json delete mode 100644 libs/lootcli/README.md delete mode 100644 libs/lootcli/cmake/config.cmake.in delete mode 100644 libs/lootcli/include/lootcli/lootcli.h delete mode 100644 libs/lootcli/src/CMakeLists.txt delete mode 100644 libs/lootcli/src/game_settings.cpp delete mode 100644 libs/lootcli/src/game_settings.h delete mode 100644 libs/lootcli/src/lootthread.cpp delete mode 100644 libs/lootcli/src/lootthread.h delete mode 100644 libs/lootcli/src/main.cpp delete mode 100644 libs/lootcli/src/pch.cpp delete mode 100644 libs/lootcli/src/pch.h delete mode 100644 libs/lootcli/src/version.h delete mode 100644 libs/lootcli/src/version.rc delete mode 100644 libs/lootcli/vcpkg.json delete mode 100644 src/src/loot.cpp delete mode 100644 src/src/loot.h delete mode 100644 src/src/lootdialog.cpp delete mode 100644 src/src/lootdialog.h delete mode 100644 src/src/lootdialog.ui create mode 100644 src/src/markdowndocument.cpp create mode 100644 src/src/markdowndocument.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a0e4569..2fd7ea7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -152,7 +152,6 @@ add_subdirectory(libs/bsatk) # static lib, needs dds-header + boost + zlib add_subdirectory(libs/libbsarch) # shared + static, needs dds-header add_subdirectory(libs/7zip) # builds 7z.so from source (Linux only) add_subdirectory(libs/archive) # shared lib, needs dl -add_subdirectory(libs/lootcli) # executable, needs Qt6 + boost + libloot + curl add_subdirectory(libs/uibase) # shared lib, needs Qt6 + spdlog diff --git a/docker/Dockerfile b/docker/Dockerfile index 6c92aa2..60caaf6 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -72,35 +72,6 @@ RUN /opt/python-bundled/bin/pip3 install --no-cache-dir \ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \ sh -s -- -y --default-toolchain stable --profile minimal -# ── Build and install libloot ── -ARG LIBLOOT_REF=master -RUN git clone --depth 1 --branch ${LIBLOOT_REF} \ - https://github.com/loot/libloot.git /tmp/libloot && \ - cmake -S /tmp/libloot/cpp -B /tmp/libloot/build -G Ninja \ - -DCMAKE_BUILD_TYPE=Release \ - -DBUILD_SHARED_LIBS=ON \ - -DLIBLOOT_INSTALL_DOCS=OFF \ - -DBUILD_TESTING=OFF \ - -DCMAKE_INSTALL_PREFIX=/usr/local && \ - cmake --build /tmp/libloot/build --parallel && \ - cmake --install /tmp/libloot/build && \ - # Create pkg-config metadata - mkdir -p /usr/local/lib/pkgconfig && \ - printf '%s\n' \ - 'prefix=/usr/local' \ - 'exec_prefix=${prefix}' \ - 'libdir=${prefix}/lib' \ - 'includedir=${prefix}/include' \ - '' \ - 'Name: libloot' \ - 'Description: LOOT C++ API library' \ - 'Version: 0.29.0' \ - 'Libs: -L${libdir} -lloot' \ - 'Cflags: -I${includedir}' \ - > /usr/local/lib/pkgconfig/libloot.pc && \ - ldconfig && \ - rm -rf /tmp/libloot - # ── Pre-download linuxdeploy tooling (optional, only for AppImage builds) ── # Set BUILD_APPIMAGE=1 to include linuxdeploy in the image. # Without it, only tarball and installer builds are available. diff --git a/docker/build-inner.sh b/docker/build-inner.sh index cfbb061..a5c8c0b 100755 --- a/docker/build-inner.sh +++ b/docker/build-inner.sh @@ -49,10 +49,6 @@ cp -f "${RUNDIR}/ModOrganizer" "${OUT_DIR}/ModOrganizer-core" [ -f "${RUNDIR}/README-PORTABLE.txt" ] && cp -f "${RUNDIR}/README-PORTABLE.txt" "${OUT_DIR}/" [ -f "/src/src/fluorine-manager" ] && cp -f "/src/src/fluorine-manager" "${OUT_DIR}/" -# lootcli (spawned by MO2 for load-order sorting). -LOOTCLI="build/libs/lootcli/src/lootcli" -[ -f "${LOOTCLI}" ] && cp -f "${LOOTCLI}" "${OUT_DIR}/" - # wrestool/icotool no longer needed — icon extraction is built into the C++ PE parser # ── MO2 plugins (.so) ── @@ -164,8 +160,6 @@ collect_deps "${OUT_DIR}/ModOrganizer-core" >> "${ALL_DEPS}" find "${OUT_DIR}/plugins" -name "*.so" -exec sh -c 'ldd "$1" 2>/dev/null | grep "=>" | awk "{print \$3}" | grep "^/"' _ {} \; >> "${ALL_DEPS}" # Our own libs find "${OUT_DIR}/lib" -name "*.so*" -exec sh -c 'ldd "$1" 2>/dev/null | grep "=>" | awk "{print \$3}" | grep "^/"' _ {} \; >> "${ALL_DEPS}" -# lootcli -[ -f "${OUT_DIR}/lootcli" ] && collect_deps "${OUT_DIR}/lootcli" >> "${ALL_DEPS}" sort -u "${ALL_DEPS}" | while read -r dep; do dep_name="$(basename "${dep}")" # Skip system libs @@ -232,13 +226,6 @@ else echo "WARNING: Could not find Qt6 plugin directory" fi -# libloot (custom-built, never on user systems). -if [ -f /usr/local/lib/libloot.so.0 ]; then - cp -Lf /usr/local/lib/libloot.so.0 "${OUT_DIR}/lib/" - # Create the unversioned symlink too. - ln -sf libloot.so.0 "${OUT_DIR}/lib/libloot.so" -fi - # ── Bundle PBS Python 3.12 runtime ── # PYTHONHOME only needs lib/python3.12/ (the stdlib). We do NOT copy the # binary, headers, static lib, or .py sources — only stripped .pyc + .so. @@ -328,16 +315,12 @@ strip --strip-unneeded "${OUT_DIR}/ModOrganizer-core" 2>/dev/null || true find "${OUT_DIR}/plugins" -name "*.so" -exec strip --strip-unneeded {} \; 2>/dev/null || true find "${OUT_DIR}/dlls" -name "*.so" -o -name "*.dll" | xargs -r strip --strip-unneeded 2>/dev/null || true find "${OUT_DIR}/lib" -name "*.so" -exec strip --strip-unneeded {} \; 2>/dev/null || true -for tool in lootcli; do - [ -f "${OUT_DIR}/${tool}" ] && strip --strip-unneeded "${OUT_DIR}/${tool}" 2>/dev/null || true -done # ── Fix RPATH so binaries find libs without LD_LIBRARY_PATH ── # Use --force-rpath to set DT_RPATH (not DT_RUNPATH) for reliable # library resolution regardless of LD_LIBRARY_PATH. echo "Patching RPATH..." patchelf --force-rpath --set-rpath '$ORIGIN/lib' "${OUT_DIR}/ModOrganizer-core" -[ -f "${OUT_DIR}/lootcli" ] && patchelf --force-rpath --set-rpath '$ORIGIN/lib' "${OUT_DIR}/lootcli" find "${OUT_DIR}/plugins" -maxdepth 1 -name "*.so" -exec patchelf --force-rpath --set-rpath '$ORIGIN/../lib' {} \; 2>/dev/null || true find "${OUT_DIR}/plugins/libs" -name "*.so" -exec patchelf --force-rpath --set-rpath '$ORIGIN/../../lib' {} \; 2>/dev/null || true find "${OUT_DIR}/lib" \( -name "*.so" -o -name "*.so.*" \) -exec patchelf --force-rpath --set-rpath '$ORIGIN' {} \; 2>/dev/null || true @@ -624,7 +607,6 @@ build_appimage() { fi patchelf --force-rpath --set-rpath '$ORIGIN/../lib' "${APPDIR}/usr/bin/ModOrganizer-core" - [ -f "${APPDIR}/usr/bin/lootcli" ] && patchelf --force-rpath --set-rpath '$ORIGIN/../lib' "${APPDIR}/usr/bin/lootcli" find "${APPDIR}/usr/bin/plugins" -name "*.so" -exec patchelf --force-rpath --set-rpath '$ORIGIN/../../lib' {} \; 2>/dev/null || true find "${APPDIR}/usr/lib" -name "*.so" -exec patchelf --force-rpath --set-rpath '$ORIGIN' {} \; 2>/dev/null || true diff --git a/libs/lootcli/.clang-format b/libs/lootcli/.clang-format deleted file mode 100644 index 6098e1f..0000000 --- a/libs/lootcli/.clang-format +++ /dev/null @@ -1,41 +0,0 @@ ---- -# We'll use defaults from the LLVM style, but with 4 columns indentation. -BasedOnStyle: LLVM -IndentWidth: 2 ---- -Language: Cpp -DeriveLineEnding: false -UseCRLF: true -DerivePointerAlignment: false -PointerAlignment: Left -AlignConsecutiveAssignments: true -AllowShortFunctionsOnASingleLine: Inline -AllowShortIfStatementsOnASingleLine: Never -AllowShortLambdasOnASingleLine: Empty -AlwaysBreakTemplateDeclarations: Yes -AccessModifierOffset: -2 -AlignTrailingComments: true -SpacesBeforeTrailingComments: 2 -NamespaceIndentation: Inner -MaxEmptyLinesToKeep: 1 -BreakBeforeBraces: Custom -BraceWrapping: - AfterCaseLabel: false - AfterClass: true - AfterControlStatement: false - AfterEnum: true - AfterFunction: true - AfterNamespace: true - AfterStruct: true - AfterUnion: true - AfterExternBlock: true - BeforeCatch: false - BeforeElse: false - BeforeLambdaBody: false - BeforeWhile: false - IndentBraces: false - SplitEmptyFunction: false - SplitEmptyRecord: false - SplitEmptyNamespace: true -ColumnLimit: 88 -ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/libs/lootcli/.git-blame-ignore-revs b/libs/lootcli/.git-blame-ignore-revs deleted file mode 100644 index 28904f1..0000000 --- a/libs/lootcli/.git-blame-ignore-revs +++ /dev/null @@ -1,2 +0,0 @@ -dfac8a52f654858beacb3f908234388e70fd88a2 -95a7b8b4226e401db8b7c1a13b1bbbbd8cb5c2cf diff --git a/libs/lootcli/.gitattributes b/libs/lootcli/.gitattributes deleted file mode 100644 index f869712..0000000 --- a/libs/lootcli/.gitattributes +++ /dev/null @@ -1,7 +0,0 @@ -# Set the default behavior, in case people don't have core.autocrlf set. -* text=auto - -# Explicitly declare text files you want to always be normalized and converted -# to native line endings on checkout. -*.cpp text eol=crlf -*.h text eol=crlf diff --git a/libs/lootcli/.github/workflows/build.yml b/libs/lootcli/.github/workflows/build.yml deleted file mode 100644 index d9ca97e..0000000 --- a/libs/lootcli/.github/workflows/build.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Build LootCLI -on: - push: - branches: [master] - pull_request: - types: [opened, synchronize, reopened] - -env: - VCPKG_BINARY_SOURCES: clear;x-azblob,${{ vars.AZ_BLOB_VCPKG_URL }},${{ secrets.AZ_BLOB_SAS }},readwrite - -jobs: - build: - runs-on: windows-2022 - steps: - - name: Build LootCLI - uses: ModOrganizer2/build-with-mob-action@master - with: - mo2-dependencies: cmake_common diff --git a/libs/lootcli/.github/workflows/linting.yml b/libs/lootcli/.github/workflows/linting.yml deleted file mode 100644 index b457e70..0000000 --- a/libs/lootcli/.github/workflows/linting.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Lint LootCLI -on: - push: - pull_request: - types: [opened, synchronize, reopened] -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Run clang-format - uses: jidicula/clang-format-action@v4.11.0 - with: - clang-format-version: "15" - check-path: "." diff --git a/libs/lootcli/.gitignore b/libs/lootcli/.gitignore deleted file mode 100644 index cf71be7..0000000 --- a/libs/lootcli/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -edit -CMakeLists.txt.user -/msbuild.log -/*std*.log -/*build diff --git a/libs/lootcli/.pre-commit-config.yaml b/libs/lootcli/.pre-commit-config.yaml deleted file mode 100644 index 3103a1f..0000000 --- a/libs/lootcli/.pre-commit-config.yaml +++ /dev/null @@ -1,20 +0,0 @@ -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-merge-conflict - - id: check-case-conflict - - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v19.1.5 - hooks: - - id: clang-format - 'types_or': [c++, c] - -ci: - autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." - autofix_prs: true - autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." - autoupdate_schedule: quarterly - submodules: false diff --git a/libs/lootcli/CMakeLists.txt b/libs/lootcli/CMakeLists.txt deleted file mode 100644 index 35e9d98..0000000 --- a/libs/lootcli/CMakeLists.txt +++ /dev/null @@ -1,32 +0,0 @@ -cmake_minimum_required(VERSION 3.16) - -include(CMakePackageConfigHelpers) - -project(lootcli) - -add_subdirectory(src) - -# install the header helper -configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/cmake/config.cmake.in - "${CMAKE_CURRENT_BINARY_DIR}/mo2-lootcli-header-config.cmake" - INSTALL_DESTINATION "lib/cmake/mo2-lootcli-header" - NO_SET_AND_CHECK_MACRO - NO_CHECK_REQUIRED_COMPONENTS_MACRO -) - -file(READ "${CMAKE_CURRENT_SOURCE_DIR}/src/version.h" lootcli_version) -string(REGEX MATCH "#define LOOTCLI_VERSION_STRING[ \t]*\"([0-9.]+)\"" _ ${lootcli_version}) -set(lootcli_version ${CMAKE_MATCH_1}) - -write_basic_package_version_file( - "${CMAKE_CURRENT_BINARY_DIR}/mo2-lootcli-header-config-version.cmake" - VERSION "${lootcli_version}" - COMPATIBILITY AnyNewerVersion - ARCH_INDEPENDENT -) - -install(FILES - ${CMAKE_CURRENT_BINARY_DIR}/mo2-lootcli-header-config.cmake - ${CMAKE_CURRENT_BINARY_DIR}/mo2-lootcli-header-config-version.cmake - DESTINATION lib/cmake/mo2-lootcli-header -) diff --git a/libs/lootcli/CMakePresets.json b/libs/lootcli/CMakePresets.json deleted file mode 100644 index 023baf5..0000000 --- a/libs/lootcli/CMakePresets.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "configurePresets": [ - { - "errors": { - "deprecated": true - }, - "hidden": true, - "name": "cmake-dev", - "warnings": { - "deprecated": true, - "dev": true - } - }, - { - "cacheVariables": { - "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { - "type": "BOOL", - "value": "ON" - } - }, - "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", - "hidden": true, - "name": "vcpkg" - }, - { - "binaryDir": "${sourceDir}/vsbuild", - "architecture": { - "strategy": "set", - "value": "x64" - }, - "cacheVariables": { - "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", - "VCPKG_TARGET_TRIPLET": { - "type": "STRING", - "value": "x64-windows-static-md" - } - }, - "generator": "Visual Studio 17 2022", - "inherits": ["cmake-dev", "vcpkg"], - "name": "vs2022-windows", - "toolset": "v143" - } - ], - "buildPresets": [ - { - "name": "vs2022-windows", - "resolvePackageReferences": "on", - "configurePreset": "vs2022-windows" - } - ], - "version": 4 -} diff --git a/libs/lootcli/README.md b/libs/lootcli/README.md deleted file mode 100644 index 1dc0ded..0000000 --- a/libs/lootcli/README.md +++ /dev/null @@ -1,3 +0,0 @@ -[![Build status](https://ci.appveyor.com/api/projects/status/e9k28w726ucmd0cu?svg=true)](https://ci.appveyor.com/project/Modorganizer2/modorganizer-lootcli) - -# modorganizer-lootcli diff --git a/libs/lootcli/cmake/config.cmake.in b/libs/lootcli/cmake/config.cmake.in deleted file mode 100644 index 4c845a2..0000000 --- a/libs/lootcli/cmake/config.cmake.in +++ /dev/null @@ -1,3 +0,0 @@ -@PACKAGE_INIT@ - -include ( "${CMAKE_CURRENT_LIST_DIR}/mo2-lootcli-header-targets.cmake" ) diff --git a/libs/lootcli/include/lootcli/lootcli.h b/libs/lootcli/include/lootcli/lootcli.h deleted file mode 100644 index 90305a6..0000000 --- a/libs/lootcli/include/lootcli/lootcli.h +++ /dev/null @@ -1,116 +0,0 @@ -#ifndef MODORGANIZER_LOOTCLI_INCLUDED -#define MODORGANIZER_LOOTCLI_INCLUDED - -#include - -namespace lootcli -{ - -enum class LogLevels -{ - Trace = 0, - Debug, - Info, - Warning, - Error -}; - -inline LogLevels logLevelFromString(const std::string& s) -{ - if (s == "trace") { - return LogLevels::Trace; - } else if (s == "debug") { - return LogLevels::Debug; - } else if (s == "info") { - return LogLevels::Info; - } else if (s == "warning") { - return LogLevels::Warning; - } else if (s == "error") { - return LogLevels::Error; - } else { - return LogLevels::Info; - } -} - -inline std::string logLevelToString(LogLevels level) -{ - switch (level) { - case LogLevels::Trace: - return "trace"; - case LogLevels::Debug: - return "debug"; - case LogLevels::Info: - return "info"; - case LogLevels::Warning: - return "warning"; - case LogLevels::Error: - return "error"; - default: - return "info"; - } -} - -enum class Progress -{ - None = 0, - CheckingMasterlistExistence, - UpdatingMasterlist, - LoadingLists, - ReadingPlugins, - SortingPlugins, - WritingLoadorder, - ParsingLootMessages, - Done -}; - -enum class MessageType -{ - None = 0, - Progress, - Log -}; - -struct Message -{ - MessageType type = MessageType::None; - Progress progress = Progress::None; - LogLevels logLevel = LogLevels::Info; - std::string log; - - static Message fromProgress(Progress p) - { - return {MessageType::Progress, p, LogLevels::Info, ""}; - } - - static Message fromLog(LogLevels level, std::string log) - { - return {MessageType::Log, Progress::None, level, std::move(log)}; - } -}; - -inline Message parseMessage(const std::string_view& line) -{ - static std::regex e(R"(^\[([a-z]+)\] (.+)$)"); - - std::match_results m; - if (!std::regex_match(line.begin(), line.end(), m, e)) { - return {}; - } - - const auto type = m[1]; - - if (type == "progress") { - try { - const auto p = std::stoi(m[2]); - return Message::fromProgress(static_cast(p)); - } catch (std::exception&) { - return {}; - } - } else { - return Message::fromLog(logLevelFromString(type), m[2]); - } -} - -} // namespace lootcli - -#endif // MODORGANIZER_LOOTCLI_INCLUDED diff --git a/libs/lootcli/src/CMakeLists.txt b/libs/lootcli/src/CMakeLists.txt deleted file mode 100644 index fdc292e..0000000 --- a/libs/lootcli/src/CMakeLists.txt +++ /dev/null @@ -1,138 +0,0 @@ -cmake_minimum_required(VERSION 3.16) - -# Qt is not required, this allows us to skip building the executable and only create -# the single-header interface when using this as a VCPKG dependency - -find_package(Qt6 CONFIG COMPONENTS Core) - -if (Qt6_FOUND) - -message(STATUS "Qt6 found, building lootcli executable") - -find_package(tomlplusplus CONFIG REQUIRED) -find_package(Boost REQUIRED CONFIG COMPONENTS locale) - -# Try to find libloot via cmake config, then fall back to pkg-config -find_package(libloot CONFIG QUIET) -if(NOT libloot_FOUND) - # On Linux with AUR package, libloot installs a .pc file - find_package(PkgConfig) - if(PkgConfig_FOUND) - pkg_check_modules(LIBLOOT REQUIRED IMPORTED_TARGET libloot) - endif() -endif() - -if(TARGET libloot::loot) - # avoid CMake error/warning - set_target_properties(libloot::loot PROPERTIES - MAP_IMPORTED_CONFIG_RELEASE RelWithDebInfo - MAP_IMPORTED_CONFIG_MINSIZEREL RelWithDebInfo - ) -endif() - -if(WIN32) - add_executable(lootcli WIN32) - set_target_properties(lootcli PROPERTIES - CXX_STANDARD 20 - WIN32_EXECUTABLE TRUE) -else() - add_executable(lootcli) - set_target_properties(lootcli PROPERTIES - CXX_STANDARD 20) -endif() - -set(LOOTCLI_SOURCES - game_settings.cpp - game_settings.h - lootthread.cpp - lootthread.h - main.cpp - pch.h - version.h - ${CMAKE_CURRENT_SOURCE_DIR}/../include/lootcli/lootcli.h -) - -if(WIN32) - list(APPEND LOOTCLI_SOURCES version.rc) -endif() - -target_sources(lootcli PRIVATE ${LOOTCLI_SOURCES}) - -if(WIN32) - target_compile_definitions(lootcli - PRIVATE - _UNICODE UNICODE - _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING) -endif() - -target_include_directories(lootcli PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include) -target_precompile_headers(lootcli PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/pch.h) - -# Link libraries -if(TARGET libloot::loot) - target_link_libraries(lootcli PRIVATE libloot::loot) -elseif(TARGET PkgConfig::LIBLOOT) - target_link_libraries(lootcli PRIVATE PkgConfig::LIBLOOT) -else() - # Fallback: try to find the library manually - find_library(LIBLOOT_LIBRARY NAMES loot libloot) - find_path(LIBLOOT_INCLUDE_DIR NAMES loot/api.h) - if(LIBLOOT_LIBRARY AND LIBLOOT_INCLUDE_DIR) - target_link_libraries(lootcli PRIVATE ${LIBLOOT_LIBRARY}) - target_include_directories(lootcli PRIVATE ${LIBLOOT_INCLUDE_DIR}) - else() - message(FATAL_ERROR "libloot not found. Install libloot or set CMAKE_PREFIX_PATH.") - endif() -endif() - -target_link_libraries(lootcli - PRIVATE Boost::headers Boost::locale - tomlplusplus::tomlplusplus Qt6::Core) - -if(NOT WIN32) - find_package(CURL REQUIRED) - target_link_libraries(lootcli PRIVATE CURL::libcurl) -endif() - -if (MSVC) - target_compile_options(lootcli - PRIVATE - "/MP" - "/W4" - "/external:anglebrackets" - "/external:W0" - ) - target_link_options(lootcli - PRIVATE - $<$:/LTCG /INCREMENTAL:NO /OPT:REF /OPT:ICF> - ) - target_compile_definitions(lootcli PRIVATE _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING) - - set_target_properties(lootcli PROPERTIES VS_STARTUP_PROJECT lootcli) -endif() - -if(WIN32) - install(FILES - $ - $ - DESTINATION bin/loot) -else() - install(TARGETS lootcli DESTINATION bin/loot) -endif() - -endif() - -# library to make the header available -add_library(lootcli-header INTERFACE) -target_sources(lootcli-header INTERFACE - FILE_SET HEADERS - BASE_DIRS ${CMAKE_CURRENT_LIST_DIR}/../include - FILES ${CMAKE_CURRENT_LIST_DIR}/../include/lootcli/lootcli.h) -add_library(mo2::lootcli-header ALIAS lootcli-header) - -install(TARGETS lootcli-header EXPORT lootcliHeaderTargets FILE_SET HEADERS) -install(EXPORT lootcliHeaderTargets - FILE mo2-lootcli-header-targets.cmake - NAMESPACE mo2:: - DESTINATION lib/cmake/mo2-lootcli-header -) diff --git a/libs/lootcli/src/game_settings.cpp b/libs/lootcli/src/game_settings.cpp deleted file mode 100644 index 00d74c0..0000000 --- a/libs/lootcli/src/game_settings.cpp +++ /dev/null @@ -1,435 +0,0 @@ -#include "game_settings.h" -#include - -namespace fs = std::filesystem; -namespace loot -{ -static constexpr float MORROWIND_MINIMUM_HEADER_VERSION = 1.2f; -static constexpr float OBLIVION_MINIMUM_HEADER_VERSION = 0.8f; -static constexpr float SKYRIM_FO3_MINIMUM_HEADER_VERSION = 0.94f; -static constexpr float SKYRIM_SE_MINIMUM_HEADER_VERSION = 1.7f; -static constexpr float FONV_MINIMUM_HEADER_VERSION = 1.32f; -static constexpr float FO4_MINIMUM_HEADER_VERSION = 0.95f; -static constexpr float STARFIELD_MINIMUM_HEADER_VERSION = 0.96f; - -std::filesystem::path GetOpenMWDataPath(const std::filesystem::path& gamePath) -{ -#ifndef _WIN32 - if (gamePath == "/usr/games") { - // Ubuntu, Debian - return "/usr/share/games/openmw/resources/vfs"; - } else if (gamePath == "/run/host/usr/games") { - // Ubuntu, Debian from inside a Flatpak sandbox - return "/run/host/usr/share/games/openmw/resources/vfs"; - } else if (gamePath == "/usr/bin") { - const auto path = "/usr/share/games/openmw/resources/vfs"; - if (std::filesystem::exists(path)) { - // Arch - return path; - } - - // OpenSUSE - return "/usr/share/openmw/resources/vfs"; - } else if (gamePath == "/run/host/usr/bin") { - const auto path = "/run/host/usr/share/games/openmw/resources/vfs"; - if (std::filesystem::exists(path)) { - // Arch from inside a Flatpak sandbox - return path; - } - - // OpenSUSE from inside a Flatpak sandbox - return "/run/host/usr/share/openmw/resources/vfs"; - } else if (boost::ends_with(gamePath.u8string(), - "/app/org.openmw.OpenMW/current/active/files/bin")) { - // Flatpak - return gamePath / "../share/games/openmw/resources/vfs"; - } -#endif - return gamePath / "resources" / "vfs"; -} - -GameType GetGameType(const GameId gameId) -{ - switch (gameId) { - case GameId::tes3: - case GameId::openmw: - return GameType::tes3; - case GameId::tes4: - case GameId::nehrim: - return GameType::tes4; - case GameId::tes5: - case GameId::enderal: - return GameType::tes5; - case GameId::tes5se: - case GameId::enderalse: - return GameType::tes5se; - case GameId::tes5vr: - return GameType::tes5vr; - case GameId::fo3: - return GameType::fo3; - case GameId::fonv: - return GameType::fonv; - case GameId::fo4: - return GameType::fo4; - case GameId::fo4vr: - return GameType::fo4vr; - case GameId::starfield: - return GameType::starfield; - case GameId::oblivionRemastered: - return GameType::oblivionRemastered; - default: - throw std::logic_error("Unrecognised game ID"); - } -} - -float GetMinimumHeaderVersion(const GameId gameId) -{ - switch (gameId) { - case GameId::tes3: - case GameId::openmw: - return MORROWIND_MINIMUM_HEADER_VERSION; - case GameId::tes4: - case GameId::nehrim: - case GameId::oblivionRemastered: - return OBLIVION_MINIMUM_HEADER_VERSION; - case GameId::tes5: - case GameId::enderal: - return SKYRIM_FO3_MINIMUM_HEADER_VERSION; - case GameId::tes5se: - case GameId::tes5vr: - case GameId::enderalse: - return SKYRIM_SE_MINIMUM_HEADER_VERSION; - case GameId::fo3: - return SKYRIM_FO3_MINIMUM_HEADER_VERSION; - case GameId::fonv: - return FONV_MINIMUM_HEADER_VERSION; - case GameId::fo4: - case GameId::fo4vr: - return FO4_MINIMUM_HEADER_VERSION; - case GameId::starfield: - return STARFIELD_MINIMUM_HEADER_VERSION; - default: - throw std::logic_error("Unrecognised game ID"); - } -} - -std::filesystem::path GetDataPath(const GameId gameId, - const std::filesystem::path& gamePath) -{ - switch (gameId) { - case GameId::tes3: - return gamePath / "Data Files"; - case GameId::tes4: - case GameId::nehrim: - case GameId::tes5: - case GameId::enderal: - case GameId::tes5se: - case GameId::enderalse: - case GameId::tes5vr: - case GameId::fo3: - case GameId::fonv: - case GameId::fo4: - case GameId::fo4vr: - case GameId::starfield: - return gamePath / "Data"; - case GameId::openmw: - return GetOpenMWDataPath(gamePath); - case GameId::oblivionRemastered: - return gamePath / "OblivionRemastered" / "Content" / "Dev" / "ObvData" / "Data"; - default: - throw std::logic_error("Unrecognised game ID"); - } -} - -std::string ToString(const GameId gameId) -{ - switch (gameId) { - case GameId::tes3: - return "Morrowind"; - case GameId::tes4: - return "Oblivion"; - case GameId::nehrim: - return "Nehrim"; - case GameId::tes5: - return "Skyrim"; - case GameId::enderal: - return "Enderal"; - case GameId::tes5se: - return "Skyrim Special Edition"; - case GameId::enderalse: - return "Enderal Special Edition"; - case GameId::tes5vr: - return "Skyrim VR"; - case GameId::fo3: - return "Fallout3"; - case GameId::fonv: - return "FalloutNV"; - case GameId::fo4: - return "Fallout4"; - case GameId::fo4vr: - return "Fallout4VR"; - case GameId::starfield: - return "Starfield"; - case GameId::openmw: - return "OpenMW"; - case GameId::oblivionRemastered: - return "Oblivion Remastered"; - default: - throw std::logic_error("Unrecognised game ID"); - } -} - -bool SupportsLightPlugins(const GameType gameType) -{ - return gameType == GameType::tes5se || gameType == GameType::tes5vr || - gameType == GameType::fo4 || gameType == GameType::fo4vr; -} - -std::string GetMasterFilename(const GameId gameId) -{ - switch (gameId) { - case GameId::tes3: - return "Morrowind.esm"; - case GameId::tes4: - case GameId::oblivionRemastered: - return "Oblivion.esm"; - case GameId::nehrim: - return "Nehrim.esm"; - case GameId::tes5: - case GameId::tes5se: - case GameId::tes5vr: - case GameId::enderal: - case GameId::enderalse: - return "Skyrim.esm"; - case GameId::fo3: - return "Fallout3.esm"; - case GameId::fonv: - return "FalloutNV.esm"; - case GameId::fo4: - case GameId::fo4vr: - return "Fallout4.esm"; - case GameId::starfield: - return "Starfield.esm"; - case GameId::openmw: - // This isn't actually a master file, but it's hardcoded to load first, - // and the value is only used to check the game is installed and to - // skip fully loading this file before sorting - and omwscripts files - // don't get loaded anyway. - return "builtin.omwscripts"; - default: - throw std::logic_error("Unrecognised game ID"); - } -} - -std::string GetGameName(const GameId gameId) -{ - switch (gameId) { - case GameId::tes3: - return "TES III: Morrowind"; - case GameId::tes4: - return "TES IV: Oblivion"; - case GameId::nehrim: - return "Nehrim - At Fate's Edge"; - case GameId::tes5: - return "TES V: Skyrim"; - case GameId::enderal: - return "Enderal: Forgotten Stories"; - case GameId::tes5se: - return "TES V: Skyrim Special Edition"; - case GameId::enderalse: - return "Enderal: Forgotten Stories (Special Edition)"; - case GameId::tes5vr: - return "TES V: Skyrim VR"; - case GameId::fo3: - return "Fallout 3"; - case GameId::fonv: - return "Fallout: New Vegas"; - case GameId::fo4: - return "Fallout 4"; - case GameId::fo4vr: - return "Fallout 4 VR"; - case GameId::starfield: - return "Starfield"; - case GameId::openmw: - return "OpenMW"; - case GameId::oblivionRemastered: - return "TES IV: Oblivion Remastered"; - default: - throw std::logic_error("Unrecognised game ID"); - } -} - -std::string GetDefaultMasterlistRepositoryName(const GameId gameId) -{ - switch (gameId) { - case GameId::tes3: - return "morrowind"; - case GameId::tes4: - case GameId::nehrim: - case GameId::oblivionRemastered: - return "oblivion"; - case GameId::tes5: - return "skyrim"; - case GameId::enderal: - case GameId::enderalse: - return "enderal"; - case GameId::tes5se: - return "skyrimse"; - case GameId::tes5vr: - return "skyrimvr"; - case GameId::fo3: - return "fallout3"; - case GameId::fonv: - return "falloutnv"; - case GameId::fo4: - return "fallout4"; - case GameId::fo4vr: - return "fallout4vr"; - case GameId::starfield: - return "starfield"; - default: - throw std::logic_error("Unrecognised game type"); - } -} - -std::string GetDefaultMasterlistUrl(const std::string& repositoryName) -{ - return std::string("https://raw.githubusercontent.com/loot/") + repositoryName + "/" + - DEFAULT_MASTERLIST_BRANCH + "/masterlist.yaml"; -} - -std::string GetDefaultMasterlistUrl(const GameId gameId) -{ - const auto repoName = GetDefaultMasterlistRepositoryName(gameId); - - return GetDefaultMasterlistUrl(repoName); -} - -GameSettings::GameSettings(const GameId gameId, const std::string& lootFolder) - : id_(gameId), type_(GetGameType(gameId)), name_(GetGameName(gameId)), - masterFile_(GetMasterFilename(gameId)), - minimumHeaderVersion_(GetMinimumHeaderVersion(gameId)), - lootFolderName_(lootFolder), masterlistSource_(GetDefaultMasterlistUrl(gameId)) -{} - -bool GameSettings::operator==(const GameSettings& rhs) const -{ - return name_ == rhs.Name() || lootFolderName_ == rhs.FolderName(); -} - -GameId GameSettings::Id() const -{ - return id_; -} - -GameType GameSettings::Type() const -{ - return type_; -} - -std::string GameSettings::Name() const -{ - return name_; -} - -std::string GameSettings::FolderName() const -{ - return lootFolderName_; -} - -std::string GameSettings::Master() const -{ - return masterFile_; -} - -float GameSettings::MinimumHeaderVersion() const -{ - return minimumHeaderVersion_; -} - -std::string GameSettings::MasterlistSource() const -{ - return masterlistSource_; -} - -std::filesystem::path GameSettings::GamePath() const -{ - return gamePath_; -} - -std::filesystem::path GameSettings::GameLocalPath() const -{ - return gameLocalPath_; -} - -std::filesystem::path GameSettings::DataPath() const -{ - return GetDataPath(id_, gamePath_); -} - -GameSettings& GameSettings::SetName(const std::string& name) -{ - name_ = name; - return *this; -} - -GameSettings& GameSettings::SetMaster(const std::string& masterFile) -{ - masterFile_ = masterFile; - return *this; -} - -GameSettings& GameSettings::SetMinimumHeaderVersion(float mininumHeaderVersion) -{ - minimumHeaderVersion_ = mininumHeaderVersion; - return *this; -} - -GameSettings& GameSettings::SetMasterlistSource(const std::string& source) -{ - masterlistSource_ = source; - return *this; -} - -GameSettings& GameSettings::SetGamePath(const std::filesystem::path& path) -{ - gamePath_ = path; - return *this; -} - -GameSettings& GameSettings::SetGameLocalPath(const std::filesystem::path& path) -{ - gameLocalPath_ = path; - return *this; -} - -GameSettings& GameSettings::SetGameLocalFolder(const std::string& folderName) -{ - fs::path appData; -#ifdef _WIN32 - TCHAR path[MAX_PATH]; - - HRESULT res = ::SHGetFolderPath(nullptr, CSIDL_LOCAL_APPDATA, nullptr, - SHGFP_TYPE_CURRENT, path); - if (res == S_OK) { - appData = fs::path(path); - } else { - appData = fs::path(""); - } -#else - const char* xdgData = std::getenv("XDG_DATA_HOME"); - if (xdgData && xdgData[0] != '\0') { - appData = fs::path(xdgData); - } else { - const char* home = std::getenv("HOME"); - if (home && home[0] != '\0') { - appData = fs::path(home) / ".local" / "share"; - } else { - appData = fs::path(""); - } - } -#endif - gameLocalPath_ = appData / fs::path(folderName); - return *this; -} -} // namespace loot diff --git a/libs/lootcli/src/game_settings.h b/libs/lootcli/src/game_settings.h deleted file mode 100644 index bc775c9..0000000 --- a/libs/lootcli/src/game_settings.h +++ /dev/null @@ -1,101 +0,0 @@ -#ifndef LOOT_GUI_STATE_GAME_GAME_SETTINGS -#define LOOT_GUI_STATE_GAME_GAME_SETTINGS - -#include -#include -#include -#include -#include - -#include "loot/enum/game_type.h" - -namespace loot -{ -constexpr inline std::string_view NEHRIM_STEAM_REGISTRY_KEY = - "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Steam App " - "1014940\\InstallLocation"; -static constexpr const char* DEFAULT_MASTERLIST_BRANCH = "v0.26"; - -enum struct GameId : uint8_t -{ - tes3, - tes4, - nehrim, - tes5, - enderal, - tes5se, - enderalse, - tes5vr, - fo3, - fonv, - fo4, - fo4vr, - starfield, - openmw, - oblivionRemastered -}; - -GameType GetGameType(const GameId gameId); - -float GetMinimumHeaderVersion(const GameId gameId); - -std::filesystem::path GetDataPath(const GameId gamiId, - const std::filesystem::path& gamePath); - -std::string ToString(const GameId gameId); - -bool SupportsLightPlugins(const GameType gameType); - -std::string GetMasterFilename(const GameId gameId); - -std::string GetGameName(const GameId gameId); - -std::string GetDefaultMasterlistRepositoryName(GameId gameId); - -std::string GetDefaultMasterlistUrl(const std::string& repositoryName); -std::string GetDefaultMasterlistUrl(const GameId gameId); - -class GameSettings -{ -public: - GameSettings() = default; - explicit GameSettings(const GameId gameId, const std::string& lootFolder = ""); - - bool operator==(const GameSettings& rhs) const; // Compares names and folder names. - - GameId Id() const; - GameType Type() const; - std::string Name() const; // Returns the game's name, eg. "TES IV: Oblivion". - std::string FolderName() const; - std::string Master() const; - float MinimumHeaderVersion() const; - std::string MasterlistSource() const; - std::filesystem::path GamePath() const; - std::filesystem::path GameLocalPath() const; - std::filesystem::path DataPath() const; - - GameSettings& SetName(const std::string& name); - GameSettings& SetMaster(const std::string& masterFile); - GameSettings& SetMinimumHeaderVersion(float minimumHeaderVersion); - GameSettings& SetMasterlistSource(const std::string& source); - GameSettings& SetGamePath(const std::filesystem::path& path); - GameSettings& SetGameLocalPath(const std::filesystem::path& GameLocalPath); - GameSettings& SetGameLocalFolder(const std::string& folderName); - -private: - GameId id_{GameId::tes4}; - GameType type_{GameType::tes4}; - std::string name_; - std::string masterFile_; - float minimumHeaderVersion_{0.0f}; - - std::string lootFolderName_; - - std::string masterlistSource_; - - std::filesystem::path gamePath_; // Path to the game's folder. - std::filesystem::path gameLocalPath_; -}; -} // namespace loot - -#endif diff --git a/libs/lootcli/src/lootthread.cpp b/libs/lootcli/src/lootthread.cpp deleted file mode 100644 index 9f7f60c..0000000 --- a/libs/lootcli/src/lootthread.cpp +++ /dev/null @@ -1,1197 +0,0 @@ -#ifdef _WIN32 -#pragma comment(lib, "winhttp.lib") -#endif - -#include "lootthread.h" -#include "game_settings.h" -#include "version.h" -#include -#include - -#ifdef _WIN32 -#include -#include -#include -#else -#include -#include -#endif - -// using namespace loot; -namespace fs = std::filesystem; - -using std::lock_guard; -using std::recursive_mutex; - -namespace lootcli -{ -static const std::set - oldDefaultBranches({"master", "v0.7", "v0.8", "v0.10", "v0.13", "v0.14", "v0.15", - "v0.17", "v0.18", "v0.21"}); -static const std::regex GITHUB_REPO_URL_REGEX = - std::regex(R"(^https://github\.com/([^/]+)/([^/]+?)(?:\.git)?/?$)", - std::regex::ECMAScript | std::regex::icase); - -std::string toString(loot::MessageType type) -{ - switch (type) { - case loot::MessageType::say: - return "info"; - case loot::MessageType::warn: - return "warn"; - case loot::MessageType::error: - return "error"; - default: - return "unknown"; - } -} - -LOOTWorker::LOOTWorker() - : m_GameId(loot::GameId::tes5), m_GameName("Skyrim"), - m_LogLevel(loot::LogLevel::info) -{} - -std::string ToLower(std::string text) -{ - std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) { - return static_cast(std::tolower(c)); - }); - - return text; -} - -void LOOTWorker::setGame(const std::string& gameName) -{ - static std::map gameMap = { - {"morrowind", loot::GameId::tes3}, - {"oblivion", loot::GameId::tes4}, - {"fallout3", loot::GameId::fo3}, - {"fallout4", loot::GameId::fo4}, - {"fallout4vr", loot::GameId::fo4vr}, - {"falloutnv", loot::GameId::fonv}, - {"skyrim", loot::GameId::tes5}, - {"skyrimse", loot::GameId::tes5se}, - {"skyrimvr", loot::GameId::tes5vr}, - {"nehrim", loot::GameId::nehrim}, - {"enderal", loot::GameId::enderal}, - {"enderalse", loot::GameId::enderalse}, - {"starfield", loot::GameId::starfield}, - {"oblivionremastered", loot::GameId::oblivionRemastered}}; - - auto iter = gameMap.find(ToLower(gameName)); - - if (iter != gameMap.end()) { - m_GameId = iter->second; - m_GameName = loot::ToString(m_GameId); - } else { - throw std::runtime_error("invalid game name \"" + gameName + "\""); - } -} - -void LOOTWorker::setGamePath(const std::string& gamePath) -{ - m_GamePath = gamePath; -} - -void LOOTWorker::setOutput(const std::string& outputPath) -{ - m_OutputPath = outputPath; -} - -void LOOTWorker::setUpdateMasterlist(bool update) -{ - m_UpdateMasterlist = update; -} - -void LOOTWorker::setPluginListPath(const std::string& pluginListPath) -{ - m_PluginListPath = pluginListPath; -} - -void LOOTWorker::setLanguageCode(const std::string& languageCode) -{ - m_Language = languageCode; -} - -void LOOTWorker::setLogLevel(loot::LogLevel level) -{ - m_LogLevel = level; -} - -fs::path GetLOOTAppData() -{ -#ifdef _WIN32 - TCHAR path[MAX_PATH]; - - HRESULT res = ::SHGetFolderPath(nullptr, CSIDL_LOCAL_APPDATA, nullptr, - SHGFP_TYPE_CURRENT, path); - - if (res == S_OK) { - return fs::path(path) / "LOOT"; - } else { - return fs::path(""); - } -#else - // On Linux, use XDG_DATA_HOME or fallback to ~/.local/share - const char* xdgData = std::getenv("XDG_DATA_HOME"); - if (xdgData && xdgData[0] != '\0') { - return fs::path(xdgData) / "LOOT"; - } - const char* home = std::getenv("HOME"); - if (home && home[0] != '\0') { - return fs::path(home) / ".local" / "share" / "LOOT"; - } - return fs::path(""); -#endif -} - -fs::path LOOTWorker::gamePath() const -{ - return GetLOOTAppData() / "games" / m_GameSettings.FolderName(); -} - -fs::path LOOTWorker::masterlistPath() const -{ - return gamePath() / "masterlist.yaml"; -} - -fs::path LOOTWorker::userlistPath() const -{ - return gamePath() / "userlist.yaml"; -} -fs::path LOOTWorker::settingsPath() const -{ - return GetLOOTAppData() / "settings.toml"; -} - -fs::path LOOTWorker::l10nPath() const -{ - return GetLOOTAppData() / "resources" / "l10n"; -} - -fs::path LOOTWorker::dataPath() const -{ - return m_GameSettings.DataPath(); -} - -void LOOTWorker::getSettings(const fs::path& file) -{ - lock_guard guard(mutex_); - // Don't use cpptoml::parse_file() as it just uses a std stream, - // which don't support UTF-8 paths on Windows. - std::ifstream in(file); - if (!in.is_open()) - throw std::runtime_error(file.string() + " could not be opened for parsing"); - - const auto settings = toml::parse(in, file.string()); - const auto games = settings["games"]; - if (games.is_array_of_tables()) { - for (const auto& game : *games.as_array()) { - try { - if (!game.is_table()) { - throw std::runtime_error("games array element is not a table"); - } - auto gameTable = *game.as_table(); - - using loot::GameId; - using loot::GameSettings; - - auto id = gameTable["gameId"].value(); - if (!id) { - throw std::runtime_error( - "'gameId' and 'type' keys both missing from game settings table"); - } - const auto gameType = *id; - GameId gameId; - - if (gameType == "Morrowind") { - gameId = GameId::tes3; - } else if (gameType == "Oblivion") { - // The Oblivion game type is shared between Oblivon and Nehrim. - gameId = IsNehrim(gameTable) ? GameId::nehrim : GameId::tes4; - } else if (gameType == "Skyrim") { - // The Skyrim game type is shared between Skyrim and Enderal. - gameId = IsEnderal(gameTable) ? GameId::enderal : GameId::tes5; - } else if (gameType == "SkyrimSE" || gameType == "Skyrim Special Edition") { - // The Skyrim SE game type is shared between Skyrim SE and Enderal SE. - gameId = IsEnderalSE(gameTable) ? GameId::enderalse : GameId::tes5se; - } else if (gameType == "Skyrim VR") { - gameId = GameId::tes5vr; - } else if (gameType == "Fallout3") { - gameId = GameId::fo3; - } else if (gameType == "FalloutNV") { - gameId = GameId::fonv; - } else if (gameType == "Fallout4") { - gameId = GameId::fo4; - } else if (gameType == "Fallout4VR") { - gameId = GameId::fo4vr; - } else if (gameType == "Starfield") { - gameId = GameId::starfield; - } else if (gameType == "OpenMW") { - gameId = GameId::openmw; - } else if (gameType == "Oblivion Remastered") { - gameId = GameId::oblivionRemastered; - } else { - throw std::runtime_error( - "invalid value for 'type' key in game settings table"); - } - - auto folder = gameTable["folder"].value(); - if (!folder) { - throw std::runtime_error("'folder' key missing from game settings table"); - } - - const auto type = gameTable["type"].value(); - - // SkyrimSE was a previous serialised value for GameType::tes5se, - // and the game folder name LOOT created for that game type. - if (type && *type == "SkyrimSE" && *folder == *type) { - folder = "Skyrim Special Edition"; - } - - GameSettings newSettings(gameId, folder.value()); - - if (newSettings.Type() == m_GameSettings.Type()) { - - auto name = gameTable["name"].value(); - if (name) { - newSettings.SetName(*name); - } - - auto master = gameTable["master"].value(); - if (master) { - newSettings.SetMaster(*master); - } - - const auto minimumHeaderVersion = - gameTable["minimumHeaderVersion"].value(); - if (minimumHeaderVersion) { - newSettings.SetMinimumHeaderVersion((float)*minimumHeaderVersion); - } - - auto source = gameTable["masterlistSource"].value(); - if (source) { - newSettings.SetMasterlistSource(migrateMasterlistSource(*source)); - } else { - auto url = gameTable["repo"].value(); - auto branch = gameTable["branch"].value(); - auto migratedSource = - migrateMasterlistRepoSettings(newSettings.Id(), *url, *branch); - if (migratedSource.has_value()) { - newSettings.SetMasterlistSource(migratedSource.value()); - } - } - - auto path = gameTable["path"].value(); - if (path) { - newSettings.SetGamePath(std::filesystem::path(*path)); - } - - auto localPath = gameTable["local_path"].value(); - auto localFolder = gameTable["local_folder"].value(); - if (localPath && localFolder) { - throw std::runtime_error( - "Game settings have local_path and local_folder set, use only one."); - } else if (localPath) { - newSettings.SetGameLocalPath(std::filesystem::path(*localPath)); - } else if (localFolder) { - newSettings.SetGameLocalFolder(*localFolder); - } - - m_GameSettings = newSettings; - break; - } - } catch (...) { - // Skip invalid games. - } - } - } - - if (m_Language.empty()) { - m_Language = settings["language"].value_or(loot::MessageContent::DEFAULT_LANGUAGE); - } -} - -std::optional LOOTWorker::GetLocalFolder(const toml::table& table) -{ - const auto localPath = table["local_path"].value(); - const auto localFolder = table["local_folder"].value(); - - if (localFolder.has_value()) { - return localFolder; - } - - if (localPath.has_value()) { - return std::filesystem::path(*localPath).filename().string(); - } - - return std::nullopt; -} - -bool LOOTWorker::IsNehrim(const toml::table& table) -{ - const auto installPath = table["path"].value(); - - if (installPath.has_value() && !installPath.value().empty()) { - const auto path = std::filesystem::path(installPath.value()); - if (std::filesystem::exists(path)) { - return std::filesystem::exists(path / "NehrimLauncher.exe"); - } - } - - // Fall back to using heuristics based on the existing settings. - // Return true if any of these heuristics return a positive match. - const auto gameName = table["name"].value(); - const auto masterFilename = table["master"].value(); - const auto isBaseGameInstance = table["isBaseGameInstance"].value(); - const auto folder = table["folder"].value(); - - return - // Nehrim uses a different main master file from Oblivion. - (masterFilename.has_value() && - masterFilename.value() == loot::GetMasterFilename(loot::GameId::nehrim)) || - // Game name probably includes "nehrim". - (gameName.has_value() && boost::icontains(gameName.value(), "nehrim")) || - // LOOT folder name probably includes "nehrim". - (folder.has_value() && boost::icontains(folder.value(), "nehrim")) || - // Between 0.18.1 and 0.19.0 inclusive, LOOT had an isBaseGameInstance - // game setting that was false for Nehrim, Enderal and Enderal SE. - (isBaseGameInstance.has_value() && !isBaseGameInstance.value()); -} - -bool LOOTWorker::IsEnderal(const toml::table& table, - const std::string& expectedLocalFolder) -{ - const auto installPath = table["path"].value(); - - if (installPath.has_value() && !installPath.value().empty()) { - const auto path = std::filesystem::path(installPath.value()); - if (std::filesystem::exists(path)) { - return std::filesystem::exists(path / "Enderal Launcher.exe"); - } - } - - // Fall back to using heuristics based on the existing settings. - // Return true if any of these heuristics return a positive match. - const auto gameName = table["name"].value(); - const auto isBaseGameInstance = table["isBaseGameInstance"].value(); - const auto localFolder = GetLocalFolder(table); - const auto folder = table["folder"].value(); - - return - // Enderal and Enderal SE use different local folders than their base - // games. - (localFolder.has_value() && localFolder.value() == expectedLocalFolder) || - // Game name probably includes "enderal". - (gameName.has_value() && boost::icontains(gameName.value(), "enderal")) || - // LOOT folder name probably includes "enderal". - (folder.has_value() && boost::icontains(folder.value(), "enderal")) || - // Between 0.18.1 and 0.19.0 inclusive, LOOT had an isBaseGameInstance - // game setting that was false for Nehrim, Enderal and Enderal SE. - (isBaseGameInstance.has_value() && !isBaseGameInstance.value()); -} - -bool LOOTWorker::IsEnderal(const toml::table& table) -{ - return IsEnderal(table, "enderal"); -} - -bool LOOTWorker::IsEnderalSE(const toml::table& table) -{ - return IsEnderal(table, "Enderal Special Edition"); -} - -std::string LOOTWorker::getOldDefaultRepoUrl(loot::GameId GameId) -{ - switch (GameId) { - case loot::GameId::tes3: - return "https://github.com/loot/morrowind.git"; - case loot::GameId::tes4: - return "https://github.com/loot/oblivion.git"; - case loot::GameId::tes5: - return "https://github.com/loot/skyrim.git"; - case loot::GameId::tes5se: - return "https://github.com/loot/skyrimse.git"; - case loot::GameId::tes5vr: - return "https://github.com/loot/skyrimvr.git"; - case loot::GameId::fo3: - return "https://github.com/loot/fallout3.git"; - case loot::GameId::fonv: - return "https://github.com/loot/falloutnv.git"; - case loot::GameId::fo4: - return "https://github.com/loot/fallout4.git"; - case loot::GameId::fo4vr: - return "https://github.com/loot/fallout4vr.git"; - default: - throw std::runtime_error( - "Unrecognised game type: " + - std::to_string(static_cast>(GameId))); - } -} - -bool LOOTWorker::isLocalPath(const std::string& location, const std::string& filename) -{ - if (boost::starts_with(location, "http://") || - boost::starts_with(location, "https://")) { - return false; - } - - // Could be a local path. Only return true if it points to a non-bare - // Git repository that currently has the given branch checked out and - // the given filename exists in the repo root. - auto locationPath = std::filesystem::path(location); - - auto filePath = locationPath / std::filesystem::path(filename); - - if (!std::filesystem::is_regular_file(filePath)) { - return false; - } - - auto headFilePath = locationPath / ".git" / "HEAD"; - - return std::filesystem::is_regular_file(headFilePath); -} - -bool LOOTWorker::isBranchCheckedOut(const std::filesystem::path& localGitRepo, - const std::string& branch) -{ - auto headFilePath = localGitRepo / ".git" / "HEAD"; - - std::ifstream in(headFilePath); - if (!in.is_open()) { - return false; - } - - std::string line; - std::getline(in, line); - in.close(); - - return line == "ref: refs/heads/" + branch; -} - -std::optional -LOOTWorker::migrateMasterlistRepoSettings(loot::GameId GameId, std::string url, - std::string branch) -{ - - if (oldDefaultBranches.count(branch) == 1) { - // Update to the latest masterlist branch. - log(loot::LogLevel::info, "Updating masterlist repository branch from " + branch + - " to " + loot::DEFAULT_MASTERLIST_BRANCH); - branch = loot::DEFAULT_MASTERLIST_BRANCH; - } - - if (GameId == loot::GameId::tes5vr && url == "https://github.com/loot/skyrimse.git") { - // Switch to the VR-specific repository (introduced for LOOT v0.17.0). - auto newUrl = "https://github.com/loot/skyrimvr.git"; - log(loot::LogLevel::info, - "Updating masterlist repository URL from" + url + " to " + newUrl); - url = newUrl; - } - - if (GameId == loot::GameId::fo4vr && url == "https://github.com/loot/fallout4.git") { - // Switch to the VR-specific repository (introduced for LOOT v0.17.0). - auto newUrl = "https://github.com/loot/fallout4vr.git"; - log(loot::LogLevel::info, - "Updating masterlist repository URL from " + url + " to " + newUrl); - url = newUrl; - } - - auto filename = "masterlist.yaml"; - if (isLocalPath(url, filename)) { - auto localRepoPath = std::filesystem::path(url); - if (!isBranchCheckedOut(localRepoPath, branch)) { - log(loot::LogLevel::warning, - "The URL " + url + - " is a local Git repository path but the configured branch " + branch + - " is not checked out. LOOT will use the path as the masterlist " - "source, but there may be unexpected differences in the loaded " - "metadata if the " + - branch + - " branch is not manually checked out before the " - "next time the masterlist is updated."); - } - - return (localRepoPath / filename).string(); - } - - std::smatch regexMatches; - std::regex_match(url, regexMatches, GITHUB_REPO_URL_REGEX); - if (regexMatches.size() != 3) { - log(loot::LogLevel::warning, - "Cannot migrate masterlist repository settings as the URL does not " - "point to a repository on GitHub."); - return std::nullopt; - } - - auto githubOwner = regexMatches.str(1); - auto githubRepo = regexMatches.str(2); - - return "https://raw.githubusercontent.com/" + githubOwner + "/" + githubRepo + "/" + - branch + "/masterlist.yaml"; -} - -std::string LOOTWorker::migrateMasterlistSource(const std::string& source) -{ - static const std::vector officialMasterlistRepos = { - "morrowind", "oblivion", "skyrim", "skyrimse", "skyrimvr", - "fallout3", "falloutnv", "fallout4", "fallout4vr", "enderal"}; - - for (const auto& repo : officialMasterlistRepos) { - for (const auto& branch : oldDefaultBranches) { - const auto url = "https://raw.githubusercontent.com/loot/" + repo + "/" + branch + - "/masterlist.yaml"; - - if (source == url) { - const auto newSource = loot::GetDefaultMasterlistUrl(repo); - - log(loot::LogLevel::info, - "Migrating masterlist source from " + source + " to " + newSource); - - return newSource; - } - } - } - - return source; -} - -#ifdef _WIN32 -DWORD LOOTWorker::GetFile(const WCHAR* szUrl, // Full URL - const CHAR* szFileName) // Local file name -{ - BYTE szTemp[25]; - DWORD dwSize = 0; - DWORD dwDownloaded = 0; - LPSTR pszOutBuffer; - BOOL bResults = FALSE; - HINTERNET hSession = NULL, hConnect = NULL, hRequest = NULL; - FILE* pFile; - std::wstring_convert> converter; - - URL_COMPONENTS urlComp; - DWORD dwUrlLen = 0; - - DWORD result = ERROR_SUCCESS; - - // Initialize the URL_COMPONENTS structure. - ZeroMemory(&urlComp, sizeof(urlComp)); - urlComp.dwStructSize = sizeof(urlComp); - - // Set required component lengths to non-zero - // so that they are cracked. - wchar_t szHostName[MAX_PATH] = L""; - wchar_t szURLPath[MAX_PATH * 4] = L""; - urlComp.lpszHostName = szHostName; - urlComp.lpszUrlPath = szURLPath; - urlComp.dwSchemeLength = (DWORD)-1; - urlComp.dwHostNameLength = (DWORD)-1; - urlComp.dwUrlPathLength = (DWORD)-1; - urlComp.dwExtraInfoLength = (DWORD)-1; - if (WinHttpCrackUrl(szUrl, (DWORD)wcslen(szUrl), 0, &urlComp)) { - // Use WinHttpOpen to obtain a session handle. - hSession = WinHttpOpen(L"lootcli/1.5.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, - WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); - - // Specify an HTTP server. - if (hSession) - hConnect = WinHttpConnect(hSession, szHostName, urlComp.nPort, 0); - - // Create an HTTP request handle. - if (hConnect) - hRequest = - WinHttpOpenRequest(hConnect, L"GET", szURLPath, NULL, WINHTTP_NO_REFERER, - WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE); - - // Send a request. - if (hRequest) - bResults = WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, - WINHTTP_NO_REQUEST_DATA, 0, 0, 0); - - // End the request. - if (bResults) - bResults = WinHttpReceiveResponse(hRequest, NULL); - - // Keep checking for data until there is nothing left. - if (bResults) { - if (!(pFile = fopen(szFileName, "wb"))) { - log(loot::LogLevel::debug, "File open failure"); - result = GetLastError(); - } - do { - // Check for available data. - dwSize = 0; - if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) { - log(loot::LogLevel::debug, "No data"); - result = GetLastError(); - break; - } - - // No more available data. - if (!dwSize) { - log(loot::LogLevel::debug, "No data"); - result = GetLastError(); - break; - } - - // Allocate space for the buffer. - pszOutBuffer = new char[dwSize + 1]; - if (!pszOutBuffer) { - log(loot::LogLevel::debug, "Bad buffer"); - result = GetLastError(); - } - - // Read the Data. - ZeroMemory(pszOutBuffer, dwSize + 1); - - if (!WinHttpReadData(hRequest, (LPVOID)pszOutBuffer, dwSize, &dwDownloaded)) { - log(loot::LogLevel::debug, "Read data failure"); - result = GetLastError(); - } else { - fwrite(pszOutBuffer, sizeof(char), dwSize, pFile); - } - - // Free the memory allocated to the buffer. - delete[] pszOutBuffer; - - // This condition should never be reached since WinHttpQueryDataAvailable - // reported that there are bits to read. - if (!dwDownloaded) - break; - - } while (dwSize > 0); - } else { - log(loot::LogLevel::debug, "Response failure"); - result = GetLastError(); - } - - // Close any open handles. - if (hRequest) - WinHttpCloseHandle(hRequest); - if (hConnect) - WinHttpCloseHandle(hConnect); - if (hSession) - WinHttpCloseHandle(hSession); - fflush(pFile); - fclose(pFile); - } else { - log(loot::LogLevel::debug, "URL parse failure: " + converter.to_bytes(szUrl)); - result = GetLastError(); - } - return result; -} -#else -// Linux implementation using libcurl -static size_t curlWriteCallback(void* contents, size_t size, size_t nmemb, void* userp) -{ - FILE* fp = static_cast(userp); - return fwrite(contents, size, nmemb, fp); -} - -int LOOTWorker::GetFile(const std::string& url, const std::string& fileName) -{ - CURL* curl = curl_easy_init(); - if (!curl) { - log(loot::LogLevel::error, "Failed to initialize curl"); - return 1; - } - - FILE* fp = fopen(fileName.c_str(), "wb"); - if (!fp) { - log(loot::LogLevel::debug, "File open failure: " + fileName); - curl_easy_cleanup(curl); - return 1; - } - - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(curl, CURLOPT_USERAGENT, "lootcli/" LOOTCLI_VERSION_STRING); - - CURLcode res = curl_easy_perform(curl); - - fflush(fp); - fclose(fp); - curl_easy_cleanup(curl); - - if (res != CURLE_OK) { - log(loot::LogLevel::error, - std::string("curl download failed: ") + curl_easy_strerror(res)); - return 1; - } - - return 0; -} -#endif - -std::string escape(const std::string& s) -{ - return boost::replace_all_copy(s, "\"", "\\\""); -} - -int LOOTWorker::run() -{ - m_startTime = std::chrono::high_resolution_clock::now(); - - { - // Do some preliminary locale / UTF-8 support setup here, in case the settings file - // reading requires it. - // Boost.Locale initialisation: Specify location of language dictionaries. - boost::locale::generator gen; - gen.add_messages_path(l10nPath().string()); - gen.add_messages_domain("loot"); - - // Boost.Locale initialisation: Generate and imbue locales. - std::locale::global(gen("en.UTF-8")); - } - - loot::SetLoggingCallback([&](loot::LogLevel level, std::string_view message) { - log(level, message); - }); - - try { - fs::path profile(m_PluginListPath); - profile = profile.parent_path(); - - m_GameSettings = loot::GameSettings(m_GameId, loot::ToString(m_GameId)); - - fs::path settings = settingsPath(); - - if (fs::exists(settings)) - getSettings(settings); - - m_GameSettings.SetGamePath(m_GamePath); - - std::unique_ptr gameHandle = CreateGameHandle( - m_GameSettings.Type(), m_GameSettings.GamePath(), profile.string()); - - if (!GetLOOTAppData().empty()) { - // Make sure that the LOOT game path exists. - auto lootGamePath = gamePath(); - if (!fs::is_directory(lootGamePath)) { - if (fs::exists(lootGamePath)) { - throw std::runtime_error( - "Could not create LOOT folder for game, the path exists but is not " - "a directory"); - } - - std::vector legacyGamePaths{GetLOOTAppData() / - fs::path(m_GameSettings.FolderName())}; - - if (m_GameSettings.Id() == loot::GameId::tes5se) { - // LOOT v0.10.0 used SkyrimSE as its folder name for Skyrim SE, so - // migrate from that if it's present. - legacyGamePaths.insert(legacyGamePaths.begin(), - GetLOOTAppData() / "SkyrimSE"); - } - - for (const auto& legacyGamePath : legacyGamePaths) { - if (fs::is_directory(legacyGamePath)) { - log(loot::LogLevel::info, - "Found a folder for this game in the LOOT data folder, " - "assuming " - "that it's a legacy game folder and moving into the correct " - "subdirectory..."); - - fs::create_directories(lootGamePath.parent_path()); - fs::rename(legacyGamePath, lootGamePath); - break; - } - } - - fs::create_directories(lootGamePath); - } - } - - if (m_Language != loot::MessageContent::DEFAULT_LANGUAGE) { - log(loot::LogLevel::debug, "initialising language settings"); - log(loot::LogLevel::debug, "selected language: " + m_Language); - - // Boost.Locale initialisation: Generate and imbue locales. - boost::locale::generator gen; - std::locale::global(gen(m_Language + ".UTF-8")); - } - - progress(Progress::CheckingMasterlistExistence); - if (!fs::exists(masterlistPath())) { - if (!m_UpdateMasterlist) { - log(loot::LogLevel::error, - "Masterlist not found at: " + masterlistPath().string()); - return 0; // was FALSE on Windows - } - fs::create_directories(masterlistPath().parent_path()); - } - - if (m_UpdateMasterlist) { - progress(Progress::UpdatingMasterlist); - -#ifdef _WIN32 - std::wstring_convert> converter; - std::wstring masterlistSource = - converter.from_bytes(m_GameSettings.MasterlistSource()); - - log(loot::LogLevel::info, "Downloading latest masterlist file from " + - m_GameSettings.MasterlistSource() + " to " + - masterlistPath().string()); - DWORD result = - GetFile(masterlistSource.c_str(), masterlistPath().string().c_str()); - if (result != ERROR_SUCCESS) { - LPVOID lpMsgBuf; - LPVOID lpDisplayBuf; - LPCWSTR lpszFunction = TEXT("GetFile"); - DWORD dw = result; - - FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, dw, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - (LPTSTR)&lpMsgBuf, 0, NULL); - - lpDisplayBuf = - (LPVOID)LocalAlloc(LMEM_ZEROINIT, (lstrlen((LPCTSTR)lpMsgBuf) + - lstrlen((LPCTSTR)lpszFunction) + 40) * - sizeof(TCHAR)); - StringCchPrintf((LPTSTR)lpDisplayBuf, LocalSize(lpDisplayBuf) / sizeof(TCHAR), - TEXT("%s failed with error %d: %s"), lpszFunction, dw, - lpMsgBuf); - - std::wstring errorMessage = (LPTSTR)lpDisplayBuf; - - log(loot::LogLevel::error, - "Error downloading masterlist: " + converter.to_bytes(errorMessage)); - return 0; // was FALSE - } -#else - log(loot::LogLevel::info, "Downloading latest masterlist file from " + - m_GameSettings.MasterlistSource() + " to " + - masterlistPath().string()); - int result = GetFile(m_GameSettings.MasterlistSource(), - masterlistPath().string()); - if (result != 0) { - log(loot::LogLevel::error, "Error downloading masterlist"); - return 0; - } -#endif - } - - progress(Progress::LoadingLists); - - gameHandle->GetDatabase().LoadMasterlist(masterlistPath().string()); - fs::path userlist = userlistPath(); - if (fs::exists(userlist)) - gameHandle->GetDatabase().LoadUserlist(userlist.string()); - - progress(Progress::ReadingPlugins); - gameHandle->LoadCurrentLoadOrderState(); - auto loadOrder = gameHandle->GetLoadOrder(); - std::vector pluginsList; - for (auto plugin : gameHandle->GetLoadOrder()) { - std::filesystem::path pluginPath(plugin); - pluginsList.push_back(pluginPath); - } - gameHandle->LoadPlugins(pluginsList, false); - - progress(Progress::SortingPlugins); - std::vector sortedPlugins = gameHandle->SortPlugins(loadOrder); - - progress(Progress::WritingLoadorder); - - std::ofstream outf(m_PluginListPath); - if (!outf) { - log(loot::LogLevel::error, - "failed to open " + m_PluginListPath + " to rewrite it"); - return 1; - } - outf << "# This file was automatically generated by Mod Organizer." << std::endl; - for (const std::string& plugin : sortedPlugins) { - outf << plugin << std::endl; - } - outf.close(); - - progress(Progress::ParsingLootMessages); - std::ofstream(m_OutputPath) << createJsonReport(*gameHandle, sortedPlugins); - } catch (std::system_error& e) { - log(loot::LogLevel::error, e.what()); - return 1; - } catch (const std::exception& e) { - log(loot::LogLevel::error, e.what()); - return 1; - } - - progress(Progress::Done); - - return 0; -} - -void set(QJsonObject& o, const char* e, const QJsonValue& v) -{ - if (v.isObject() && v.toObject().isEmpty()) { - return; - } - - if (v.isArray() && v.toArray().isEmpty()) { - return; - } - - if (v.isString() && v.toString().isEmpty()) { - return; - } - - o[e] = v; -} - -std::string -LOOTWorker::createJsonReport(loot::GameInterface& game, - const std::vector& sortedPlugins) const -{ - QJsonObject root; - - set(root, "messages", createMessages(game.GetDatabase().GetGeneralMessages(true))); - set(root, "plugins", createPlugins(game, sortedPlugins)); - - const auto end = std::chrono::high_resolution_clock::now(); - - set(root, "stats", - QJsonObject{{"time", static_cast(std::chrono::duration_cast( - end - m_startTime) - .count())}, - {"lootcliVersion", LOOTCLI_VERSION_STRING}, - {"lootVersion", QString::fromStdString(loot::GetLiblootVersion())}}); - - QJsonDocument doc(root); - return doc.toJson(QJsonDocument::Indented).toStdString(); -} - -template -QJsonArray createStringArray(const Container& c) -{ - QJsonArray array; - - for (auto&& e : c) { - array.push_back(QString::fromStdString(e)); - } - - return array; -} - -QJsonArray -LOOTWorker::createPlugins(loot::GameInterface& game, - const std::vector& sortedPlugins) const -{ - QJsonArray plugins; - - for (auto&& pluginName : sortedPlugins) { - - auto plugin = game.GetPlugin(pluginName); - - QJsonObject o; - o["name"] = QString::fromStdString(pluginName); - - if (auto metaData = game.GetDatabase().GetPluginMetadata(pluginName, true, true)) { - set(o, "incompatibilities", - createIncompatibilities(game, metaData->GetIncompatibilities())); - set(o, "messages", createMessages(metaData->GetMessages())); - set(o, "dirty", createDirty(metaData->GetDirtyInfo())); - set(o, "clean", createClean(metaData->GetCleanInfo())); - } - - set(o, "missingMasters", createMissingMasters(game, pluginName)); - - if (plugin->LoadsArchive()) { - o["loadsArchive"] = true; - } - - if (plugin->IsMaster()) { - o["isMaster"] = true; - } - - if (plugin->IsLightPlugin()) { - o["isLightMaster"] = true; - } - - // don't add if the name is the only thing in there - if (o.size() > 1) { - plugins.push_back(o); - } - } - - return plugins; -} - -QJsonValue LOOTWorker::createMessages(const std::vector& list) const -{ - QJsonArray messages; - - for (loot::Message m : list) { - auto simpleMessage = loot::SelectMessageContent(m.GetContent(), m_Language); - if (simpleMessage.has_value()) { - messages.push_back(QJsonObject{ - {"type", QString::fromStdString(toString(m.GetType()))}, - {"text", QString::fromStdString(simpleMessage.value().GetText())}}); - } - } - - return messages; -} - -QJsonValue -LOOTWorker::createDirty(const std::vector& data) const -{ - QJsonArray array; - - for (const auto& d : data) { - QJsonObject o{ - {"crc", static_cast(d.GetCRC())}, - {"itm", static_cast(d.GetITMCount())}, - {"deletedReferences", static_cast(d.GetDeletedReferenceCount())}, - {"deletedNavmesh", static_cast(d.GetDeletedNavmeshCount())}, - }; - - set(o, "cleaningUtility", QString::fromStdString(d.GetCleaningUtility())); - auto simpleMessage = loot::SelectMessageContent( - loot::Message(loot::MessageType::say, d.GetDetail()).GetContent(), m_Language); - if (simpleMessage.has_value()) { - set(o, "info", QString::fromStdString(simpleMessage.value().GetText())); - } else { - set(o, "info", QString::fromStdString("")); - } - - array.push_back(o); - } - - return array; -} - -QJsonValue -LOOTWorker::createClean(const std::vector& data) const -{ - QJsonArray array; - - for (const auto& d : data) { - QJsonObject o{ - {"crc", static_cast(d.GetCRC())}, - }; - - set(o, "cleaningUtility", QString::fromStdString(d.GetCleaningUtility())); - auto simpleMessage = loot::SelectMessageContent( - loot::Message(loot::MessageType::say, d.GetDetail()).GetContent(), m_Language); - if (simpleMessage.has_value()) { - set(o, "info", QString::fromStdString(simpleMessage.value().GetText())); - } else { - set(o, "info", QString::fromStdString("")); - } - - array.push_back(o); - } - - return array; -} - -QJsonValue -LOOTWorker::createIncompatibilities(loot::GameInterface& game, - const std::vector& data) const -{ - QJsonArray array; - - for (auto&& f : data) { - const auto n = static_cast(f.GetName()); - if (!game.GetPlugin(n)) { - continue; - } - - const auto name = QString::fromStdString(n); - const auto displayName = QString::fromStdString(f.GetDisplayName()); - - QJsonObject o{{"name", name}}; - - if (displayName != name) { - set(o, "displayName", displayName); - } - - array.push_back(std::move(o)); - } - - return array; -} - -QJsonValue LOOTWorker::createMissingMasters(loot::GameInterface& game, - const std::string& pluginName) const -{ - QJsonArray array; - - for (auto&& master : game.GetPlugin(pluginName)->GetMasters()) { - if (!game.GetPlugin(master)) { - array.push_back(QString::fromStdString(master)); - } - } - - return array; -} - -void LOOTWorker::progress(Progress p) -{ - std::cout << "[progress] " << static_cast(p) << "\n"; - std::cout.flush(); -} - -void LOOTWorker::log(loot::LogLevel level, const std::string_view message) const -{ - if (level < m_LogLevel) { - return; - } - - const auto ll = fromLootLogLevel(level); - const auto levelName = logLevelToString(ll); - - std::cout << "[" << levelName << "] " << message << "\n"; - std::cout.flush(); -} - -loot::LogLevel toLootLogLevel(lootcli::LogLevels level) -{ - using L = loot::LogLevel; - using LC = lootcli::LogLevels; - - switch (level) { - case LC::Trace: - return L::trace; - case LC::Debug: - return L::debug; - case LC::Info: - return L::info; - case LC::Warning: - return L::warning; - case LC::Error: - return L::error; - default: - return L::info; - } -} - -lootcli::LogLevels fromLootLogLevel(loot::LogLevel level) -{ - using L = loot::LogLevel; - using LC = lootcli::LogLevels; - - switch (level) { - case L::trace: - return LC::Trace; - - case L::debug: - return LC::Debug; - - case L::info: - return LC::Info; - - case L::warning: - return LC::Warning; - - case L::error: - return LC::Error; - - default: - return LC::Info; - } -} - -} // namespace lootcli diff --git a/libs/lootcli/src/lootthread.h b/libs/lootcli/src/lootthread.h deleted file mode 100644 index d4f4e65..0000000 --- a/libs/lootcli/src/lootthread.h +++ /dev/null @@ -1,107 +0,0 @@ -#ifndef LOOTTHREAD_H -#define LOOTTHREAD_H - -#include "game_settings.h" -#include "loot/database_interface.h" -#include - -namespace loot -{ -class Game; -} - -namespace lootcli -{ - -loot::LogLevel toLootLogLevel(lootcli::LogLevels level); -lootcli::LogLevels fromLootLogLevel(loot::LogLevel level); - -class LOOTWorker -{ -public: - explicit LOOTWorker(); - - void setGame(const std::string& gameName); - void setGamePath(const std::string& gamePath); - void setOutput(const std::string& outputPath); - void setPluginListPath(const std::string& pluginListPath); - void - setLanguageCode(const std::string& language_code); // Will add this when I figure out - // how languages work on MO - void setLogLevel(loot::LogLevel level); - - void setUpdateMasterlist(bool update); - - int run(); - -private: - void progress(Progress p); - void log(loot::LogLevel level, const std::string_view message) const; - -#ifdef _WIN32 - DWORD GetFile(const WCHAR* szUrl, const CHAR* szFileName); -#else - int GetFile(const std::string& url, const std::string& fileName); -#endif - void getSettings(const std::filesystem::path& file); - std::string getOldDefaultRepoUrl(loot::GameId gameType); - std::optional GetLocalFolder(const toml::table& table); - bool IsNehrim(const toml::table& table); - bool IsEnderal(const toml::table& table, const std::string& expectedLocalFolder); - bool IsEnderal(const toml::table& table); - bool IsEnderalSE(const toml::table& table); - bool isLocalPath(const std::string& location, const std::string& filename); - bool isBranchCheckedOut(const std::filesystem::path& localGitRepo, - const std::string& branch); - std::optional migrateMasterlistRepoSettings(loot::GameId gameType, - std::string url, - std::string branch); - std::string migrateMasterlistSource(const std::string& source); - - std::filesystem::path gamePath() const; - std::filesystem::path masterlistPath() const; - std::filesystem::path settingsPath() const; - std::filesystem::path userlistPath() const; - std::filesystem::path l10nPath() const; - std::filesystem::path dataPath() const; - -private: - // void handleErr(unsigned int resultCode, const char *description); - bool sort(loot::Game& game); - // const char *lootErrorString(unsigned int errorCode); - // template T resolveVariable(HMODULE lib, const char *name); - // template T resolveFunction(HMODULE lib, const char *name); - -private: - loot::GameId m_GameId; - std::string m_Language; - std::string m_GameName; - std::string m_GamePath; - std::string m_OutputPath; - std::string m_PluginListPath; - loot::LogLevel m_LogLevel; - bool m_UpdateMasterlist; - mutable std::recursive_mutex mutex_; - loot::GameSettings m_GameSettings; - std::chrono::high_resolution_clock::time_point m_startTime; - - std::string createJsonReport(loot::GameInterface& game, - const std::vector& sortedPlugins) const; - - QJsonArray createPlugins(loot::GameInterface& game, - const std::vector& sortedPlugins) const; - - QJsonValue createMessages(const std::vector& list) const; - QJsonValue createDirty(const std::vector& data) const; - QJsonValue createClean(const std::vector& data) const; - - QJsonValue createIncompatibilities(loot::GameInterface& game, - const std::vector& data) const; - - QJsonValue createMissingMasters(loot::GameInterface& game, - const std::string& pluginName) const; -}; - -} // namespace lootcli - -#endif // LOOTTHREAD_H diff --git a/libs/lootcli/src/main.cpp b/libs/lootcli/src/main.cpp deleted file mode 100644 index d06e0ef..0000000 --- a/libs/lootcli/src/main.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "lootthread.h" -#include - -using namespace std; - -template -T getParameter(const std::vector& arguments, const std::string& key) -{ - auto iter = std::find(arguments.begin(), arguments.end(), std::string("--") + key); - if ((iter != arguments.end()) && ((iter + 1) != arguments.end())) { - return boost::lexical_cast(*(iter + 1)); - } else { - throw std::runtime_error(std::string("argument missing " + key)); - } -} - -template <> -bool getParameter(const std::vector& arguments, - const std::string& key) -{ - auto iter = std::find(arguments.begin(), arguments.end(), std::string("--") + key); - if (iter != arguments.end()) { - return true; - } else { - return false; - } -} - -template -T getOptionalParameter(const std::vector& arguments, - const std::string& key, T def) -{ - try { - return getParameter(arguments, key); - } catch (std::runtime_error&) { - return def; - } -} - -loot::LogLevel getLogLevel(const std::vector& arguments) -{ - const auto s = getOptionalParameter(arguments, "logLevel", ""); - const auto level = lootcli::logLevelFromString(s); - - return lootcli::toLootLogLevel(level); -} - -#ifdef _WIN32 -int wWinMain(HINSTANCE, HINSTANCE, LPTSTR, int) -{ - _setmode(_fileno(stdout), _O_BINARY); - setlocale(LC_ALL, "en.UTF-8"); - - std::vector arguments; - int argc; - LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc); - - if (argv) { - for (int i = 0; i < argc; ++i) { - size_t num_converted; - std::vector arg(wcslen(argv[i]) * sizeof(wchar_t) + 1); - - wcstombs_s(&num_converted, &(arg[0]), arg.size(), argv[i], arg.size() - 1); - - arguments.push_back(&(arg[0])); - } - } -#else -int main(int argc, char* argv[]) -{ - setlocale(LC_ALL, "en_US.UTF-8"); - - std::vector arguments; - for (int i = 0; i < argc; ++i) { - arguments.push_back(argv[i]); - } -#endif - - // design rationale: this was designed to have the actual loot stuff run in a separate - // thread. That turned out to be unnecessary atm. - - try { - lootcli::LOOTWorker worker; - - worker.setUpdateMasterlist(!getParameter(arguments, "skipUpdateMasterlist")); - worker.setGame(getParameter(arguments, "game")); - worker.setGamePath(getParameter(arguments, "gamePath")); - worker.setPluginListPath(getParameter(arguments, "pluginListPath")); - worker.setOutput(getParameter(arguments, "out")); - worker.setLogLevel(getLogLevel(arguments)); - - const auto lang = getOptionalParameter(arguments, "language", ""); - if (!lang.empty()) { - worker.setLanguageCode(lang); - } - - return worker.run(); - } catch (const std::exception& e) { - std::cerr << "Error: " << e.what(); - return 1; - } -} diff --git a/libs/lootcli/src/pch.cpp b/libs/lootcli/src/pch.cpp deleted file mode 100644 index 1d9f38c..0000000 --- a/libs/lootcli/src/pch.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "pch.h" diff --git a/libs/lootcli/src/pch.h b/libs/lootcli/src/pch.h deleted file mode 100644 index cf460fd..0000000 --- a/libs/lootcli/src/pch.h +++ /dev/null @@ -1,84 +0,0 @@ -#ifdef _MSC_VER -#pragma warning(disable : 4251) // neds to have dll-interface -#pragma warning(disable : 4355) // this used in initializer list -#pragma warning(disable : 4371) // layout may have changed -#pragma warning(disable : 4514) // unreferenced inline function removed -#pragma warning(disable : 4571) // catch semantics changed -#pragma warning(disable : 4619) // no warning X -#pragma warning(disable : 4623) // default constructor deleted -#pragma warning(disable : 4625) // copy constructor deleted -#pragma warning(disable : 4626) // copy assignment operator deleted -#pragma warning(disable : 4710) // function not inlined -#pragma warning(disable : 4820) // padding -#pragma warning(disable : 4866) // left-to-right evaluation order -#pragma warning(disable : 4868) // left-to-right evaluation order -#pragma warning(disable : 5026) // move constructor deleted -#pragma warning(disable : 5027) // move assignment operator deleted -#pragma warning(disable : 5045) // spectre mitigation - -#pragma warning(push, 3) -#pragma warning(disable : 4365) // signed/unsigned mismatch -#pragma warning(disable : 4774) // bad format string -#pragma warning(disable : 4946) // reinterpret_cast used between related classes -#pragma warning(disable : 4800) // implicit conversion -#endif - -// std -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// qt -#include -#include -#include -#include -#include - -// boost -#include -#include -#include - -// loot -#include -#include - -// third-party -#include - -#ifdef _WIN32 -// windows -#define WIN32_LEAN_AND_MEAN -#include -#include -#include -#include -#include -#else -// Linux equivalents -#include -#include -#endif - -#ifdef _MSC_VER -#undef _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING -#pragma warning(pop) -#endif diff --git a/libs/lootcli/src/version.h b/libs/lootcli/src/version.h deleted file mode 100644 index d496a56..0000000 --- a/libs/lootcli/src/version.h +++ /dev/null @@ -1,4 +0,0 @@ -#define LOOTCLI_VERSION_MAJOR 1 -#define LOOTCLI_VERSION_MINOR 7 -#define LOOTCLI_VERSION_MAINTENANCE 0 -#define LOOTCLI_VERSION_STRING "1.7.0" diff --git a/libs/lootcli/src/version.rc b/libs/lootcli/src/version.rc deleted file mode 100644 index bf6ebb4..0000000 --- a/libs/lootcli/src/version.rc +++ /dev/null @@ -1,35 +0,0 @@ -#include "version.h" -#include "Winver.h" - -#define VER_FILEVERSION LOOTCLI_VERSION_MAJOR,LOOTCLI_VERSION_MINOR,LOOTCLI_VERSION_MAINTENANCE,0 -#define VER_FILEVERSION_STR LOOTCLI_VERSION_STRING - -VS_VERSION_INFO VERSIONINFO -FILEVERSION VER_FILEVERSION -PRODUCTVERSION VER_FILEVERSION -FILEFLAGSMASK VS_FFI_FILEFLAGSMASK -FILEFLAGS (0) -FILEOS VOS__WINDOWS32 -FILETYPE VFT_APP -FILESUBTYPE (0) -BEGIN -BLOCK "StringFileInfo" -BEGIN -BLOCK "040904B0" -BEGIN -VALUE "FileVersion", VER_FILEVERSION_STR -VALUE "CompanyName", "Mod Organizer 2 Team\0" -VALUE "FileDescription", "LOOT Command line interface\0" -VALUE "OriginalFilename", "lootcli.exe\0" -VALUE "InternalName", "lootcli\0" -VALUE "LegalCopyright", "Copyright 2011-2016 Sebastian Herbord\r\nCopyright 2016-2025 Mod Organizer 2 contributors\0" -VALUE "ProductName", "lootcli\0" -VALUE "ProductVersion", VER_FILEVERSION_STR -END -END - -BLOCK "VarFileInfo" -BEGIN -VALUE "Translation", 0x0409, 1200 -END -END diff --git a/libs/lootcli/vcpkg.json b/libs/lootcli/vcpkg.json deleted file mode 100644 index d890106..0000000 --- a/libs/lootcli/vcpkg.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "dependencies": ["boost-locale", "tomlplusplus", "libloot"], - "overrides": [ - { - "name": "tomlplusplus", - "version": "3.1.0" - } - ], - "vcpkg-configuration": { - "default-registry": { - "kind": "git", - "repository": "https://github.com/Microsoft/vcpkg", - "baseline": "294f76666c3000630d828703e675814c05a4fd43" - }, - "registries": [ - { - "kind": "git", - "repository": "https://github.com/Microsoft/vcpkg", - "baseline": "294f76666c3000630d828703e675814c05a4fd43", - "packages": ["boost*", "boost-*"] - }, - { - "kind": "git", - "repository": "https://github.com/ModOrganizer2/vcpkg-registry", - "baseline": "8beb2e0efa9c17dd6d17bb05288dd1e40727f673", - "packages": ["libloot"] - } - ] - } -} diff --git a/src/src/CMakeLists.txt b/src/src/CMakeLists.txt index c22442c..b0eb235 100644 --- a/src/src/CMakeLists.txt +++ b/src/src/CMakeLists.txt @@ -104,7 +104,6 @@ target_link_libraries(organizer PRIVATE libbsarch_OOP mo2::bsatk mo2::esptk - mo2::lootcli-header # Qt6 Qt6::Widgets Qt6::Concurrent diff --git a/src/src/loot.cpp b/src/src/loot.cpp deleted file mode 100644 index 3204689..0000000 --- a/src/src/loot.cpp +++ /dev/null @@ -1,1674 +0,0 @@ -#include "loot.h" -#include "json.h" -#include "lootdialog.h" -#include "organizercore.h" -#include "spawn.h" -#include -#include - -#ifndef _WIN32 -#include "fluorinepaths.h" -#include -#include -extern char** environ; -#endif - -using namespace MOBase; -using namespace json; - -static QString LootReportPath = QDir::temp().absoluteFilePath("lootreport.json"); -#ifdef _WIN32 -static const DWORD PipeTimeout = 500; -#endif - -#ifdef _WIN32 -class AsyncPipe -{ -public: - AsyncPipe() : m_ioPending(false) - { - std::fill(std::begin(m_buffer), std::end(m_buffer), 0); - std::memset(&m_ov, 0, sizeof(m_ov)); - } - - env::HandlePtr create() - { - // creating pipe - env::HandlePtr out(createPipe()); - if (out.get() == INVALID_HANDLE_VALUE) { - return {}; - } - - HANDLE readEventHandle = ::CreateEvent(nullptr, TRUE, FALSE, nullptr); - - if (readEventHandle == NULL) { - const auto e = GetLastError(); - log::error("CreateEvent failed for loot, {}", formatSystemMessage(e)); - return {}; - } - - m_ov.hEvent = readEventHandle; - m_readEvent.reset(readEventHandle); - - return out; - } - - std::string read() - { - if (m_ioPending) { - return checkPending(); - } else { - return tryRead(); - } - } - -private: - static const std::size_t bufferSize = 50000; - - env::HandlePtr m_stdout; - env::HandlePtr m_readEvent; - char m_buffer[bufferSize]; - OVERLAPPED m_ov; - bool m_ioPending; - - HANDLE createPipe() - { - static const wchar_t* PipeName = L"\\\\.\\pipe\\lootcli_pipe"; - - SECURITY_ATTRIBUTES sa = {}; - sa.nLength = sizeof(SECURITY_ATTRIBUTES); - sa.bInheritHandle = TRUE; - - env::HandlePtr pipe; - - // creating pipe - { - HANDLE pipeHandle = - ::CreateNamedPipe(PipeName, PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, - PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, 1, 50'000, - 50'000, PipeTimeout, &sa); - - if (pipeHandle == INVALID_HANDLE_VALUE) { - const auto e = GetLastError(); - log::error("CreateNamedPipe failed, {}", formatSystemMessage(e)); - return INVALID_HANDLE_VALUE; - } - - pipe.reset(pipeHandle); - } - - { - // duplicating the handle to read from it - HANDLE outputRead = INVALID_HANDLE_VALUE; - - const auto r = - DuplicateHandle(GetCurrentProcess(), pipe.get(), GetCurrentProcess(), - &outputRead, 0, TRUE, DUPLICATE_SAME_ACCESS); - - if (!r) { - const auto e = GetLastError(); - log::error("DuplicateHandle for pipe failed, {}", formatSystemMessage(e)); - return INVALID_HANDLE_VALUE; - } - - m_stdout.reset(outputRead); - } - - // creating handle to pipe which is passed to CreateProcess() - HANDLE outputWrite = ::CreateFileW(PipeName, FILE_WRITE_DATA | SYNCHRONIZE, 0, &sa, - OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); - - if (outputWrite == INVALID_HANDLE_VALUE) { - const auto e = GetLastError(); - log::error("CreateFileW for pipe failed, {}", formatSystemMessage(e)); - return INVALID_HANDLE_VALUE; - } - - return outputWrite; - } - - std::string tryRead() - { - DWORD bytesRead = 0; - - if (!::ReadFile(m_stdout.get(), m_buffer, bufferSize, &bytesRead, &m_ov)) { - const auto e = GetLastError(); - - switch (e) { - case ERROR_IO_PENDING: { - m_ioPending = true; - break; - } - - case ERROR_BROKEN_PIPE: { - // broken pipe probably means lootcli is finished - break; - } - - default: { - log::error("{}", formatSystemMessage(e)); - break; - } - } - - return {}; - } - - return {m_buffer, m_buffer + bytesRead}; - } - - std::string checkPending() - { - DWORD bytesRead = 0; - - const auto r = WaitForSingleObject(m_readEvent.get(), PipeTimeout); - - if (r == WAIT_FAILED) { - const auto e = GetLastError(); - log::error("WaitForSingleObject in AsyncPipe failed, {}", formatSystemMessage(e)); - return {}; - } - - if (!::GetOverlappedResult(m_stdout.get(), &m_ov, &bytesRead, FALSE)) { - const auto e = GetLastError(); - - switch (e) { - case ERROR_IO_INCOMPLETE: { - break; - } - - case WAIT_TIMEOUT: { - break; - } - - case ERROR_BROKEN_PIPE: { - // broken pipe probably means lootcli is finished - break; - } - - default: { - log::error("GetOverlappedResult failed, {}", formatSystemMessage(e)); - break; - } - } - - return {}; - } - - ::ResetEvent(m_readEvent.get()); - m_ioPending = false; - - return {m_buffer, m_buffer + bytesRead}; - } -}; -#else -// Linux implementation using POSIX pipes -class AsyncPipe -{ -public: - AsyncPipe() : m_readFd(-1), m_writeFd(-1) {} - - ~AsyncPipe() - { - if (m_readFd >= 0) - ::close(m_readFd); - if (m_writeFd >= 0) - ::close(m_writeFd); - } - - env::HandlePtr create() - { - int fds[2]; - if (::pipe(fds) != 0) { - log::error("pipe() failed: {}", strerror(errno)); - return {}; - } - m_readFd = fds[0]; - m_writeFd = fds[1]; - - // set read end to non-blocking - int flags = ::fcntl(m_readFd, F_GETFL, 0); - ::fcntl(m_readFd, F_SETFL, flags | O_NONBLOCK); - - // return a non-null HandlePtr to indicate success - return env::HandlePtr(reinterpret_cast(static_cast(1))); - } - - int readFd() const { return m_readFd; } - int writeFd() const { return m_writeFd; } - - void closeWriteEnd() - { - if (m_writeFd >= 0) { - ::close(m_writeFd); - m_writeFd = -1; - } - } - - std::string read() - { - if (m_readFd < 0) - return {}; - - char buffer[50000]; - ssize_t n = ::read(m_readFd, buffer, sizeof(buffer)); - if (n > 0) { - return std::string(buffer, n); - } - return {}; - } - -private: - int m_readFd; - int m_writeFd; -}; -#endif - -log::Levels levelFromLoot(lootcli::LogLevels level) -{ - using LC = lootcli::LogLevels; - - switch (level) { - case LC::Trace: // fall-through - case LC::Debug: - return log::Debug; - - case LC::Info: - return log::Info; - - case LC::Warning: - return log::Warning; - - case LC::Error: - return log::Error; - - default: - return log::Info; - } -} - -QString Loot::Report::toMarkdown() const -{ - QString s; - - if (!okay) { - s += "## " + tr("Loot failed to run") + "\n"; - - if (errors.empty() && warnings.empty()) { - s += tr("No errors were reported. The log below might have more information.\n"); - } - } - - s += errorsMarkdown(); - - if (okay) { - s += "\n" + successMarkdown(); - } - - return s; -} - -QString Loot::Report::successMarkdown() const -{ - QString s; - - if (!messages.empty()) { - s += "### " + QObject::tr("General messages") + "\n"; - - for (auto&& m : messages) { - s += " - " + m.toMarkdown() + "\n"; - } - } - - if (!plugins.empty()) { - if (!s.isEmpty()) { - s += "\n"; - } - - s += "### " + QObject::tr("Plugins") + "\n"; - - for (auto&& p : plugins) { - const auto ps = p.toMarkdown(); - if (!ps.isEmpty()) { - s += ps + "\n"; - } - } - } - - if (s.isEmpty()) { - s += "**" + QObject::tr("No messages.") + "**\n"; - } - - s += stats.toMarkdown(); - - return s; -} - -QString Loot::Report::errorsMarkdown() const -{ - QString s; - - if (!errors.empty()) { - s += "### " + tr("Errors") + ":\n"; - - for (auto&& e : errors) { - s += " - " + e + "\n"; - } - } - - if (!warnings.empty()) { - if (!s.isEmpty()) { - s += "\n"; - } - - s += "### " + tr("Warnings") + ":\n"; - - for (auto&& w : warnings) { - s += " - " + w + "\n"; - } - } - - return s; -} - -QString Loot::Stats::toMarkdown() const -{ - return QString("`stats: %1s, lootcli %2, loot %3`") - .arg(QString::number(time / 1000.0, 'f', 2)) - .arg(lootcliVersion) - .arg(lootVersion); -} - -QString Loot::Plugin::toMarkdown() const -{ - QString s; - - if (!incompatibilities.empty()) { - s += " - **" + QObject::tr("Incompatibilities") + ": "; - - QString fs; - for (auto&& f : incompatibilities) { - if (!fs.isEmpty()) { - fs += ", "; - } - - fs += f.displayName.isEmpty() ? f.name : f.displayName; - } - - s += fs + "**\n"; - } - - if (!missingMasters.empty()) { - s += " - **" + QObject::tr("Missing masters") + ": "; - - QString ms; - for (auto&& m : missingMasters) { - if (!ms.isEmpty()) { - ms += ", "; - } - - ms += m; - } - - s += ms + "**\n"; - } - - for (auto&& m : messages) { - s += " - " + m.toMarkdown() + "\n"; - } - - for (auto&& d : dirty) { - s += " - " + d.toMarkdown(false) + "\n"; - } - - if (!s.isEmpty()) { - s = "#### " + name + "\n" + s; - } - - return s; -} - -QString Loot::Dirty::toString(bool isClean) const -{ - if (isClean) { - return QObject::tr("Verified clean by %1") - .arg(cleaningUtility.isEmpty() ? "?" : cleaningUtility); - } - - QString s = cleaningString(); - - if (!info.isEmpty()) { - s += " " + info; - } - - return s; -} - -QString Loot::Dirty::toMarkdown(bool isClean) const -{ - return toString(isClean); -} - -QString Loot::Dirty::cleaningString() const -{ - return QObject::tr("%1 found %2 ITM record(s), %3 deleted reference(s) and %4 " - "deleted navmesh(es).") - .arg(cleaningUtility.isEmpty() ? "?" : cleaningUtility) - .arg(itm) - .arg(deletedReferences) - .arg(deletedNavmesh); -} - -QString Loot::Message::toMarkdown() const -{ - QString s; - - switch (type) { - case log::Error: { - s += "**" + QObject::tr("Error") + "**: "; - break; - } - - case log::Warning: { - s += "**" + QObject::tr("Warning") + "**: "; - break; - } - - default: { - break; - } - } - - s += text; - - return s; -} - -Loot::Loot(OrganizerCore& core) - : m_core(core), m_thread(nullptr), m_cancel(false), m_result(false) -{} - -Loot::~Loot() -{ - if (m_thread) { - m_thread->wait(); - } - - deleteReportFile(); -} - -bool Loot::start(QWidget* parent, bool didUpdateMasterList) -{ - deleteReportFile(); - - log::debug("starting loot"); - - m_pipe.reset(new AsyncPipe); - - env::HandlePtr stdoutHandle = m_pipe->create(); - if (!stdoutHandle) { - return false; - } - - // vfs - m_core.prepareVFS(); - - // spawning - if (!spawnLootcli(parent, didUpdateMasterList, std::move(stdoutHandle))) { - return false; - } - - // starting thread - log::debug("starting loot thread"); - m_thread.reset(QThread::create([&] { - lootThread(); - })); - m_thread->start(); - - return true; -} - -bool Loot::spawnLootcli(QWidget* parent, bool didUpdateMasterList, - env::HandlePtr stdoutHandle) -{ -#ifdef _WIN32 - const auto logLevel = m_core.settings().diagnostics().lootLogLevel(); - - QStringList parameters; - parameters << "--game" << m_core.managedGame()->lootGameName() - - << "--gamePath" - << QString("\"%1\"").arg( - m_core.managedGame()->gameDirectory().absolutePath()) - - << "--pluginListPath" - << QString("\"%1/loadorder.txt\"").arg(m_core.profilePath()) - - << "--logLevel" - << QString::fromStdString(lootcli::logLevelToString(logLevel)) - - << "--out" << QString("\"%1\"").arg(LootReportPath) - - << "--language" << m_core.settings().interface().language(); - - if (didUpdateMasterList) { - parameters << "--skipUpdateMasterlist"; - } - - spawn::SpawnParameters sp; - sp.binary = QFileInfo(qApp->applicationDirPath() + "/loot/lootcli.exe"); - sp.arguments = parameters.join(" "); - sp.currentDirectory.setPath(qApp->applicationDirPath() + "/loot"); - sp.hooked = true; - sp.stdOut = stdoutHandle.get(); - - HANDLE lootHandle = spawn::startBinary(parent, sp); - - if (lootHandle == INVALID_HANDLE_VALUE) { - emit log(log::Levels::Error, tr("failed to start loot")); - return false; - } - - m_lootProcess.reset(lootHandle); - - return true; -#else - Q_UNUSED(parent); - Q_UNUSED(stdoutHandle); - - const auto logLevel = m_core.settings().diagnostics().lootLogLevel(); - - QStringList parameters; - parameters << "--game" << m_core.managedGame()->lootGameName() - - << "--gamePath" << m_core.managedGame()->gameDirectory().absolutePath() - - << "--pluginListPath" - << QString("%1/loadorder.txt").arg(m_core.profilePath()) - - << "--logLevel" - << QString::fromStdString(lootcli::logLevelToString(logLevel)) - - << "--out" << LootReportPath - - << "--language" << m_core.settings().interface().language(); - - if (didUpdateMasterList) { - parameters << "--skipUpdateMasterlist"; - } - - QString binary = qApp->applicationDirPath() + "/lootcli"; - - if (!QFile::exists(binary)) { - emit log(log::Levels::Error, tr("lootcli not found at %1").arg(binary)); - return false; - } - - // build argv for posix_spawn - std::string binaryStr = binary.toStdString(); - std::vector argStrs; - argStrs.push_back(binaryStr); - for (const auto& p : parameters) { - argStrs.push_back(p.toStdString()); - } - - std::vector argv; - for (auto& s : argStrs) { - argv.push_back(s.data()); - } - argv.push_back(nullptr); - - int writeFd = m_pipe->writeFd(); - int readFd = m_pipe->readFd(); - - pid_t pid = fork(); - if (pid == -1) { - emit log(log::Levels::Error, - tr("failed to start lootcli: %1").arg(strerror(errno))); - return false; - } - - if (pid == 0) { - // child process — only async-signal-safe calls allowed here - dup2(writeFd, STDOUT_FILENO); - close(writeFd); - close(readFd); - - std::string workDir = qApp->applicationDirPath().toStdString(); - chdir(workDir.c_str()); - - execv(binaryStr.c_str(), argv.data()); - _exit(127); - } - - // parent process - m_childPid = pid; - m_pipe->closeWriteEnd(); - - log::debug("lootcli spawned with pid {}", pid); - return true; -#endif -} - -void Loot::cancel() -{ - if (!m_cancel) { - log::debug("loot received cancel request"); - m_cancel = true; - } -} - -bool Loot::result() const -{ - return m_result; -} - -const QString& Loot::outPath() const -{ - return LootReportPath; -} - -const Loot::Report& Loot::report() const -{ - return m_report; -} - -const std::vector& Loot::errors() const -{ - return m_errors; -} - -const std::vector& Loot::warnings() const -{ - return m_warnings; -} - -void Loot::lootThread() -{ - try { - m_result = false; - - if (waitForCompletion()) { - m_result = true; - } - - m_report = createReport(); - } catch (...) { - log::error("unhandled exception in loot thread"); - } - - log::debug("finishing loot thread"); - emit finished(); -} - -bool Loot::waitForCompletion() -{ -#ifdef _WIN32 - bool terminating = false; - - log::debug("loot thread waiting for completion on lootcli"); - - for (;;) { - DWORD res = WaitForSingleObject(m_lootProcess.get(), 100); - - if (res == WAIT_OBJECT_0) { - log::debug("lootcli has completed"); - // done - break; - } - - if (res == WAIT_FAILED) { - const auto e = GetLastError(); - log::error("failed to wait on loot process, {}", formatSystemMessage(e)); - return false; - } - - if (m_cancel) { - log::debug("terminating lootcli process"); - ::TerminateProcess(m_lootProcess.get(), 1); - - log::debug("waiting for loocli process to terminate"); - WaitForSingleObject(m_lootProcess.get(), INFINITE); - - log::debug("lootcli terminated"); - return false; - } - - processStdout(m_pipe->read()); - } - - if (m_cancel) { - return false; - } - - processStdout(m_pipe->read()); - - // checking exit code - DWORD exitCode = 0; - - if (!::GetExitCodeProcess(m_lootProcess.get(), &exitCode)) { - const auto e = GetLastError(); - log::error("failed to get exit code for loot, {}", formatSystemMessage(e)); - return false; - } - - if (exitCode != 0UL) { - emit log(log::Levels::Error, - tr("Loot failed. Exit code was: 0x%1").arg(exitCode, 0, 16)); - return false; - } - - return true; -#else - if (m_childPid <= 0) { - return false; - } - - log::debug("loot thread waiting for completion on lootcli"); - - for (;;) { - // poll the pipe for data with 100ms timeout - struct pollfd pfd; - pfd.fd = m_pipe->readFd(); - pfd.events = POLLIN; - ::poll(&pfd, 1, 100); - - processStdout(m_pipe->read()); - - int status; - pid_t result = waitpid(m_childPid, &status, WNOHANG); - - if (result == m_childPid) { - log::debug("lootcli has completed"); - - // read any remaining output - processStdout(m_pipe->read()); - - if (WIFEXITED(status)) { - int exitCode = WEXITSTATUS(status); - if (exitCode != 0) { - emit log(log::Levels::Error, - tr("Loot failed. Exit code was: 0x%1").arg(exitCode, 0, 16)); - return false; - } - return true; - } else { - emit log(log::Levels::Error, tr("lootcli terminated abnormally")); - return false; - } - } - - if (result == -1) { - log::error("waitpid failed: {}", strerror(errno)); - return false; - } - - if (m_cancel) { - log::debug("terminating lootcli process"); - ::kill(m_childPid, SIGTERM); - - log::debug("waiting for lootcli process to terminate"); - waitpid(m_childPid, &status, 0); - - log::debug("lootcli terminated"); - return false; - } - } -#endif -} - -void Loot::processStdout(const std::string& lootOut) -{ - emit output(QString::fromStdString(lootOut)); - - m_outputBuffer += lootOut; - if (m_outputBuffer.empty()) { - return; - } - - std::size_t start = 0; - - for (;;) { - const auto newline = m_outputBuffer.find("\n", start); - if (newline == std::string::npos) { - break; - } - - const std::string_view line(m_outputBuffer.c_str() + start, newline - start); - const auto m = lootcli::parseMessage(line); - - if (m.type == lootcli::MessageType::None) { - log::error("unrecognised loot output: '{}'", line); - continue; - } - - processMessage(m); - - start = newline + 1; - } - - m_outputBuffer.erase(0, start); -} - -void Loot::processMessage(const lootcli::Message& m) -{ - switch (m.type) { - case lootcli::MessageType::Log: { - const auto level = levelFromLoot(m.logLevel); - - if (level == log::Error) { - m_errors.push_back(QString::fromStdString(m.log)); - } else if (level == log::Warning) { - m_warnings.push_back(QString::fromStdString(m.log)); - } - - emit log(level, QString::fromStdString(m.log)); - break; - } - - case lootcli::MessageType::Progress: { - emit progress(m.progress); - break; - } - } -} - -Loot::Report Loot::createReport() const -{ - Report r; - - r.okay = m_result; - r.errors = m_errors; - r.warnings = m_warnings; - - if (m_result) { - processOutputFile(r); - } - - return r; -} - -void Loot::deleteReportFile() -{ - if (QFile::exists(LootReportPath)) { - log::debug("deleting temporary loot report '{}'", LootReportPath); - const auto r = shell::Delete(QFileInfo(LootReportPath)); - - if (!r) { - log::error("failed to remove temporary loot json report '{}': {}", LootReportPath, - r.toString()); - } - } -} - -void Loot::processOutputFile(Report& r) const -{ - log::debug("parsing json output file at '{}'", LootReportPath); - - QFile outFile(LootReportPath); - if (!outFile.open(QIODevice::ReadOnly)) { - emit log(MOBase::log::Error, QString("failed to open file, %1 (error %2)") - .arg(outFile.errorString()) - .arg(outFile.error())); - - return; - } - - QJsonParseError e; - const QJsonDocument doc = QJsonDocument::fromJson(outFile.readAll(), &e); - if (doc.isNull()) { - emit log(MOBase::log::Error, - QString("invalid json, %1 (error %2)").arg(e.errorString()).arg(e.error)); - - return; - } - - requireObject(doc, "root"); - - const QJsonObject object = doc.object(); - - r.messages = reportMessages(getOpt(object, "messages")); - r.plugins = reportPlugins(getOpt(object, "plugins")); - r.stats = reportStats(getWarn(object, "stats")); -} - -std::vector Loot::reportPlugins(const QJsonArray& plugins) const -{ - std::vector v; - - for (auto pluginValue : plugins) { - const auto o = convertWarn(pluginValue, "plugin"); - if (o.isEmpty()) { - continue; - } - - auto p = reportPlugin(o); - if (!p.name.isEmpty()) { - v.emplace_back(std::move(p)); - } - } - - return v; -} - -Loot::Plugin Loot::reportPlugin(const QJsonObject& plugin) const -{ - Plugin p; - - p.name = getWarn(plugin, "name"); - if (p.name.isEmpty()) { - return {}; - } - - // ignore disabled plugins; lootcli doesn't know if a plugin is enabled or not - // and will report information on any plugin that's in the filesystem - if (!m_core.pluginList()->isEnabled(p.name)) { - return {}; - } - - if (plugin.contains("incompatibilities")) { - p.incompatibilities = reportFiles(getOpt(plugin, "incompatibilities")); - } - - if (plugin.contains("messages")) { - p.messages = reportMessages(getOpt(plugin, "messages")); - } - - if (plugin.contains("dirty")) { - p.dirty = reportDirty(getOpt(plugin, "dirty")); - } - - if (plugin.contains("clean")) { - p.clean = reportDirty(getOpt(plugin, "clean")); - } - - if (plugin.contains("missingMasters")) { - p.missingMasters = reportStringArray(getOpt(plugin, "missingMasters")); - } - - p.loadsArchive = getOpt(plugin, "loadsArchive", false); - p.isMaster = getOpt(plugin, "isMaster", false); - p.isLightMaster = getOpt(plugin, "isLightMaster", false); - - return p; -} - -Loot::Stats Loot::reportStats(const QJsonObject& stats) const -{ - Stats s; - - s.time = getWarn(stats, "time"); - s.lootcliVersion = getWarn(stats, "lootcliVersion"); - s.lootVersion = getWarn(stats, "lootVersion"); - - return s; -} - -std::vector Loot::reportMessages(const QJsonArray& array) const -{ - std::vector v; - - for (auto messageValue : array) { - const auto o = convertWarn(messageValue, "message"); - if (o.isEmpty()) { - continue; - } - - Message m; - - const auto type = getWarn(o, "type"); - - if (type == "info") { - m.type = log::Info; - } else if (type == "warn") { - m.type = log::Warning; - } else if (type == "error") { - m.type = log::Error; - } else { - log::error("unknown message type '{}'", type); - m.type = log::Info; - } - - m.text = getWarn(o, "text"); - - if (!m.text.isEmpty()) { - v.emplace_back(std::move(m)); - } - } - - return v; -} - -std::vector Loot::reportFiles(const QJsonArray& array) const -{ - std::vector v; - - for (auto&& fileValue : array) { - const auto o = convertWarn(fileValue, "file"); - if (o.isEmpty()) { - continue; - } - - File f; - - f.name = getWarn(o, "name"); - f.displayName = getOpt(o, "displayName"); - - if (!f.name.isEmpty()) { - v.emplace_back(std::move(f)); - } - } - - return v; -} - -std::vector Loot::reportDirty(const QJsonArray& array) const -{ - std::vector v; - - for (auto&& dirtyValue : array) { - const auto o = convertWarn(dirtyValue, "dirty"); - - Dirty d; - - d.crc = getWarn(o, "crc"); - d.itm = getOpt(o, "itm"); - d.deletedReferences = getOpt(o, "deletedReferences"); - d.deletedNavmesh = getOpt(o, "deletedNavmesh"); - d.cleaningUtility = getOpt(o, "cleaningUtility"); - d.info = getOpt(o, "info"); - - v.emplace_back(std::move(d)); - } - - return v; -} - -std::vector Loot::reportStringArray(const QJsonArray& array) const -{ - std::vector v; - - for (auto&& sv : array) { - auto s = convertWarn(sv, "string"); - if (s.isEmpty()) { - continue; - } - - v.emplace_back(std::move(s)); - } - - return v; -} - -#ifndef _WIN32 - -static const QString LootGitHubRepo = - "SulfurNitride/loot-linux-build-for-fluorine"; - -static QString lootAppImagePath() -{ - return fluorineDataDir() + "/bin/LOOT.AppImage"; -} - -// Fetch the download URL for the latest Linux AppImage from GitHub releases. -static QString fetchLootDownloadUrl() -{ - QNetworkAccessManager nam; - QEventLoop loop; - - QUrl apiUrl(QString("https://api.github.com/repos/%1/releases/latest") - .arg(LootGitHubRepo)); - QNetworkRequest req(apiUrl); - req.setRawHeader("Accept", "application/vnd.github.v3+json"); - req.setRawHeader("User-Agent", "Fluorine-Manager"); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, - QNetworkRequest::NoLessSafeRedirectPolicy); - - auto* reply = nam.get(req); - QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); - loop.exec(); - - if (reply->error() != QNetworkReply::NoError) { - log::error("Failed to query GitHub releases: {} (HTTP {})", - reply->errorString(), - reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) - .toInt()); - reply->deleteLater(); - return {}; - } - - auto doc = QJsonDocument::fromJson(reply->readAll()); - reply->deleteLater(); - - auto assets = doc.object()["assets"].toArray(); - for (const auto& asset : assets) { - auto obj = asset.toObject(); - auto name = obj["name"].toString(); - if (name.contains("linux", Qt::CaseInsensitive) && - name.endsWith(".AppImage", Qt::CaseInsensitive)) { - return obj["browser_download_url"].toString(); - } - } - - // fallback: try any .AppImage - for (const auto& asset : assets) { - auto obj = asset.toObject(); - auto name = obj["name"].toString(); - if (name.endsWith(".AppImage", Qt::CaseInsensitive)) { - return obj["browser_download_url"].toString(); - } - } - - log::error("No Linux AppImage found in latest release of {}", LootGitHubRepo); - return {}; -} - -// Download the LOOT AppImage with a progress dialog. -static bool downloadLootAppImage(QWidget* parent, const QString& url, - const QString& destPath) -{ - QNetworkAccessManager nam; - QEventLoop loop; - QProgressDialog progress(QObject::tr("Downloading LOOT..."), QObject::tr("Cancel"), - 0, 100, parent); - progress.setWindowModality(Qt::WindowModal); - progress.setMinimumDuration(0); - progress.setValue(0); - - QNetworkRequest req{QUrl(url)}; - req.setRawHeader("User-Agent", "Fluorine-Manager"); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, - QNetworkRequest::NoLessSafeRedirectPolicy); - - auto* reply = nam.get(req); - - QObject::connect(reply, &QNetworkReply::downloadProgress, - [&](qint64 received, qint64 total) { - if (total > 0) { - progress.setMaximum(static_cast(total / 1024)); - progress.setValue(static_cast(received / 1024)); - } - }); - - QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); - QObject::connect(&progress, &QProgressDialog::canceled, reply, &QNetworkReply::abort); - loop.exec(); - - if (reply->error() != QNetworkReply::NoError) { - if (reply->error() != QNetworkReply::OperationCanceledError) { - log::error("Failed to download LOOT: {}", reply->errorString()); - } - reply->deleteLater(); - return false; - } - - QDir().mkpath(QFileInfo(destPath).absolutePath()); - - QFile f(destPath); - if (!f.open(QIODevice::WriteOnly)) { - log::error("Failed to write LOOT AppImage: {}", f.errorString()); - reply->deleteLater(); - return false; - } - - f.write(reply->readAll()); - f.close(); - reply->deleteLater(); - - // Make executable - QFile::setPermissions(destPath, - QFileDevice::ReadOwner | QFileDevice::WriteOwner | - QFileDevice::ExeOwner | QFileDevice::ReadGroup | - QFileDevice::ExeGroup); - - log::info("LOOT AppImage downloaded to {}", destPath); - return true; -} - -// Ensure the LOOT AppImage is available, downloading if necessary. -static bool ensureLootAvailable(QWidget* parent) -{ - QString path = lootAppImagePath(); - if (QFile::exists(path)) { - return true; - } - - log::info("LOOT AppImage not found, downloading from GitHub..."); - - QString url = fetchLootDownloadUrl(); - if (url.isEmpty()) { - QMessageBox::critical( - parent, QObject::tr("LOOT"), - QObject::tr("Could not find a LOOT release to download.\n\n" - "Please check https://github.com/%1/releases") - .arg(LootGitHubRepo)); - return false; - } - - return downloadLootAppImage(parent, url, path); -} - -// Map MO2's lootGameName() to LOOT's internal folder name (used by --game). -// LOOT's getDefaultLootFolderName() uses full names for some games. -static QString lootFolderName(const QString& mo2GameName) -{ - static const QMap map = { - {"SkyrimSE", "Skyrim Special Edition"}, - {"SkyrimVR", "Skyrim VR"}, - {"EnderalSE", "Enderal Special Edition"}, - {"Fallout4VR", "Fallout4VR"}, - }; - return map.value(mo2GameName, mo2GameName); -} - -// Write (or update) LOOT's settings.toml so that --game / --game-path work. -// We always overwrite the game path and local_path to match the current -// Fluorine instance, since the user may switch between instances. -static void ensureLootSettings(const QString& lootDataPath, - const QString& mo2GameName, - const QString& folderName, - const QString& gamePath, - const QString& localPath) -{ - QDir().mkpath(lootDataPath); - QDir().mkpath(localPath); - - // Map MO2 game names to LOOT masterlist repository names. - static const QMap masterlistRepos = { - {"FalloutNV", "falloutnv"}, {"Fallout3", "fallout3"}, - {"Fallout4", "fallout4"}, {"Fallout4VR", "fallout4vr"}, - {"Skyrim", "skyrim"}, {"SkyrimSE", "skyrimse"}, - {"SkyrimVR", "skyrimvr"}, {"Enderal", "enderal"}, - {"EnderalSE", "enderalse"}, {"Oblivion", "oblivion"}, - {"Morrowind", "morrowind"}, {"Starfield", "starfield"}, - {"Nehrim", "oblivion"}, - }; - QString repoName = masterlistRepos.value(mo2GameName, mo2GameName.toLower()); - QString masterlistUrl = - QString("https://raw.githubusercontent.com/loot/%1/v0.26/masterlist.yaml") - .arg(repoName); - - // Always rewrite the settings file so the path matches the current - // Fluorine instance. Use the "type" key (not "gameId") because LOOT's - // type parser accepts MO2's short names like "SkyrimSE". - QString toml = QString( - "updateMasterlist = true\n" - "\n" - "[[games]]\n" - "type = \"%1\"\n" - "name = \"%2\"\n" - "folder = \"%2\"\n" - "path = \"%3\"\n" - "local_path = \"%4\"\n" - "masterlistSource = \"%5\"\n" - "\n") - .arg(mo2GameName, folderName, gamePath, localPath, masterlistUrl); - - QString settingsPath = lootDataPath + "/settings.toml"; - QFile f(settingsPath); - if (f.open(QIODevice::WriteOnly | QIODevice::Text)) { - f.write(toml.toUtf8()); - f.close(); - log::info("Created LOOT settings at {}", settingsPath); - } -} - -// Launch the full LOOT GUI and wait for it to exit. -static bool launchLootGui(QWidget* parent, OrganizerCore& core) -{ - QString appImage = lootAppImagePath(); - if (!QFile::exists(appImage)) { - return false; - } - - QString mo2GameName = core.managedGame()->lootGameName(); - QString folderName = lootFolderName(mo2GameName); - QString gamePath = core.managedGame()->gameDirectory().absolutePath(); - QString lootDataPath = fluorineDataDir() + "/loot"; - - // Resolve the Fluorine prefix AppData/Local/ path — this is where - // LOOT auto-detects the plugins.txt / loadorder.txt from, so we must - // use it as local_path and deploy our load order files there. - QString localPath; - { - QString prefixBase = fluorineDataDir() + "/Prefix/pfx/drive_c/users/steamuser/AppData/Local"; - // Use documentsDirectory leaf name as the AppData/Local subfolder — - // this matches how Bethesda games map their local data folder. - QString dataDirName = core.managedGame()->documentsDirectory().dirName(); - if (dataDirName.isEmpty() || dataDirName == ".") { - dataDirName = core.managedGame()->gameShortName(); - } - QString candidate = prefixBase + "/" + dataDirName; - if (!dataDirName.isEmpty() && QDir(prefixBase).exists()) { - localPath = candidate; - } else { - localPath = lootDataPath + "/local/" + folderName; - } - } - - // Pre-seed LOOT settings so --game resolution works on first launch. - ensureLootSettings(lootDataPath, mo2GameName, folderName, gamePath, localPath); - - // Copy the profile's load order files to the local path so LOOT sees - // the current load order. MO2's plugins.txt omits implicitly-active - // plugins (base game ESMs) — LOOT needs every plugin listed or it warns - // about an "ambiguous load order". Merge loadorder.txt entries into - // plugins.txt so LOOT sees the complete picture. - QDir().mkpath(localPath); - QString profilePlugins = core.profilePath() + "/plugins.txt"; - QString profileLoadOrder = core.profilePath() + "/loadorder.txt"; - QString lootPlugins = localPath + "/plugins.txt"; - QString lootLoadOrder = localPath + "/loadorder.txt"; - - QFile::remove(lootPlugins); - QFile::remove(lootLoadOrder); - if (QFile::exists(profileLoadOrder)) { - QFile::copy(profileLoadOrder, lootLoadOrder); - } - if (QFile::exists(profilePlugins) && QFile::exists(profileLoadOrder)) { - // Build a set of active plugins from plugins.txt (case-insensitive). - QFile pluginsFile(profilePlugins); - QSet activePlugins; - if (pluginsFile.open(QIODevice::ReadOnly | QIODevice::Text)) { - QTextStream in(&pluginsFile); - while (!in.atEnd()) { - QString line = in.readLine().trimmed(); - if (line.startsWith('*')) - line = line.mid(1); - if (!line.isEmpty() && !line.startsWith('#')) - activePlugins.insert(line.toLower()); - } - pluginsFile.close(); - } - - // Use loadorder.txt as the canonical order. Mark each plugin as - // active (*) or inactive based on plugins.txt. Base game ESMs that - // aren't in plugins.txt are always active. - QFile loFile(profileLoadOrder); - QFile outFile(lootPlugins); - if (loFile.open(QIODevice::ReadOnly | QIODevice::Text) && - outFile.open(QIODevice::WriteOnly | QIODevice::Text)) { - QTextStream in(&loFile); - QTextStream out(&outFile); - while (!in.atEnd()) { - QString line = in.readLine().trimmed(); - if (line.isEmpty() || line.startsWith('#')) - continue; - bool active = activePlugins.contains(line.toLower()); - // Base game ESMs (not in plugins.txt) are implicitly active. - if (!active && !activePlugins.isEmpty() && - (line.endsWith(".esm", Qt::CaseInsensitive) || - line.endsWith(".esl", Qt::CaseInsensitive))) { - // Check if it's missing from plugins.txt entirely (base game file). - if (!activePlugins.contains(line.toLower())) - active = true; - } - out << (active ? "*" : "") << line << "\n"; - } - loFile.close(); - outFile.close(); - } - } else if (QFile::exists(profilePlugins)) { - QFile::copy(profilePlugins, lootPlugins); - } - - // Mount the FUSE VFS so LOOT sees the merged mod files in the Data directory. - log::info("Mounting VFS for LOOT..."); - core.prepareVFS(); - - QStringList args; - args << "--game" << folderName - << "--game-path" << gamePath - << "--loot-data-path" << lootDataPath; - - log::info("Launching LOOT GUI: {} {}", appImage, args.join(" ")); - - QProcess lootProcess; - lootProcess.setProgram(appImage); - lootProcess.setArguments(args); - - // Restore the original environment so LOOT's AppImage doesn't inherit - // Fluorine's bundled library/plugin paths (which are incompatible with - // LOOT's own bundled Qt). - QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); - QString origLdPath = env.value("FLUORINE_ORIG_LD_LIBRARY_PATH"); - if (!origLdPath.isEmpty()) { - env.insert("LD_LIBRARY_PATH", origLdPath); - } else { - env.remove("LD_LIBRARY_PATH"); - } - QString origQtPluginPath = env.value("FLUORINE_ORIG_QT_PLUGIN_PATH"); - if (!origQtPluginPath.isEmpty()) { - env.insert("QT_PLUGIN_PATH", origQtPluginPath); - } else { - env.remove("QT_PLUGIN_PATH"); - } - QString origPath = env.value("FLUORINE_ORIG_PATH"); - if (!origPath.isEmpty()) { - env.insert("PATH", origPath); - } - lootProcess.setProcessEnvironment(env); - - lootProcess.start(); - if (!lootProcess.waitForStarted(5000)) { - log::error("Failed to start LOOT: {}", lootProcess.errorString()); - QMessageBox::critical( - parent, QObject::tr("LOOT"), - QObject::tr("Failed to start LOOT:\n%1").arg(lootProcess.errorString())); - core.unmountVFS(); - return false; - } - - // Show a non-modal message while LOOT is running. - QMessageBox infoBox(QMessageBox::Information, QObject::tr("LOOT"), - QObject::tr("LOOT is running.\n\n" - "Sort your load order in LOOT, then close it " - "to apply the changes in Fluorine."), - QMessageBox::Cancel, parent); - infoBox.setWindowModality(Qt::WindowModal); - - // Poll for LOOT to exit or user to cancel. - QTimer pollTimer; - QObject::connect(&pollTimer, &QTimer::timeout, [&]() { - if (lootProcess.state() == QProcess::NotRunning) { - infoBox.accept(); - } - }); - pollTimer.start(500); - - int dialogResult = infoBox.exec(); - pollTimer.stop(); - - if (dialogResult != QMessageBox::Accepted && - lootProcess.state() == QProcess::Running) { - // User cancelled — kill LOOT - lootProcess.terminate(); - if (!lootProcess.waitForFinished(3000)) { - lootProcess.kill(); - lootProcess.waitForFinished(2000); - } - core.unmountVFS(); - return false; - } - - lootProcess.waitForFinished(-1); - - int exitCode = lootProcess.exitCode(); - log::info("LOOT exited with code {}", exitCode); - - // For FileTime-based games (FalloutNV, Fallout3, Oblivion), LOOT sets - // load order via file timestamps rather than modifying loadorder.txt. - // Read the plugin timestamps from the still-mounted VFS and write a - // sorted loadorder.txt before unmounting (timestamps are lost on unmount). - if (core.managedGame()->loadOrderMechanism() == - MOBase::IPluginGame::LoadOrderMechanism::FileTime) { - QString dataPath = core.managedGame()->dataDirectory().absolutePath(); - QDir dataDir(dataPath); - QStringList pluginFilters = {"*.esp", "*.esm", "*.esl"}; - - struct PluginMtime { - QString name; - qint64 mtime; - }; - std::vector plugins; - - for (const auto& entry : dataDir.entryInfoList(pluginFilters, QDir::Files)) { - plugins.push_back({entry.fileName(), - entry.lastModified().toSecsSinceEpoch()}); - } - - std::sort(plugins.begin(), plugins.end(), - [](const PluginMtime& a, const PluginMtime& b) { - return a.mtime < b.mtime; - }); - - if (!plugins.empty()) { - QFile lo(profileLoadOrder); - if (lo.open(QIODevice::WriteOnly | QIODevice::Text)) { - QTextStream out(&lo); - for (const auto& p : plugins) { - out << p.name << "\n"; - } - lo.close(); - log::info("Wrote sorted load order ({} plugins) from VFS timestamps", - plugins.size()); - } - } - } - - // Discard any COW'd files in staging (LOOT opens plugins with write access - // to set timestamps, which triggers copy-on-write — we don't want those - // copies ending up in overwrite). - core.discardVFSStagingOnUnmount(); - - // Unmount the VFS now that LOOT has finished. - log::info("Unmounting VFS after LOOT..."); - core.unmountVFS(); - - // Copy LOOT's updated load order files back to the profile. - // For FileTime games, we already wrote loadorder.txt from VFS timestamps - // above — skip the copy-back so we don't overwrite it with the old order. - bool isFileTime = core.managedGame()->loadOrderMechanism() == - MOBase::IPluginGame::LoadOrderMechanism::FileTime; - - // LOOT may write "Plugins.txt" (capital P) while we deployed "plugins.txt" - // (lowercase). On case-sensitive Linux these are two different files — the - // lowercase one is our pre-LOOT copy, the capital-P one is LOOT's output. - // Prefer the capital-P variant when it exists; otherwise fall back to the - // lowercase file (in case a future LOOT version writes lowercase). - QString lootPluginsActual; - { - QString capitalP = localPath + "/Plugins.txt"; - if (QFile::exists(capitalP)) { - lootPluginsActual = capitalP; - } else if (QFile::exists(lootPlugins)) { - lootPluginsActual = lootPlugins; - } - } - if (!lootPluginsActual.isEmpty()) { - QFile::remove(profilePlugins); - QFile::copy(lootPluginsActual, profilePlugins); - log::info("Copied LOOT {} back to profile", lootPluginsActual); - } - - bool isPluginsTxt = core.managedGame()->loadOrderMechanism() == - MOBase::IPluginGame::LoadOrderMechanism::PluginsTxt; - - if (!isFileTime && !isPluginsTxt) { - // For games that use a separate loadorder.txt (not FileTime, not - // PluginsTxt), copy LOOT's loadorder.txt back. - QString lootLoadOrderActual; - { - QString capitalL = localPath + "/Loadorder.txt"; - if (QFile::exists(capitalL)) { - lootLoadOrderActual = capitalL; - } else if (QFile::exists(lootLoadOrder)) { - lootLoadOrderActual = lootLoadOrder; - } - } - if (!lootLoadOrderActual.isEmpty()) { - QFile::remove(profileLoadOrder); - QFile::copy(lootLoadOrderActual, profileLoadOrder); - log::info("Copied LOOT {} back to profile", lootLoadOrderActual); - } - } - - if (isPluginsTxt && !lootPluginsActual.isEmpty()) { - // For PluginsTxt games (FO4, SkyrimSE, etc.), LOOT writes the sorted - // order into plugins.txt but does NOT update loadorder.txt. If we - // blindly copy the stale loadorder.txt back, readPluginLists() will - // prefer the old order from loadorder.txt over the freshly-sorted - // plugins.txt. Fix this by regenerating loadorder.txt from the - // sorted plugins.txt so both files are consistent. - QFile pluginsFile(profilePlugins); - if (pluginsFile.open(QIODevice::ReadOnly | QIODevice::Text)) { - QStringList sortedOrder; - while (!pluginsFile.atEnd()) { - QString line = QString::fromUtf8(pluginsFile.readLine()).trimmed(); - if (line.isEmpty() || line.startsWith('#')) - continue; - if (line.startsWith('*')) - line.remove(0, 1); - if (!line.isEmpty()) - sortedOrder.append(line); - } - pluginsFile.close(); - - if (!sortedOrder.isEmpty()) { - QFile lo(profileLoadOrder); - if (lo.open(QIODevice::WriteOnly | QIODevice::Text)) { - QTextStream out(&lo); - for (const auto& p : sortedOrder) { - out << p << "\n"; - } - lo.close(); - log::info("Regenerated loadorder.txt from LOOT-sorted plugins.txt " - "({} plugins)", sortedOrder.size()); - } - } - } - } - - return true; -} -#endif - -bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) -{ - core.savePluginList(); - -#ifndef _WIN32 - // Linux: download and launch the full LOOT GUI - Q_UNUSED(didUpdateMasterList); - - try { - if (!ensureLootAvailable(parent)) { - return false; - } - - return launchLootGui(parent, core); - } catch (const std::exception& e) { - reportError(QObject::tr("failed to run loot: %1").arg(e.what())); - return false; - } -#else - try { - Loot loot(core); - LootDialog dialog(parent, core, loot); - - if (!loot.start(parent, didUpdateMasterList)) { - return false; - } - - dialog.exec(); - - return dialog.result(); - } catch (const UsvfsConnectorException& e) { - log::debug("{}", e.what()); - return false; - } catch (const std::exception& e) { - reportError(QObject::tr("failed to run loot: %1").arg(e.what())); - return false; - } -#endif -} diff --git a/src/src/loot.h b/src/src/loot.h deleted file mode 100644 index 387ba76..0000000 --- a/src/src/loot.h +++ /dev/null @@ -1,149 +0,0 @@ -#ifndef MODORGANIZER_LOOT_H -#define MODORGANIZER_LOOT_H - -#include "envmodule.h" -#include -#include -#include -#ifdef _WIN32 -#include -#else -#include -#endif - -Q_DECLARE_METATYPE(lootcli::Progress); -Q_DECLARE_METATYPE(MOBase::log::Levels); - -class OrganizerCore; -class AsyncPipe; - -class Loot : public QObject -{ - Q_OBJECT; - -public: - struct Message - { - MOBase::log::Levels type; - QString text; - - QString toMarkdown() const; - }; - - struct File - { - QString name; - QString displayName; - }; - - struct Dirty - { - qint64 crc = 0; - qint64 itm = 0; - qint64 deletedReferences = 0; - qint64 deletedNavmesh = 0; - QString cleaningUtility; - QString info; - - QString toString(bool isClean) const; - QString toMarkdown(bool isClean) const; - QString cleaningString() const; - }; - - struct Plugin - { - QString name; - std::vector incompatibilities; - std::vector messages; - std::vector dirty, clean; - std::vector missingMasters; - bool loadsArchive = false; - bool isMaster = false; - bool isLightMaster = false; - - QString toMarkdown() const; - }; - - struct Stats - { - qint64 time = 0; - QString lootcliVersion; - QString lootVersion; - - QString toMarkdown() const; - }; - - struct Report - { - bool okay = false; - std::vector errors, warnings; - std::vector messages; - std::vector plugins; - Stats stats; - - QString toMarkdown() const; - - private: - QString successMarkdown() const; - QString errorsMarkdown() const; - }; - - Loot(OrganizerCore& core); - ~Loot(); - - bool start(QWidget* parent, bool didUpdateMasterList); - void cancel(); - bool result() const; - - const QString& outPath() const; - const Report& report() const; - const std::vector& errors() const; - const std::vector& warnings() const; - -signals: - void output(const QString& s); - void progress(const lootcli::Progress p); - void log(MOBase::log::Levels level, const QString& s) const; - void finished(); - -private: - OrganizerCore& m_core; - std::unique_ptr m_thread; - std::atomic m_cancel; - std::atomic m_result; - env::HandlePtr m_lootProcess; -#ifndef _WIN32 - pid_t m_childPid = -1; -#endif - std::unique_ptr m_pipe; - std::string m_outputBuffer; - std::vector m_errors, m_warnings; - Report m_report; - - bool spawnLootcli(QWidget* parent, bool didUpdateMasterList, - env::HandlePtr stdoutHandle); - - void lootThread(); - bool waitForCompletion(); - - void processStdout(const std::string& lootOut); - void processMessage(const lootcli::Message& m); - - Report createReport() const; - void processOutputFile(Report& r) const; - void deleteReportFile(); - - Message reportMessage(const QJsonObject& message) const; - std::vector reportPlugins(const QJsonArray& plugins) const; - Loot::Plugin reportPlugin(const QJsonObject& plugin) const; - Loot::Stats reportStats(const QJsonObject& stats) const; - - std::vector reportMessages(const QJsonArray& array) const; - std::vector reportFiles(const QJsonArray& array) const; - std::vector reportDirty(const QJsonArray& array) const; - std::vector reportStringArray(const QJsonArray& array) const; -}; - -bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList); - -#endif // MODORGANIZER_LOOT_H diff --git a/src/src/lootdialog.cpp b/src/src/lootdialog.cpp deleted file mode 100644 index 9b36f2d..0000000 --- a/src/src/lootdialog.cpp +++ /dev/null @@ -1,350 +0,0 @@ -#include "lootdialog.h" -#include "loot.h" -#include "organizercore.h" -#include "ui_lootdialog.h" -#ifdef MO2_WEBENGINE -#include -#endif -#include - -using namespace MOBase; - -QString progressToString(lootcli::Progress p) -{ - using P = lootcli::Progress; - - static const std::map map = { - {P::CheckingMasterlistExistence, QObject::tr("Checking masterlist existence")}, - {P::UpdatingMasterlist, QObject::tr("Updating masterlist")}, - {P::LoadingLists, QObject::tr("Loading lists")}, - {P::ReadingPlugins, QObject::tr("Reading plugins")}, - {P::SortingPlugins, QObject::tr("Sorting plugins")}, - {P::WritingLoadorder, QObject::tr("Writing loadorder.txt")}, - {P::ParsingLootMessages, QObject::tr("Parsing loot messages")}, - {P::Done, QObject::tr("Done")}}; - - auto itor = map.find(p); - if (itor == map.end()) { - return QString("unknown progress %1").arg(static_cast(p)); - } else { - return itor->second; - } -} - -MarkdownDocument::MarkdownDocument(QObject* parent) : QObject(parent) {} - -void MarkdownDocument::setText(const QString& text) -{ - if (m_text == text) - return; - - m_text = text; - emit textChanged(m_text); -} - -#ifdef MO2_WEBENGINE -MarkdownPage::MarkdownPage(QObject* parent) : QWebEnginePage(parent) {} - -bool MarkdownPage::acceptNavigationRequest(const QUrl& url, NavigationType, bool) -{ - static const QStringList allowed = {"qrc", "data"}; - - if (!allowed.contains(url.scheme())) { - shell::Open(url); - return false; - } - - return true; -} -#endif - -// Convert simple markdown to HTML for QTextBrowser -static QString markdownToHtml(const QString& md) -{ - QString html = ""; - - const auto lines = md.split('\n'); - bool inList = false; - - for (const auto& line : lines) { - QString trimmed = line.trimmed(); - - if (trimmed.isEmpty()) { - if (inList) { - html += ""; - inList = false; - } - html += "
"; - continue; - } - - // headers - if (trimmed.startsWith("#### ")) { - if (inList) { html += ""; inList = false; } - html += "

" + trimmed.mid(5).toHtmlEscaped() + "

"; - } else if (trimmed.startsWith("### ")) { - if (inList) { html += ""; inList = false; } - html += "

" + trimmed.mid(4).toHtmlEscaped() + "

"; - } else if (trimmed.startsWith("## ")) { - if (inList) { html += ""; inList = false; } - html += "

" + trimmed.mid(3).toHtmlEscaped() + "

"; - } else if (trimmed.startsWith(" - ") || trimmed.startsWith("- ")) { - if (!inList) { html += "
    "; inList = true; } - QString item = trimmed.startsWith(" - ") ? trimmed.mid(3) : trimmed.mid(2); - // bold - item.replace(QRegularExpression("\\*\\*(.+?)\\*\\*"), "\\1"); - html += "
  • " + item + "
  • "; - } else { - if (inList) { html += "
"; inList = false; } - // bold - trimmed.replace(QRegularExpression("\\*\\*(.+?)\\*\\*"), "\\1"); - // code - trimmed.replace(QRegularExpression("`(.+?)`"), "\\1"); - html += "

" + trimmed + "

"; - } - } - - if (inList) html += ""; - html += ""; - return html; -} - -LootDialog::LootDialog(QWidget* parent, OrganizerCore& core, Loot& loot) - : QDialog(parent, Qt::WindowMaximizeButtonHint), ui(new Ui::LootDialog), - m_core(core), m_loot(loot), m_finished(false), m_cancelling(false) -{ - createUI(); - - QObject::connect( - &m_loot, &Loot::output, this, - [&](auto&& s) { - addOutput(s); - }, - Qt::QueuedConnection); - - QObject::connect( - &m_loot, &Loot::progress, this, - [&](auto&& p) { - setProgress(p); - }, - Qt::QueuedConnection); - - QObject::connect( - &m_loot, &Loot::log, this, - [&](auto&& lv, auto&& s) { - log(lv, s); - }, - Qt::QueuedConnection); - - QObject::connect( - &m_loot, &Loot::finished, this, - [&] { - onFinished(); - }, - Qt::QueuedConnection); -} - -LootDialog::~LootDialog() = default; - -void LootDialog::setText(const QString& s) -{ - ui->progressText->setText(s); -} - -void LootDialog::setProgress(lootcli::Progress p) -{ - // don't overwrite the "stopping loot" message even if lootcli generates a new - // progress message - if (!m_cancelling) { - setText(progressToString(p)); - } - - if (p == lootcli::Progress::Done) { - ui->progressBar->setRange(0, 1); - ui->progressBar->setValue(1); - } -} - -void LootDialog::addOutput(const QString& s) -{ - if (m_core.settings().diagnostics().lootLogLevel() > lootcli::LogLevels::Debug) { - return; - } - - const auto lines = s.split(QRegularExpression("[\\r\\n]"), Qt::SkipEmptyParts); - - for (auto&& line : lines) { - if (line.isEmpty()) { - continue; - } - - addLineOutput(line); - } -} - -bool LootDialog::result() const -{ - return m_loot.result(); -} - -void LootDialog::cancel() -{ - if (!m_finished && !m_cancelling) { - log::debug("loot dialog: cancelling"); - m_loot.cancel(); - - setText(tr("Stopping LOOT...")); - addLineOutput("stopping loot"); - - ui->buttons->setEnabled(false); - m_cancelling = true; - } -} - -void LootDialog::openReport() -{ - const auto path = m_loot.outPath(); - shell::Open(path); -} - -int LootDialog::exec() -{ - auto& s = m_core.settings(); - - GeometrySaver gs(s, this); - s.geometry().restoreState(&m_expander); - - const auto r = QDialog::exec(); - - s.geometry().saveState(&m_expander); - - return r; -} - -void LootDialog::accept() -{ - // no-op -} - -void LootDialog::reject() -{ - if (m_finished) { - log::debug("loot dialog reject: loot finished, closing"); - QDialog::reject(); - } else { - log::debug("loot dialog reject: not finished, cancelling"); - cancel(); - } -} - -void LootDialog::createUI() -{ - ui->setupUi(this); - ui->progressBar->setMaximum(0); - -#ifdef MO2_WEBENGINE - auto* page = new MarkdownPage(this); - ui->report->setPage(page); - - auto* channel = new QWebChannel(this); - channel->registerObject("content", &m_report); - page->setWebChannel(channel); - - const QString path = QApplication::applicationDirPath() + "/resources/markdown.html"; - QFile f(path); - - if (f.open(QFile::ReadOnly)) { - const QString html = f.readAll(); - if (!html.isEmpty()) { - ui->report->setHtml(html); - } else { - log::error("failed to read '{}', {}", path, f.errorString()); - } - } else { - log::error("can't open '{}', {}", path, f.errorString()); - } -#else - ui->report->setHtml(markdownToHtml(tr("Running LOOT..."))); - connect(&m_report, &MarkdownDocument::textChanged, this, [this](const QString& md) { - ui->report->setHtml(markdownToHtml(md)); - }); -#endif - - m_expander.set(ui->details, ui->detailsPanel); - ui->openJsonReport->setEnabled(false); - connect(ui->openJsonReport, &QPushButton::clicked, [&] { - openReport(); - }); - - ui->buttons->setStandardButtons(QDialogButtonBox::Cancel); - - m_report.setText(tr("Running LOOT...")); - - resize(650, 450); - setSizeGripEnabled(true); -} - -void LootDialog::closeEvent(QCloseEvent* e) -{ - if (m_finished) { - log::debug("loot dialog close event: finished, closing"); - QDialog::closeEvent(e); - } else { - log::debug("loot dialog close event: not finished, cancelling"); - cancel(); - e->ignore(); - } -} - -void LootDialog::addLineOutput(const QString& line) -{ - ui->output->appendPlainText(line); -} - -void LootDialog::onFinished() -{ - log::debug("loot dialog: loot is finished"); - - m_finished = true; - - if (m_cancelling) { - log::debug("loot dialog: was cancelling, closing"); - close(); - } else { - log::debug("loot dialog: showing report"); - - showReport(); - - ui->openJsonReport->setEnabled(true); - ui->buttons->setStandardButtons(QDialogButtonBox::Close); - - // if loot failed, the Done progress won't be received; this makes sure - // the progress bar is stopped - setProgress(lootcli::Progress::Done); - } -} - -void LootDialog::log(log::Levels lv, const QString& s) -{ - if (lv >= log::Levels::Warning) { - log::log(lv, "{}", s); - } - - if (m_core.settings().diagnostics().lootLogLevel() > lootcli::LogLevels::Debug) { - addLineOutput(QString("[%1] %2").arg(log::levelToString(lv)).arg(s)); - } -} - -void LootDialog::showReport() -{ - const auto& lootReport = m_loot.report(); - - if (m_loot.result()) { - m_core.pluginList()->clearAdditionalInformation(); - for (auto&& p : lootReport.plugins) { - m_core.pluginList()->addLootReport(p.name, p); - } - } - - m_report.setText(lootReport.toMarkdown()); -} diff --git a/src/src/lootdialog.h b/src/src/lootdialog.h deleted file mode 100644 index 51e0aeb..0000000 --- a/src/src/lootdialog.h +++ /dev/null @@ -1,82 +0,0 @@ -#ifndef MODORGANIZER_LOOTDIALOG_H -#define MODORGANIZER_LOOTDIALOG_H - -#include -#include -#include - -namespace Ui -{ -class LootDialog; -} - -class OrganizerCore; -class Loot; - -class MarkdownDocument : public QObject -{ - Q_OBJECT; - Q_PROPERTY(QString text MEMBER m_text NOTIFY textChanged FINAL); - -public: - explicit MarkdownDocument(QObject* parent = nullptr); - void setText(const QString& text); - -signals: - void textChanged(const QString& text); - -private: - QString m_text; -}; - -#ifdef MO2_WEBENGINE -class MarkdownPage : public QWebEnginePage -{ - Q_OBJECT; - -public: - explicit MarkdownPage(QObject* parent = nullptr); - -protected: - bool acceptNavigationRequest(const QUrl& url, NavigationType, bool) override; -}; -#endif - -class LootDialog : public QDialog -{ - Q_OBJECT; - -public: - LootDialog(QWidget* parent, OrganizerCore& core, Loot& loot); - ~LootDialog(); - - void setText(const QString& s); - void setProgress(lootcli::Progress p); - - void addOutput(const QString& s); - bool result() const; - void cancel(); - void openReport(); - - int exec() override; - void accept() override; - void reject() override; - -private: - std::unique_ptr ui; - MOBase::ExpanderWidget m_expander; - OrganizerCore& m_core; - Loot& m_loot; - bool m_finished; - bool m_cancelling; - MarkdownDocument m_report; - - void createUI(); - void closeEvent(QCloseEvent* e) override; - void addLineOutput(const QString& line); - void onFinished(); - void log(MOBase::log::Levels lv, const QString& s); - void showReport(); -}; - -#endif // MODORGANIZER_LOOTDIALOG_H diff --git a/src/src/lootdialog.ui b/src/src/lootdialog.ui deleted file mode 100644 index b9e62f1..0000000 --- a/src/src/lootdialog.ui +++ /dev/null @@ -1,230 +0,0 @@ - - - LootDialog - - - - 0 - 0 - 457 - 600 - - - - LOOT - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Progress - - - - - - - 24 - - - false - - - - - - - QFrame::StyledPanel - - - QFrame::Sunken - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Details - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Open JSON report - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Close - - - - - - - - - - buttons - accepted() - LootDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttons - rejected() - LootDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/src/src/mainwindow.cpp b/src/src/mainwindow.cpp index d61273c..e148c24 100644 --- a/src/src/mainwindow.cpp +++ b/src/src/mainwindow.cpp @@ -204,8 +204,6 @@ QString UnmanagedModName() return QObject::tr(""); } -bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList); - void setFilterShortcuts(QWidget* widget, QLineEdit* edit) { auto activate = [=] { @@ -394,8 +392,6 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore, ui->groupCombo->installEventFilter(noWheel); ui->profileBox->installEventFilter(noWheel); - updateSortButton(); - connect(&m_PluginContainer, SIGNAL(diagnosisUpdate()), this, SLOT(scheduleCheckForProblems())); @@ -417,10 +413,6 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore, SLOT(updateAvailable())); connect(m_OrganizerCore.updater(), SIGNAL(motdAvailable(QString)), this, SLOT(motdReceived(QString))); - connect(&m_OrganizerCore, &OrganizerCore::refreshTriggered, this, [this]() { - updateSortButton(); - }); - connect(&NexusInterface::instance(), SIGNAL(requestNXMDownload(QString)), &m_OrganizerCore, SLOT(downloadRequestedNXM(QString))); connect(&NexusInterface::instance(), @@ -656,11 +648,6 @@ void MainWindow::resetButtonIcons() QIcon::fromTheme("edit-undo", QIcon(":/MO/gui/restore"))); ui->saveModsButton->setIcon( QIcon::fromTheme("document-save", QIcon(":/MO/gui/backup"))); -#ifdef _WIN32 - ui->sortButton->setIcon(QIcon::fromTheme("view-sort-ascending", QIcon(":/MO/gui/sort"))); -#else - ui->sortButton->setVisible(false); -#endif ui->restoreButton->setIcon( QIcon::fromTheme("edit-undo", QIcon(":/MO/gui/restore"))); ui->saveButton->setIcon(QIcon::fromTheme("document-save", QIcon(":/MO/gui/backup"))); @@ -669,9 +656,6 @@ void MainWindow::resetButtonIcons() ui->openFolderMenu->setIconSize(QSize(16, 16)); ui->restoreModsButton->setIconSize(QSize(16, 16)); ui->saveModsButton->setIconSize(QSize(16, 16)); -#ifdef _WIN32 - ui->sortButton->setIconSize(QSize(16, 16)); -#endif ui->restoreButton->setIconSize(QSize(16, 16)); ui->saveButton->setIconSize(QSize(16, 16)); } @@ -2967,8 +2951,6 @@ void MainWindow::on_actionSettings_triggered() m_OrganizerCore.refreshLists(); - updateSortButton(); - if (settings.paths().profiles() != oldProfilesDirectory) { refreshProfiles(); } @@ -3249,21 +3231,6 @@ void MainWindow::toggleUpdateAction() ui->actionUpdate->setVisible(s.checkForUpdates()); } -void MainWindow::updateSortButton() -{ -#ifdef _WIN32 - if (m_OrganizerCore.managedGame()->sortMechanism() != - IPluginGame::SortMechanism::NONE) { - ui->sortButton->setEnabled(true); - ui->sortButton->setToolTip(tr("Sort the plugins using LOOT.")); - } else { - ui->sortButton->setDisabled(true); - ui->sortButton->setToolTip(tr("There is no supported sort mechanism for this game. " - "You will probably have to use a third-party tool.")); - } -#endif -} - void MainWindow::nxmEndorsementsAvailable(QVariant userData, QVariant resultData, int) { QVariantList data = resultData.toList(); diff --git a/src/src/mainwindow.h b/src/src/mainwindow.h index 3e22fd1..b6c515a 100644 --- a/src/src/mainwindow.h +++ b/src/src/mainwindow.h @@ -249,8 +249,6 @@ private: void toggleMO2EndorseState(); void toggleUpdateAction(); - void updateSortButton(); - // update info struct NxmUpdateInfoData { @@ -440,10 +438,10 @@ private slots: void toolBar_customContextMenuRequested(const QPoint& point); void removeFromToolbar(QAction* action); - void about(); - - void resetActionIcons(); - void resetButtonIcons(); + void about(); + + void resetActionIcons(); + void resetButtonIcons(); private slots: // ui slots // actions diff --git a/src/src/mainwindow.ui b/src/src/mainwindow.ui index caeda51..317057b 100644 --- a/src/src/mainwindow.ui +++ b/src/src/mainwindow.ui @@ -806,23 +806,6 @@ - - - - Sort the plugins using LOOT. - - - Sort the plugins using LOOT. - - - Sort - - - - :/MO/gui/sort:/MO/gui/sort - - - diff --git a/src/src/markdowndocument.cpp b/src/src/markdowndocument.cpp new file mode 100644 index 0000000..ec2593a --- /dev/null +++ b/src/src/markdowndocument.cpp @@ -0,0 +1,33 @@ +#include "markdowndocument.h" + +#ifdef MO2_WEBENGINE +#include +#include +#endif + +MarkdownDocument::MarkdownDocument(QObject* parent) : QObject(parent) {} + +void MarkdownDocument::setText(const QString& text) +{ + if (m_text == text) + return; + + m_text = text; + emit textChanged(m_text); +} + +#ifdef MO2_WEBENGINE +MarkdownPage::MarkdownPage(QObject* parent) : QWebEnginePage(parent) {} + +bool MarkdownPage::acceptNavigationRequest(const QUrl& url, NavigationType, bool) +{ + static const QStringList allowed = {"qrc", "data"}; + + if (!allowed.contains(url.scheme())) { + MOBase::shell::Open(url); + return false; + } + + return true; +} +#endif diff --git a/src/src/markdowndocument.h b/src/src/markdowndocument.h new file mode 100644 index 0000000..18e33d9 --- /dev/null +++ b/src/src/markdowndocument.h @@ -0,0 +1,40 @@ +#ifndef MODORGANIZER_MARKDOWNDOCUMENT_H +#define MODORGANIZER_MARKDOWNDOCUMENT_H + +#include +#include + +#ifdef MO2_WEBENGINE +#include +#endif + +class MarkdownDocument : public QObject +{ + Q_OBJECT; + Q_PROPERTY(QString text MEMBER m_text NOTIFY textChanged FINAL); + +public: + explicit MarkdownDocument(QObject* parent = nullptr); + void setText(const QString& text); + +signals: + void textChanged(const QString& text); + +private: + QString m_text; +}; + +#ifdef MO2_WEBENGINE +class MarkdownPage : public QWebEnginePage +{ + Q_OBJECT; + +public: + explicit MarkdownPage(QObject* parent = nullptr); + +protected: + bool acceptNavigationRequest(const QUrl& url, NavigationType, bool) override; +}; +#endif + +#endif // MODORGANIZER_MARKDOWNDOCUMENT_H diff --git a/src/src/pluginlist.cpp b/src/src/pluginlist.cpp index 8fac338..3961dff 100644 --- a/src/src/pluginlist.cpp +++ b/src/src/pluginlist.cpp @@ -636,17 +636,6 @@ void PluginList::addInformation(const QString& name, const QString& message) } } -void PluginList::addLootReport(const QString& name, Loot::Plugin plugin) -{ - auto iter = m_ESPsByName.find(name); - - if (iter != m_ESPsByName.end()) { - m_AdditionalInfo[name].loot = std::move(plugin); - } else { - log::warn("failed to associate loot report for \"{}\"", name); - } -} - bool PluginList::isEnabled(int index) { return m_ESPs.at(index).enabled; @@ -662,10 +651,10 @@ void PluginList::readLockedOrderFrom(const QString& fileName) return; } - if (!file.open(QIODevice::ReadOnly)) { - log::error("failed to open locked order file '{}': {}", fileName, file.errorString()); - return; - } + if (!file.open(QIODevice::ReadOnly)) { + log::error("failed to open locked order file '{}': {}", fileName, file.errorString()); + return; + } int lineNumber = 0; while (!file.atEnd()) { QByteArray line = file.readLine(); @@ -1595,69 +1584,11 @@ QVariant PluginList::tooltipData(const QModelIndex& modelIndex) const toolTip += ""; } - - // loot - toolTip += makeLootTooltip(itor->second.loot); } return toolTip; } -QString PluginList::makeLootTooltip(const Loot::Plugin& loot) const -{ - QString s; - - for (auto&& f : loot.incompatibilities) { - s += "
  • " + - tr("Incompatible with %1") - .arg(f.displayName.isEmpty() ? f.name : f.displayName) + - "
  • "; - } - - for (auto&& m : loot.missingMasters) { - s += "
  • " + tr("Depends on missing %1").arg(m) + "
  • "; - } - - for (auto&& m : loot.messages) { - s += "
  • "; - - switch (m.type) { - case log::Warning: - s += tr("Warning") + ": "; - break; - - case log::Error: - s += tr("Error") + ": "; - break; - - case log::Info: // fall-through - case log::Debug: - default: - // nothing - break; - } - - s += m.text + "
  • "; - } - - for (auto&& d : loot.dirty) { - s += "
  • " + d.toString(false) + "
  • "; - } - - for (auto&& c : loot.clean) { - s += "
  • " + c.toString(true) + "
  • "; - } - - if (!s.isEmpty()) { - s = "
    " - "
      " + - s + "
    "; - } - - return s; -} - QVariant PluginList::iconData(const QModelIndex& modelIndex) const { int index = modelIndex.row(); @@ -1712,45 +1643,17 @@ QVariant PluginList::iconData(const QModelIndex& modelIndex) const result.append(":/MO/gui/unchecked-checkbox"); } - if (info && !info->loot.dirty.empty()) { - result.append(":/MO/gui/edit_clear"); - } - return result; } -bool PluginList::isProblematic(const ESPInfo& esp, const AdditionalInfo* info) const +bool PluginList::isProblematic(const ESPInfo& esp, const AdditionalInfo*) const { - if (esp.masterUnset.size() > 0) { - return true; - } - - if (info) { - if (!info->loot.incompatibilities.empty()) { - return true; - } - - if (!info->loot.missingMasters.empty()) { - return true; - } - } - - return false; + return esp.masterUnset.size() > 0; } -bool PluginList::hasInfo(const ESPInfo& esp, const AdditionalInfo* info) const +bool PluginList::hasInfo(const ESPInfo&, const AdditionalInfo* info) const { - if (info) { - if (!info->messages.empty()) { - return true; - } - - if (!info->loot.messages.empty()) { - return true; - } - } - - return false; + return info && !info->messages.empty(); } bool PluginList::setData(const QModelIndex& modIndex, const QVariant& value, int role) diff --git a/src/src/pluginlist.h b/src/src/pluginlist.h index 2cdf49a..56745ff 100644 --- a/src/src/pluginlist.h +++ b/src/src/pluginlist.h @@ -20,7 +20,6 @@ along with Mod Organizer. If not, see . #ifndef PLUGINLIST_H #define PLUGINLIST_H -#include "loot.h" #include "profile.h" #include #include @@ -161,17 +160,12 @@ public: void clearInformation(const QString& name); /** - * @brief add additional information on a mod (i.e. from loot) + * @brief add additional information on a mod * @param name name of the plugin to add information about * @param message the message to add to the plugin */ void addInformation(const QString& name, const QString& message); - /** - * adds information from a loot report - */ - void addLootReport(const QString& name, Loot::Plugin plugin); - /** * @brief test if a plugin is enabled * @@ -368,7 +362,6 @@ private: struct AdditionalInfo { QStringList messages; - Loot::Plugin loot; }; private: @@ -435,7 +428,6 @@ private: QVariant tooltipData(const QModelIndex& modelIndex) const; QVariant iconData(const QModelIndex& modelIndex) const; - QString makeLootTooltip(const Loot::Plugin& loot) const; bool isProblematic(const ESPInfo& esp, const AdditionalInfo* info) const; bool hasInfo(const ESPInfo& esp, const AdditionalInfo* info) const; }; diff --git a/src/src/pluginlistview.cpp b/src/src/pluginlistview.cpp index ffdae3d..7fc4c93 100644 --- a/src/src/pluginlistview.cpp +++ b/src/src/pluginlistview.cpp @@ -25,8 +25,7 @@ using namespace MOBase; PluginListView::PluginListView(QWidget* parent) : QTreeView(parent), m_sortProxy(nullptr), - m_Scrollbar(new ViewMarkingScrollBar(this, Qt::BackgroundRole)), - m_didUpdateMasterList(false) + m_Scrollbar(new ViewMarkingScrollBar(this, Qt::BackgroundRole)) { setVerticalScrollBar(m_Scrollbar); MOBase::setCustomizableColumns(this); @@ -177,50 +176,6 @@ void PluginListView::onFilterChanged(const QString& filter) updatePluginCount(); } -void PluginListView::onSortButtonClicked() -{ - const bool offline = m_core->settings().network().offlineMode(); - - auto r = QMessageBox::No; - - if (offline) { - r = QMessageBox::question(topLevelWidget(), tr("Sorting plugins"), - tr("Are you sure you want to sort your plugins list?") + - "\r\n\r\n" + - tr("Note: You are currently in offline mode and LOOT " - "will not update the master list."), - QMessageBox::Yes | QMessageBox::No); - } else { - r = QMessageBox::question(topLevelWidget(), tr("Sorting plugins"), - tr("Are you sure you want to sort your plugins list?"), - QMessageBox::Yes | QMessageBox::No); - } - - if (r != QMessageBox::Yes) { - return; - } - - m_core->savePluginList(); - - topLevelWidget()->setEnabled(false); - Guard g([=, this]() { - topLevelWidget()->setEnabled(true); - }); - - // don't try to update the master list in offline mode - const bool didUpdateMasterList = offline ? true : m_didUpdateMasterList; - - if (runLoot(topLevelWidget(), *m_core, didUpdateMasterList)) { - // don't assume the master list was updated in offline mode - if (!offline) { - m_didUpdateMasterList = true; - } - - m_core->refreshESPList(false); - m_core->savePluginList(); - } -} - std::pair PluginListView::selected() const { return {indexViewToModel(currentIndex()), @@ -258,13 +213,6 @@ void PluginListView::setup(OrganizerCore& core, MainWindow* mw, Ui::MainWindow* updatePluginCount(); }); -#ifdef _WIN32 - // sort - connect(mwui->sortButton, &QPushButton::clicked, [=, this] { - onSortButtonClicked(); - }); -#endif - // filter connect(ui.filter, &QLineEdit::textChanged, m_sortProxy, &PluginListSortProxy::updateFilter); diff --git a/src/src/pluginlistview.h b/src/src/pluginlistview.h index e27f290..3b47471 100644 --- a/src/src/pluginlistview.h +++ b/src/src/pluginlistview.h @@ -38,7 +38,6 @@ protected slots: void onDoubleClicked(const QModelIndex& index); void onFilterChanged(const QString& filter); - void onSortButtonClicked(); protected: friend class PluginListContextMenu; @@ -79,8 +78,6 @@ private: PluginListSortProxy* m_sortProxy; ModListViewActions* m_modActions; ViewMarkingScrollBar* m_Scrollbar; - - bool m_didUpdateMasterList; }; #endif // PLUGINLISTVIEW_H diff --git a/src/src/settings.cpp b/src/src/settings.cpp index fc6316e..74204c7 100644 --- a/src/src/settings.cpp +++ b/src/src/settings.cpp @@ -2429,17 +2429,6 @@ void DiagnosticsSettings::setLogLevel(log::Levels level) set(m_Settings, "Settings", "log_level", level); } -lootcli::LogLevels DiagnosticsSettings::lootLogLevel() const -{ - return get(m_Settings, "Settings", "loot_log_level", - lootcli::LogLevels::Info); -} - -void DiagnosticsSettings::setLootLogLevel(lootcli::LogLevels level) -{ - set(m_Settings, "Settings", "loot_log_level", level); -} - env::CoreDumpTypes DiagnosticsSettings::coreDumpType() const { return get(m_Settings, "Settings", "crash_dumps_type", diff --git a/src/src/settings.h b/src/src/settings.h index a4a9e89..8abb650 100644 --- a/src/src/settings.h +++ b/src/src/settings.h @@ -21,7 +21,6 @@ along with Mod Organizer. If not, see . #define SETTINGS_H #include "envdump.h" -#include #include #include #include @@ -703,15 +702,11 @@ class DiagnosticsSettings public: DiagnosticsSettings(QSettings& settings); - // log level for both MO and usvfs + // log level for MO // MOBase::log::Levels logLevel() const; void setLogLevel(MOBase::log::Levels level); - // log level for loot - lootcli::LogLevels lootLogLevel() const; - void setLootLogLevel(lootcli::LogLevels level); - // crash dump type for both MO and usvfs // env::CoreDumpTypes coreDumpType() const; diff --git a/src/src/settingsdialog.ui b/src/src/settingsdialog.ui index 3e1c54a..6b4ebc6 100644 --- a/src/src/settingsdialog.ui +++ b/src/src/settingsdialog.ui @@ -2440,28 +2440,6 @@ programs you are intentionally running.
    - - - - Integrated LOOT - - - - QFormLayout::FieldsStayAtSizeHint - - - - - LOOT Log Level - - - - - - - - - diff --git a/src/src/settingsdialogdiagnostics.cpp b/src/src/settingsdialogdiagnostics.cpp index ab3c922..267599d 100644 --- a/src/src/settingsdialogdiagnostics.cpp +++ b/src/src/settingsdialogdiagnostics.cpp @@ -10,7 +10,6 @@ DiagnosticsSettingsTab::DiagnosticsSettingsTab(Settings& s, SettingsDialog& d) : SettingsTab(s, d) { setLogLevel(); - setLootLogLevel(); setCrashDumpTypesBox(); ui->dumpsMaxEdit->setValue(settings().diagnostics().maxCoreDumps()); @@ -49,32 +48,6 @@ void DiagnosticsSettingsTab::setLogLevel() } } -void DiagnosticsSettingsTab::setLootLogLevel() -{ - using L = lootcli::LogLevels; - - auto v = [](L level) { - return QVariant(static_cast(level)); - }; - - ui->lootLogLevel->clear(); - - ui->lootLogLevel->addItem(QObject::tr("Trace"), v(L::Trace)); - ui->lootLogLevel->addItem(QObject::tr("Debug"), v(L::Debug)); - ui->lootLogLevel->addItem(QObject::tr("Info (recommended)"), v(L::Info)); - ui->lootLogLevel->addItem(QObject::tr("Warning"), v(L::Warning)); - ui->lootLogLevel->addItem(QObject::tr("Error"), v(L::Error)); - - const auto sel = settings().diagnostics().lootLogLevel(); - - for (int i = 0; i < ui->lootLogLevel->count(); ++i) { - if (ui->lootLogLevel->itemData(i) == v(sel)) { - ui->lootLogLevel->setCurrentIndex(i); - break; - } - } -} - void DiagnosticsSettingsTab::setCrashDumpTypesBox() { ui->dumpsTypeBox->clear(); @@ -107,7 +80,4 @@ void DiagnosticsSettingsTab::update() static_cast(ui->dumpsTypeBox->currentData().toInt())); settings().diagnostics().setMaxCoreDumps(ui->dumpsMaxEdit->value()); - - settings().diagnostics().setLootLogLevel( - static_cast(ui->lootLogLevel->currentData().toInt())); } diff --git a/src/src/settingsdialogdiagnostics.h b/src/src/settingsdialogdiagnostics.h index 0888ced..23a5ed5 100644 --- a/src/src/settingsdialogdiagnostics.h +++ b/src/src/settingsdialogdiagnostics.h @@ -13,7 +13,6 @@ public: private: void setLogLevel(); - void setLootLogLevel(); void setCrashDumpTypesBox(); }; diff --git a/src/src/updatedialog.cpp b/src/src/updatedialog.cpp index ac019fb..ba81c87 100644 --- a/src/src/updatedialog.cpp +++ b/src/src/updatedialog.cpp @@ -2,7 +2,6 @@ #include "ui_updatedialog.h" #ifdef MO2_WEBENGINE -#include "lootdialog.h" // for MarkdownPage #include #endif diff --git a/src/src/updatedialog.h b/src/src/updatedialog.h index ca49a23..ec8e762 100644 --- a/src/src/updatedialog.h +++ b/src/src/updatedialog.h @@ -3,7 +3,7 @@ #include -#include "lootdialog.h" // for MarkdownDocument +#include "markdowndocument.h" #include namespace Ui -- cgit v1.3.1