aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt1
-rw-r--r--docker/Dockerfile29
-rwxr-xr-xdocker/build-inner.sh18
-rw-r--r--libs/lootcli/.clang-format41
-rw-r--r--libs/lootcli/.git-blame-ignore-revs2
-rw-r--r--libs/lootcli/.gitattributes7
-rw-r--r--libs/lootcli/.github/workflows/build.yml18
-rw-r--r--libs/lootcli/.github/workflows/linting.yml15
-rw-r--r--libs/lootcli/.gitignore5
-rw-r--r--libs/lootcli/.pre-commit-config.yaml20
-rw-r--r--libs/lootcli/CMakeLists.txt32
-rw-r--r--libs/lootcli/CMakePresets.json52
-rw-r--r--libs/lootcli/README.md3
-rw-r--r--libs/lootcli/cmake/config.cmake.in3
-rw-r--r--libs/lootcli/include/lootcli/lootcli.h116
-rw-r--r--libs/lootcli/src/CMakeLists.txt138
-rw-r--r--libs/lootcli/src/game_settings.cpp435
-rw-r--r--libs/lootcli/src/game_settings.h101
-rw-r--r--libs/lootcli/src/lootthread.cpp1197
-rw-r--r--libs/lootcli/src/lootthread.h107
-rw-r--r--libs/lootcli/src/main.cpp102
-rw-r--r--libs/lootcli/src/pch.cpp1
-rw-r--r--libs/lootcli/src/pch.h84
-rw-r--r--libs/lootcli/src/version.h4
-rw-r--r--libs/lootcli/src/version.rc35
-rw-r--r--libs/lootcli/vcpkg.json30
-rw-r--r--src/src/CMakeLists.txt1
-rw-r--r--src/src/loot.cpp1674
-rw-r--r--src/src/loot.h149
-rw-r--r--src/src/lootdialog.cpp350
-rw-r--r--src/src/lootdialog.h82
-rw-r--r--src/src/lootdialog.ui230
-rw-r--r--src/src/mainwindow.cpp33
-rw-r--r--src/src/mainwindow.h10
-rw-r--r--src/src/mainwindow.ui17
-rw-r--r--src/src/markdowndocument.cpp33
-rw-r--r--src/src/markdowndocument.h40
-rw-r--r--src/src/pluginlist.cpp113
-rw-r--r--src/src/pluginlist.h10
-rw-r--r--src/src/pluginlistview.cpp54
-rw-r--r--src/src/pluginlistview.h3
-rw-r--r--src/src/settings.cpp11
-rw-r--r--src/src/settings.h7
-rw-r--r--src/src/settingsdialog.ui22
-rw-r--r--src/src/settingsdialogdiagnostics.cpp30
-rw-r--r--src/src/settingsdialogdiagnostics.h1
-rw-r--r--src/src/updatedialog.cpp1
-rw-r--r--src/src/updatedialog.h2
48 files changed, 89 insertions, 5380 deletions
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 <regex>
-
-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<std::string_view::const_iterator> 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<Progress>(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
- $<$<CONFIG:RelWithDebInfo>:/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
- $<TARGET_FILE:lootcli>
- $<TARGET_FILE:libloot::loot>
- 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 <cstdlib>
-
-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 <filesystem>
-#include <optional>
-#include <set>
-#include <string>
-#include <vector>
-
-#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 <codecvt>
-#include <fstream>
-
-#ifdef _WIN32
-#include <strsafe.h>
-#include <windows.h>
-#include <winhttp.h>
-#else
-#include <curl/curl.h>
-#include <cstring>
-#endif
-
-// using namespace loot;
-namespace fs = std::filesystem;
-
-using std::lock_guard;
-using std::recursive_mutex;
-
-namespace lootcli
-{
-static const std::set<std::string>
- 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<char>(std::tolower(c));
- });
-
- return text;
-}
-
-void LOOTWorker::setGame(const std::string& gameName)
-{
- static std::map<std::string, loot::GameId> 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<recursive_mutex> 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<std::string>();
- 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<std::string>();
- if (!folder) {
- throw std::runtime_error("'folder' key missing from game settings table");
- }
-
- const auto type = gameTable["type"].value<std::string>();
-
- // 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<std::string>();
- if (name) {
- newSettings.SetName(*name);
- }
-
- auto master = gameTable["master"].value<std::string>();
- if (master) {
- newSettings.SetMaster(*master);
- }
-
- const auto minimumHeaderVersion =
- gameTable["minimumHeaderVersion"].value<double>();
- if (minimumHeaderVersion) {
- newSettings.SetMinimumHeaderVersion((float)*minimumHeaderVersion);
- }
-
- auto source = gameTable["masterlistSource"].value<std::string>();
- if (source) {
- newSettings.SetMasterlistSource(migrateMasterlistSource(*source));
- } else {
- auto url = gameTable["repo"].value<std::string>();
- auto branch = gameTable["branch"].value<std::string>();
- auto migratedSource =
- migrateMasterlistRepoSettings(newSettings.Id(), *url, *branch);
- if (migratedSource.has_value()) {
- newSettings.SetMasterlistSource(migratedSource.value());
- }
- }
-
- auto path = gameTable["path"].value<std::string>();
- if (path) {
- newSettings.SetGamePath(std::filesystem::path(*path));
- }
-
- auto localPath = gameTable["local_path"].value<std::string>();
- auto localFolder = gameTable["local_folder"].value<std::string>();
- 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<std::string> LOOTWorker::GetLocalFolder(const toml::table& table)
-{
- const auto localPath = table["local_path"].value<std::string>();
- const auto localFolder = table["local_folder"].value<std::string>();
-
- 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<std::string>();
-
- 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<std::string>();
- const auto masterFilename = table["master"].value<std::string>();
- const auto isBaseGameInstance = table["isBaseGameInstance"].value<bool>();
- const auto folder = table["folder"].value<std::string>();
-
- 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<std::string>();
-
- 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<std::string>();
- const auto isBaseGameInstance = table["isBaseGameInstance"].value<bool>();
- const auto localFolder = GetLocalFolder(table);
- const auto folder = table["folder"].value<std::string>();
-
- 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<std::underlying_type_t<loot::GameId>>(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<std::string>
-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<std::string> 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<std::codecvt_utf8_utf16<wchar_t>> 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<FILE*>(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<loot::GameInterface> 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<fs::path> 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<std::codecvt_utf8_utf16<wchar_t>> 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<std::filesystem::path> pluginsList;
- for (auto plugin : gameHandle->GetLoadOrder()) {
- std::filesystem::path pluginPath(plugin);
- pluginsList.push_back(pluginPath);
- }
- gameHandle->LoadPlugins(pluginsList, false);
-
- progress(Progress::SortingPlugins);
- std::vector<std::string> 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<std::string>& 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<qint64>(std::chrono::duration_cast<std::chrono::milliseconds>(
- end - m_startTime)
- .count())},
- {"lootcliVersion", LOOTCLI_VERSION_STRING},
- {"lootVersion", QString::fromStdString(loot::GetLiblootVersion())}});
-
- QJsonDocument doc(root);
- return doc.toJson(QJsonDocument::Indented).toStdString();
-}
-
-template <class Container>
-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<std::string>& 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<loot::Message>& 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<loot::PluginCleaningData>& data) const
-{
- QJsonArray array;
-
- for (const auto& d : data) {
- QJsonObject o{
- {"crc", static_cast<qint64>(d.GetCRC())},
- {"itm", static_cast<qint64>(d.GetITMCount())},
- {"deletedReferences", static_cast<qint64>(d.GetDeletedReferenceCount())},
- {"deletedNavmesh", static_cast<qint64>(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<loot::PluginCleaningData>& data) const
-{
- QJsonArray array;
-
- for (const auto& d : data) {
- QJsonObject o{
- {"crc", static_cast<qint64>(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<loot::File>& data) const
-{
- QJsonArray array;
-
- for (auto&& f : data) {
- const auto n = static_cast<std::string>(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<int>(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 <lootcli/lootcli.h>
-
-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<std::string> 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<std::string> 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 <typename T> T resolveVariable(HMODULE lib, const char *name);
- // template <typename T> 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<std::string>& sortedPlugins) const;
-
- QJsonArray createPlugins(loot::GameInterface& game,
- const std::vector<std::string>& sortedPlugins) const;
-
- QJsonValue createMessages(const std::vector<loot::Message>& list) const;
- QJsonValue createDirty(const std::vector<loot::PluginCleaningData>& data) const;
- QJsonValue createClean(const std::vector<loot::PluginCleaningData>& data) const;
-
- QJsonValue createIncompatibilities(loot::GameInterface& game,
- const std::vector<loot::File>& 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 <lootcli/lootcli.h>
-
-using namespace std;
-
-template <typename T>
-T getParameter(const std::vector<std::string>& 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<T>(*(iter + 1));
- } else {
- throw std::runtime_error(std::string("argument missing " + key));
- }
-}
-
-template <>
-bool getParameter<bool>(const std::vector<std::string>& 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 <typename T>
-T getOptionalParameter(const std::vector<std::string>& arguments,
- const std::string& key, T def)
-{
- try {
- return getParameter<T>(arguments, key);
- } catch (std::runtime_error&) {
- return def;
- }
-}
-
-loot::LogLevel getLogLevel(const std::vector<std::string>& arguments)
-{
- const auto s = getOptionalParameter<std::string>(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<std::string> arguments;
- int argc;
- LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
-
- if (argv) {
- for (int i = 0; i < argc; ++i) {
- size_t num_converted;
- std::vector<char> 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<std::string> 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<bool>(arguments, "skipUpdateMasterlist"));
- worker.setGame(getParameter<std::string>(arguments, "game"));
- worker.setGamePath(getParameter<std::string>(arguments, "gamePath"));
- worker.setPluginListPath(getParameter<std::string>(arguments, "pluginListPath"));
- worker.setOutput(getParameter<std::string>(arguments, "out"));
- worker.setLogLevel(getLogLevel(arguments));
-
- const auto lang = getOptionalParameter<std::string>(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 <algorithm>
-#include <cstdint>
-#include <cstring>
-#include <exception>
-#include <filesystem>
-#include <fstream>
-#include <iostream>
-#include <locale>
-#include <map>
-#include <memory>
-#include <mutex>
-#include <optional>
-#include <regex>
-#include <set>
-#include <sstream>
-#include <stdexcept>
-#include <string>
-#include <thread>
-#include <utility>
-#include <vector>
-
-// qt
-#include <QDir>
-#include <QFileInfo>
-#include <QJsonArray>
-#include <QJsonDocument>
-#include <QJsonObject>
-
-// boost
-#include <boost/algorithm/string.hpp>
-#include <boost/lexical_cast.hpp>
-#include <boost/locale.hpp>
-
-// loot
-#include <loot/api.h>
-#include <loot/enum/game_type.h>
-
-// third-party
-#include <toml++/toml.h>
-
-#ifdef _WIN32
-// windows
-#define WIN32_LEAN_AND_MEAN
-#include <Shlobj.h>
-#include <Windows.h>
-#include <fcntl.h>
-#include <io.h>
-#include <shellapi.h>
-#else
-// Linux equivalents
-#include <cstdlib>
-#include <unistd.h>
-#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 <log.h>
-#include <report.h>
-
-#ifndef _WIN32
-#include "fluorinepaths.h"
-#include <poll.h>
-#include <fcntl.h>
-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<HANDLE>(static_cast<intptr_t>(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<std::string> argStrs;
- argStrs.push_back(binaryStr);
- for (const auto& p : parameters) {
- argStrs.push_back(p.toStdString());
- }
-
- std::vector<char*> 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<QString>& Loot::errors() const
-{
- return m_errors;
-}
-
-const std::vector<QString>& 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<QJsonArray>(object, "messages"));
- r.plugins = reportPlugins(getOpt<QJsonArray>(object, "plugins"));
- r.stats = reportStats(getWarn<QJsonObject>(object, "stats"));
-}
-
-std::vector<Loot::Plugin> Loot::reportPlugins(const QJsonArray& plugins) const
-{
- std::vector<Loot::Plugin> v;
-
- for (auto pluginValue : plugins) {
- const auto o = convertWarn<QJsonObject>(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<QString>(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<QJsonArray>(plugin, "incompatibilities"));
- }
-
- if (plugin.contains("messages")) {
- p.messages = reportMessages(getOpt<QJsonArray>(plugin, "messages"));
- }
-
- if (plugin.contains("dirty")) {
- p.dirty = reportDirty(getOpt<QJsonArray>(plugin, "dirty"));
- }
-
- if (plugin.contains("clean")) {
- p.clean = reportDirty(getOpt<QJsonArray>(plugin, "clean"));
- }
-
- if (plugin.contains("missingMasters")) {
- p.missingMasters = reportStringArray(getOpt<QJsonArray>(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<qint64>(stats, "time");
- s.lootcliVersion = getWarn<QString>(stats, "lootcliVersion");
- s.lootVersion = getWarn<QString>(stats, "lootVersion");
-
- return s;
-}
-
-std::vector<Loot::Message> Loot::reportMessages(const QJsonArray& array) const
-{
- std::vector<Loot::Message> v;
-
- for (auto messageValue : array) {
- const auto o = convertWarn<QJsonObject>(messageValue, "message");
- if (o.isEmpty()) {
- continue;
- }
-
- Message m;
-
- const auto type = getWarn<QString>(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<QString>(o, "text");
-
- if (!m.text.isEmpty()) {
- v.emplace_back(std::move(m));
- }
- }
-
- return v;
-}
-
-std::vector<Loot::File> Loot::reportFiles(const QJsonArray& array) const
-{
- std::vector<Loot::File> v;
-
- for (auto&& fileValue : array) {
- const auto o = convertWarn<QJsonObject>(fileValue, "file");
- if (o.isEmpty()) {
- continue;
- }
-
- File f;
-
- f.name = getWarn<QString>(o, "name");
- f.displayName = getOpt<QString>(o, "displayName");
-
- if (!f.name.isEmpty()) {
- v.emplace_back(std::move(f));
- }
- }
-
- return v;
-}
-
-std::vector<Loot::Dirty> Loot::reportDirty(const QJsonArray& array) const
-{
- std::vector<Loot::Dirty> v;
-
- for (auto&& dirtyValue : array) {
- const auto o = convertWarn<QJsonObject>(dirtyValue, "dirty");
-
- Dirty d;
-
- d.crc = getWarn<qint64>(o, "crc");
- d.itm = getOpt<qint64>(o, "itm");
- d.deletedReferences = getOpt<qint64>(o, "deletedReferences");
- d.deletedNavmesh = getOpt<qint64>(o, "deletedNavmesh");
- d.cleaningUtility = getOpt<QString>(o, "cleaningUtility");
- d.info = getOpt<QString>(o, "info");
-
- v.emplace_back(std::move(d));
- }
-
- return v;
-}
-
-std::vector<QString> Loot::reportStringArray(const QJsonArray& array) const
-{
- std::vector<QString> v;
-
- for (auto&& sv : array) {
- auto s = convertWarn<QString>(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<int>(total / 1024));
- progress.setValue(static_cast<int>(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<QString, QString> 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<QString, QString> 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/<game> 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<QString> 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<PluginMtime> 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 <QWidget>
-#include <log.h>
-#include <lootcli/lootcli.h>
-#ifdef _WIN32
-#include <windows.h>
-#else
-#include <sys/types.h>
-#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<File> incompatibilities;
- std::vector<Message> messages;
- std::vector<Dirty> dirty, clean;
- std::vector<QString> 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<QString> errors, warnings;
- std::vector<Message> messages;
- std::vector<Plugin> 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<QString>& errors() const;
- const std::vector<QString>& 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<QThread> m_thread;
- std::atomic<bool> m_cancel;
- std::atomic<bool> m_result;
- env::HandlePtr m_lootProcess;
-#ifndef _WIN32
- pid_t m_childPid = -1;
-#endif
- std::unique_ptr<AsyncPipe> m_pipe;
- std::string m_outputBuffer;
- std::vector<QString> 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<Plugin> reportPlugins(const QJsonArray& plugins) const;
- Loot::Plugin reportPlugin(const QJsonObject& plugin) const;
- Loot::Stats reportStats(const QJsonObject& stats) const;
-
- std::vector<Message> reportMessages(const QJsonArray& array) const;
- std::vector<Loot::File> reportFiles(const QJsonArray& array) const;
- std::vector<Loot::Dirty> reportDirty(const QJsonArray& array) const;
- std::vector<QString> 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 <QWebChannel>
-#endif
-#include <utility.h>
-
-using namespace MOBase;
-
-QString progressToString(lootcli::Progress p)
-{
- using P = lootcli::Progress;
-
- static const std::map<P, QString> 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<int>(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 = "<html><body style='font-family: sans-serif; font-size: 10pt;'>";
-
- const auto lines = md.split('\n');
- bool inList = false;
-
- for (const auto& line : lines) {
- QString trimmed = line.trimmed();
-
- if (trimmed.isEmpty()) {
- if (inList) {
- html += "</ul>";
- inList = false;
- }
- html += "<br>";
- continue;
- }
-
- // headers
- if (trimmed.startsWith("#### ")) {
- if (inList) { html += "</ul>"; inList = false; }
- html += "<h4>" + trimmed.mid(5).toHtmlEscaped() + "</h4>";
- } else if (trimmed.startsWith("### ")) {
- if (inList) { html += "</ul>"; inList = false; }
- html += "<h3>" + trimmed.mid(4).toHtmlEscaped() + "</h3>";
- } else if (trimmed.startsWith("## ")) {
- if (inList) { html += "</ul>"; inList = false; }
- html += "<h2>" + trimmed.mid(3).toHtmlEscaped() + "</h2>";
- } else if (trimmed.startsWith(" - ") || trimmed.startsWith("- ")) {
- if (!inList) { html += "<ul>"; inList = true; }
- QString item = trimmed.startsWith(" - ") ? trimmed.mid(3) : trimmed.mid(2);
- // bold
- item.replace(QRegularExpression("\\*\\*(.+?)\\*\\*"), "<b>\\1</b>");
- html += "<li>" + item + "</li>";
- } else {
- if (inList) { html += "</ul>"; inList = false; }
- // bold
- trimmed.replace(QRegularExpression("\\*\\*(.+?)\\*\\*"), "<b>\\1</b>");
- // code
- trimmed.replace(QRegularExpression("`(.+?)`"), "<code>\\1</code>");
- html += "<p>" + trimmed + "</p>";
- }
- }
-
- if (inList) html += "</ul>";
- html += "</body></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 <expanderwidget.h>
-#include <log.h>
-#include <lootcli/lootcli.h>
-
-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::LootDialog> 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 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>LootDialog</class>
- <widget class="QDialog" name="LootDialog">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>457</width>
- <height>600</height>
- </rect>
- </property>
- <property name="windowTitle">
- <string>LOOT</string>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout">
- <item>
- <widget class="QWidget" name="widget" native="true">
- <layout class="QVBoxLayout" name="verticalLayout_5" stretch="1,0">
- <property name="leftMargin">
- <number>0</number>
- </property>
- <property name="topMargin">
- <number>0</number>
- </property>
- <property name="rightMargin">
- <number>0</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- <item>
- <widget class="QWidget" name="widget_2" native="true">
- <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,1">
- <property name="leftMargin">
- <number>0</number>
- </property>
- <property name="topMargin">
- <number>0</number>
- </property>
- <property name="rightMargin">
- <number>0</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- <item>
- <widget class="QLabel" name="progressText">
- <property name="text">
- <string>Progress</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QProgressBar" name="progressBar">
- <property name="value">
- <number>24</number>
- </property>
- <property name="textVisible">
- <bool>false</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QFrame" name="frame">
- <property name="frameShape">
- <enum>QFrame::StyledPanel</enum>
- </property>
- <property name="frameShadow">
- <enum>QFrame::Sunken</enum>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout_6">
- <property name="leftMargin">
- <number>0</number>
- </property>
- <property name="topMargin">
- <number>0</number>
- </property>
- <property name="rightMargin">
- <number>0</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- <item>
- <widget class="QTextBrowser" name="report">
- </widget>
- </item>
- </layout>
- </widget>
- </item>
- </layout>
- </widget>
- </item>
- <item>
- <widget class="QWidget" name="widget_3" native="true">
- <layout class="QVBoxLayout" name="verticalLayout_4">
- <property name="leftMargin">
- <number>0</number>
- </property>
- <property name="topMargin">
- <number>0</number>
- </property>
- <property name="rightMargin">
- <number>0</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- <item>
- <widget class="QToolButton" name="details">
- <property name="text">
- <string>Details</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QWidget" name="detailsPanel" native="true">
- <layout class="QVBoxLayout" name="verticalLayout_3">
- <property name="leftMargin">
- <number>0</number>
- </property>
- <property name="topMargin">
- <number>0</number>
- </property>
- <property name="rightMargin">
- <number>0</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- <item>
- <widget class="QWidget" name="widget_5" native="true">
- <layout class="QHBoxLayout" name="horizontalLayout">
- <property name="leftMargin">
- <number>0</number>
- </property>
- <property name="topMargin">
- <number>0</number>
- </property>
- <property name="rightMargin">
- <number>0</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- <item>
- <widget class="QPushButton" name="openJsonReport">
- <property name="text">
- <string>Open JSON report</string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
- </widget>
- </item>
- <item>
- <widget class="QPlainTextEdit" name="output"/>
- </item>
- </layout>
- </widget>
- </item>
- </layout>
- </widget>
- </item>
- </layout>
- </widget>
- </item>
- <item>
- <widget class="QDialogButtonBox" name="buttons">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="standardButtons">
- <set>QDialogButtonBox::Close</set>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- <customwidgets/>
- <resources/>
- <connections>
- <connection>
- <sender>buttons</sender>
- <signal>accepted()</signal>
- <receiver>LootDialog</receiver>
- <slot>accept()</slot>
- <hints>
- <hint type="sourcelabel">
- <x>248</x>
- <y>254</y>
- </hint>
- <hint type="destinationlabel">
- <x>157</x>
- <y>274</y>
- </hint>
- </hints>
- </connection>
- <connection>
- <sender>buttons</sender>
- <signal>rejected()</signal>
- <receiver>LootDialog</receiver>
- <slot>reject()</slot>
- <hints>
- <hint type="sourcelabel">
- <x>316</x>
- <y>260</y>
- </hint>
- <hint type="destinationlabel">
- <x>286</x>
- <y>274</y>
- </hint>
- </hints>
- </connection>
- </connections>
-</ui>
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("<Unmanaged>");
}
-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
@@ -807,23 +807,6 @@
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
- <widget class="QPushButton" name="sortButton">
- <property name="toolTip">
- <string>Sort the plugins using LOOT.</string>
- </property>
- <property name="whatsThis">
- <string>Sort the plugins using LOOT.</string>
- </property>
- <property name="text">
- <string>Sort</string>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/sort</normaloff>:/MO/gui/sort</iconset>
- </property>
- </widget>
- </item>
- <item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
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 <shared/util.h>
+#include <utility.h>
+#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 <QObject>
+#include <QString>
+
+#ifdef MO2_WEBENGINE
+#include <QWebEnginePage>
+#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 += "</ul>";
}
-
- // loot
- toolTip += makeLootTooltip(itor->second.loot);
}
return toolTip;
}
-QString PluginList::makeLootTooltip(const Loot::Plugin& loot) const
-{
- QString s;
-
- for (auto&& f : loot.incompatibilities) {
- s += "<li>" +
- tr("Incompatible with %1")
- .arg(f.displayName.isEmpty() ? f.name : f.displayName) +
- "</li>";
- }
-
- for (auto&& m : loot.missingMasters) {
- s += "<li>" + tr("Depends on missing %1").arg(m) + "</li>";
- }
-
- for (auto&& m : loot.messages) {
- s += "<li>";
-
- 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 + "</li>";
- }
-
- for (auto&& d : loot.dirty) {
- s += "<li>" + d.toString(false) + "</li>";
- }
-
- for (auto&& c : loot.clean) {
- s += "<li>" + c.toString(true) + "</li>";
- }
-
- if (!s.isEmpty()) {
- s = "<hr>"
- "<ul style=\"margin-top:0px; padding-top:0px; margin-left:15px; "
- "-qt-list-indent: 0;\">" +
- s + "</ul>";
- }
-
- 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 <http://www.gnu.org/licenses/>.
#ifndef PLUGINLIST_H
#define PLUGINLIST_H
-#include "loot.h"
#include "profile.h"
#include <ifiletree.h>
#include <ipluginlist.h>
@@ -161,18 +160,13 @@ 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
*
* @param index index of the plugin to look up
@@ -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<QModelIndex, QModelIndexList> 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<lootcli::LogLevels>(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<env::CoreDumpTypes>(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 <http://www.gnu.org/licenses/>.
#define SETTINGS_H
#include "envdump.h"
-#include <lootcli/lootcli.h>
#include <questionboxmemory.h>
#include <uibase/filterwidget.h>
#include <uibase/log.h>
@@ -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
@@ -2441,28 +2441,6 @@ programs you are intentionally running.</string>
</widget>
</item>
<item>
- <widget class="QGroupBox" name="groupBox_7">
- <property name="title">
- <string>Integrated LOOT</string>
- </property>
- <layout class="QFormLayout" name="formLayout_2">
- <property name="fieldGrowthPolicy">
- <enum>QFormLayout::FieldsStayAtSizeHint</enum>
- </property>
- <item row="0" column="0">
- <widget class="QLabel" name="label_32">
- <property name="text">
- <string>LOOT Log Level</string>
- </property>
- </widget>
- </item>
- <item row="0" column="1">
- <widget class="QComboBox" name="lootLogLevel"/>
- </item>
- </layout>
- </widget>
- </item>
- <item>
<widget class="LinkLabel" name="diagnosticsExplainedLabel">
<property name="toolTip">
<string>Click a link to open the location</string>
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<int>(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<env::CoreDumpTypes>(ui->dumpsTypeBox->currentData().toInt()));
settings().diagnostics().setMaxCoreDumps(ui->dumpsMaxEdit->value());
-
- settings().diagnostics().setLootLogLevel(
- static_cast<lootcli::LogLevels>(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 <QWebChannel>
#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 <QDialog>
-#include "lootdialog.h" // for MarkdownDocument
+#include "markdowndocument.h"
#include <expanderwidget.h>
namespace Ui