diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..56166b207 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +# EditorConfig specs and documentation: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# C++ Code Style settings +[*.{c++,cc,cpp,cppm,cxx,h,h++,hh,hpp,hxx,inl,ipp,ixx,tlh,tli}] +cpp_generate_documentation_comments = doxygen_slash_star diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..2163db45b --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,4 @@ +# .git-blame-ignore-revs + +# tabs -> spaces +bbb3b3e6f6e3c0f95873f22e6d0a4aaf350f49d9 diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 000000000..77c1a8802 --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,32 @@ +name: Backport +on: + pull_request_target: + types: [closed, labeled] + +# WARNING: +# When extending this action, be aware that $GITHUB_TOKEN allows write access to +# the GitHub repository. This means that it should not evaluate user input in a +# way that allows code injection. + +permissions: + contents: read + +jobs: + backport: + permissions: + contents: write # for korthout/backport-action to create branch + pull-requests: write # for korthout/backport-action to create PR to backport + name: Backport Pull Request + if: github.repository_owner == 'PrismLauncher' && github.event.pull_request.merged == true && (github.event_name != 'labeled' || startsWith('backport', github.event.label.name)) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Create backport PRs + uses: korthout/backport-action@v1.3.1 + with: + # Config README: https://github.com/korthout/backport-action#backport-action + pull_description: |- + Bot-based backport to `${target_branch}`, triggered by a label in #${pull_number}. + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a6a6eceaa..bbfe4ab44 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -191,7 +191,7 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get -y update - sudo apt-get -y install ninja-build extra-cmake-modules scdoc + sudo apt-get -y install ninja-build extra-cmake-modules scdoc appstream - name: Install Dependencies (macOS) if: runner.os == 'macOS' @@ -250,6 +250,7 @@ jobs: wget "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage" ${{ github.workspace }}/.github/scripts/prepare_JREs.sh + sudo apt install libopengl0 - name: Add QT_HOST_PATH var (Windows MSVC arm64) if: runner.os == 'Windows' && matrix.architecture == 'arm64' @@ -263,23 +264,23 @@ jobs: - name: Configure CMake (macOS) if: runner.os == 'macOS' && matrix.qt_ver == 6 run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -G Ninja - name: Configure CMake (macOS-Legacy) if: runner.os == 'macOS' && matrix.qt_ver == 5 run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DMACOSX_SPARKLE_UPDATE_PUBLIC_KEY="" -DMACOSX_SPARKLE_UPDATE_FEED_URL="" -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DMACOSX_SPARKLE_UPDATE_PUBLIC_KEY="" -DMACOSX_SPARKLE_UPDATE_FEED_URL="" -G Ninja - name: Configure CMake (Windows MinGW-w64) if: runner.os == 'Windows' && matrix.msystem != '' shell: msys2 {0} run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=6 -DCMAKE_OBJDUMP=/mingw64/bin/objdump.exe -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=6 -DCMAKE_OBJDUMP=/mingw64/bin/objdump.exe -G Ninja - name: Configure CMake (Windows MSVC) if: runner.os == 'Windows' && matrix.msystem == '' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -A${{ matrix.architecture}} -DLauncher_FORCE_BUNDLED_LIBS=ON + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -A${{ matrix.architecture}} -DLauncher_FORCE_BUNDLED_LIBS=ON # https://github.com/ccache/ccache/wiki/MS-Visual-Studio (I coudn't figure out the compiler prefix) if ("${{ env.CCACHE_VAR }}") { @@ -294,7 +295,7 @@ jobs: - name: Configure CMake (Linux) if: runner.os == 'Linux' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=Linux -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -G Ninja ## # BUILD @@ -467,7 +468,8 @@ jobs: shell: bash run: | cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr - + mv ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.metainfo.xml ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.appdata.xml + export "NO_APPSTREAM=1" # we have to skip appstream checking because appstream on ubuntu 20.04 is outdated export OUTPUT="PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage" chmod +x linuxdeploy-*.AppImage @@ -482,7 +484,8 @@ jobs: cp -r /home/runner/work/PrismLauncher/Qt/${{ matrix.qt_version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ - cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}//usr/lib/ + cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ + cp /usr/lib/x86_64-linux-gnu/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib" LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64/server" @@ -599,7 +602,7 @@ jobs: submodules: 'true' - name: Install nix if: inputs.build_type == 'Debug' - uses: cachix/install-nix-action@v21 + uses: cachix/install-nix-action@v22 with: install_url: https://nixos.org/nix/install extra_nix_config: | diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index 3c56a38ea..f19b83986 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -47,7 +47,7 @@ jobs: mv PrismLauncher-macOS-Legacy*/PrismLauncher.tar.gz PrismLauncher-macOS-Legacy-${{ env.VERSION }}.tar.gz mv PrismLauncher-macOS*/PrismLauncher.tar.gz PrismLauncher-macOS-${{ env.VERSION }}.tar.gz - tar -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }} + tar --exclude='.git' -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }} for d in PrismLauncher-Windows-MSVC*; do cd "${d}" || continue diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml new file mode 100644 index 000000000..ad22120ee --- /dev/null +++ b/.github/workflows/update-flake.yml @@ -0,0 +1,28 @@ +name: Update Flake Lockfile + +on: + schedule: + # run weekly on sunday + - cron: "0 0 * * 0" + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + update-flake: + if: github.repository == 'PrismLauncher/PrismLauncher' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: cachix/install-nix-action@v22 + + - uses: DeterminateSystems/update-flake-lock@v19 + with: + commit-msg: "chore(nix): update lockfile" + pr-title: "chore(nix): update lockfile" + pr-labels: | + Linux + simple change diff --git a/.gitmodules b/.gitmodules index 87703fee5..0f437d277 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,6 @@ [submodule "libraries/cmark"] path = libraries/cmark url = https://github.com/commonmark/cmark.git +[submodule "flatpak/shared-modules"] + path = flatpak/shared-modules + url = https://github.com/flathub/shared-modules.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 6bd12630d..41634f68a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,6 +85,38 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DTOML_ENABLE_FLOAT16=0") # set CXXFLAGS for build targets set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}") +option(DEBUG_ADDRESS_SANITIZER "Enable Address Sanitizer in Debug builds" on) + +# If this is a Debug build turn on address sanitiser +if (CMAKE_BUILD_TYPE STREQUAL "Debug" AND DEBUG_ADDRESS_SANITIZER) + message(STATUS "Address Sanitizer enabled for Debug builds, Turn it off with -DDEBUG_ADDRESS_SANITIZER=off") + if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") + if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") + # using clang with clang-cl front end + message(STATUS "Address Sanitizer available on Clang MSVC frontend") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /O1 /Oy-") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /O1 /Oy-") + else() + # AppleClang and Clang + message(STATUS "Address Sanitizer available on Clang") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer") + endif() + elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") + # GCC + message(STATUS "Address Sanitizer available on GCC") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer") + link_libraries("asan") + elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") + message(STATUS "Address Sanitizer available on MSVC") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /O1 /Oy-") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /O1 /Oy-") + else() + message(STATUS "Address Sanitizer not available on compiler ${CMAKE_CXX_COMPILER_ID}") + endif() +endif() + option(ENABLE_LTO "Enable Link Time Optimization" off) if(ENABLE_LTO) @@ -138,7 +170,7 @@ set(Launcher_NEWS_OPEN_URL "https://prismlauncher.org/news" CACHE STRING "URL th set(Launcher_HELP_URL "https://prismlauncher.org/wiki/help-pages/%1" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help") ######## Set version numbers ######## -set(Launcher_VERSION_MAJOR 7) +set(Launcher_VERSION_MAJOR 8) set(Launcher_VERSION_MINOR 0) set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}") @@ -146,7 +178,7 @@ set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}. set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},0,0") # Build platform. -set(Launcher_BUILD_PLATFORM "" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.") +set(Launcher_BUILD_PLATFORM "unknown" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.") # Channel list URL set(Launcher_UPDATER_BASE "" CACHE STRING "Base URL for the updater.") @@ -332,7 +364,7 @@ elseif(UNIX) set(BINARY_DEST_DIR "bin") set(LIBRARY_DEST_DIR "lib${LIB_SUFFIX}") - set(JARS_DEST_DIR "share/${Launcher_APP_BINARY_NAME}") + set(JARS_DEST_DIR "share/${Launcher_Name}") # install as bundle with no dependencies included set(INSTALL_BUNDLE "nodeps") @@ -345,7 +377,7 @@ elseif(UNIX) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_SVG} DESTINATION "${KDE_INSTALL_ICONDIR}/hicolor/scalable/apps") install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_mrpack_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR}) - install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "${KDE_INSTALL_DATADIR}/${Launcher_Name}") + install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "share/${Launcher_Name}") if(Launcher_ManPage) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_ManPage} DESTINATION "${KDE_INSTALL_MANDIR}/man6") diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4bca126f8..b2795ce49 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,3 +61,10 @@ As a bonus, you can also [cryptographically sign your commits][gh-signing-commit [gh-signing-commits]: https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits [gh-vigilant-mode]: https://docs.github.com/en/authentication/managing-commit-signature-verification/displaying-verification-statuses-for-all-of-your-commits + + +## Backporting to Release Branches + +We use [automated backports](https://github.com/PrismLauncher/PrismLauncher/blob/develop/.github/workflows/backport.yml) to merge specific contributions from develop into `release` branches. + +This is done when pull requests are merged and have labels such as `backport release-7.x` - which should be added along with the milestone for the release. diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 353731896..140731fe0 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -65,7 +65,7 @@ Config::Config() MAC_SPARKLE_PUB_KEY = "@MACOSX_SPARKLE_UPDATE_PUBLIC_KEY@"; MAC_SPARKLE_APPCAST_URL = "@MACOSX_SPARKLE_UPDATE_FEED_URL@"; - if (BUILD_PLATFORM == "macOS" && !MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty()) + if (!MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty()) { UPDATER_ENABLED = true; } @@ -82,6 +82,7 @@ Config::Config() { GIT_REFSPEC = "refs/heads/stable"; GIT_TAG = versionString(); + GIT_COMMIT = ""; } if (GIT_REFSPEC.startsWith("refs/heads/")) diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 8543d7241..11773d887 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -68,7 +68,7 @@ class Config { bool UPDATER_ENABLED = false; - /// A short string identifying this build's platform. For example, "lin64" or "win32". + /// A short string identifying this build's platform or distribution. QString BUILD_PLATFORM; /// A string containing the build timestamp diff --git a/flake.lock b/flake.lock index 875866438..370250c4b 100644 --- a/flake.lock +++ b/flake.lock @@ -21,11 +21,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1683560683, - "narHash": "sha256-XAygPMN5Xnk/W2c1aW0jyEa6lfMDZWlQgiNtmHXytPc=", + "lastModified": 1688466019, + "narHash": "sha256-VeM2akYrBYMsb4W/MmBo1zmaMfgbL4cH3Pu8PGyIwJ0=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "006c75898cf814ef9497252b022e91c946ba8e17", + "rev": "8e8d955c22df93dbe24f19ea04f47a74adbdc5ec", "type": "github" }, "original": { @@ -35,12 +35,15 @@ } }, "flake-utils": { + "inputs": { + "systems": "systems" + }, "locked": { - "lastModified": 1667395993, - "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", + "lastModified": 1685518550, + "narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=", "owner": "numtide", "repo": "flake-utils", - "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", + "rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef", "type": "github" }, "original": { @@ -88,11 +91,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1685012353, - "narHash": "sha256-U3oOge4cHnav8OLGdRVhL45xoRj4Ppd+It6nPC9nNIU=", + "lastModified": 1689413807, + "narHash": "sha256-exuzOvOhGAEKWQKwDuZAL4N8a1I837hH5eocaTcIbLc=", "owner": "nixos", "repo": "nixpkgs", - "rev": "aeb75dba965e790de427b73315d5addf91a54955", + "rev": "46ed466081b9cad1125b11f11a2af5cc40b942c7", "type": "github" }, "original": { @@ -105,11 +108,11 @@ "nixpkgs-lib": { "locked": { "dir": "lib", - "lastModified": 1682879489, - "narHash": "sha256-sASwo8gBt7JDnOOstnps90K1wxmVfyhsTPPNTGBPjjg=", + "lastModified": 1688049487, + "narHash": "sha256-100g4iaKC9MalDjUW9iN6Jl/OocTDtXdeAj7pEGIRh4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "da45bf6ec7bbcc5d1e14d3795c025199f28e0de0", + "rev": "4bc72cae107788bf3f24f30db2e2f685c9298dc9", "type": "github" }, "original": { @@ -135,11 +138,11 @@ ] }, "locked": { - "lastModified": 1684842236, - "narHash": "sha256-rYWsIXHvNhVQ15RQlBUv67W3YnM+Pd+DuXGMvCBq2IE=", + "lastModified": 1689328505, + "narHash": "sha256-9B3+OeUn1a/CvzE3GW6nWNwS5J7PDHTyHGlpL3wV5oA=", "owner": "cachix", "repo": "pre-commit-hooks.nix", - "rev": "61e567d6497bc9556f391faebe5e410e6623217f", + "rev": "5e28316db471d1ac234beb70031b635437421dd6", "type": "github" }, "original": { @@ -156,6 +159,21 @@ "nixpkgs": "nixpkgs", "pre-commit-hooks": "pre-commit-hooks" } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flatpak/libdecor.json b/flatpak/libdecor.json new file mode 100644 index 000000000..12afad69f --- /dev/null +++ b/flatpak/libdecor.json @@ -0,0 +1,22 @@ +{ + "name": "libdecor", + "buildsystem": "meson", + "config-opts": [ + "-Ddemo=false" + ], + "sources": [ + { + "type": "git", + "url": "https://gitlab.freedesktop.org/libdecor/libdecor.git", + "commit": "73260393a97291c887e1074ab7f318e031be0ac6" + }, + { + "type": "patch", + "path": "patches/weird_libdecor.patch" + } + ], + "cleanup": [ + "/include", + "/lib/pkgconfig" + ] +} diff --git a/flatpak/org.prismlauncher.PrismLauncher.yml b/flatpak/org.prismlauncher.PrismLauncher.yml index 0524946f0..a9b9762f5 100644 --- a/flatpak/org.prismlauncher.PrismLauncher.yml +++ b/flatpak/org.prismlauncher.PrismLauncher.yml @@ -5,13 +5,6 @@ sdk: org.kde.Sdk sdk-extensions: - org.freedesktop.Sdk.Extension.openjdk17 - org.freedesktop.Sdk.Extension.openjdk8 -add-extensions: - com.valvesoftware.Steam.Utility.gamescope: - version: stable - add-ld-path: lib - no-autodownload: true - autodelete: false - directory: utils/gamescope command: prismlauncher finish-args: @@ -26,21 +19,31 @@ finish-args: # Mod drag&drop - --filesystem=xdg-download:ro +cleanup: + - /lib/libGLU* + modules: + # Might be needed by some Controller mods (see https://github.com/isXander/Controlify/issues/31) + - shared-modules/libusb/libusb.json + + # Needed for proper Wayland support + - libdecor.json + - name: prismlauncher buildsystem: cmake-ninja + builddir: true config-opts: - -DLauncher_BUILD_PLATFORM=flatpak - - -DCMAKE_BUILD_TYPE=Debug + - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DLauncher_QT_VERSION_MAJOR=5 build-options: env: JAVA_HOME: /usr/lib/sdk/openjdk17/jvm/openjdk-17 JAVA_COMPILER: /usr/lib/sdk/openjdk17/jvm/openjdk-17/bin/javac sources: - - type: dir - path: ../ - builddir: true + - type: dir + path: ../ + - name: openjdk buildsystem: simple build-commands: @@ -49,14 +52,45 @@ modules: - mv /app/jre /app/jdk/17 - /usr/lib/sdk/openjdk8/install.sh - mv /app/jre /app/jdk/8 - cleanup: [/jre] + cleanup: + - /jre + + - name: glfw + buildsystem: cmake-ninja + config-opts: + - -DCMAKE_BUILD_TYPE=RelWithDebInfo + - -DBUILD_SHARED_LIBS:BOOL=ON + - -DGLFW_USE_WAYLAND=ON + sources: + - type: git + url: https://github.com/glfw/glfw.git + commit: 3fa2360720eeba1964df3c0ecf4b5df8648a8e52 + - type: patch + path: patches/0003-Don-t-crash-on-calls-to-focus-or-icon.patch + - type: patch + path: patches/0005-Add-warning-about-being-an-unofficial-patch.patch + - type: patch + path: patches/0007-Platform-Prefer-Wayland-over-X11.patch + cleanup: + - /include + - /lib/cmake + - /lib/pkgconfig + - name: xrandr buildsystem: autotools sources: - type: archive - url: https://xorg.freedesktop.org/archive/individual/app/xrandr-1.5.1.tar.xz - sha256: 7bc76daf9d72f8aff885efad04ce06b90488a1a169d118dea8a2b661832e8762 - cleanup: [/share/man, /bin/xkeystone] + url: https://xorg.freedesktop.org/archive/individual/app/xrandr-1.5.2.tar.xz + sha256: c8bee4790d9058bacc4b6246456c58021db58a87ddda1a9d0139bf5f18f1f240 + x-checker-data: + type: anitya + project-id: 14957 + stable-only: true + url-template: https://xorg.freedesktop.org/archive/individual/app/xrandr-$version.tar.xz + cleanup: + - /share/man + - /bin/xkeystone + - name: gamemode buildsystem: meson config-opts: @@ -67,19 +101,56 @@ modules: # post-install is running inside the build dir, we need it from the source though - install -Dm755 ../data/gamemoderun -t /app/bin sources: - - type: git - url: https://github.com/FeralInteractive/gamemode - tag: "1.7" - commit: 4dc99dff76218718763a6b07fc1900fa6d1dafd9 + - type: archive + archive-type: tar-gzip + url: https://api.github.com/repos/FeralInteractive/gamemode/tarball/1.7 + sha256: 57ce73ba605d1cf12f8d13725006a895182308d93eba0f69f285648449641803 + x-checker-data: + type: json + url: https://api.github.com/repos/FeralInteractive/gamemode/releases/latest + version-query: .tag_name + url-query: .tarball_url + timestamp-query: .published_at + cleanup: + - /include + - /lib/pkgconfig + - /lib/libgamemodeauto.a + + - name: glxinfo + buildsystem: meson + config-opts: + - --bindir=/app/mesa-demos + - -Degl=disabled + - -Dglut=disabled + - -Dosmesa=disabled + - -Dvulkan=disabled + - -Dwayland=disabled + post-install: + - mv -v /app/mesa-demos/glxinfo /app/bin + sources: + - type: archive + url: https://archive.mesa3d.org/demos/mesa-demos-9.0.0.tar.xz + sha256: 3046a3d26a7b051af7ebdd257a5f23bfeb160cad6ed952329cdff1e9f1ed496b + x-checker-data: + type: anitya + project-id: 16781 + stable-only: true + url-template: https://archive.mesa3d.org/demos/mesa-demos-$version.tar.xz + cleanup: + - /include + - /mesa-demos + - /share + modules: + - shared-modules/glu/glu-9.json + - name: enhance buildsystem: simple build-commands: - - mkdir -p /app/utils/gamescope - install -Dm755 prime-run /app/bin/prime-run - mv /app/bin/prismlauncher /app/bin/prismrun - install -Dm755 prismlauncher /app/bin/prismlauncher sources: - type: file - path: ../flatpak/prime-run + path: prime-run - type: file - path: ../flatpak/prismlauncher + path: prismlauncher diff --git a/flatpak/patches/0003-Don-t-crash-on-calls-to-focus-or-icon.patch b/flatpak/patches/0003-Don-t-crash-on-calls-to-focus-or-icon.patch new file mode 100644 index 000000000..9130e856c --- /dev/null +++ b/flatpak/patches/0003-Don-t-crash-on-calls-to-focus-or-icon.patch @@ -0,0 +1,24 @@ +diff --git a/src/wl_window.c b/src/wl_window.c +index 52d3b9eb..4ac4eb5d 100644 +--- a/src/wl_window.c ++++ b/src/wl_window.c +@@ -2117,8 +2117,7 @@ void _glfwSetWindowTitleWayland(_GLFWwindow* window, const char* title) + void _glfwSetWindowIconWayland(_GLFWwindow* window, + int count, const GLFWimage* images) + { +- _glfwInputError(GLFW_FEATURE_UNAVAILABLE, +- "Wayland: The platform does not support setting the window icon"); ++ fprintf(stderr, "!!! Ignoring Error: Wayland: The platform does not support setting the window icon\n"); + } + + void _glfwGetWindowPosWayland(_GLFWwindow* window, int* xpos, int* ypos) +@@ -2361,8 +2360,7 @@ void _glfwRequestWindowAttentionWayland(_GLFWwindow* window) + + void _glfwFocusWindowWayland(_GLFWwindow* window) + { +- _glfwInputError(GLFW_FEATURE_UNAVAILABLE, +- "Wayland: The platform does not support setting the input focus"); ++ fprintf(stderr, "!!! Ignoring Error: Wayland: The platform does not support setting the input focus\n"); + } + + void _glfwSetWindowMonitorWayland(_GLFWwindow* window, diff --git a/flatpak/patches/0005-Add-warning-about-being-an-unofficial-patch.patch b/flatpak/patches/0005-Add-warning-about-being-an-unofficial-patch.patch new file mode 100644 index 000000000..b031d739f --- /dev/null +++ b/flatpak/patches/0005-Add-warning-about-being-an-unofficial-patch.patch @@ -0,0 +1,17 @@ +diff --git a/src/init.c b/src/init.c +index 06dbb3f2..a7c6da86 100644 +--- a/src/init.c ++++ b/src/init.c +@@ -449,6 +449,12 @@ GLFWAPI int glfwInit(void) + _glfw.initialized = GLFW_TRUE; + + glfwDefaultWindowHints(); ++ ++ fprintf(stderr, "!!! Patched GLFW from https://github.com/Admicos/minecraft-wayland\n" ++ "!!! If any issues with the window, or some issues with rendering, occur, " ++ "first try with the built-in GLFW, and if that solves the issue, report there first.\n" ++ "!!! Use outside Minecraft is untested, and things might break.\n"); ++ + return GLFW_TRUE; + } + diff --git a/flatpak/patches/0007-Platform-Prefer-Wayland-over-X11.patch b/flatpak/patches/0007-Platform-Prefer-Wayland-over-X11.patch new file mode 100644 index 000000000..4eeb81309 --- /dev/null +++ b/flatpak/patches/0007-Platform-Prefer-Wayland-over-X11.patch @@ -0,0 +1,20 @@ +diff --git a/src/platform.c b/src/platform.c +index c5966ae7..3e7442f9 100644 +--- a/src/platform.c ++++ b/src/platform.c +@@ -49,12 +49,12 @@ static const struct + #if defined(_GLFW_COCOA) + { GLFW_PLATFORM_COCOA, _glfwConnectCocoa }, + #endif +-#if defined(_GLFW_X11) +- { GLFW_PLATFORM_X11, _glfwConnectX11 }, +-#endif + #if defined(_GLFW_WAYLAND) + { GLFW_PLATFORM_WAYLAND, _glfwConnectWayland }, + #endif ++#if defined(_GLFW_X11) ++ { GLFW_PLATFORM_X11, _glfwConnectX11 }, ++#endif + }; + + GLFWbool _glfwSelectPlatform(int desiredID, _GLFWplatform* platform) diff --git a/flatpak/patches/weird_libdecor.patch b/flatpak/patches/weird_libdecor.patch new file mode 100644 index 000000000..3a400b820 --- /dev/null +++ b/flatpak/patches/weird_libdecor.patch @@ -0,0 +1,40 @@ +diff --git a/src/libdecor.c b/src/libdecor.c +index a9c1106..1aa38b3 100644 +--- a/src/libdecor.c ++++ b/src/libdecor.c +@@ -1391,22 +1391,32 @@ calculate_priority(const struct libdecor_plugin_description *plugin_description) + static bool + check_symbol_conflicts(const struct libdecor_plugin_description *plugin_description) + { ++ bool ret = true; + char * const *symbol; ++ void* main_prog = dlopen(NULL, RTLD_LAZY); ++ if (!main_prog) { ++ fprintf(stderr, "Plugin \"%s\" couldn't check conflicting symbols: \"%s\".\n", ++ plugin_description->description, dlerror()); ++ return false; ++ } ++ + + symbol = plugin_description->conflicting_symbols; + while (*symbol) { + dlerror(); +- dlsym (RTLD_DEFAULT, *symbol); ++ dlsym (main_prog, *symbol); + if (!dlerror()) { + fprintf(stderr, "Plugin \"%s\" uses conflicting symbol \"%s\".\n", + plugin_description->description, *symbol); +- return false; ++ ret = false; ++ break; + } + + symbol++; + } + +- return true; ++ dlclose(main_prog); ++ return ret; + } + + static struct plugin_loader * diff --git a/flatpak/prismlauncher b/flatpak/prismlauncher index bb8767113..039d890d2 100644 --- a/flatpak/prismlauncher +++ b/flatpak/prismlauncher @@ -5,7 +5,7 @@ for i in {0..9}; do test -S "$XDG_RUNTIME_DIR"/discord-ipc-"$i" || ln -sf {app/com.discordapp.Discord,"$XDG_RUNTIME_DIR"}/discord-ipc-"$i"; done -export PATH="${PATH}${PATH:+:}/app/utils/gamescope/bin:/usr/lib/extensions/vulkan/MangoHud/bin" -export LD_LIBRARY_PATH="${LD_LIBRARY_PATH}${LD_LIBRARY_PATH:+:}/usr/lib/extensions/vulkan/MangoHud/\$LIB/" +export PATH="${PATH}${PATH:+:}/usr/lib/extensions/vulkan/gamescope/bin:/usr/lib/extensions/vulkan/MangoHud/bin" +export VK_LAYER_PATH="/usr/lib/extensions/vulkan/share/vulkan/implicit_layer.d/" exec /app/bin/prismrun "$@" diff --git a/flatpak/shared-modules b/flatpak/shared-modules new file mode 160000 index 000000000..45094ca57 --- /dev/null +++ b/flatpak/shared-modules @@ -0,0 +1 @@ +Subproject commit 45094ca570be383d06df729b6972830ec63bd3df diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 1659eb445..d6c135de7 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -6,9 +6,10 @@ * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 Lenny McLennington - * Copyright (C) 2022 Tayou + * Copyright (C) 2022 Tayou * Copyright (C) 2023 TheKodeToad * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * Copyright (C) 2023 seth * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -376,33 +377,33 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // init the logger { - static const QString logBase = BuildConfig.LAUNCHER_NAME + "-%0.log"; - auto moveFile = [](const QString &oldName, const QString &newName) - { + static const QString baseLogFile = BuildConfig.LAUNCHER_NAME + "-%0.log"; + static const QString logBase = FS::PathCombine("logs", baseLogFile); + auto moveFile = [](const QString& oldName, const QString& newName) { QFile::remove(newName); QFile::copy(oldName, newName); QFile::remove(oldName); }; + if (FS::ensureFolderPathExists("logs")) { // if this did not fail + for (auto i = 0; i <= 4; i++) + if (auto oldName = baseLogFile.arg(i); + QFile::exists(oldName)) // do not pointlessly delete new files if the old ones are not there + moveFile(oldName, logBase.arg(i)); + } - moveFile(logBase.arg(3), logBase.arg(4)); - moveFile(logBase.arg(2), logBase.arg(3)); - moveFile(logBase.arg(1), logBase.arg(2)); - moveFile(logBase.arg(0), logBase.arg(1)); + for (auto i = 4; i > 0; i--) + moveFile(logBase.arg(i - 1), logBase.arg(i)); logFile = std::unique_ptr(new QFile(logBase.arg(0))); - if(!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) - { - showFatalErrorMessage( - "The launcher data folder is not writable!", - QString( - "The launcher couldn't create a log file - the data folder is not writable.\n" - "\n" - "Make sure you have write permissions to the data folder.\n" - "(%1)\n" - "\n" - "The launcher cannot continue until you fix this problem." - ).arg(dataPath) - ); + if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { + showFatalErrorMessage("The launcher data folder is not writable!", + QString("The launcher couldn't create a log file - the data folder is not writable.\n" + "\n" + "Make sure you have write permissions to the data folder.\n" + "(%1)\n" + "\n" + "The launcher cannot continue until you fix this problem.") + .arg(dataPath)); return; } qInstallMessageHandler(appDebugOutput); @@ -433,7 +434,11 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) } // seach root path if(!foundLoggingRules) { +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + logRulesPath = FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME, logRulesFile); +#else logRulesPath = FS::PathCombine(m_rootPath, logRulesFile); +#endif qDebug() << "Testing" << logRulesPath << "..."; foundLoggingRules = QFile::exists(logRulesPath); } @@ -471,6 +476,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) qDebug() << BuildConfig.LAUNCHER_DISPLAYNAME << ", (c) 2013-2021 " << BuildConfig.LAUNCHER_COPYRIGHT; qDebug() << "Version : " << BuildConfig.printableVersionString(); + qDebug() << "Platform : " << BuildConfig.BUILD_PLATFORM; qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT; qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC; if (adjustedBy.size()) @@ -568,6 +574,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // Language m_settings->registerSetting("Language", QString()); + m_settings->registerSetting("UseSystemLocale", false); // Console m_settings->registerSetting("ShowConsole", false); @@ -594,7 +601,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // Java Settings m_settings->registerSetting("JavaPath", ""); - m_settings->registerSetting("JavaTimestamp", 0); + m_settings->registerSetting("JavaSignature", ""); m_settings->registerSetting("JavaArchitecture", ""); m_settings->registerSetting("JavaRealArchitecture", ""); m_settings->registerSetting("JavaVersion", ""); @@ -604,6 +611,9 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("IgnoreJavaCompatibility", false); m_settings->registerSetting("IgnoreJavaWizard", false); + // Mod loader settings + m_settings->registerSetting("DisableQuiltBeacon", false); + // Native library workarounds m_settings->registerSetting("UseNativeOpenAL", false); m_settings->registerSetting("UseNativeGLFW", false); @@ -687,8 +697,16 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->reset("PastebinCustomAPIBase"); } } - // meta URL - m_settings->registerSetting("MetaURLOverride", ""); + { + // Meta URL + m_settings->registerSetting("MetaURLOverride", ""); + + QUrl metaUrl(m_settings->get("MetaURLOverride").toString()); + + // get rid of invalid meta urls + if (!metaUrl.isValid() || metaUrl.scheme() != "http" || metaUrl.scheme() != "https") + m_settings->reset("MetaURLOverride"); + } m_settings->registerSetting("CloseAfterLaunch", false); m_settings->registerSetting("QuitAfterGameStop", false); @@ -910,12 +928,7 @@ bool Application::createSetupWizard() } return false; }(); - bool languageRequired = [&]() - { - if (settings()->get("Language").toString().isEmpty()) - return true; - return false; - }(); + bool languageRequired = settings()->get("Language").toString().isEmpty(); bool pasteInterventionRequired = settings()->get("PastebinURL") != ""; bool themeInterventionRequired = settings()->get("ApplicationTheme") == ""; bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired || themeInterventionRequired; @@ -1559,7 +1572,7 @@ QString Application::getJarPath(QString jarFile) { QStringList potentialPaths = { #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) - FS::PathCombine(m_rootPath, "share/" + BuildConfig.LAUNCHER_APP_BINARY_NAME), + FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME), #endif FS::PathCombine(m_rootPath, "jars"), FS::PathCombine(applicationDirPath(), "jars"), @@ -1699,6 +1712,7 @@ bool Application::handleDataMigration(const QString& currentData, matcher->add(std::make_shared(configFile)); matcher->add(std::make_shared( BuildConfig.LAUNCHER_CONFIGFILE)); // it's possible that we already used that directory before + matcher->add(std::make_shared("logs/")); matcher->add(std::make_shared("accounts.json")); matcher->add(std::make_shared("accounts/")); matcher->add(std::make_shared("assets/")); diff --git a/launcher/Application.h b/launcher/Application.h index ced0af17d..527c536b3 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -2,7 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (C) 2022 Tayou + * Copyright (C) 2022 Tayou * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify diff --git a/launcher/BaseVersion.h b/launcher/BaseVersion.h index ca0e45027..c7cedbe10 100644 --- a/launcher/BaseVersion.h +++ b/launcher/BaseVersion.h @@ -15,16 +15,15 @@ #pragma once -#include -#include #include +#include +#include /*! * An abstract base class for versions. */ -class BaseVersion -{ -public: +class BaseVersion { + public: using Ptr = std::shared_ptr; virtual ~BaseVersion() {} /*! @@ -45,14 +44,8 @@ public: */ virtual QString typeString() const = 0; - virtual bool operator<(BaseVersion &a) - { - return name() < a.name(); - }; - virtual bool operator>(BaseVersion &a) - { - return name() > a.name(); - }; + virtual bool operator<(BaseVersion& a) { return name() < a.name(); }; + virtual bool operator>(BaseVersion& a) { return name() > a.name(); }; }; Q_DECLARE_METATYPE(BaseVersion::Ptr) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index ce2771a49..7cba97b4f 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -362,6 +362,8 @@ set(MINECRAFT_SOURCES minecraft/mod/tasks/LocalWorldSaveParseTask.cpp minecraft/mod/tasks/LocalResourceParse.h minecraft/mod/tasks/LocalResourceParse.cpp + minecraft/mod/tasks/GetModDependenciesTask.h + minecraft/mod/tasks/GetModDependenciesTask.cpp # Assets minecraft/AssetsUtils.h @@ -375,8 +377,6 @@ set(MINECRAFT_SOURCES minecraft/services/SkinDelete.cpp minecraft/services/SkinDelete.h - mojang/PackageManifest.h - mojang/PackageManifest.cpp minecraft/Agent.h) # the screenshots feature @@ -487,6 +487,9 @@ set(API_SOURCES modplatform/helpers/HashUtils.cpp modplatform/helpers/OverrideUtils.h modplatform/helpers/OverrideUtils.cpp + + modplatform/helpers/ExportToModList.h + modplatform/helpers/ExportToModList.cpp ) set(FTB_SOURCES @@ -514,6 +517,8 @@ set(FLAME_SOURCES modplatform/flame/FlameCheckUpdate.h modplatform/flame/FlameInstanceCreationTask.h modplatform/flame/FlameInstanceCreationTask.cpp + modplatform/flame/FlamePackExportTask.h + modplatform/flame/FlamePackExportTask.cpp ) set(MODRINTH_SOURCES @@ -682,6 +687,7 @@ SET(LAUNCHER_SOURCES VersionProxyModel.h VersionProxyModel.cpp Markdown.h + Markdown.cpp # Super secret! KonamiCode.h @@ -825,8 +831,8 @@ SET(LAUNCHER_SOURCES ui/pages/global/APIPage.h # GUI - platform pages - ui/pages/modplatform/VanillaPage.cpp - ui/pages/modplatform/VanillaPage.h + ui/pages/modplatform/CustomPage.cpp + ui/pages/modplatform/CustomPage.h ui/pages/modplatform/ResourcePage.cpp ui/pages/modplatform/ResourcePage.h @@ -906,8 +912,10 @@ SET(LAUNCHER_SOURCES ui/dialogs/EditAccountDialog.h ui/dialogs/ExportInstanceDialog.cpp ui/dialogs/ExportInstanceDialog.h - ui/dialogs/ExportMrPackDialog.cpp - ui/dialogs/ExportMrPackDialog.h + ui/dialogs/ExportPackDialog.cpp + ui/dialogs/ExportPackDialog.h + ui/dialogs/ExportToModListDialog.cpp + ui/dialogs/ExportToModListDialog.h ui/dialogs/IconPickerDialog.cpp ui/dialogs/IconPickerDialog.h ui/dialogs/ImportResourceDialog.cpp @@ -1032,7 +1040,7 @@ qt_wrap_ui(LAUNCHER_UI ui/pages/instance/ScreenshotsPage.ui ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui ui/pages/modplatform/atlauncher/AtlPage.ui - ui/pages/modplatform/VanillaPage.ui + ui/pages/modplatform/CustomPage.ui ui/pages/modplatform/ResourcePage.ui ui/pages/modplatform/flame/FlamePage.ui ui/pages/modplatform/legacy_ftb/Page.ui @@ -1054,7 +1062,8 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/ProfileSelectDialog.ui ui/dialogs/SkinUploadDialog.ui ui/dialogs/ExportInstanceDialog.ui - ui/dialogs/ExportMrPackDialog.ui + ui/dialogs/ExportPackDialog.ui + ui/dialogs/ExportToModListDialog.ui ui/dialogs/IconPickerDialog.ui ui/dialogs/ImportResourceDialog.ui ui/dialogs/MSALoginDialog.ui diff --git a/launcher/FileIgnoreProxy.cpp b/launcher/FileIgnoreProxy.cpp index a3b7d5054..4c8c64c72 100644 --- a/launcher/FileIgnoreProxy.cpp +++ b/launcher/FileIgnoreProxy.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include "FileSystem.h" #include "SeparatorPrefixTree.h" #include "StringUtils.h" @@ -254,3 +255,25 @@ bool FileIgnoreProxy::filterAcceptsColumn(int source_column, const QModelIndex& return true; } + +bool FileIgnoreProxy::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const +{ + QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); + QFileSystemModel* fsm = qobject_cast(sourceModel()); + + auto fileInfo = fsm->fileInfo(index); + return !ignoreFile(fileInfo); +} + +bool FileIgnoreProxy::ignoreFile(QFileInfo fileInfo) const +{ + auto fileName = fileInfo.fileName(); + auto path = relPath(fileInfo.absoluteFilePath()); + return std::any_of(m_ignoreFiles.cbegin(), m_ignoreFiles.cend(), [fileName](auto iFileName) { return fileName == iFileName; }) || + m_ignoreFilePaths.covers(path); +} + +bool FileIgnoreProxy::filterFile(const QString& fileName) const +{ + return blocked.covers(fileName) || ignoreFile(QFileInfo(QDir(root), fileName)); +} diff --git a/launcher/FileIgnoreProxy.h b/launcher/FileIgnoreProxy.h index a5a1153d8..e01a2651e 100644 --- a/launcher/FileIgnoreProxy.h +++ b/launcher/FileIgnoreProxy.h @@ -36,6 +36,7 @@ #pragma once +#include #include #include "SeparatorPrefixTree.h" @@ -63,10 +64,22 @@ class FileIgnoreProxy : public QSortFilterProxyModel { inline const SeparatorPrefixTree<'/'>& blockedPaths() const { return blocked; } inline SeparatorPrefixTree<'/'>& blockedPaths() { return blocked; } + // list of file names that need to be removed completely from model + inline QStringList& ignoreFilesWithName() { return m_ignoreFiles; } + // list of relative paths that need to be removed completely from model + inline SeparatorPrefixTree<'/'>& ignoreFilesWithPath() { return m_ignoreFilePaths; } + + bool filterFile(const QString& fileName) const; + protected: bool filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const; + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const; + + bool ignoreFile(QFileInfo file) const; private: const QString root; SeparatorPrefixTree<'/'> blocked; + QStringList m_ignoreFiles; + SeparatorPrefixTree<'/'> m_ignoreFilePaths; }; diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index d98526dfd..4538702f2 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -36,6 +36,7 @@ */ #include "FileSystem.h" +#include #include "BuildConfig.h" @@ -102,7 +103,7 @@ namespace fs = ghc::filesystem; #include #include #include -#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) +#elif defined(Q_OS_MACOS) #include #include #elif defined(Q_OS_WIN) @@ -246,6 +247,7 @@ bool copy::operator()(const QString& offset, bool dryRun) { using copy_opts = fs::copy_options; m_copied = 0; // reset counter + m_failedPaths.clear(); // NOTE always deep copy on windows. the alternatives are too messy. #if defined Q_OS_WIN32 @@ -277,6 +279,9 @@ bool copy::operator()(const QString& offset, bool dryRun) qWarning() << "Failed to copy files:" << QString::fromStdString(err.message()); qDebug() << "Source file:" << src_path; qDebug() << "Destination file:" << dst_path; + m_failedPaths.append(dst_path); + emit copyFailed(relative_dst_path); + return; } m_copied++; emit fileCopied(relative_dst_path); @@ -372,7 +377,7 @@ void create_link::make_link_list(const QString& offset) auto src_path = source_it.next(); auto relative_path = src_dir.relativeFilePath(src_path); - if (m_max_depth >= 0 && pathDepth(relative_path) > m_max_depth){ + if (m_max_depth >= 0 && pathDepth(relative_path) > m_max_depth) { relative_path = pathTruncate(relative_path, m_max_depth); src_path = src_dir.filePath(relative_path); if (linkedPaths.contains(src_path)) { @@ -663,7 +668,7 @@ QString pathTruncate(const QString& path, int depth) QString trunc = QFileInfo(path).path(); - if (pathDepth(trunc) > depth ) { + if (pathDepth(trunc) > depth) { return pathTruncate(trunc, depth); } @@ -769,10 +774,47 @@ QString getDesktopDir() // Cross-platform Shortcut creation bool createShortcut(QString destination, QString target, QStringList args, QString name, QString icon) { + if (destination.isEmpty()) { + destination = PathCombine(getDesktopDir(), RemoveInvalidFilenameChars(name)); + } #if defined(Q_OS_MACOS) - destination += ".command"; + // Create the Application + QDir applicationDirectory = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + "/" + BuildConfig.LAUNCHER_NAME + " Instances/"; - QFile f(destination); + if (!applicationDirectory.mkpath(".")) { + qWarning() << "Couldn't create application directory"; + return false; + } + + QDir application = applicationDirectory.path() + "/" + name + ".app/"; + + if (application.exists()) { + qWarning() << "Application already exists!"; + return false; + } + + if (!application.mkpath(".")) { + qWarning() << "Couldn't create application"; + return false; + } + + QDir content = application.path() + "/Contents/"; + QDir resources = content.path() + "/Resources/"; + QDir binaryDir = content.path() + "/MacOS/"; + QFile info = content.path() + "/Info.plist"; + + if (!(content.mkpath(".") && resources.mkpath(".") && binaryDir.mkpath("."))) { + qWarning() << "Couldn't create directories within application"; + return false; + } + info.open(QIODevice::WriteOnly | QIODevice::Text); + + QFile(icon).rename(resources.path() + "/Icon.icns"); + + // Create the Command file + QString exec = binaryDir.path() + "/Run.command"; + + QFile f(exec); f.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream stream(&f); @@ -789,8 +831,32 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther); + // Generate the Info.plist + QTextStream infoStream(&info); + infoStream << " \n" + "" + "\n" + "\n" + " CFBundleExecutable\n" + " Run.command\n" // The path to the executable + " CFBundleIconFile\n" + " Icon.icns\n" + " CFBundleName\n" + " " << name << "\n" // Name of the application + " CFBundlePackageType\n" + " APPL\n" + " CFBundleShortVersionString\n" + " 1.0\n" + " CFBundleVersion\n" + " 1.0\n" + "\n" + ""; + return true; #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + if (!destination.endsWith(".desktop")) // in case of isFlatpak destination is already populated + destination += ".desktop"; QFile f(destination); f.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream stream(&f); @@ -974,7 +1040,7 @@ FilesystemType getFilesystemType(const QString& name) { for (auto iter = s_filesystem_type_names.constBegin(); iter != s_filesystem_type_names.constEnd(); ++iter) { auto fs_names = iter.value(); - if(fs_names.contains(name.toUpper())) + if (fs_names.contains(name.toUpper())) return iter.key(); } return FilesystemType::UNKNOWN; @@ -1072,6 +1138,7 @@ bool clone::operator()(const QString& offset, bool dryRun) } m_cloned = 0; // reset counter + m_failedClones.clear(); auto src = PathCombine(m_src.absolutePath(), offset); auto dst = PathCombine(m_dst.absolutePath(), offset); @@ -1092,6 +1159,9 @@ bool clone::operator()(const QString& offset, bool dryRun) qDebug() << "Failed to clone files: error" << err.value() << "message" << QString::fromStdString(err.message()); qDebug() << "Source file:" << src_path; qDebug() << "Destination file:" << dst_path; + m_failedClones.append(qMakePair(src_path, dst_path)); + emit cloneFailed(src_path, dst_path); + return; } m_cloned++; emit fileCloned(src_path, dst_path); @@ -1151,7 +1221,7 @@ bool clone_file(const QString& src, const QString& dst, std::error_code& ec) return false; } -#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) +#elif defined(Q_OS_MACOS) if (!macos_bsd_clonefile(src_path, dst_path, ec)) { qDebug() << "failed macos_bsd_clonefile:"; @@ -1380,7 +1450,7 @@ bool linux_ficlone(const std::string& src_path, const std::string& dst_path, std return true; } -#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) +#elif defined(Q_OS_MACOS) bool macos_bsd_clonefile(const std::string& src_path, const std::string& dst_path, std::error_code& ec) { diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index cb581d0c5..f8a82baef 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -43,6 +43,7 @@ #include #include +#include #include #include #include @@ -112,9 +113,12 @@ class copy : public QObject { bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); } int totalCopied() { return m_copied; } + int totalFailed() { return m_failedPaths.length(); } + QStringList failed() { return m_failedPaths; } signals: void fileCopied(const QString& relativeName); + void copyFailed(const QString& relativeName); // TODO: maybe add a "shouldCopy" signal in the future? private: @@ -127,6 +131,7 @@ class copy : public QObject { QDir m_src; QDir m_dst; int m_copied; + QStringList m_failedPaths; }; struct LinkPair { @@ -471,6 +476,9 @@ class clone : public QObject { bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); } int totalCloned() { return m_cloned; } + int totalFailed() { return m_failedClones.length(); } + + QList> failed() { return m_failedClones; } signals: void fileCloned(const QString& src, const QString& dst); @@ -485,6 +493,7 @@ class clone : public QObject { QDir m_src; QDir m_dst; int m_cloned; + QList> m_failedClones; }; /** diff --git a/launcher/InstanceTask.cpp b/launcher/InstanceTask.cpp index 066827829..b16a40ba6 100644 --- a/launcher/InstanceTask.cpp +++ b/launcher/InstanceTask.cpp @@ -45,7 +45,10 @@ QString InstanceName::name() const { if (!m_modified_name.isEmpty()) return modifiedName(); - return QString("%1 %2").arg(m_original_name, m_original_version); + if (!m_original_version.isEmpty()) + return QString("%1 %2").arg(m_original_name, m_original_version); + + return m_original_name; } QString InstanceName::originalName() const diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 070ee283c..2b52cac0b 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -187,8 +187,8 @@ void LaunchController::login() { switch(m_accountToUse->accountState()) { case AccountState::Offline: { m_session->wants_online = false; - // NOTE: fallthrough is intentional } + /* fallthrough */ case AccountState::Online: { if(!m_session->wants_online) { // we ask the user for a player name @@ -267,8 +267,8 @@ void LaunchController::login() { // This means some sort of soft error that we can fix with a refresh ... so let's refresh. case AccountState::Unchecked: { m_accountToUse->refresh(); - // NOTE: fallthrough intentional } + /* fallthrough */ case AccountState::Working: { // refresh is in progress, we need to wait for it to finish to proceed. ProgressDialog progDialog(m_parentWidget); @@ -390,7 +390,10 @@ void LaunchController::launchInstance() m_launcher->prependStep(makeShared(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher)); // Prepend Version - m_launcher->prependStep(makeShared(m_launcher.get(), BuildConfig.LAUNCHER_DISPLAYNAME + " version: " + BuildConfig.printableVersionString() + "\n\n", MessageLevel::Launcher)); + { + auto versionString = QString("%1 version: %2 (%3)").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString(), BuildConfig.BUILD_PLATFORM); + m_launcher->prependStep(makeShared(m_launcher.get(), versionString + "\n\n", MessageLevel::Launcher)); + } m_launcher->start(); } diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index 1a336375b..acd6bf7e4 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -33,56 +34,50 @@ * limitations under the License. */ +#include "MMCZip.h" #include #include #include -#include "MMCZip.h" #include "FileSystem.h" #include #include +#include +namespace MMCZip { // ours -bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet &contained, const FilterFunction filter) +bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet& contained, const FilterFunction filter) { QuaZip modZip(from.filePath()); modZip.open(QuaZip::mdUnzip); QuaZipFile fileInsideMod(&modZip); QuaZipFile zipOutFile(into); - for (bool more = modZip.goToFirstFile(); more; more = modZip.goToNextFile()) - { + for (bool more = modZip.goToFirstFile(); more; more = modZip.goToNextFile()) { QString filename = modZip.getCurrentFileName(); - if (filter && !filter(filename)) - { - qDebug() << "Skipping file " << filename << " from " - << from.fileName() << " - filtered"; + if (filter && !filter(filename)) { + qDebug() << "Skipping file " << filename << " from " << from.fileName() << " - filtered"; continue; } - if (contained.contains(filename)) - { - qDebug() << "Skipping already contained file " << filename << " from " - << from.fileName(); + if (contained.contains(filename)) { + qDebug() << "Skipping already contained file " << filename << " from " << from.fileName(); continue; } contained.insert(filename); - if (!fileInsideMod.open(QIODevice::ReadOnly)) - { + if (!fileInsideMod.open(QIODevice::ReadOnly)) { qCritical() << "Failed to open " << filename << " from " << from.fileName(); return false; } QuaZipNewInfo info_out(fileInsideMod.getActualFileName()); - if (!zipOutFile.open(QIODevice::WriteOnly, info_out)) - { + if (!zipOutFile.open(QIODevice::WriteOnly, info_out)) { qCritical() << "Failed to open " << filename << " in the jar"; fileInsideMod.close(); return false; } - if (!JlCompress::copyData(fileInsideMod, zipOutFile)) - { + if (!JlCompress::copyData(fileInsideMod, zipOutFile)) { zipOutFile.close(); fileInsideMod.close(); qCritical() << "Failed to copy data of " << filename << " into the jar"; @@ -94,10 +89,11 @@ bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet &containe return true; } -bool MMCZip::compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files, bool followSymlinks) +bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool followSymlinks) { QDir directory(dir); - if (!directory.exists()) return false; + if (!directory.exists()) + return false; for (auto e : files) { auto filePath = directory.relativeFilePath(e.absoluteFilePath()); @@ -109,17 +105,18 @@ bool MMCZip::compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files, boo srcPath = e.canonicalFilePath(); } } - if( !JlCompress::compressFile(zip, srcPath, filePath)) return false; + if (!JlCompress::compressFile(zip, srcPath, filePath)) + return false; } return true; } -bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks) +bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks) { QuaZip zip(fileCompressed); QDir().mkpath(QFileInfo(fileCompressed).absolutePath()); - if(!zip.open(QuaZip::mdCreate)) { + if (!zip.open(QuaZip::mdCreate)) { QFile::remove(fileCompressed); return false; } @@ -127,7 +124,7 @@ bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList auto result = compressDirFiles(&zip, dir, files, followSymlinks); zip.close(); - if(zip.getZipError()!=0) { + if (zip.getZipError() != 0) { QFile::remove(fileCompressed); return false; } @@ -136,11 +133,10 @@ bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList } // ours -bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods) +bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods) { QuaZip zipOut(targetJarPath); - if (!zipOut.open(QuaZip::mdCreate)) - { + if (!zipOut.open(QuaZip::mdCreate)) { QFile::remove(targetJarPath); qCritical() << "Failed to open the minecraft.jar for modding"; return false; @@ -151,37 +147,29 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const // Modify the jar // This needs to be done in reverse-order to ensure we respect the loading order of components - for (auto i = mods.crbegin(); i != mods.crend(); i++) - { + for (auto i = mods.crbegin(); i != mods.crend(); i++) { const auto* mod = *i; // do not merge disabled mods. if (!mod->enabled()) continue; - if (mod->type() == ResourceType::ZIPFILE) - { - if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles)) - { + if (mod->type() == ResourceType::ZIPFILE) { + if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles)) { zipOut.close(); QFile::remove(targetJarPath); qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; return false; } - } - else if (mod->type() == ResourceType::SINGLEFILE) - { + } else if (mod->type() == ResourceType::SINGLEFILE) { // FIXME: buggy - does not work with addedFiles auto filename = mod->fileinfo(); - if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) - { + if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) { zipOut.close(); QFile::remove(targetJarPath); qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; return false; } addedFiles.insert(filename.fileName()); - } - else if (mod->type() == ResourceType::FOLDER) - { + } else if (mod->type() == ResourceType::FOLDER) { // untested, but seems to be unused / not possible to reach // FIXME: buggy - does not work with addedFiles auto filename = mod->fileinfo(); @@ -190,25 +178,21 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const dir.cdUp(); QString parent_dir = dir.absolutePath(); auto files = QFileInfoList(); - MMCZip::collectFileListRecursively(what_to_zip, nullptr, &files, nullptr); + collectFileListRecursively(what_to_zip, nullptr, &files, nullptr); for (auto e : files) { if (addedFiles.contains(e.filePath())) files.removeAll(e); } - if (!MMCZip::compressDirFiles(&zipOut, parent_dir, files)) - { + if (!compressDirFiles(&zipOut, parent_dir, files)) { zipOut.close(); QFile::remove(targetJarPath); qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; return false; } - qDebug() << "Adding folder " << filename.fileName() << " from " - << filename.absoluteFilePath(); - } - else - { + qDebug() << "Adding folder " << filename.fileName() << " from " << filename.absoluteFilePath(); + } else { // Make sure we do not continue launching when something is missing or undefined... zipOut.close(); QFile::remove(targetJarPath); @@ -217,8 +201,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const } } - if (!mergeZipFiles(&zipOut, QFileInfo(sourceJarPath), addedFiles, [](const QString key){return !key.contains("META-INF");})) - { + if (!mergeZipFiles(&zipOut, QFileInfo(sourceJarPath), addedFiles, [](const QString key) { return !key.contains("META-INF"); })) { zipOut.close(); QFile::remove(targetJarPath); qCritical() << "Failed to insert minecraft.jar contents."; @@ -227,8 +210,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const // Recompress the jar zipOut.close(); - if (zipOut.getZipError() != 0) - { + if (zipOut.getZipError() != 0) { QFile::remove(targetJarPath); qCritical() << "Failed to finalize minecraft.jar!"; return false; @@ -237,7 +219,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const } // ours -QString MMCZip::findFolderOfFileInZip(QuaZip* zip, const QString& what, const QStringList& ignore_paths, const QString& root) +QString findFolderOfFileInZip(QuaZip* zip, const QString& what, const QStringList& ignore_paths, const QString& root) { QuaZipDir rootDir(zip, root); for (auto&& fileName : rootDir.entryList(QDir::Files)) { @@ -261,27 +243,23 @@ QString MMCZip::findFolderOfFileInZip(QuaZip* zip, const QString& what, const QS } // ours -bool MMCZip::findFilesInZip(QuaZip * zip, const QString & what, QStringList & result, const QString &root) +bool findFilesInZip(QuaZip* zip, const QString& what, QStringList& result, const QString& root) { QuaZipDir rootDir(zip, root); - for(auto fileName: rootDir.entryList(QDir::Files)) - { - if(fileName == what) - { + for (auto fileName : rootDir.entryList(QDir::Files)) { + if (fileName == what) { result.append(root); return true; } } - for(auto fileName: rootDir.entryList(QDir::Dirs)) - { + for (auto fileName : rootDir.entryList(QDir::Dirs)) { findFilesInZip(zip, what, result, root + fileName); } return !result.isEmpty(); } - // ours -std::optional MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target) +std::optional extractSubDir(QuaZip* zip, const QString& subdir, const QString& target) { auto target_top_dir = QUrl::fromLocalFile(target); @@ -289,16 +267,13 @@ std::optional MMCZip::extractSubDir(QuaZip *zip, const QString & su qDebug() << "Extracting subdir" << subdir << "from" << zip->getZipName() << "to" << target; auto numEntries = zip->getEntriesCount(); - if(numEntries < 0) { + if (numEntries < 0) { qWarning() << "Failed to enumerate files in archive"; return std::nullopt; - } - else if(numEntries == 0) { + } else if (numEntries == 0) { qDebug() << "Extracting empty archives seems odd..."; return extracted; - } - else if (!zip->goToFirstFile()) - { + } else if (!zip->goToFirstFile()) { qWarning() << "Failed to seek to first file in zip"; return std::nullopt; } @@ -334,7 +309,8 @@ std::optional MMCZip::extractSubDir(QuaZip *zip, const QString & su } if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) { - qWarning() << "Extracting" << relative_file_name << "was cancelled, because it was effectively outside of the target path" << target; + qWarning() << "Extracting" << relative_file_name << "was cancelled, because it was effectively outside of the target path" + << target; return std::nullopt; } @@ -345,7 +321,8 @@ std::optional MMCZip::extractSubDir(QuaZip *zip, const QString & su } extracted.append(target_file_path); - QFile::setPermissions(target_file_path, QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser); + QFile::setPermissions(target_file_path, + QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser); qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; } while (zip->goToNextFile()); @@ -354,66 +331,66 @@ std::optional MMCZip::extractSubDir(QuaZip *zip, const QString & su } // ours -bool MMCZip::extractRelFile(QuaZip *zip, const QString &file, const QString &target) +bool extractRelFile(QuaZip* zip, const QString& file, const QString& target) { return JlCompress::extractFile(zip, file, target); } // ours -std::optional MMCZip::extractDir(QString fileCompressed, QString dir) +std::optional extractDir(QString fileCompressed, QString dir) { QuaZip zip(fileCompressed); - if (!zip.open(QuaZip::mdUnzip)) - { + if (!zip.open(QuaZip::mdUnzip)) { // check if this is a minimum size empty zip file... QFileInfo fileInfo(fileCompressed); - if(fileInfo.size() == 22) { + if (fileInfo.size() == 22) { return QStringList(); } - qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();; + qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError(); + ; return std::nullopt; } - return MMCZip::extractSubDir(&zip, "", dir); + return extractSubDir(&zip, "", dir); } // ours -std::optional MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir) +std::optional extractDir(QString fileCompressed, QString subdir, QString dir) { QuaZip zip(fileCompressed); - if (!zip.open(QuaZip::mdUnzip)) - { + if (!zip.open(QuaZip::mdUnzip)) { // check if this is a minimum size empty zip file... QFileInfo fileInfo(fileCompressed); - if(fileInfo.size() == 22) { + if (fileInfo.size() == 22) { return QStringList(); } - qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();; + qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError(); + ; return std::nullopt; } - return MMCZip::extractSubDir(&zip, subdir, dir); + return extractSubDir(&zip, subdir, dir); } // ours -bool MMCZip::extractFile(QString fileCompressed, QString file, QString target) +bool extractFile(QString fileCompressed, QString file, QString target) { QuaZip zip(fileCompressed); - if (!zip.open(QuaZip::mdUnzip)) - { + if (!zip.open(QuaZip::mdUnzip)) { // check if this is a minimum size empty zip file... QFileInfo fileInfo(fileCompressed); - if(fileInfo.size() == 22) { + if (fileInfo.size() == 22) { return true; } qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError(); return false; } - return MMCZip::extractRelFile(&zip, file, target); + return extractRelFile(&zip, file, target); } -bool MMCZip::collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList *files, - MMCZip::FilterFunction excludeFilter) { +bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFunction excludeFilter) +{ QDir rootDirectory(rootDir); - if (!rootDirectory.exists()) return false; + if (!rootDirectory.exists()) + return false; QDir directory; if (subDir == nullptr) @@ -421,25 +398,107 @@ bool MMCZip::collectFileListRecursively(const QString& rootDir, const QString& s else directory = QDir(subDir); - if (!directory.exists()) return false; // shouldn't ever happen + if (!directory.exists()) + return false; // shouldn't ever happen // recurse directories QFileInfoList entries = directory.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::Hidden); - for (const auto& e: entries) { + for (const auto& e : entries) { if (!collectFileListRecursively(rootDir, e.filePath(), files, excludeFilter)) return false; } // collect files entries = directory.entryInfoList(QDir::Files); - for (const auto& e: entries) { + for (const auto& e : entries) { QString relativeFilePath = rootDirectory.relativeFilePath(e.absoluteFilePath()); if (excludeFilter && excludeFilter(relativeFilePath)) { qDebug() << "Skipping file " << relativeFilePath; continue; } - files->append(e); // we want the original paths for MMCZip::compressDirFiles + files->append(e); // we want the original paths for compressDirFiles } return true; } + +void ExportToZipTask::executeTask() +{ + setStatus("Adding files..."); + setProgress(0, m_files.length()); + m_build_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return exportZip(); }); + connect(&m_build_zip_watcher, &QFutureWatcher::finished, this, &ExportToZipTask::finish); + m_build_zip_watcher.setFuture(m_build_zip_future); +} + +auto ExportToZipTask::exportZip() -> ZipResult +{ + if (!m_dir.exists()) { + return ZipResult(tr("Folder doesn't exist")); + } + if (!m_output.isOpen() && !m_output.open(QuaZip::mdCreate)) { + return ZipResult(tr("Could not create file")); + } + + for (auto fileName : m_extra_files.keys()) { + if (m_build_zip_future.isCanceled()) + return ZipResult(); + QuaZipFile indexFile(&m_output); + if (!indexFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileName))) { + return ZipResult(tr("Could not create:") + fileName); + } + indexFile.write(m_extra_files[fileName]); + } + + for (const QFileInfo& file : m_files) { + if (m_build_zip_future.isCanceled()) + return ZipResult(); + + auto absolute = file.absoluteFilePath(); + auto relative = m_dir.relativeFilePath(absolute); + setStatus("Compresing: " + relative); + setProgress(m_progress + 1, m_progressTotal); + if (m_follow_symlinks) { + if (file.isSymLink()) + absolute = file.symLinkTarget(); + else + absolute = file.canonicalFilePath(); + } + + if (!m_exclude_files.contains(relative) && !JlCompress::compressFile(&m_output, absolute, m_destination_prefix + relative)) { + return ZipResult(tr("Could not read and compress %1").arg(relative)); + } + } + + m_output.close(); + if (m_output.getZipError() != 0) { + return ZipResult(tr("A zip error occurred")); + } + return ZipResult(); +} + +void ExportToZipTask::finish() +{ + if (m_build_zip_future.isCanceled()) { + QFile::remove(m_output_path); + emitAborted(); + } else if (auto result = m_build_zip_future.result(); result.has_value()) { + QFile::remove(m_output_path); + emitFailed(result.value()); + } else { + emitSucceeded(); + } +} + +bool ExportToZipTask::abort() +{ + if (m_build_zip_future.isRunning()) { + m_build_zip_future.cancel(); + // NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur + // immediately. + return true; + } + return false; +} + +} // namespace MMCZip \ No newline at end of file diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h index 2a78f830f..bc527ad1b 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -35,110 +36,157 @@ #pragma once -#include -#include -#include -#include "minecraft/mod/Mod.h" -#include - +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include "minecraft/mod/Mod.h" +#include "tasks/Task.h" -namespace MMCZip -{ - using FilterFunction = std::function; +namespace MMCZip { +using FilterFunction = std::function; - /** - * Merge two zip files, using a filter function - */ - bool mergeZipFiles(QuaZip *into, QFileInfo from, QSet &contained, - const FilterFunction filter = nullptr); +/** + * Merge two zip files, using a filter function + */ +bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet& contained, const FilterFunction filter = nullptr); - /** - * Compress directory, by providing a list of files to compress - * \param zip target archive - * \param dir directory that will be compressed (to compress with relative paths) - * \param files list of files to compress - * \param followSymlinks should follow symlinks when compressing file data - * \return true for success or false for failure - */ - bool compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files, bool followSymlinks = false); +/** + * Compress directory, by providing a list of files to compress + * \param zip target archive + * \param dir directory that will be compressed (to compress with relative paths) + * \param files list of files to compress + * \param followSymlinks should follow symlinks when compressing file data + * \return true for success or false for failure + */ +bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool followSymlinks = false); - /** - * Compress directory, by providing a list of files to compress - * \param fileCompressed target archive file - * \param dir directory that will be compressed (to compress with relative paths) - * \param files list of files to compress - * \param followSymlinks should follow symlinks when compressing file data - * \return true for success or false for failure - */ - bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks = false); +/** + * Compress directory, by providing a list of files to compress + * \param fileCompressed target archive file + * \param dir directory that will be compressed (to compress with relative paths) + * \param files list of files to compress + * \param followSymlinks should follow symlinks when compressing file data + * \return true for success or false for failure + */ +bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks = false); - /** - * take a source jar, add mods to it, resulting in target jar - */ - bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods); +/** + * take a source jar, add mods to it, resulting in target jar + */ +bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods); - /** - * Find a single file in archive by file name (not path) - * - * \param ignore_paths paths to skip when recursing the search - * - * \return the path prefix where the file is - */ - QString findFolderOfFileInZip(QuaZip * zip, const QString & what, const QStringList& ignore_paths = {}, const QString &root = QString("")); +/** + * Find a single file in archive by file name (not path) + * + * \param ignore_paths paths to skip when recursing the search + * + * \return the path prefix where the file is + */ +QString findFolderOfFileInZip(QuaZip* zip, const QString& what, const QStringList& ignore_paths = {}, const QString& root = QString("")); - /** - * Find a multiple files of the same name in archive by file name - * If a file is found in a path, no deeper paths are searched - * - * \return true if anything was found - */ - bool findFilesInZip(QuaZip * zip, const QString & what, QStringList & result, const QString &root = QString()); +/** + * Find a multiple files of the same name in archive by file name + * If a file is found in a path, no deeper paths are searched + * + * \return true if anything was found + */ +bool findFilesInZip(QuaZip* zip, const QString& what, QStringList& result, const QString& root = QString()); - /** - * Extract a subdirectory from an archive - */ - std::optional extractSubDir(QuaZip *zip, const QString & subdir, const QString &target); +/** + * Extract a subdirectory from an archive + */ +std::optional extractSubDir(QuaZip* zip, const QString& subdir, const QString& target); - bool extractRelFile(QuaZip *zip, const QString & file, const QString &target); +bool extractRelFile(QuaZip* zip, const QString& file, const QString& target); - /** - * Extract a whole archive. - * - * \param fileCompressed The name of the archive. - * \param dir The directory to extract to, the current directory if left empty. - * \return The list of the full paths of the files extracted, empty on failure. - */ - std::optional extractDir(QString fileCompressed, QString dir); +/** + * Extract a whole archive. + * + * \param fileCompressed The name of the archive. + * \param dir The directory to extract to, the current directory if left empty. + * \return The list of the full paths of the files extracted, empty on failure. + */ +std::optional extractDir(QString fileCompressed, QString dir); - /** - * Extract a subdirectory from an archive - * - * \param fileCompressed The name of the archive. - * \param subdir The directory within the archive to extract - * \param dir The directory to extract to, the current directory if left empty. - * \return The list of the full paths of the files extracted, empty on failure. - */ - std::optional extractDir(QString fileCompressed, QString subdir, QString dir); +/** + * Extract a subdirectory from an archive + * + * \param fileCompressed The name of the archive. + * \param subdir The directory within the archive to extract + * \param dir The directory to extract to, the current directory if left empty. + * \return The list of the full paths of the files extracted, empty on failure. + */ +std::optional extractDir(QString fileCompressed, QString subdir, QString dir); - /** - * Extract a single file from an archive into a directory - * - * \param fileCompressed The name of the archive. - * \param file The file within the archive to extract - * \param dir The directory to extract to, the current directory if left empty. - * \return true for success or false for failure - */ - bool extractFile(QString fileCompressed, QString file, QString dir); +/** + * Extract a single file from an archive into a directory + * + * \param fileCompressed The name of the archive. + * \param file The file within the archive to extract + * \param dir The directory to extract to, the current directory if left empty. + * \return true for success or false for failure + */ +bool extractFile(QString fileCompressed, QString file, QString dir); - /** - * Populate a QFileInfoList with a directory tree recursively, while allowing to excludeFilter what shouldn't be included. - * \param rootDir directory to start off - * \param subDir subdirectory, should be nullptr for first invocation - * \param files resulting list of QFileInfo - * \param excludeFilter function to excludeFilter which files shouldn't be included (returning true means to excude) - * \return true for success or false for failure - */ - bool collectFileListRecursively(const QString &rootDir, const QString &subDir, QFileInfoList *files, FilterFunction excludeFilter); -} +/** + * Populate a QFileInfoList with a directory tree recursively, while allowing to excludeFilter what shouldn't be included. + * \param rootDir directory to start off + * \param subDir subdirectory, should be nullptr for first invocation + * \param files resulting list of QFileInfo + * \param excludeFilter function to excludeFilter which files shouldn't be included (returning true means to excude) + * \return true for success or false for failure + */ +bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFunction excludeFilter); + +class ExportToZipTask : public Task { + public: + ExportToZipTask(QString outputPath, QDir dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false) + : m_output_path(outputPath) + , m_output(outputPath) + , m_dir(dir) + , m_files(files) + , m_destination_prefix(destinationPrefix) + , m_follow_symlinks(followSymlinks) + { + setAbortable(true); + }; + ExportToZipTask(QString outputPath, QString dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false) + : ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks){}; + + virtual ~ExportToZipTask() = default; + + void setExcludeFiles(QStringList excludeFiles) { m_exclude_files = excludeFiles; } + void addExtraFile(QString fileName, QByteArray data) { m_extra_files.insert(fileName, data); } + + typedef std::optional ZipResult; + + protected: + virtual void executeTask() override; + bool abort() override; + + ZipResult exportZip(); + void finish(); + + private: + QString m_output_path; + QuaZip m_output; + QDir m_dir; + QFileInfoList m_files; + QString m_destination_prefix; + bool m_follow_symlinks; + QStringList m_exclude_files; + QHash m_extra_files; + + QFuture m_build_zip_future; + QFutureWatcher m_build_zip_watcher; +}; +} // namespace MMCZip diff --git a/launcher/MTPixmapCache.h b/launcher/MTPixmapCache.h index 57847a0e1..65cbe032a 100644 --- a/launcher/MTPixmapCache.h +++ b/launcher/MTPixmapCache.h @@ -3,6 +3,8 @@ #include #include #include +#include +#include #define GET_TYPE() \ Qt::ConnectionType type; \ @@ -60,6 +62,8 @@ class PixmapCache final : public QObject { DEFINE_FUNC_ONE_PARAM(remove, bool, const QPixmapCache::Key&) DEFINE_FUNC_TWO_PARAM(replace, bool, const QPixmapCache::Key&, const QPixmap&) DEFINE_FUNC_ONE_PARAM(setCacheLimit, bool, int) + DEFINE_FUNC_NO_PARAM(markCacheMissByEviciton, bool) + DEFINE_FUNC_ONE_PARAM(setFastEvictionThreshold, bool, int) // NOTE: Every function returns something non-void to simplify the macros. private slots: @@ -90,6 +94,43 @@ class PixmapCache final : public QObject { return true; } + /** + * Mark that a cache miss occurred because of a eviction if too many of these occur too fast the cache size is increased + * @return if the cache size was increased + */ + bool _markCacheMissByEviciton() + { + auto now = QTime::currentTime(); + if (!m_last_cache_miss_by_eviciton.isNull()) { + auto diff = m_last_cache_miss_by_eviciton.msecsTo(now); + if (diff < 1000) { // less than a second ago + ++m_consecutive_fast_evicitons; + } else { + m_consecutive_fast_evicitons = 0; + } + } + m_last_cache_miss_by_eviciton = now; + if (m_consecutive_fast_evicitons >= m_consecutive_fast_evicitons_threshold) { + // double the cache size + auto newSize = _cacheLimit() * 2; + qDebug() << m_consecutive_fast_evicitons << "pixmap cache misses by eviction happened too fast, doubling cache size to" + << newSize; + _setCacheLimit(newSize); + m_consecutive_fast_evicitons = 0; + return true; + } + return false; + } + + bool _setFastEvictionThreshold(int threshold) + { + m_consecutive_fast_evicitons_threshold = threshold; + return true; + } + private: static PixmapCache* s_instance; + QTime m_last_cache_miss_by_eviciton; + int m_consecutive_fast_evicitons = 0; + int m_consecutive_fast_evicitons_threshold = 15; }; diff --git a/launcher/Markdown.cpp b/launcher/Markdown.cpp new file mode 100644 index 000000000..426067bf6 --- /dev/null +++ b/launcher/Markdown.cpp @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Joshua Goins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "Markdown.h" + +QString markdownToHTML(const QString& markdown) +{ + const QByteArray markdownData = markdown.toUtf8(); + char* buffer = cmark_markdown_to_html(markdownData.constData(), markdownData.length(), CMARK_OPT_NOBREAKS | CMARK_OPT_UNSAFE); + + QString htmlStr(buffer); + + free(buffer); + + return htmlStr; +} \ No newline at end of file diff --git a/launcher/Markdown.h b/launcher/Markdown.h index f115dd570..6b261e60c 100644 --- a/launcher/Markdown.h +++ b/launcher/Markdown.h @@ -21,14 +21,4 @@ #include #include -static QString markdownToHTML(const QString& markdown) -{ - const QByteArray markdownData = markdown.toUtf8(); - char* buffer = cmark_markdown_to_html(markdownData.constData(), markdownData.length(), CMARK_OPT_NOBREAKS | CMARK_OPT_UNSAFE); - - QString htmlStr(buffer); - - free(buffer); - - return htmlStr; -} \ No newline at end of file +QString markdownToHTML(const QString& markdown); \ No newline at end of file diff --git a/launcher/ResourceDownloadTask.h b/launcher/ResourceDownloadTask.h index 09147c8cb..2baddf8a8 100644 --- a/launcher/ResourceDownloadTask.h +++ b/launcher/ResourceDownloadTask.h @@ -38,6 +38,8 @@ class ResourceDownloadTask : public SequentialTask { const QString& getFilename() const { return m_pack_version.fileName; } const QString& getCustomPath() const { return m_custom_target_folder; } const QVariant& getVersionID() const { return m_pack_version.fileId; } + const ModPlatform::IndexedVersion& getVersion() const { return m_pack_version; } + const ModPlatform::ResourceProvider& getProvider() const { return m_pack->provider; } const QString& getName() const { return m_pack->name; } ModPlatform::IndexedPack::Ptr getPack() { return m_pack; } diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp index 6aba268d8..63a43465c 100644 --- a/launcher/VersionProxyModel.cpp +++ b/launcher/VersionProxyModel.cpp @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -54,9 +55,14 @@ public: bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { const auto &filters = m_parent->filters(); + const QString &search = m_parent->search(); + const QModelIndex idx = sourceModel()->index(source_row, 0, source_parent); + + if (!search.isEmpty() && !sourceModel()->data(idx, BaseVersionList::VersionRole).toString().contains(search, Qt::CaseInsensitive)) + return false; + for (auto it = filters.begin(); it != filters.end(); ++it) { - auto idx = sourceModel()->index(source_row, 0, source_parent); auto data = sourceModel()->data(idx, it.key()); auto match = data.toString(); if(!it.value()->accepts(match)) @@ -187,35 +193,21 @@ QVariant VersionProxyModel::data(const QModelIndex &index, int role) const } case Qt::ToolTipRole: { - switch(column) + if(column == Name && hasRecommended) { - case Name: + auto value = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole); + if(value.toBool()) { - if(hasRecommended) + return tr("Recommended"); + } else if(hasLatest) { + auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); + if(value.toBool()) { - auto value = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole); - if(value.toBool()) - { - return tr("Recommended"); - } - else if(hasLatest) - { - auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); - if(value.toBool()) - { - return tr("Latest"); - } - } - else if(index.row() == 0) - { - return tr("Latest"); - } + return tr("Latest"); } } - default: - { - return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole); - } + } else { + return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole); } } case Qt::DecorationRole: @@ -239,10 +231,6 @@ QVariant VersionProxyModel::data(const QModelIndex &index, int role) const return APPLICATION->getThemedIcon("bug"); } } - else if(index.row() == 0) - { - return APPLICATION->getThemedIcon("bug"); - } QPixmap pixmap; QPixmapCache::find("placeholder", &pixmap); if(!pixmap) @@ -431,6 +419,7 @@ QModelIndex VersionProxyModel::getVersion(const QString& version) const void VersionProxyModel::clearFilters() { m_filters.clear(); + m_search.clear(); filterModel->filterChanged(); } @@ -440,11 +429,21 @@ void VersionProxyModel::setFilter(const BaseVersionList::ModelRoles column, Filt filterModel->filterChanged(); } +void VersionProxyModel::setSearch(const QString &search) { + m_search = search; + filterModel->filterChanged(); +} + const VersionProxyModel::FilterMap &VersionProxyModel::filters() const { return m_filters; } +const QString &VersionProxyModel::search() const +{ + return m_search; +} + void VersionProxyModel::sourceAboutToBeReset() { beginResetModel(); diff --git a/launcher/VersionProxyModel.h b/launcher/VersionProxyModel.h index 8991c31b0..6434376c6 100644 --- a/launcher/VersionProxyModel.h +++ b/launcher/VersionProxyModel.h @@ -38,7 +38,9 @@ public: virtual void setSourceModel(QAbstractItemModel *sourceModel) override; const FilterMap &filters() const; + const QString &search() const; void setFilter(const BaseVersionList::ModelRoles column, Filter * filter); + void setSearch(const QString &search); void clearFilters(); QModelIndex getRecommended() const; QModelIndex getVersion(const QString & version) const; @@ -59,6 +61,7 @@ private slots: private: QList m_columns; FilterMap m_filters; + QString m_search; BaseVersionList::RoleList roles; VersionFilterModel * filterModel; bool hasRecommended = false; diff --git a/launcher/java/JavaChecker.cpp b/launcher/java/JavaChecker.cpp index b4c55b3d7..e4a686c27 100644 --- a/launcher/java/JavaChecker.cpp +++ b/launcher/java/JavaChecker.cpp @@ -85,7 +85,7 @@ void JavaChecker::performCheck() process->setProgram(m_path); process->setProcessChannelMode(QProcess::SeparateChannels); process->setProcessEnvironment(CleanEnviroment()); - qDebug() << "Running java checker: " + m_path + args.join(" ");; + qDebug() << "Running java checker:" << m_path << args.join(" "); connect(process.get(), QOverload::of(&QProcess::finished), this, &JavaChecker::finished); connect(process.get(), &QProcess::errorOccurred, this, &JavaChecker::error); @@ -128,7 +128,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) result.outLog = m_stdout; qDebug() << "STDOUT" << m_stdout; qWarning() << "STDERR" << m_stderr; - qDebug() << "Java checker finished with status " << status << " exit code " << exitcode; + qDebug() << "Java checker finished with status" << status << "exit code" << exitcode; if (status == QProcess::CrashExit || exitcode == 1) { diff --git a/launcher/java/JavaInstall.cpp b/launcher/java/JavaInstall.cpp index d5932bcb9..cfa471402 100644 --- a/launcher/java/JavaInstall.cpp +++ b/launcher/java/JavaInstall.cpp @@ -1,29 +1,64 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + #include "JavaInstall.h" +#include "BaseVersion.h" #include "StringUtils.h" -bool JavaInstall::operator<(const JavaInstall &rhs) +bool JavaInstall::operator<(const JavaInstall& rhs) { auto archCompare = StringUtils::naturalCompare(arch, rhs.arch, Qt::CaseInsensitive); - if(archCompare != 0) + if (archCompare != 0) return archCompare < 0; - if(id < rhs.id) - { + if (id < rhs.id) { return true; } - if(id > rhs.id) - { + if (id > rhs.id) { return false; } return StringUtils::naturalCompare(path, rhs.path, Qt::CaseInsensitive) < 0; } -bool JavaInstall::operator==(const JavaInstall &rhs) +bool JavaInstall::operator==(const JavaInstall& rhs) { return arch == rhs.arch && id == rhs.id && path == rhs.path; } -bool JavaInstall::operator>(const JavaInstall &rhs) +bool JavaInstall::operator>(const JavaInstall& rhs) { return (!operator<(rhs)) && (!operator==(rhs)); } + +bool JavaInstall::operator<(BaseVersion& a) +{ + try { + return operator<(dynamic_cast(a)); + } catch (const std::bad_cast& e) { + return BaseVersion::operator<(a); + } +} + +bool JavaInstall::operator>(BaseVersion& a) +{ + try { + return operator>(dynamic_cast(a)); + } catch (const std::bad_cast& e) { + return BaseVersion::operator>(a); + } +} diff --git a/launcher/java/JavaInstall.h b/launcher/java/JavaInstall.h index 64be40d19..30815b5a8 100644 --- a/launcher/java/JavaInstall.h +++ b/launcher/java/JavaInstall.h @@ -1,33 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + #pragma once #include "BaseVersion.h" #include "JavaVersion.h" -struct JavaInstall : public BaseVersion -{ - JavaInstall(){} - JavaInstall(QString id, QString arch, QString path) - : id(id), arch(arch), path(path) - { - } - virtual QString descriptor() - { - return id.toString(); - } +struct JavaInstall : public BaseVersion { + JavaInstall() {} + JavaInstall(QString id, QString arch, QString path) : id(id), arch(arch), path(path) {} + virtual QString descriptor() { return id.toString(); } - virtual QString name() - { - return id.toString(); - } + virtual QString name() { return id.toString(); } - virtual QString typeString() const - { - return arch; - } + virtual QString typeString() const { return arch; } - bool operator<(const JavaInstall & rhs); - bool operator==(const JavaInstall & rhs); - bool operator>(const JavaInstall & rhs); + virtual bool operator<(BaseVersion& a) override; + virtual bool operator>(BaseVersion& a) override; + bool operator<(const JavaInstall& rhs); + bool operator==(const JavaInstall& rhs); + bool operator>(const JavaInstall& rhs); JavaVersion id; QString arch; diff --git a/launcher/java/JavaInstallList.cpp b/launcher/java/JavaInstallList.cpp index b29af8576..3407fdf72 100644 --- a/launcher/java/JavaInstallList.cpp +++ b/launcher/java/JavaInstallList.cpp @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -98,6 +99,8 @@ QVariant JavaInstallList::data(const QModelIndex &index, int role) const auto version = std::dynamic_pointer_cast(m_vlist[index.row()]); switch (role) { + case SortRole: + return -index.row(); case VersionPointerRole: return QVariant::fromValue(m_vlist[index.row()]); case VersionIdRole: diff --git a/launcher/launch/steps/CheckJava.cpp b/launcher/launch/steps/CheckJava.cpp index f01875861..7d697ba96 100644 --- a/launcher/launch/steps/CheckJava.cpp +++ b/launcher/launch/steps/CheckJava.cpp @@ -81,15 +81,20 @@ void CheckJava::executeTask() } QFileInfo javaInfo(realJavaPath); - qlonglong javaUnixTime = javaInfo.lastModified().toMSecsSinceEpoch(); - auto storedUnixTime = settings->get("JavaTimestamp").toLongLong(); + qint64 javaUnixTime = javaInfo.lastModified().toMSecsSinceEpoch(); + auto storedSignature = settings->get("JavaSignature").toString(); auto storedArchitecture = settings->get("JavaArchitecture").toString(); auto storedRealArchitecture = settings->get("JavaRealArchitecture").toString(); auto storedVersion = settings->get("JavaVersion").toString(); auto storedVendor = settings->get("JavaVendor").toString(); - m_javaUnixTime = javaUnixTime; + + QCryptographicHash hash(QCryptographicHash::Sha1); + hash.addData(QByteArray::number(javaUnixTime)); + hash.addData(m_javaPath.toUtf8()); + m_javaSignature = hash.result().toHex(); + // if timestamps are not the same, or something is missing, check! - if (javaUnixTime != storedUnixTime || storedVersion.size() == 0 + if (m_javaSignature != storedSignature || storedVersion.size() == 0 || storedArchitecture.size() == 0 || storedRealArchitecture.size() == 0 || storedVendor.size() == 0) { @@ -140,7 +145,7 @@ void CheckJava::checkJavaFinished(JavaCheckResult result) instance->settings()->set("JavaArchitecture", result.mojangPlatform); instance->settings()->set("JavaRealArchitecture", result.realPlatform); instance->settings()->set("JavaVendor", result.javaVendor); - instance->settings()->set("JavaTimestamp", m_javaUnixTime); + instance->settings()->set("JavaSignature", m_javaSignature); emitSucceeded(); return; } diff --git a/launcher/launch/steps/CheckJava.h b/launcher/launch/steps/CheckJava.h index d084b1321..bbf06b7c7 100644 --- a/launcher/launch/steps/CheckJava.h +++ b/launcher/launch/steps/CheckJava.h @@ -40,6 +40,6 @@ private: private: QString m_javaPath; - qlonglong m_javaUnixTime; + QString m_javaSignature; JavaCheckerPtr m_JavaChecker; }; diff --git a/launcher/meta/Index.cpp b/launcher/meta/Index.cpp index 242aad9f9..4dccccca8 100644 --- a/launcher/meta/Index.cpp +++ b/launcher/meta/Index.cpp @@ -45,10 +45,10 @@ QVariant Index::data(const QModelIndex &index, int role) const switch (role) { case Qt::DisplayRole: - switch (index.column()) - { - case 0: return list->humanReadable(); - default: break; + if (index.column() == 0) { + return list->humanReadable(); + } else { + break; } case UidRole: return list->uid(); case NameRole: return list->name(); diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index d18c6e0eb..e0e4cdcf0 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -4,6 +4,7 @@ * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 Jamie Mansfield * Copyright (C) 2022 TheKodeToad + * Copyright (c) 2023 seth * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -148,10 +149,11 @@ void MinecraftInstance::loadSpecificSettings() m_settings->registerOverride(global_settings->getSetting("IgnoreJavaCompatibility"), javaOrLocation); // special! - m_settings->registerPassthrough(global_settings->getSetting("JavaTimestamp"), javaOrLocation); - m_settings->registerPassthrough(global_settings->getSetting("JavaVersion"), javaOrLocation); + m_settings->registerPassthrough(global_settings->getSetting("JavaSignature"), javaOrLocation); m_settings->registerPassthrough(global_settings->getSetting("JavaArchitecture"), javaOrLocation); m_settings->registerPassthrough(global_settings->getSetting("JavaRealArchitecture"), javaOrLocation); + m_settings->registerPassthrough(global_settings->getSetting("JavaVersion"), javaOrLocation); + m_settings->registerPassthrough(global_settings->getSetting("JavaVendor"), javaOrLocation); // Window Size auto windowSetting = m_settings->registerSetting("OverrideWindow", false); @@ -185,6 +187,10 @@ void MinecraftInstance::loadSpecificSettings() m_settings->registerOverride(global_settings->getSetting("CloseAfterLaunch"), miscellaneousOverride); m_settings->registerOverride(global_settings->getSetting("QuitAfterGameStop"), miscellaneousOverride); + // Mod loader specific options + auto modLoaderSettings = m_settings->registerSetting("OverrideModLoaderSettings", false); + m_settings->registerOverride(global_settings->getSetting("DisableQuiltBeacon"), modLoaderSettings); + m_settings->set("InstanceType", "OneSix"); } @@ -392,6 +398,12 @@ QStringList MinecraftInstance::extraArguments() agent->library()->getApplicableFiles(runtimeContext(), jar, temp1, temp2, temp3, getLocalLibraryPath()); list.append("-javaagent:"+jar[0]+(agent->argument().isEmpty() ? "" : "="+agent->argument())); } + + { + const auto loaders = version->getModLoaders(); + if (loaders.has_value() && loaders.value() & ResourceAPI::Quilt && settings()->get("DisableQuiltBeacon").toBool()) + list.append("-Dloader.disable_beacon=true"); + } return list; } @@ -1124,36 +1136,27 @@ JavaVersion MinecraftInstance::getJavaVersion() std::shared_ptr MinecraftInstance::loaderModList() { - if (!m_loader_mod_list) - { + if (!m_loader_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_loader_mod_list.reset(new ModFolderModel(modsRoot(), this, is_indexed)); - m_loader_mod_list->disableInteraction(isRunning()); - connect(this, &BaseInstance::runningStatusChanged, m_loader_mod_list.get(), &ModFolderModel::disableInteraction); } return m_loader_mod_list; } std::shared_ptr MinecraftInstance::coreModList() { - if (!m_core_mod_list) - { + if (!m_core_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_core_mod_list.reset(new ModFolderModel(coreModsDir(), this, is_indexed)); - m_core_mod_list->disableInteraction(isRunning()); - connect(this, &BaseInstance::runningStatusChanged, m_core_mod_list.get(), &ModFolderModel::disableInteraction); } return m_core_mod_list; } std::shared_ptr MinecraftInstance::nilModList() { - if (!m_nil_mod_list) - { + if (!m_nil_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_nil_mod_list.reset(new ModFolderModel(nilModsDir(), this, is_indexed, false)); - m_nil_mod_list->disableInteraction(isRunning()); - connect(this, &BaseInstance::runningStatusChanged, m_nil_mod_list.get(), &ModFolderModel::disableInteraction); } return m_nil_mod_list; } diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index aff05dbc8..e8fd21572 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -65,7 +65,8 @@ static const QMap modloaderMapping{ {"net.minecraftforge", ResourceAPI::Forge}, {"net.fabricmc.fabric-loader", ResourceAPI::Fabric}, - {"org.quiltmc.quilt-loader", ResourceAPI::Quilt} + {"org.quiltmc.quilt-loader", ResourceAPI::Quilt}, + {"com.mumfrey.liteloader", ResourceAPI::LiteLoader} }; PackProfile::PackProfile(MinecraftInstance * instance) diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index 9e2fd1113..d6f42b75c 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -328,18 +328,21 @@ QVariant AccountList::data(const QModelIndex &index, int role) const case AccountState::Gone: { return tr("Gone", "Account status"); } + default: { + return tr("Unknown", "Account status"); + } } } case MigrationColumn: { if(account->isMSA() || account->isOffline()) { - return tr("N/A", "Can Migrate?"); + return tr("N/A", "Can Migrate"); } if (account->canMigrate()) { - return tr("Yes", "Can Migrate?"); + return tr("Yes", "Can Migrate"); } else { - return tr("No", "Can Migrate?"); + return tr("No", "Can Migrate"); } } @@ -354,11 +357,12 @@ QVariant AccountList::data(const QModelIndex &index, int role) const return QVariant::fromValue(account); case Qt::CheckStateRole: - switch (index.column()) - { - case ProfileNameColumn: - return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked; + if (index.column() == ProfileNameColumn) { + return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked; + } else { + return QVariant(); } + default: return QVariant(); diff --git a/launcher/minecraft/auth/AuthSession.cpp b/launcher/minecraft/auth/AuthSession.cpp index 6bea74a37..2c06beaa2 100644 --- a/launcher/minecraft/auth/AuthSession.cpp +++ b/launcher/minecraft/auth/AuthSession.cpp @@ -26,6 +26,7 @@ bool AuthSession::MakeOffline(QString offline_playername) return false; } session = "-"; + access_token = "0"; player_name = offline_playername; status = PlayableOffline; return true; diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index 3b050ac0f..d7b061e5e 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -37,6 +37,7 @@ #include "MinecraftAccount.h" +#include #include #include #include @@ -100,7 +101,7 @@ MinecraftAccountPtr MinecraftAccount::createOffline(const QString &username) account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); account->data.minecraftEntitlement.ownsMinecraft = true; account->data.minecraftEntitlement.canPlayMinecraft = true; - account->data.minecraftProfile.id = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); + account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(QRegularExpression("[{}-]")); account->data.minecraftProfile.name = username; account->data.minecraftProfile.validity = Katabasis::Validity::Certain; return account; @@ -334,3 +335,32 @@ void MinecraftAccount::incrementUses() qWarning() << "Profile" << data.profileId() << "is now in use."; } } + +QUuid MinecraftAccount::uuidFromUsername(QString username) { + auto input = QString("OfflinePlayer:%1").arg(username).toUtf8(); + + // basically a reimplementation of Java's UUID#nameUUIDFromBytes + QByteArray digest = QCryptographicHash::hash(input, QCryptographicHash::Md5); + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + auto bOr = [](QByteArray& array, int index, char value) { + array[index] = array.at(index) | value; + }; + auto bAnd = [](QByteArray& array, int index, char value) { + array[index] = array.at(index) & value; + }; +#else + auto bOr = [](QByteArray& array, qsizetype index, char value) { + array[index] |= value; + }; + auto bAnd = [](QByteArray& array, qsizetype index, char value) { + array[index] &= value; + }; +#endif + bAnd(digest, 6, (char) 0x0f); // clear version + bOr(digest, 6, (char) 0x30); // set to version 3 + bAnd(digest, 8, (char) 0x3f); // clear variant + bOr(digest, 8, (char) 0x80); // set to IETF variant + + return QUuid::fromRfc4122(digest); +} diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h index 0dcaeb531..67623a5ab 100644 --- a/launcher/minecraft/auth/MinecraftAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -98,6 +98,8 @@ public: /* construction */ static MinecraftAccountPtr loadFromJsonV2(const QJsonObject &json); static MinecraftAccountPtr loadFromJsonV3(const QJsonObject &json); + static QUuid uuidFromUsername(QString username); + //! Saves a MinecraftAccount to a JSON object and returns it. QJsonObject saveToJson() const; diff --git a/launcher/minecraft/auth/Yggdrasil.cpp b/launcher/minecraft/auth/Yggdrasil.cpp index 299784119..d3e7ccddb 100644 --- a/launcher/minecraft/auth/Yggdrasil.cpp +++ b/launcher/minecraft/auth/Yggdrasil.cpp @@ -273,6 +273,7 @@ void Yggdrasil::processReply() { AccountTaskState::STATE_FAILED_GONE, tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account.") ); + return; } default: changeState( diff --git a/launcher/minecraft/mod/DataPack.cpp b/launcher/minecraft/mod/DataPack.cpp index ca75cd2aa..c5754638a 100644 --- a/launcher/minecraft/mod/DataPack.cpp +++ b/launcher/minecraft/mod/DataPack.cpp @@ -74,6 +74,7 @@ std::pair DataPack::compare(const Resource& other, SortType type) con auto res = Resource::compare(other, type); if (res.first != 0) return res; + break; } case SortType::PACK_FORMAT: { auto this_ver = packFormat(); @@ -83,6 +84,7 @@ std::pair DataPack::compare(const Resource& other, SortType type) con return { 1, type == SortType::PACK_FORMAT }; if (this_ver < other_ver) return { -1, type == SortType::PACK_FORMAT }; + break; } } return { 0, false }; diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index c495cd47e..880dacb15 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -41,9 +41,11 @@ #include #include +#include "MTPixmapCache.h" #include "MetadataHandler.h" #include "Version.h" #include "minecraft/mod/ModDetails.h" +#include "minecraft/mod/tasks/LocalModParseTask.h" static ModPlatform::ProviderCapabilities ProviderCaps; @@ -89,6 +91,7 @@ std::pair Mod::compare(const Resource& other, SortType type) const auto res = Resource::compare(other, type); if (res.first != 0) return res; + break; } case SortType::VERSION: { auto this_ver = Version(version()); @@ -97,11 +100,13 @@ std::pair Mod::compare(const Resource& other, SortType type) const return { 1, type == SortType::VERSION }; if (this_ver < other_ver) return { -1, type == SortType::VERSION }; + break; } case SortType::PROVIDER: { auto compare_result = QString::compare(provider().value_or("Unknown"), cast_other->provider().value_or("Unknown"), Qt::CaseInsensitive); if (compare_result != 0) return { compare_result, type == SortType::PROVIDER }; + break; } } return { 0, false }; @@ -121,7 +126,7 @@ bool Mod::applyFilter(QRegularExpression filter) const return Resource::applyFilter(filter); } -auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool +auto Mod::destroy(QDir& index_dir, bool preserve_metadata, bool attempt_trash) -> bool { if (!preserve_metadata) { qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name()); @@ -134,7 +139,7 @@ auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool } } - return Resource::destroy(); + return Resource::destroy(attempt_trash); } auto Mod::details() const -> const ModDetails& @@ -164,6 +169,13 @@ auto Mod::homeurl() const -> QString return details().homeurl; } +auto Mod::metaurl() const -> QString +{ + if (metadata() == nullptr) + return homeurl(); + return ModPlatform::getMetaURL(metadata()->provider, metadata()->project_id); +} + auto Mod::description() const -> QString { return details().description; @@ -201,7 +213,10 @@ void Mod::finishResolvingWithDetails(ModDetails&& details) m_local_details = std::move(details); if (metadata) setMetadata(std::move(metadata)); -}; + if (!iconPath().isEmpty()) { + m_pack_image_cache_key.was_read_attempt = false; + } +} auto Mod::provider() const -> std::optional { @@ -210,6 +225,56 @@ auto Mod::provider() const -> std::optional return {}; } +auto Mod::licenses() const -> const QList& +{ + return details().licenses; +} + + auto Mod::issueTracker() const -> QString +{ + return details().issue_tracker; +} + +void Mod::setIcon(QImage new_image) const +{ + QMutexLocker locker(&m_data_lock); + + Q_ASSERT(!new_image.isNull()); + + if (m_pack_image_cache_key.key.isValid()) + PixmapCache::remove(m_pack_image_cache_key.key); + + // scale the image to avoid flooding the pixmapcache + auto pixmap = QPixmap::fromImage(new_image.scaled({64, 64}, Qt::AspectRatioMode::KeepAspectRatioByExpanding)); + + m_pack_image_cache_key.key = PixmapCache::insert(pixmap); + m_pack_image_cache_key.was_ever_used = true; + m_pack_image_cache_key.was_read_attempt = true; +} + +QPixmap Mod::icon(QSize size, Qt::AspectRatioMode mode) const +{ + QPixmap cached_image; + if (PixmapCache::find(m_pack_image_cache_key.key, &cached_image)) { + if (size.isNull()) + return cached_image; + return cached_image.scaled(size, mode); + } + + // No valid image we can get + if ((!m_pack_image_cache_key.was_ever_used && m_pack_image_cache_key.was_read_attempt) || iconPath().isEmpty()) + return {}; + + if (m_pack_image_cache_key.was_ever_used) { + qDebug() << "Mod" << name() << "Had it's icon evicted form the cache. reloading..."; + PixmapCache::markCacheMissByEviciton(); + } + // Image got evicted from the cache or an attempt to load it has not been made. load it and retry. + m_pack_image_cache_key.was_read_attempt = true; + ModUtils::loadIconFile(*this); + return icon(size); +} + bool Mod::valid() const { return !m_local_details.mod_id.isEmpty(); diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index c40325387..b67bd4659 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -38,6 +38,10 @@ #include #include #include +#include +#include +#include +#include #include @@ -64,6 +68,16 @@ public: auto authors() const -> QStringList; auto status() const -> ModStatus; auto provider() const -> std::optional; + auto licenses() const -> const QList&; + auto issueTracker() const -> QString; + auto metaurl() const -> QString; + + /** Get the intneral path to the mod's icon file*/ + QString iconPath() const { return m_local_details.icon_file; }; + /** Gets the icon of the mod, converted to a QPixmap for drawing, and scaled to size. */ + [[nodiscard]] QPixmap icon(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; + /** Thread-safe. */ + void setIcon(QImage new_image) const; auto metadata() -> std::shared_ptr; auto metadata() const -> const std::shared_ptr; @@ -79,10 +93,19 @@ public: [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; // Delete all the files of this mod - auto destroy(QDir& index_dir, bool preserve_metadata = false) -> bool; + auto destroy(QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool; void finishResolvingWithDetails(ModDetails&& details); protected: ModDetails m_local_details; + + mutable QMutex m_data_lock; + + struct { + QPixmapCache::Key key; + bool was_ever_used = false; + bool was_read_attempt = false; + } mutable m_pack_image_cache_key; + }; diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h index 176e4fc14..b4e59d52d 100644 --- a/launcher/minecraft/mod/ModDetails.h +++ b/launcher/minecraft/mod/ModDetails.h @@ -39,6 +39,7 @@ #include #include +#include #include "minecraft/mod/MetadataHandler.h" @@ -49,6 +50,84 @@ enum class ModStatus { Unknown, // Default status }; +struct ModLicense { + QString name = {}; + QString id = {}; + QString url = {}; + QString description = {}; + + ModLicense() {} + + ModLicense(const QString license) { + // FIXME: come up with a better license parseing. + // handle SPDX identifiers? https://spdx.org/licenses/ + auto parts = license.split(' '); + QStringList notNameParts = {}; + for (auto part : parts) { + auto url = QUrl(part); + if (part.startsWith("(") && part.endsWith(")")) + url = QUrl(part.mid(1, part.size() - 2)); + + if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) { + this->url = url.toString(); + notNameParts.append(part); + continue; + } + } + + for (auto part : notNameParts) { + parts.removeOne(part); + } + + auto licensePart = parts.join(' '); + this->name = licensePart; + this->description = licensePart; + + if (parts.size() == 1) { + this->id = parts.first(); + } + + } + + ModLicense(const QString name, const QString id, const QString url, const QString description) { + this->name = name; + this->id = id; + this->url = url; + this->description = description; + } + + ModLicense(const ModLicense& other) + : name(other.name) + , id(other.id) + , url(other.url) + , description(other.description) + {} + + ModLicense& operator=(const ModLicense& other) + { + this->name = other.name; + this->id = other.id; + this->url = other.url; + this->description = other.description; + + return *this; + } + + ModLicense& operator=(const ModLicense&& other) + { + this->name = other.name; + this->id = other.id; + this->url = other.url; + this->description = other.description; + + return *this; + } + + bool isEmpty() { + return this->name.isEmpty() && this->id.isEmpty() && this->url.isEmpty() && this->description.isEmpty(); + } +}; + struct ModDetails { /* Mod ID as defined in the ModLoader-specific metadata */ @@ -72,6 +151,15 @@ struct ModDetails /* List of the author's names */ QStringList authors = {}; + /* Issue Tracker URL */ + QString issue_tracker = {}; + + /* License */ + QList licenses = {}; + + /* Path of mod logo */ + QString icon_file = {}; + /* Installation status of the mod */ ModStatus status = ModStatus::Unknown; @@ -89,6 +177,9 @@ struct ModDetails , homeurl(other.homeurl) , description(other.description) , authors(other.authors) + , issue_tracker(other.issue_tracker) + , licenses(other.licenses) + , icon_file(other.icon_file) , status(other.status) {} @@ -101,6 +192,9 @@ struct ModDetails this->homeurl = other.homeurl; this->description = other.description; this->authors = other.authors; + this->issue_tracker = other.issue_tracker; + this->licenses = other.licenses; + this->icon_file = other.icon_file; this->status = other.status; return *this; @@ -115,6 +209,9 @@ struct ModDetails this->homeurl = other.homeurl; this->description = other.description; this->authors = other.authors; + this->issue_tracker = other.issue_tracker; + this->licenses = other.licenses; + this->icon_file = other.icon_file; this->status = other.status; return *this; diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 5e3b31e08..51383edf0 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -37,6 +37,7 @@ #include "ModFolderModel.h" #include +#include #include #include #include @@ -52,12 +53,14 @@ #include "minecraft/mod/tasks/LocalModParseTask.h" #include "minecraft/mod/tasks/ModFolderLoadTask.h" -#include "modplatform/ModIndex.h" ModFolderModel::ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir) : ResourceFolderModel(QDir(dir), instance, nullptr, create_dir), m_is_indexed(is_indexed) { - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE, SortType::PROVIDER }; + m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider" }); + m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME , SortType::VERSION, SortType::DATE, SortType::PROVIDER}; + m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::ResizeToContents, QHeaderView::ResizeToContents, QHeaderView::ResizeToContents}; } QVariant ModFolderModel::data(const QModelIndex &index, int role) const @@ -118,7 +121,9 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const case Qt::DecorationRole: { if (column == NAME_COLUMN && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink())) return APPLICATION->getThemedIcon("status-yellow"); - + if (column == ImageColumn) { + return at(row)->icon({32, 32}, Qt::AspectRatioMode::KeepAspectRatioByExpanding); + } return {}; } case Qt::CheckStateRole: @@ -142,15 +147,12 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in switch (section) { case ActiveColumn: - return QString(); case NameColumn: - return tr("Name"); case VersionColumn: - return tr("Version"); case DateColumn: - return tr("Last changed"); case ProviderColumn: - return tr("Provider"); + case ImageColumn: + return columnNames().at(section); default: return QVariant(); } @@ -197,10 +199,10 @@ Task* ModFolderModel::createParseTask(Resource& resource) bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata) { - for(auto mod : allMods()){ - if(mod->fileinfo().fileName() == filename){ + for(auto mod : allMods()) { + if(mod->fileinfo().fileName() == filename) { auto index_dir = indexDir(); - mod->destroy(index_dir, preserve_metadata); + mod->destroy(index_dir, preserve_metadata, false); update(); @@ -213,16 +215,11 @@ bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadat bool ModFolderModel::deleteMods(const QModelIndexList& indexes) { - if(!m_can_interact) { - return false; - } - - if(indexes.isEmpty()) + if (indexes.isEmpty()) return true; - for (auto i: indexes) - { - if(i.column() != 0) { + for (auto i : indexes) { + if (i.column() != 0) { continue; } auto m = at(i.row()); diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index d337fe296..6ccaba235 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -64,6 +64,7 @@ public: enum Columns { ActiveColumn = 0, + ImageColumn, NameColumn, VersionColumn, DateColumn, @@ -77,6 +78,8 @@ public: }; ModFolderModel(const QString &dir, BaseInstance* instance, bool is_indexed = false, bool create_dir = true); + virtual QString id() const override { return "mods"; } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; diff --git a/launcher/minecraft/mod/Resource.cpp b/launcher/minecraft/mod/Resource.cpp index a0b8a4bbc..098a617f8 100644 --- a/launcher/minecraft/mod/Resource.cpp +++ b/launcher/minecraft/mod/Resource.cpp @@ -71,6 +71,7 @@ std::pair Resource::compare(const Resource& other, SortType type) con return { 1, type == SortType::ENABLED }; if (!enabled() && other.enabled()) return { -1, type == SortType::ENABLED }; + break; case SortType::NAME: { QString this_name{ name() }; QString other_name{ other.name() }; @@ -81,12 +82,14 @@ std::pair Resource::compare(const Resource& other, SortType type) con auto compare_result = QString::compare(this_name, other_name, Qt::CaseInsensitive); if (compare_result != 0) return { compare_result, type == SortType::NAME }; + break; } case SortType::DATE: if (dateTimeChanged() > other.dateTimeChanged()) return { 1, type == SortType::DATE }; if (dateTimeChanged() < other.dateTimeChanged()) return { -1, type == SortType::DATE }; + break; } return { 0, false }; @@ -145,14 +148,10 @@ bool Resource::enable(EnableAction action) return true; } -bool Resource::destroy() +bool Resource::destroy(bool attemptTrash) { m_type = ResourceType::UNKNOWN; - - if (FS::trash(m_file_info.filePath())) - return true; - - return FS::deletePath(m_file_info.filePath()); + return (attemptTrash && FS::trash(m_file_info.filePath())) || FS::deletePath(m_file_info.filePath()); } bool Resource::isSymLinkUnder(const QString& instPath) const diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h index a5e9ae91e..94f3160c3 100644 --- a/launcher/minecraft/mod/Resource.h +++ b/launcher/minecraft/mod/Resource.h @@ -92,7 +92,7 @@ class Resource : public QObject { } // Delete all files of this resource. - bool destroy(); + bool destroy(bool attemptTrash = true); [[nodiscard]] auto isSymLink() const -> bool { return m_file_info.isSymLink(); } diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index d2d875e48..39a61067e 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -1,9 +1,11 @@ #include "ResourceFolderModel.h" +#include #include #include #include #include +#include #include #include #include @@ -12,9 +14,12 @@ #include "Application.h" #include "FileSystem.h" +#include "QVariantUtils.h" #include "minecraft/mod/tasks/BasicFolderLoadTask.h" +#include "settings/Setting.h" #include "tasks/Task.h" +#include "ui/dialogs/CustomMessageBox.h" ResourceFolderModel::ResourceFolderModel(QDir dir, BaseInstance* instance, QObject* parent, bool create_dir) : QAbstractListModel(parent), m_dir(dir), m_instance(instance), m_watcher(this) @@ -74,10 +79,6 @@ bool ResourceFolderModel::stopWatching(const QStringList paths) bool ResourceFolderModel::installResource(QString original_path) { - if (!m_can_interact) { - return false; - } - // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName original_path = FS::NormalizePath(original_path); QFileInfo file_info(original_path); @@ -156,7 +157,7 @@ bool ResourceFolderModel::uninstallResource(QString file_name) { for (auto& resource : m_resources) { if (resource->fileinfo().fileName() == file_name) { - auto res = resource->destroy(); + auto res = resource->destroy(false); update(); @@ -168,9 +169,6 @@ bool ResourceFolderModel::uninstallResource(QString file_name) bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes) { - if (!m_can_interact) - return false; - if (indexes.isEmpty()) return true; @@ -189,11 +187,8 @@ bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes) return true; } -bool ResourceFolderModel::setResourceEnabled(const QModelIndexList &indexes, EnableAction action) +bool ResourceFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAction action) { - if (!m_can_interact) - return false; - if (indexes.isEmpty()) return true; @@ -246,15 +241,18 @@ bool ResourceFolderModel::update() connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded, Qt::ConnectionType::QueuedConnection); connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection); - connect(m_current_update_task.get(), &Task::finished, this, [=] { - m_current_update_task.reset(); - if (m_scheduled_update) { - m_scheduled_update = false; - update(); - } else { - emit updateFinished(); - } - }, Qt::ConnectionType::QueuedConnection); + connect( + m_current_update_task.get(), &Task::finished, this, + [=] { + m_current_update_task.reset(); + if (m_scheduled_update) { + m_scheduled_update = false; + update(); + } else { + emit updateFinished(); + } + }, + Qt::ConnectionType::QueuedConnection); QThreadPool::globalInstance()->start(m_current_update_task.get()); @@ -344,15 +342,9 @@ Qt::DropActions ResourceFolderModel::supportedDropActions() const Qt::ItemFlags ResourceFolderModel::flags(const QModelIndex& index) const { Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); - auto flags = defaultFlags; - if (!m_can_interact) { - flags &= ~Qt::ItemIsDropEnabled; - } else { - flags |= Qt::ItemIsDropEnabled; - if (index.isValid()) { - flags |= Qt::ItemIsUserCheckable; - } - } + auto flags = defaultFlags | Qt::ItemIsDropEnabled; + if (index.isValid()) + flags |= Qt::ItemIsUserCheckable; return flags; } @@ -425,16 +417,17 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const if (column == NAME_COLUMN) { if (at(row).isSymLinkUnder(instDirPath())) { return m_resources[row]->internal_id() + - tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." - "\nCanonical Path: %1") - .arg(at(row).fileinfo().canonicalFilePath());; + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." + "\nCanonical Path: %1") + .arg(at(row).fileinfo().canonicalFilePath()); + ; } if (at(row).isMoreThanOneHardLink()) { return m_resources[row]->internal_id() + - tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); + tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); } } - + return m_resources[row]->internal_id(); case Qt::DecorationRole: { if (column == NAME_COLUMN && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) @@ -460,8 +453,20 @@ bool ResourceFolderModel::setData(const QModelIndex& index, const QVariant& valu if (row < 0 || row >= rowCount(index.parent()) || !index.isValid()) return false; - if (role == Qt::CheckStateRole) + if (role == Qt::CheckStateRole) { + if (m_instance != nullptr && m_instance->isRunning()) { + auto response = + CustomMessageBox::selectable(nullptr, "Confirm toggle", + "If you enable/disable this resource while the game is running it may crash your game.\n" + "Are you sure you want to do this?", + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return false; + } return setResourceEnabled({ index }, EnableAction::TOGGLE); + } return false; } @@ -471,10 +476,10 @@ QVariant ResourceFolderModel::headerData(int section, Qt::Orientation orientatio switch (role) { case Qt::DisplayRole: switch (section) { + case ACTIVE_COLUMN: case NAME_COLUMN: - return tr("Name"); case DATE_COLUMN: - return tr("Last modified"); + return columnNames().at(section); default: return {}; } @@ -500,6 +505,75 @@ QVariant ResourceFolderModel::headerData(int section, Qt::Orientation orientatio return {}; } +void ResourceFolderModel::setupHeaderAction(QAction* act, int column) +{ + Q_ASSERT(act); + + act->setText(columnNames().at(column)); +} + +void ResourceFolderModel::saveHiddenColumn(int column, bool hidden) +{ + auto const setting_name = QString("UI/%1_Page/HiddenColumns").arg(id()); + auto setting = (m_instance->settings()->contains(setting_name)) ? + m_instance->settings()->getSetting(setting_name) : m_instance->settings()->registerSetting(setting_name); + + auto hiddenColumns = setting->get().toStringList(); + auto name = columnNames(false).at(column); + auto index = hiddenColumns.indexOf(name); + if (index >= 0 && !hidden) { + hiddenColumns.removeAt(index); + } else if ( index < 0 && hidden) { + hiddenColumns.append(name); + } + setting->set(hiddenColumns); +} + +void ResourceFolderModel::loadHiddenColumns(QTreeView *tree) +{ + auto const setting_name = QString("UI/%1_Page/HiddenColumns").arg(id()); + auto setting = (m_instance->settings()->contains(setting_name)) ? + m_instance->settings()->getSetting(setting_name) : m_instance->settings()->registerSetting(setting_name); + + auto hiddenColumns = setting->get().toStringList(); + auto col_names = columnNames(false); + for (auto col_name : hiddenColumns) { + auto index = col_names.indexOf(col_name); + if (index >= 0) + tree->setColumnHidden(index, true); + } + +} + +QMenu* ResourceFolderModel::createHeaderContextMenu(QTreeView* tree) +{ + auto menu = new QMenu(tree); + + menu->addSeparator()->setText(tr("Show / Hide Columns")); + + for (int col = 0; col < columnCount(); ++col) { + auto act = new QAction(menu); + setupHeaderAction(act, col); + + act->setCheckable(true); + act->setChecked(!tree->isColumnHidden(col)); + + connect(act, &QAction::toggled, tree, [this, col, tree](bool toggled){ + tree->setColumnHidden(col, !toggled); + for(int c = 0; c < columnCount(); ++c) { + if (m_column_resize_modes.at(c) == QHeaderView::ResizeToContents) + tree->resizeColumnToContents(c); + } + saveHiddenColumn(col, !toggled); + }); + + menu->addAction(act); + + } + + return menu; +} + QSortFilterProxyModel* ResourceFolderModel::createFilterProxyModel(QObject* parent) { return new ProxyModel(parent); @@ -511,16 +585,6 @@ SortType ResourceFolderModel::columnToSortKey(size_t column) const return m_column_sort_keys.at(column); } -void ResourceFolderModel::enableInteraction(bool enabled) -{ - if (m_can_interact == enabled) - return; - - m_can_interact = enabled; - if (size()) - emit dataChanged(index(0), index(size() - 1)); -} - /* Standard Proxy Model for createFilterProxyModel */ [[nodiscard]] bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { @@ -556,6 +620,7 @@ void ResourceFolderModel::enableInteraction(bool enabled) return (compare_result.first > 0); } -QString ResourceFolderModel::instDirPath() const { +QString ResourceFolderModel::instDirPath() const +{ return QFileInfo(m_instance->instanceRoot()).absoluteFilePath(); } diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index 0a35e1bca..454b84c36 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -1,5 +1,8 @@ #pragma once +#include +#include +#include #include #include #include @@ -11,8 +14,8 @@ #include "BaseInstance.h" -#include "tasks/Task.h" #include "tasks/ConcurrentTask.h" +#include "tasks/Task.h" class QSortFilterProxyModel; @@ -29,6 +32,8 @@ class ResourceFolderModel : public QAbstractListModel { ResourceFolderModel(QDir, BaseInstance* instance, QObject* parent = nullptr, bool create_dir = true); ~ResourceFolderModel() override; + virtual QString id() const { return "resource"; } + /** Starts watching the paths for changes. * * Returns whether starting to watch all the paths was successful. @@ -92,6 +97,7 @@ class ResourceFolderModel : public QAbstractListModel { /* Basic columns */ enum Columns { ACTIVE_COLUMN = 0, NAME_COLUMN, DATE_COLUMN, NUM_COLUMNS }; + QStringList columnNames(bool translated = true) const { return translated ? m_column_names_translated : m_column_names; }; [[nodiscard]] int rowCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : static_cast(size()); } [[nodiscard]] int columnCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : NUM_COLUMNS; }; @@ -110,6 +116,11 @@ class ResourceFolderModel : public QAbstractListModel { [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + void setupHeaderAction(QAction* act, int column); + void saveHiddenColumn(int column, bool hidden); + void loadHiddenColumns(QTreeView* tree); + QMenu* createHeaderContextMenu(QTreeView* tree); + /** This creates a proxy model to filter / sort the model for a UI. * * The actual comparisons and filtering are done directly by the Resource, so to modify behavior go there instead! @@ -117,6 +128,7 @@ class ResourceFolderModel : public QAbstractListModel { QSortFilterProxyModel* createFilterProxyModel(QObject* parent = nullptr); [[nodiscard]] SortType columnToSortKey(size_t column) const; + [[nodiscard]] QList columnResizeModes() const { return m_column_resize_modes; } class ProxyModel : public QSortFilterProxyModel { public: @@ -129,10 +141,6 @@ class ResourceFolderModel : public QAbstractListModel { QString instDirPath() const; - public slots: - void enableInteraction(bool enabled); - void disableInteraction(bool disabled) { enableInteraction(!disabled); } - signals: void updateFinished(); @@ -181,14 +189,19 @@ class ResourceFolderModel : public QAbstractListModel { * if the resource is complex and has more stuff to parse. */ virtual void onParseSucceeded(int ticket, QString resource_id); - virtual void onParseFailed(int ticket, QString resource_id) { Q_UNUSED(ticket); Q_UNUSED(resource_id); } + virtual void onParseFailed(int ticket, QString resource_id) + { + Q_UNUSED(ticket); + Q_UNUSED(resource_id); + } protected: // Represents the relationship between a column's index (represented by the list index), and it's sorting key. // As such, the order in with they appear is very important! QList m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE }; - - bool m_can_interact = true; + QStringList m_column_names = {"Enable", "Name", "Last Modified"}; + QStringList m_column_names_translated = {tr("Enable"), tr("Name"), tr("Last Modified")}; + QList m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Stretch, QHeaderView::ResizeToContents }; QDir m_dir; BaseInstance* m_instance; diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp index 759d2b566..6d5978d4d 100644 --- a/launcher/minecraft/mod/ResourcePack.cpp +++ b/launcher/minecraft/mod/ResourcePack.cpp @@ -40,7 +40,7 @@ void ResourcePack::setDescription(QString new_description) m_description = new_description; } -void ResourcePack::setImage(QImage new_image) +void ResourcePack::setImage(QImage new_image) const { QMutexLocker locker(&m_data_lock); @@ -49,7 +49,10 @@ void ResourcePack::setImage(QImage new_image) if (m_pack_image_cache_key.key.isValid()) PixmapCache::instance().remove(m_pack_image_cache_key.key); - m_pack_image_cache_key.key = PixmapCache::instance().insert(QPixmap::fromImage(new_image)); + // scale the image to avoid flooding the pixmapcache + auto pixmap = QPixmap::fromImage(new_image.scaled({64, 64}, Qt::AspectRatioMode::KeepAspectRatioByExpanding)); + + m_pack_image_cache_key.key = PixmapCache::instance().insert(pixmap); m_pack_image_cache_key.was_ever_used = true; // This can happen if the pixmap is too big to fit in the cache :c @@ -59,21 +62,25 @@ void ResourcePack::setImage(QImage new_image) } } -QPixmap ResourcePack::image(QSize size) +QPixmap ResourcePack::image(QSize size, Qt::AspectRatioMode mode) const { QPixmap cached_image; if (PixmapCache::instance().find(m_pack_image_cache_key.key, &cached_image)) { if (size.isNull()) return cached_image; - return cached_image.scaled(size); + return cached_image.scaled(size, mode); } // No valid image we can get - if (!m_pack_image_cache_key.was_ever_used) + if (!m_pack_image_cache_key.was_ever_used) { return {}; + } else { + qDebug() << "Resource Pack" << name() << "Had it's image evicted from the cache. reloading..."; + PixmapCache::markCacheMissByEviciton(); + } // Imaged got evicted from the cache. Re-process it and retry. - ResourcePackUtils::process(*this); + ResourcePackUtils::processPackPNG(*this); return image(size); } @@ -95,6 +102,7 @@ std::pair ResourcePack::compare(const Resource& other, SortType type) auto res = Resource::compare(other, type); if (res.first != 0) return res; + break; } case SortType::PACK_FORMAT: { auto this_ver = packFormat(); @@ -104,6 +112,7 @@ std::pair ResourcePack::compare(const Resource& other, SortType type) return { 1, type == SortType::PACK_FORMAT }; if (this_ver < other_ver) return { -1, type == SortType::PACK_FORMAT }; + break; } } return { 0, false }; diff --git a/launcher/minecraft/mod/ResourcePack.h b/launcher/minecraft/mod/ResourcePack.h index 7cb414d83..da354bc1c 100644 --- a/launcher/minecraft/mod/ResourcePack.h +++ b/launcher/minecraft/mod/ResourcePack.h @@ -31,7 +31,7 @@ class ResourcePack : public Resource { [[nodiscard]] QString description() const { return m_description; } /** Gets the image of the resource pack, converted to a QPixmap for drawing, and scaled to size. */ - [[nodiscard]] QPixmap image(QSize size); + [[nodiscard]] QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; /** Thread-safe. */ void setPackFormat(int new_format_id); @@ -40,7 +40,7 @@ class ResourcePack : public Resource { void setDescription(QString new_description); /** Thread-safe. */ - void setImage(QImage new_image); + void setImage(QImage new_image) const; bool valid() const override; @@ -67,5 +67,5 @@ class ResourcePack : public Resource { struct { QPixmapCache::Key key; bool was_ever_used = false; - } m_pack_image_cache_key; + } mutable m_pack_image_cache_key; }; diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index c12d1f237..41455599b 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -35,6 +35,8 @@ */ #include "ResourcePackFolderModel.h" +#include +#include #include #include @@ -48,7 +50,11 @@ ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance) { - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE }; + m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified" }); + m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE}; + m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::ResizeToContents, QHeaderView::ResizeToContents }; + } QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const @@ -84,9 +90,11 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const return {}; } case Qt::DecorationRole: { - if (column == NAME_COLUMN && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink())) + if (column == NameColumn && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink())) return APPLICATION->getThemedIcon("status-yellow"); - + if (column == ImageColumn) { + return at(row)->image({32, 32}, Qt::AspectRatioMode::KeepAspectRatioByExpanding); + } return {}; } case Qt::ToolTipRole: { @@ -94,7 +102,7 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const //: The string being explained by this is in the format: ID (Lower version - Upper version) return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); } - if (column == NAME_COLUMN) { + if (column == NameColumn) { if (at(row)->isSymLinkUnder(instDirPath())) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." @@ -126,13 +134,11 @@ QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orient case Qt::DisplayRole: switch (section) { case ActiveColumn: - return QString(); case NameColumn: - return tr("Name"); case PackFormatColumn: - return tr("Pack Format"); case DateColumn: - return tr("Last changed"); + case ImageColumn: + return columnNames().at(section); default: return {}; } @@ -151,6 +157,11 @@ QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orient default: return {}; } + case Qt::SizeHintRole: + if (section == ImageColumn) { + return QSize(64,0); + } + return {}; default: return {}; } diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.h b/launcher/minecraft/mod/ResourcePackFolderModel.h index db4b14fb0..531d81928 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.h +++ b/launcher/minecraft/mod/ResourcePackFolderModel.h @@ -11,6 +11,7 @@ public: enum Columns { ActiveColumn = 0, + ImageColumn, NameColumn, PackFormatColumn, DateColumn, @@ -19,6 +20,8 @@ public: explicit ResourcePackFolderModel(const QString &dir, BaseInstance* instance); + virtual QString id() const override { return "resourcepacks"; } + [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; diff --git a/launcher/minecraft/mod/ShaderPackFolderModel.h b/launcher/minecraft/mod/ShaderPackFolderModel.h index dc5acf80f..f8249962f 100644 --- a/launcher/minecraft/mod/ShaderPackFolderModel.h +++ b/launcher/minecraft/mod/ShaderPackFolderModel.h @@ -9,4 +9,6 @@ class ShaderPackFolderModel : public ResourceFolderModel { explicit ShaderPackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance) {} + + virtual QString id() const override { return "shaderpacks"; } }; diff --git a/launcher/minecraft/mod/TexturePack.cpp b/launcher/minecraft/mod/TexturePack.cpp index 99d555843..c7a50a97a 100644 --- a/launcher/minecraft/mod/TexturePack.cpp +++ b/launcher/minecraft/mod/TexturePack.cpp @@ -23,6 +23,8 @@ #include #include +#include "MTPixmapCache.h" + #include "minecraft/mod/tasks/LocalTexturePackParseTask.h" void TexturePack::setDescription(QString new_description) @@ -32,34 +34,41 @@ void TexturePack::setDescription(QString new_description) m_description = new_description; } -void TexturePack::setImage(QImage new_image) +void TexturePack::setImage(QImage new_image) const { QMutexLocker locker(&m_data_lock); Q_ASSERT(!new_image.isNull()); if (m_pack_image_cache_key.key.isValid()) - QPixmapCache::remove(m_pack_image_cache_key.key); + PixmapCache::remove(m_pack_image_cache_key.key); - m_pack_image_cache_key.key = QPixmapCache::insert(QPixmap::fromImage(new_image)); + // scale the image to avoid flooding the pixmapcache + auto pixmap = QPixmap::fromImage(new_image.scaled({64, 64}, Qt::AspectRatioMode::KeepAspectRatioByExpanding)); + + m_pack_image_cache_key.key = PixmapCache::insert(pixmap); m_pack_image_cache_key.was_ever_used = true; } -QPixmap TexturePack::image(QSize size) +QPixmap TexturePack::image(QSize size, Qt::AspectRatioMode mode) const { QPixmap cached_image; - if (QPixmapCache::find(m_pack_image_cache_key.key, &cached_image)) { + if (PixmapCache::find(m_pack_image_cache_key.key, &cached_image)) { if (size.isNull()) return cached_image; - return cached_image.scaled(size); + return cached_image.scaled(size, mode); } // No valid image we can get - if (!m_pack_image_cache_key.was_ever_used) + if (!m_pack_image_cache_key.was_ever_used) { return {}; + } else { + qDebug() << "Texture Pack" << name() << "Had it's image evicted from the cache. reloading..."; + PixmapCache::markCacheMissByEviciton(); + } // Imaged got evicted from the cache. Re-process it and retry. - TexturePackUtils::process(*this); + TexturePackUtils::processPackPNG(*this); return image(size); } diff --git a/launcher/minecraft/mod/TexturePack.h b/launcher/minecraft/mod/TexturePack.h index 81bd5c699..577005655 100644 --- a/launcher/minecraft/mod/TexturePack.h +++ b/launcher/minecraft/mod/TexturePack.h @@ -40,13 +40,13 @@ class TexturePack : public Resource { [[nodiscard]] QString description() const { return m_description; } /** Gets the image of the texture pack, converted to a QPixmap for drawing, and scaled to size. */ - [[nodiscard]] QPixmap image(QSize size); + [[nodiscard]] QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; /** Thread-safe. */ void setDescription(QString new_description); /** Thread-safe. */ - void setImage(QImage new_image); + void setImage(QImage new_image) const; bool valid() const override; @@ -65,5 +65,5 @@ class TexturePack : public Resource { struct { QPixmapCache::Key key; bool was_ever_used = false; - } m_pack_image_cache_key; + } mutable m_pack_image_cache_key; }; diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index c6609ed1e..531a70232 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -33,6 +33,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#include + +#include "Application.h" #include "TexturePackFolderModel.h" @@ -41,7 +44,13 @@ TexturePackFolderModel::TexturePackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance) -{} +{ + m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified" }); + m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE }; + m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::ResizeToContents}; + +} Task* TexturePackFolderModel::createUpdateTask() { @@ -52,3 +61,96 @@ Task* TexturePackFolderModel::createParseTask(Resource& resource) { return new LocalTexturePackParseTask(m_next_resolution_ticket, static_cast(resource)); } + + +QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const +{ + if (!validateIndex(index)) + return {}; + + int row = index.row(); + int column = index.column(); + + switch (role) { + case Qt::DisplayRole: + switch (column) { + case NameColumn: + return m_resources[row]->name(); + case DateColumn: + return m_resources[row]->dateTimeChanged(); + default: + return {}; + } + case Qt::ToolTipRole: + if (column == NameColumn) { + if (at(row)->isSymLinkUnder(instDirPath())) { + return m_resources[row]->internal_id() + + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." + "\nCanonical Path: %1") + .arg(at(row)->fileinfo().canonicalFilePath());; + } + if (at(row)->isMoreThanOneHardLink()) { + return m_resources[row]->internal_id() + + tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); + } + } + + return m_resources[row]->internal_id(); + case Qt::DecorationRole: { + if (column == NameColumn && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink())) + return APPLICATION->getThemedIcon("status-yellow"); + if (column == ImageColumn) { + return at(row)->image({32, 32}, Qt::AspectRatioMode::KeepAspectRatioByExpanding); + } + return {}; + } + case Qt::CheckStateRole: + if (column == ActiveColumn) { + return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked; + } + return {}; + default: + return {}; + } +} + +QVariant TexturePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) { + case Qt::DisplayRole: + switch (section) { + case ActiveColumn: + case NameColumn: + case DateColumn: + case ImageColumn: + return columnNames().at(section); + default: + return {}; + } + case Qt::ToolTipRole: { + switch (section) { + case ActiveColumn: + //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. + return tr("Is the resource enabled?"); + case NameColumn: + //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. + return tr("The name of the resource."); + case DateColumn: + //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. + return tr("The date and time this resource was last changed (or added)."); + default: + return {}; + } + } + default: + break; + } + + return {}; +} + +int TexturePackFolderModel::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : NUM_COLUMNS; +} + diff --git a/launcher/minecraft/mod/TexturePackFolderModel.h b/launcher/minecraft/mod/TexturePackFolderModel.h index 425a71e46..71a8bdd16 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.h +++ b/launcher/minecraft/mod/TexturePackFolderModel.h @@ -38,12 +38,35 @@ #include "ResourceFolderModel.h" +#include "TexturePack.h" + class TexturePackFolderModel : public ResourceFolderModel { Q_OBJECT public: + + enum Columns + { + ActiveColumn = 0, + ImageColumn, + NameColumn, + DateColumn, + NUM_COLUMNS + }; + + explicit TexturePackFolderModel(const QString &dir, std::shared_ptr instance); + + virtual QString id() const override { return "texturepacks"; } + + [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + [[nodiscard]] int columnCount(const QModelIndex &parent) const override; + explicit TexturePackFolderModel(const QString &dir, BaseInstance* instance); [[nodiscard]] Task* createUpdateTask() override; [[nodiscard]] Task* createParseTask(Resource&) override; + + RESOURCE_HELPERS(TexturePack) }; diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp new file mode 100644 index 000000000..f8ecdb33e --- /dev/null +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "GetModDependenciesTask.h" + +#include +#include +#include +#include "Json.h" +#include "QObjectPtr.h" +#include "minecraft/mod/MetadataHandler.h" +#include "modplatform/ModIndex.h" +#include "modplatform/ResourceAPI.h" +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/modrinth/ModrinthAPI.h" +#include "tasks/ConcurrentTask.h" +#include "tasks/SequentialTask.h" +#include "ui/pages/modplatform/ModModel.h" +#include "ui/pages/modplatform/flame/FlameResourceModels.h" +#include "ui/pages/modplatform/modrinth/ModrinthResourceModels.h" + +static Version mcVersion(BaseInstance* inst) +{ + return static_cast(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion(); +} + +static ResourceAPI::ModLoaderTypes mcLoaders(BaseInstance* inst) +{ + return static_cast(inst)->getPackProfile()->getModLoaders().value(); +} + +GetModDependenciesTask::GetModDependenciesTask(QObject* parent, + BaseInstance* instance, + ModFolderModel* folder, + QList> selected) + : SequentialTask(parent, tr("Get dependencies")) + , m_selected(selected) + , m_flame_provider{ ModPlatform::ResourceProvider::FLAME, std::make_shared(*instance), + std::make_shared() } + , m_modrinth_provider{ ModPlatform::ResourceProvider::MODRINTH, std::make_shared(*instance), + std::make_shared() } + , m_version(mcVersion(instance)) + , m_loaderType(mcLoaders(instance)) +{ + for (auto mod : folder->allMods()) + if (auto meta = mod->metadata(); meta) + m_mods.append(meta); + prepare(); +}; + +void GetModDependenciesTask::prepare() +{ + for (auto sel : m_selected) { + for (auto dep : getDependenciesForVersion(sel->version, sel->pack->provider)) { + addTask(prepareDependencyTask(dep, sel->pack->provider, 20)); + } + } +} + +ModPlatform::Dependency GetModDependenciesTask::getOverride(const ModPlatform::Dependency& dep, + const ModPlatform::ResourceProvider providerName) +{ + if (auto isQuilt = m_loaderType & ResourceAPI::Quilt; isQuilt || m_loaderType & ResourceAPI::Fabric) { + auto overide = ModPlatform::getOverrideDeps(); + auto over = std::find_if(overide.cbegin(), overide.cend(), [dep, providerName, isQuilt](auto o) { + return o.provider == providerName && dep.addonId == (isQuilt ? o.fabric : o.quilt); + }); + if (over != overide.cend()) { + return { isQuilt ? over->quilt : over->fabric, dep.type }; + } + } + return dep; +} + +QList GetModDependenciesTask::getDependenciesForVersion(const ModPlatform::IndexedVersion& version, + const ModPlatform::ResourceProvider providerName) +{ + QList c_dependencies; + for (auto ver_dep : version.dependencies) { + if (ver_dep.type != ModPlatform::DependencyType::REQUIRED) + continue; + + auto isOnlyVersion = providerName == ModPlatform::ResourceProvider::MODRINTH && ver_dep.addonId.toString().isEmpty(); + if (auto dep = std::find_if(c_dependencies.begin(), c_dependencies.end(), + [&ver_dep, isOnlyVersion](const ModPlatform::Dependency& i) { + return isOnlyVersion ? i.version == ver_dep.version : i.addonId == ver_dep.addonId; + }); + dep != c_dependencies.end()) + continue; // check the current dependency list + + if (auto dep = std::find_if(m_selected.begin(), m_selected.end(), + [&ver_dep, providerName, isOnlyVersion](std::shared_ptr i) { + return i->pack->provider == providerName && (isOnlyVersion ? i->version.version == ver_dep.version + : i->pack->addonId == ver_dep.addonId); + }); + dep != m_selected.end()) + continue; // check the selected versions + + if (auto dep = std::find_if(m_mods.begin(), m_mods.end(), + [&ver_dep, providerName, isOnlyVersion](std::shared_ptr i) { + return i->provider == providerName && + (isOnlyVersion ? i->file_id == ver_dep.version : i->project_id == ver_dep.addonId); + }); + dep != m_mods.end()) + continue; // check the existing mods + + if (auto dep = std::find_if(m_pack_dependencies.begin(), m_pack_dependencies.end(), + [&ver_dep, providerName, isOnlyVersion](std::shared_ptr i) { + return i->pack->provider == providerName && (isOnlyVersion ? i->version.version == ver_dep.addonId + : i->pack->addonId == ver_dep.addonId); + }); + dep != m_pack_dependencies.end()) // check loaded dependencies + continue; + + c_dependencies.append(getOverride(ver_dep, providerName)); + } + return c_dependencies; +}; + +Task::Ptr GetModDependenciesTask::getProjectInfoTask(std::shared_ptr pDep) +{ + auto provider = pDep->pack->provider == m_flame_provider.name ? m_flame_provider : m_modrinth_provider; + auto responseInfo = std::make_shared(); + auto info = provider.api->getProject(pDep->pack->addonId.toString(), responseInfo); + QObject::connect(info.get(), &NetJob::succeeded, [responseInfo, provider, pDep] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*responseInfo, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qDebug() << *responseInfo; + return; + } + try { + auto obj = provider.name == ModPlatform::ResourceProvider::FLAME ? Json::requireObject(Json::requireObject(doc), "data") + : Json::requireObject(doc); + provider.mod->loadIndexedPack(*pDep->pack, obj); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading mod info: " << e.cause(); + } + }); + return info; +} + +Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Dependency& dep, + const ModPlatform::ResourceProvider providerName, + int level) +{ + auto pDep = std::make_shared(); + pDep->dependency = dep; + pDep->pack = std::make_shared(); + pDep->pack->addonId = dep.addonId; + pDep->pack->provider = providerName; + + m_pack_dependencies.append(pDep); + auto provider = providerName == m_flame_provider.name ? m_flame_provider : m_modrinth_provider; + + auto tasks = makeShared( + this, QString("DependencyInfo: %1").arg(dep.addonId.toString().isEmpty() ? dep.version : dep.addonId.toString())); + + if (!dep.addonId.toString().isEmpty()) { + tasks->addTask(getProjectInfoTask(pDep)); + } + + ResourceAPI::DependencySearchArgs args = { dep, m_version, m_loaderType }; + ResourceAPI::DependencySearchCallbacks callbacks; + + callbacks.on_succeed = [dep, provider, pDep, level, this](auto& doc, auto& pack) { + try { + QJsonArray arr; + if (dep.version.length() != 0 && doc.isObject()) { + arr.append(doc.object()); + } else { + arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); + } + pDep->version = provider.mod->loadDependencyVersions(dep, arr); + if (!pDep->version.addonId.isValid()) { + if (m_loaderType & ResourceAPI::Quilt) { // falback for quilt + auto overide = ModPlatform::getOverrideDeps(); + auto over = std::find_if(overide.cbegin(), overide.cend(), + [dep, provider](auto o) { return o.provider == provider.name && dep.addonId == o.quilt; }); + if (over != overide.cend()) { + removePack(dep.addonId); + addTask(prepareDependencyTask({ over->fabric, dep.type }, provider.name, level)); + return; + } + } + qWarning() << "Error while reading mod version empty "; + qDebug() << doc; + return; + } + pDep->version.is_currently_selected = true; + pDep->pack->versions = { pDep->version }; + pDep->pack->versionsLoaded = true; + + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading mod version: " << e.cause(); + return; + } + if (level == 0) { + qWarning() << "Dependency cycle exeeded"; + return; + } + if (dep.addonId.toString().isEmpty() && !pDep->version.addonId.toString().isEmpty()) { + pDep->pack->addonId = pDep->version.addonId; + auto dep = getOverride({ pDep->version.addonId, pDep->dependency.type }, provider.name); + if (dep.addonId != pDep->version.addonId) { + removePack(pDep->version.addonId); + addTask(prepareDependencyTask(dep, provider.name, level)); + } else + addTask(getProjectInfoTask(pDep)); + } + for (auto dep : getDependenciesForVersion(pDep->version, provider.name)) { + addTask(prepareDependencyTask(dep, provider.name, level - 1)); + } + }; + + auto version = provider.api->getDependencyVersion(std::move(args), std::move(callbacks)); + tasks->addTask(version); + return tasks; +}; + +void GetModDependenciesTask::removePack(const QVariant addonId) +{ + auto pred = [addonId](const std::shared_ptr& v) { return v->pack->addonId == addonId; }; +#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) + m_pack_dependencies.removeIf(pred); +#else + for (auto it = m_pack_dependencies.begin(); it != m_pack_dependencies.end();) + if (pred(*it)) + it = m_pack_dependencies.erase(it); + else + ++it; +#endif +} diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.h b/launcher/minecraft/mod/tasks/GetModDependenciesTask.h new file mode 100644 index 000000000..50eba6afc --- /dev/null +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.h @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "minecraft/mod/MetadataHandler.h" +#include "minecraft/mod/ModFolderModel.h" +#include "modplatform/ModIndex.h" +#include "modplatform/ResourceAPI.h" +#include "tasks/SequentialTask.h" +#include "tasks/Task.h" +#include "ui/pages/modplatform/ModModel.h" + +class GetModDependenciesTask : public SequentialTask { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + + struct PackDependency { + ModPlatform::Dependency dependency; + ModPlatform::IndexedPack::Ptr pack; + ModPlatform::IndexedVersion version; + PackDependency() = default; + PackDependency(const ModPlatform::IndexedPack::Ptr p, const ModPlatform::IndexedVersion& v) + { + pack = p; + version = v; + } + }; + + struct Provider { + ModPlatform::ResourceProvider name; + std::shared_ptr mod; + std::shared_ptr api; + }; + + explicit GetModDependenciesTask(QObject* parent, + BaseInstance* instance, + ModFolderModel* folder, + QList> selected); + + auto getDependecies() const -> QList> { return m_pack_dependencies; } + + protected slots: + Task::Ptr prepareDependencyTask(const ModPlatform::Dependency&, const ModPlatform::ResourceProvider, int); + QList getDependenciesForVersion(const ModPlatform::IndexedVersion&, + const ModPlatform::ResourceProvider providerName); + void prepare(); + Task::Ptr getProjectInfoTask(std::shared_ptr pDep); + ModPlatform::Dependency getOverride(const ModPlatform::Dependency&, const ModPlatform::ResourceProvider providerName); + void removePack(const QVariant addonId); + + private: + QList> m_pack_dependencies; + QList> m_mods; + QList> m_selected; + Provider m_flame_provider; + Provider m_modrinth_provider; + + Version m_version; + ResourceAPI::ModLoaderTypes m_loaderType; +}; diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index 5342d693b..264019f84 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -52,6 +52,10 @@ ModDetails ReadMCModInfo(QByteArray contents) authors = firstObj.value("authors").toArray(); } + if (firstObj.contains("logoFile")) { + details.icon_file = firstObj.value("logoFile").toString(); + } + for (auto author : authors) { details.authors.append(author.toString()); } @@ -166,6 +170,31 @@ ModDetails ReadMCModTOML(QByteArray contents) } details.homeurl = homeurl; + QString issueTrackerURL = ""; + if (auto issueTrackerURLDatum = tomlData["issueTrackerURL"].as_string()) { + issueTrackerURL = QString::fromStdString(issueTrackerURLDatum->get()); + } else if (auto issueTrackerURLDatum = (*modsTable)["issueTrackerURL"].as_string()) { + issueTrackerURL = QString::fromStdString(issueTrackerURLDatum->get()); + } + details.issue_tracker = issueTrackerURL; + + QString license = ""; + if (auto licenseDatum = tomlData["license"].as_string()) { + license = QString::fromStdString(licenseDatum->get()); + } else if (auto licenseDatum =(*modsTable)["license"].as_string()) { + license = QString::fromStdString(licenseDatum->get()); + } + if (!license.isEmpty()) + details.licenses.append(ModLicense(license)); + + QString logoFile = ""; + if (auto logoFileDatum = tomlData["logoFile"].as_string()) { + logoFile = QString::fromStdString(logoFileDatum->get()); + } else if (auto logoFileDatum =(*modsTable)["logoFile"].as_string()) { + logoFile = QString::fromStdString(logoFileDatum->get()); + } + details.icon_file = logoFile; + return details; } @@ -201,6 +230,57 @@ ModDetails ReadFabricModInfo(QByteArray contents) if (contact.contains("homepage")) { details.homeurl = contact.value("homepage").toString(); } + if (contact.contains("issues")) { + details.issue_tracker = contact.value("issues").toString(); + } + } + + if (object.contains("license")) { + auto license = object.value("license"); + if (license.isArray()) { + for (auto l : license.toArray()) { + if (l.isString()) { + details.licenses.append(ModLicense(l.toString())); + } else if (l.isObject()) { + auto obj = l.toObject(); + details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(), + obj.value("url").toString(), obj.value("description").toString())); + } + } + } else if (license.isString()) { + details.licenses.append(ModLicense(license.toString())); + } else if (license.isObject()) { + auto obj = license.toObject(); + details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(), obj.value("url").toString(), + obj.value("description").toString())); + } + } + + if (object.contains("icon")) { + auto icon = object.value("icon"); + if (icon.isObject()) { + auto obj = icon.toObject(); + // take the largest icon + int largest = 0; + for (auto key : obj.keys()) { + auto size = key.split('x').first().toInt(); + if (size > largest) { + largest = size; + } + } + if (largest > 0) { + auto key = QString::number(largest) + "x" + QString::number(largest); + details.icon_file = obj.value(key).toString(); + } else { // parsing the sizes failed + // take the first + for (auto i : obj) { + details.icon_file = i.toString(); + break; + } + } + } else if (icon.isString()) { + details.icon_file = icon.toString(); + } } } return details; @@ -238,6 +318,58 @@ ModDetails ReadQuiltModInfo(QByteArray contents) if (modContact.contains("homepage")) { details.homeurl = Json::requireString(modContact.value("homepage")); } + if (modContact.contains("issues")) { + details.issue_tracker = Json::requireString(modContact.value("issues")); + } + + if (modMetadata.contains("license")) { + auto license = modMetadata.value("license"); + if (license.isArray()) { + for (auto l : license.toArray()) { + if (l.isString()) { + details.licenses.append(ModLicense(l.toString())); + } else if (l.isObject()) { + auto obj = l.toObject(); + details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(), + obj.value("url").toString(), obj.value("description").toString())); + } + } + } else if (license.isString()) { + details.licenses.append(ModLicense(license.toString())); + } else if (license.isObject()) { + auto obj = license.toObject(); + details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(), obj.value("url").toString(), + obj.value("description").toString())); + } + } + + if (modMetadata.contains("icon")) { + auto icon = modMetadata.value("icon"); + if (icon.isObject()) { + auto obj = icon.toObject(); + // take the largest icon + int largest = 0; + for (auto key : obj.keys()) { + auto size = key.split('x').first().toInt(); + if (size > largest) { + largest = size; + } + } + if (largest > 0) { + auto key = QString::number(largest) + "x" + QString::number(largest); + details.icon_file = obj.value(key).toString(); + } else { // parsing the sizes failed + // take the first + for (auto i : obj) { + details.icon_file = i.toString(); + break; + } + } + } else if (icon.isString()) { + details.icon_file = icon.toString(); + } + } + } return details; } @@ -515,6 +647,85 @@ bool validate(QFileInfo file) return ModUtils::process(mod, ProcessingLevel::BasicInfoOnly) && mod.valid(); } +bool processIconPNG(const Mod& mod, QByteArray&& raw_data) +{ + auto img = QImage::fromData(raw_data); + if (!img.isNull()) { + mod.setIcon(img); + } else { + qWarning() << "Failed to parse mod logo:" << mod.iconPath() << "from" << mod.name(); + return false; + } + return true; +} + +bool loadIconFile(const Mod& mod) { + if (mod.iconPath().isEmpty()) { + qWarning() << "No Iconfile set, be sure to parse the mod first"; + return false; + } + + auto png_invalid = [&mod]() { + qWarning() << "Mod at" << mod.fileinfo().filePath() << "does not have a valid icon"; + return false; + }; + + switch (mod.type()) { + case ResourceType::FOLDER: + { + QFileInfo icon_info(FS::PathCombine(mod.fileinfo().filePath(), mod.iconPath())); + if (icon_info.exists() && icon_info.isFile()) { + QFile icon(icon_info.filePath()); + if (!icon.open(QIODevice::ReadOnly)) + return false; + auto data = icon.readAll(); + + bool icon_result = ModUtils::processIconPNG(mod, std::move(data)); + + icon.close(); + + if (!icon_result) { + return png_invalid(); // icon invalid + } + } + } + case ResourceType::ZIPFILE: + { + QuaZip zip(mod.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile(mod.iconPath())) { + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open file in zip."; + zip.close(); + return png_invalid(); + } + + auto data = file.readAll(); + + bool icon_result = ModUtils::processIconPNG(mod, std::move(data)); + + file.close(); + if (!icon_result) { + return png_invalid(); // icon png invalid + } + } else { + return png_invalid(); // could not set icon as current file. + } + } + case ResourceType::LITEMOD: + { + return false; // can lightmods even have icons? + } + default: + qWarning() << "Invalid type for mod, can not load icon."; + return false; + } +} + } // namespace ModUtils LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile) diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h index 38dae1357..a03217093 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.h @@ -25,6 +25,9 @@ bool processLitemod(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); /** Checks whether a file is valid as a mod or not. */ bool validate(QFileInfo file); + +bool processIconPNG(const Mod& mod, QByteArray&& raw_data); +bool loadIconFile(const Mod& mod); } // namespace ModUtils class LocalModParseTask : public Task { diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp index cc4e252c1..4352fad91 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp @@ -1,25 +1,24 @@ // SPDX-License-Identifier: GPL-3.0-only /* -* PolyMC - Minecraft Launcher -* Copyright (c) 2022 flowln -* Copyright (C) 2022 Sefa Eyeoglu -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, version 3. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see . -*/ + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ #include "LocalModUpdateTask.h" -#include "Application.h" #include "FileSystem.h" #include "minecraft/mod/MetadataHandler.h" diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp index 4bf0b80d8..a67c56a8f 100644 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp @@ -165,15 +165,16 @@ bool processZIP(ResourcePack& pack, ProcessingLevel level) bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data)); file.close(); + zip.close(); if (!pack_png_result) { return png_invalid(); // pack.png invalid } } else { + zip.close(); return png_invalid(); // could not set pack.mcmeta as current file. } zip.close(); - return true; } @@ -193,7 +194,7 @@ bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data) return true; } -bool processPackPNG(ResourcePack& pack, QByteArray&& raw_data) +bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data) { auto img = QImage::fromData(raw_data); if (!img.isNull()) { @@ -205,6 +206,68 @@ bool processPackPNG(ResourcePack& pack, QByteArray&& raw_data) return true; } +bool processPackPNG(const ResourcePack& pack) +{ + auto png_invalid = [&pack]() { + qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png"; + return false; + }; + + switch (pack.type()) { + case ResourceType::FOLDER: + { + QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); + if (image_file_info.exists() && image_file_info.isFile()) { + QFile pack_png_file(image_file_info.filePath()); + if (!pack_png_file.open(QIODevice::ReadOnly)) + return png_invalid(); // can't open pack.png file + + auto data = pack_png_file.readAll(); + + bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data)); + + pack_png_file.close(); + if (!pack_png_result) { + return png_invalid(); // pack.png invalid + } + } else { + return png_invalid(); // pack.png does not exists or is not a valid file. + } + } + case ResourceType::ZIPFILE: + { + Q_ASSERT(pack.type() == ResourceType::ZIPFILE); + + QuaZip zip(pack.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; // can't open zip file + + QuaZipFile file(&zip); + if (zip.setCurrentFile("pack.png")) { + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open file in zip."; + zip.close(); + return png_invalid(); + } + + auto data = file.readAll(); + + bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data)); + + file.close(); + if (!pack_png_result) { + return png_invalid(); // pack.png invalid + } + } else { + return png_invalid(); // could not set pack.mcmeta as current file. + } + } + default: + qWarning() << "Invalid type for resource pack parse task!"; + return false; + } +} + bool validate(QFileInfo file) { ResourcePack rp{ file }; diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h index d0c24c2b1..58d90b3b9 100644 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h @@ -35,7 +35,10 @@ bool processZIP(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Ful bool processFolder(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data); -bool processPackPNG(ResourcePack& pack, QByteArray&& raw_data); +bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data); + +/// processes ONLY the pack.png (rest of the pack may be invalid) +bool processPackPNG(const ResourcePack& pack); /** Checks whether a file is valid as a resource pack or not. */ bool validate(QFileInfo file); diff --git a/launcher/minecraft/mod/tasks/LocalResourceParse.cpp b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp index 4d760df2b..0894049cd 100644 --- a/launcher/minecraft/mod/tasks/LocalResourceParse.cpp +++ b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp @@ -44,7 +44,11 @@ static const QMap s_packed_type_names = { namespace ResourceUtils { PackedResourceType identify(QFileInfo file){ if (file.exists() && file.isFile()) { - if (ResourcePackUtils::validate(file)) { + if (ModUtils::validate(file)) { + // mods can contain resource and data packs so they must be tested first + qDebug() << file.fileName() << "is a mod"; + return PackedResourceType::Mod; + } else if (ResourcePackUtils::validate(file)) { qDebug() << file.fileName() << "is a resource pack"; return PackedResourceType::ResourcePack; } else if (TexturePackUtils::validate(file)) { @@ -53,9 +57,6 @@ PackedResourceType identify(QFileInfo file){ } else if (DataPackUtils::validate(file)) { qDebug() << file.fileName() << "is a data pack"; return PackedResourceType::DataPack; - } else if (ModUtils::validate(file)) { - qDebug() << file.fileName() << "is a mod"; - return PackedResourceType::Mod; } else if (WorldSaveUtils::validate(file)) { qDebug() << file.fileName() << "is a world save"; return PackedResourceType::WorldSave; diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp index 38f1d7c1f..a72e81150 100644 --- a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp @@ -131,6 +131,7 @@ bool processZIP(TexturePack& pack, ProcessingLevel level) bool packPNG_result = TexturePackUtils::processPackPNG(pack, std::move(data)); file.close(); + zip.close(); if (!packPNG_result) { return false; } @@ -147,7 +148,7 @@ bool processPackTXT(TexturePack& pack, QByteArray&& raw_data) return true; } -bool processPackPNG(TexturePack& pack, QByteArray&& raw_data) +bool processPackPNG(const TexturePack& pack, QByteArray&& raw_data) { auto img = QImage::fromData(raw_data); if (!img.isNull()) { @@ -159,6 +160,70 @@ bool processPackPNG(TexturePack& pack, QByteArray&& raw_data) return true; } +bool processPackPNG(const TexturePack& pack) +{ + auto png_invalid = [&pack]() { + qWarning() << "Texture pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png"; + return false; + }; + + switch (pack.type()) { + case ResourceType::FOLDER: + { + QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); + if (image_file_info.exists() && image_file_info.isFile()) { + QFile pack_png_file(image_file_info.filePath()); + if (!pack_png_file.open(QIODevice::ReadOnly)) + return png_invalid(); // can't open pack.png file + + auto data = pack_png_file.readAll(); + + bool pack_png_result = TexturePackUtils::processPackPNG(pack, std::move(data)); + + pack_png_file.close(); + if (!pack_png_result) { + return png_invalid(); // pack.png invalid + } + } else { + return png_invalid(); // pack.png does not exists or is not a valid file. + } + } + case ResourceType::ZIPFILE: + { + Q_ASSERT(pack.type() == ResourceType::ZIPFILE); + + QuaZip zip(pack.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; // can't open zip file + + QuaZipFile file(&zip); + if (zip.setCurrentFile("pack.png")) { + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open file in zip."; + zip.close(); + return png_invalid(); + } + + auto data = file.readAll(); + + bool pack_png_result = TexturePackUtils::processPackPNG(pack, std::move(data)); + + file.close(); + if (!pack_png_result) { + zip.close(); + return png_invalid(); // pack.png invalid + } + } else { + zip.close(); + return png_invalid(); // could not set pack.mcmeta as current file. + } + } + default: + qWarning() << "Invalid type for resource pack parse task!"; + return false; + } +} + bool validate(QFileInfo file) { TexturePack rp{ file }; diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h index 1589f8cbd..6b91565ad 100644 --- a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h @@ -36,7 +36,10 @@ bool processZIP(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full bool processFolder(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processPackTXT(TexturePack& pack, QByteArray&& raw_data); -bool processPackPNG(TexturePack& pack, QByteArray&& raw_data); +bool processPackPNG(const TexturePack& pack, QByteArray&& raw_data); + +/// processes ONLY the pack.png (rest of the pack may be invalid) +bool processPackPNG(const TexturePack& pack); /** Checks whether a file is valid as a texture pack or not. */ bool validate(QFileInfo file); diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index 3677a1dca..ef353c701 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -103,7 +103,7 @@ void ModFolderLoadTask::executeTask() while (iter.hasNext()) { auto mod = iter.next().value(); if (mod->status() == ModStatus::NotInstalled) { - mod->destroy(m_index_dir, false); + mod->destroy(m_index_dir, false, false); iter.remove(); } } diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp index 34d969f02..c3eadd06d 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -10,6 +10,7 @@ #include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameModIndex.h" +#include "modplatform/helpers/HashUtils.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "modplatform/modrinth/ModrinthPackIndex.h" @@ -24,8 +25,8 @@ EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Resource auto hash_task = createNewHash(mod); if (!hash_task) return; - connect(hash_task.get(), &Task::succeeded, [this, hash_task, mod] { m_mods.insert(hash_task->getResult(), mod); }); - connect(hash_task.get(), &Task::failed, [this, hash_task, mod] { emitFail(mod, "", RemoveFromList::No); }); + connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) { m_mods.insert(hash, mod); }); + connect(hash_task.get(), &Task::failed, [this, mod] { emitFail(mod, "", RemoveFromList::No); }); hash_task->start(); } @@ -37,8 +38,8 @@ EnsureMetadataTask::EnsureMetadataTask(QList& mods, QDir dir, ModPlatform: auto hash_task = createNewHash(mod); if (!hash_task) continue; - connect(hash_task.get(), &Task::succeeded, [this, hash_task, mod] { m_mods.insert(hash_task->getResult(), mod); }); - connect(hash_task.get(), &Task::failed, [this, hash_task, mod] { emitFail(mod, "", RemoveFromList::No); }); + connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) { m_mods.insert(hash, mod); }); + connect(hash_task.get(), &Task::failed, [this, mod] { emitFail(mod, "", RemoveFromList::No); }); m_hashing_task->addTask(hash_task); } } @@ -144,7 +145,8 @@ void EnsureMetadataTask::executeTask() connect(project_task.get(), &Task::finished, this, [=] { invalidade_leftover(); project_task->deleteLater(); - m_current_task = nullptr; + if (m_current_task) + m_current_task.reset(); }); m_current_task = project_task; @@ -153,7 +155,8 @@ void EnsureMetadataTask::executeTask() connect(version_task.get(), &Task::finished, [=] { version_task->deleteLater(); - m_current_task = nullptr; + if (m_current_task) + m_current_task.reset(); }); if (m_mods.size() > 1) @@ -212,12 +215,12 @@ Task::Ptr EnsureMetadataTask::modrinthVersionsTask() { auto hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first(); - auto* response = new QByteArray(); + auto response = std::make_shared(); auto ver_task = modrinth_api.currentVersions(m_mods.keys(), hash_type, response); // Prevents unfortunate timings when aborting the task if (!ver_task) - return Task::Ptr{nullptr}; + return Task::Ptr{ nullptr }; connect(ver_task.get(), &Task::succeeded, this, [this, response] { QJsonParseError parse_error{}; @@ -264,7 +267,7 @@ Task::Ptr EnsureMetadataTask::modrinthProjectsTask() for (auto const& data : m_temp_versions) addonIds.insert(data.addonId.toString(), data.hash); - auto response = new QByteArray(); + auto response = std::make_shared(); Task::Ptr proj_task; if (addonIds.isEmpty()) { @@ -277,7 +280,7 @@ Task::Ptr EnsureMetadataTask::modrinthProjectsTask() // Prevents unfortunate timings when aborting the task if (!proj_task) - return Task::Ptr{nullptr}; + return Task::Ptr{ nullptr }; connect(proj_task.get(), &Task::succeeded, this, [this, response, addonIds] { QJsonParseError parse_error{}; @@ -345,7 +348,7 @@ Task::Ptr EnsureMetadataTask::modrinthProjectsTask() // Flame Task::Ptr EnsureMetadataTask::flameVersionsTask() { - auto* response = new QByteArray(); + auto response = std::make_shared(); QList fingerprints; for (auto& murmur : m_mods.keys()) { @@ -413,7 +416,7 @@ Task::Ptr EnsureMetadataTask::flameProjectsTask() QHash addonIds; for (auto const& hash : m_mods.keys()) { if (m_temp_versions.contains(hash)) { - auto const& data = m_temp_versions.find(hash).value(); + auto data = m_temp_versions.find(hash).value(); auto id_str = data.addonId.toString(); if (!id_str.isEmpty()) @@ -421,7 +424,7 @@ Task::Ptr EnsureMetadataTask::flameProjectsTask() } } - auto response = new QByteArray(); + auto response = std::make_shared(); Task::Ptr proj_task; if (addonIds.isEmpty()) { @@ -434,7 +437,7 @@ Task::Ptr EnsureMetadataTask::flameProjectsTask() // Prevents unfortunate timings when aborting the task if (!proj_task) - return Task::Ptr{nullptr}; + return Task::Ptr{ nullptr }; connect(proj_task.get(), &Task::succeeded, this, [this, response, addonIds] { QJsonParseError parse_error{}; diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index 6a507caf4..a1c4d8917 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -70,11 +70,17 @@ auto ProviderCapabilities::hash(ResourceProvider p, QIODevice* device, QString t } QCryptographicHash hash(algo); - if(!hash.addData(device)) + if (!hash.addData(device)) qCritical() << "Failed to read JAR to create hash!"; Q_ASSERT(hash.result().length() == hash.hashLength(algo)); return { hash.result().toHex() }; } +QString getMetaURL(ResourceProvider provider, QVariant projectID) +{ + return ((provider == ModPlatform::ResourceProvider::FLAME) ? "https://www.curseforge.com/projects/" : "https://modrinth.com/mod/") + + projectID.toString(); +} + } // namespace ModPlatform diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 82da2ab2f..2aa91602b 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln + * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -33,6 +34,8 @@ enum class ResourceProvider { MODRINTH, FLAME }; enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK }; +enum class DependencyType { REQUIRED, OPTIONAL, INCOMPATIBLE, EMBEDDED, TOOL, INCLUDE, UNKNOWN }; + class ProviderCapabilities { public: auto name(ResourceProvider) -> const char*; @@ -52,6 +55,12 @@ struct DonationData { QString url; }; +struct Dependency { + QVariant addonId; + DependencyType type; + QString version; +}; + struct IndexedVersion { QVariant addonId; QVariant fileId; @@ -66,6 +75,7 @@ struct IndexedVersion { QString hash; bool is_preferred = true; QString changelog; + QList dependencies; // For internal use, not provided by APIs bool is_currently_selected = false; @@ -118,6 +128,24 @@ struct IndexedPack { return std::any_of(versions.constBegin(), versions.constEnd(), [](auto const& v) { return v.is_currently_selected; }); } }; +QString getMetaURL(ResourceProvider provider, QVariant projectID); + +struct OverrideDep { + QString quilt; + QString fabric; + QString slug; + ModPlatform::ResourceProvider provider; +}; + +inline auto getOverrideDeps() -> QList +{ + return { { "634179", "306612", "API", ModPlatform::ResourceProvider::FLAME }, + { "720410", "308769", "KotlinLibraries", ModPlatform::ResourceProvider::FLAME }, + + { "qvIfYCYJ", "P7dR8mSH", "API", ModPlatform::ResourceProvider::MODRINTH }, + { "lwVhp9o5", "Ha28R6CL", "KotlinLibraries", ModPlatform::ResourceProvider::MODRINTH } }; +}; +QString getMetaURL(ResourceProvider provider, QVariant projectID); } // namespace ModPlatform diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h index 34f337791..d3277761e 100644 --- a/launcher/modplatform/ResourceAPI.h +++ b/launcher/modplatform/ResourceAPI.h @@ -111,6 +111,16 @@ class ResourceAPI { std::function on_succeed; }; + struct DependencySearchArgs { + ModPlatform::Dependency dependency; + Version mcVersion; + ModLoaderTypes loader; + }; + + struct DependencySearchCallbacks { + std::function on_succeed; + }; + public: /** Gets a list of available sorting methods for this API. */ [[nodiscard]] virtual auto getSortingMethods() const -> QList = 0; @@ -121,12 +131,12 @@ class ResourceAPI { qWarning() << "TODO"; return nullptr; } - [[nodiscard]] virtual Task::Ptr getProject(QString addonId, QByteArray* response) const + [[nodiscard]] virtual Task::Ptr getProject(QString addonId, std::shared_ptr response) const { qWarning() << "TODO"; return nullptr; } - [[nodiscard]] virtual Task::Ptr getProjects(QStringList addonIds, QByteArray* response) const + [[nodiscard]] virtual Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const { qWarning() << "TODO"; return nullptr; @@ -143,6 +153,12 @@ class ResourceAPI { return nullptr; } + [[nodiscard]] virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, DependencySearchCallbacks&&) const + { + qWarning() << "TODO"; + return nullptr; + } + static auto getModLoaderString(ModLoaderType type) -> const QString { switch (type) { diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 07e0bf23b..22ea02da2 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -82,9 +82,9 @@ void PackInstallTask::executeTask() { qDebug() << "PackInstallTask::executeTask: " << QThread::currentThreadId(); NetJob::Ptr netJob{ new NetJob("ATLauncher::VersionFetch", APPLICATION->network()) }; - auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json") - .arg(m_pack_safe_name).arg(m_version_name); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); + auto searchUrl = + QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json").arg(m_pack_safe_name).arg(m_version_name); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); QObject::connect(netJob.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); QObject::connect(netJob.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed); @@ -99,11 +99,12 @@ void PackInstallTask::onDownloadSucceeded() qDebug() << "PackInstallTask::onDownloadSucceeded: " << QThread::currentThreadId(); jobPtr.reset(); - QJsonParseError parse_error {}; - QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); - if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from ATLauncher at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << response; + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from ATLauncher at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response.get(); return; } auto obj = doc.object(); diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h index 90e25ae2e..b82f523fe 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h @@ -40,12 +40,13 @@ #include "ATLPackManifest.h" #include "InstanceTask.h" -#include "net/NetJob.h" -#include "settings/INISettingsObject.h" +#include "meta/Version.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" -#include "meta/Version.h" +#include "net/NetJob.h" +#include "settings/INISettingsObject.h" +#include #include namespace ATLauncher { @@ -57,8 +58,7 @@ enum class InstallMode { }; class UserInteractionSupport { - -public: + public: /** * Requests a user interaction to select which optional mods should be installed. */ @@ -74,23 +74,27 @@ public: * Requests a user interaction to display a message. */ virtual void displayMessage(QString message) = 0; + + virtual ~UserInteractionSupport() = default; }; -class PackInstallTask : public InstanceTask -{ -Q_OBJECT +class PackInstallTask : public InstanceTask { + Q_OBJECT -public: - explicit PackInstallTask(UserInteractionSupport *support, QString packName, QString version, InstallMode installMode = InstallMode::Install); - virtual ~PackInstallTask(){} + public: + explicit PackInstallTask(UserInteractionSupport* support, + QString packName, + QString version, + InstallMode installMode = InstallMode::Install); + virtual ~PackInstallTask() { delete m_support; } bool canAbort() const override { return true; } bool abort() override; -protected: + protected: virtual void executeTask() override; -private slots: + private slots: void onDownloadSucceeded(); void onDownloadFailed(QString reason); void onDownloadAborted(); @@ -98,7 +102,7 @@ private slots: void onModsDownloaded(); void onModsExtracted(); -private: + private: QString getDirForModType(ModType type, QString raw); QString getVersionForLoader(QString uid); QString detectLibrary(VersionLibrary library); @@ -110,20 +114,18 @@ private: void installConfigs(); void extractConfigs(); void downloadMods(); - bool extractMods( - const QMap &toExtract, - const QMap &toDecomp, - const QMap &toCopy - ); + bool extractMods(const QMap& toExtract, + const QMap& toDecomp, + const QMap& toCopy); void install(); -private: - UserInteractionSupport *m_support; + private: + UserInteractionSupport* m_support; bool abortable = false; NetJob::Ptr jobPtr; - QByteArray response; + std::shared_ptr response = std::make_shared(); InstallMode m_install_mode; QString m_pack_name; @@ -145,7 +147,6 @@ private: QFuture m_modExtractFuture; QFutureWatcher m_modExtractFutureWatcher; - }; -} +} // namespace ATLauncher diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index 83db642e7..34bd401d3 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -21,19 +21,24 @@ bool Flame::FileResolvingTask::abort() void Flame::FileResolvingTask::executeTask() { + if (m_toProcess.files.isEmpty()) { // no file to resolve so leave it empty and emit success immediately + emitSucceeded(); + return; + } setStatus(tr("Resolving mod IDs...")); setProgress(0, 3); m_dljob.reset(new NetJob("Mod id resolver", m_network)); result.reset(new QByteArray()); - //build json data to send + // build json data to send QJsonObject object; - object["fileIds"] = QJsonArray::fromVariantList(std::accumulate(m_toProcess.files.begin(), m_toProcess.files.end(), QVariantList(), [](QVariantList& l, const File& s) { - l.push_back(s.fileId); - return l; - })); + object["fileIds"] = QJsonArray::fromVariantList( + std::accumulate(m_toProcess.files.begin(), m_toProcess.files.end(), QVariantList(), [](QVariantList& l, const File& s) { + l.push_back(s.fileId); + return l; + })); QByteArray data = Json::toText(object); - auto dl = Net::Upload::makeByteArray(QUrl("https://api.curseforge.com/v1/mods/files"), result.get(), data); + auto dl = Net::Upload::makeByteArray(QUrl("https://api.curseforge.com/v1/mods/files"), result, data); m_dljob->addNetAction(dl); auto step_progress = std::make_shared(); @@ -87,17 +92,15 @@ void Flame::FileResolvingTask::netJobFinished() auto fileid = Json::requireInteger(Json::requireObject(file)["id"]); auto& out = m_toProcess.files[fileid]; try { - out.parseFromObject(Json::requireObject(file)); + out.parseFromObject(Json::requireObject(file)); } catch (const JSONValidationError& e) { qDebug() << "Blocked mod on curseforge" << out.fileName; auto hash = out.hash; - if(!hash.isEmpty()) { + if (!hash.isEmpty()) { auto url = QString("https://api.modrinth.com/v2/version_file/%1?algorithm=sha1").arg(hash); auto output = std::make_shared(); - auto dl = Net::Download::makeByteArray(QUrl(url), output.get()); - QObject::connect(dl.get(), &Net::Download::succeeded, [&out]() { - out.resolved = true; - }); + auto dl = Net::Download::makeByteArray(QUrl(url), output); + QObject::connect(dl.get(), &Net::Download::succeeded, [&out]() { out.resolved = true; }); m_checkJob->addNetAction(dl); blockedProjects.insert(&out, output); @@ -129,12 +132,13 @@ void Flame::FileResolvingTask::netJobFinished() m_checkJob->start(); } -void Flame::FileResolvingTask::modrinthCheckFinished() { +void Flame::FileResolvingTask::modrinthCheckFinished() +{ setProgress(2, 3); qDebug() << "Finished with blocked mods : " << blockedProjects.size(); for (auto it = blockedProjects.keyBegin(); it != blockedProjects.keyEnd(); it++) { - auto &out = *it; + auto& out = *it; auto bytes = blockedProjects[out]; if (!out->resolved) { continue; @@ -154,28 +158,26 @@ void Flame::FileResolvingTask::modrinthCheckFinished() { out->resolved = false; } } - //copy to an output list and filter out projects found on modrinth + // copy to an output list and filter out projects found on modrinth auto block = std::make_shared>(); auto it = blockedProjects.keys(); - std::copy_if(it.begin(), it.end(), std::back_inserter(*block), [](File *f) { - return !f->resolved; - }); - //Display not found mods early + std::copy_if(it.begin(), it.end(), std::back_inserter(*block), [](File* f) { return !f->resolved; }); + // Display not found mods early if (!block->empty()) { - //blocked mods found, we need the slug for displaying.... we need another job :D ! + // blocked mods found, we need the slug for displaying.... we need another job :D ! m_slugJob.reset(new NetJob("Slug Job", m_network)); int index = 0; for (auto mod : *block) { auto projectId = mod->projectId; auto output = std::make_shared(); auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(projectId); - auto dl = Net::Download::makeByteArray(url, output.get()); + auto dl = Net::Download::makeByteArray(url, output); qDebug() << "Fetching url slug for file:" << mod->fileName; QObject::connect(dl.get(), &Net::Download::succeeded, [block, index, output]() { auto mod = block->at(index); // use the shared_ptr so it is captured and only freed when we are done auto json = QJsonDocument::fromJson(*output); - auto base = Json::requireString(Json::requireObject(Json::requireObject(Json::requireObject(json),"data"),"links"), - "websiteUrl"); + auto base = + Json::requireString(Json::requireObject(Json::requireObject(Json::requireObject(json), "data"), "links"), "websiteUrl"); auto link = QString("%1/download/%2").arg(base, QString::number(mod->fileId)); mod->websiteUrl = link; }); diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index 92590a084..5b0b1d8b9 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -11,7 +11,7 @@ #include "net/NetJob.h" #include "net/Upload.h" -Task::Ptr FlameAPI::matchFingerprints(const QList& fingerprints, QByteArray* response) +Task::Ptr FlameAPI::matchFingerprints(const QList& fingerprints, std::shared_ptr response) { auto netJob = makeShared(QString("Flame::MatchFingerprints"), APPLICATION->network()); @@ -28,8 +28,6 @@ Task::Ptr FlameAPI::matchFingerprints(const QList& fingerprints, QByteArra netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/fingerprints"), response, body_raw)); - QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; }); - return netJob; } @@ -43,7 +41,7 @@ auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString netJob->addNetAction(Net::Download::makeByteArray( QString("https://api.curseforge.com/v1/mods/%1/files/%2/changelog") .arg(QString::fromStdString(std::to_string(modId)), QString::fromStdString(std::to_string(fileId))), - response.get())); + response)); QObject::connect(netJob.get(), &NetJob::succeeded, [&netJob, response, &changelog] { QJsonParseError parse_error{}; @@ -75,8 +73,8 @@ auto FlameAPI::getModDescription(int modId) -> QString auto netJob = makeShared(QString("Flame::ModDescription"), APPLICATION->network()); auto response = std::make_shared(); - netJob->addNetAction(Net::Download::makeByteArray( - QString("https://api.curseforge.com/v1/mods/%1/description").arg(QString::number(modId)), response.get())); + netJob->addNetAction( + Net::Download::makeByteArray(QString("https://api.curseforge.com/v1/mods/%1/description").arg(QString::number(modId)), response)); QObject::connect(netJob.get(), &NetJob::succeeded, [&netJob, response, &description] { QJsonParseError parse_error{}; @@ -115,7 +113,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe auto response = std::make_shared(); ModPlatform::IndexedVersion ver; - netJob->addNetAction(Net::Download::makeByteArray(versions_url, response.get())); + netJob->addNetAction(Net::Download::makeByteArray(versions_url, response)); QObject::connect(netJob.get(), &NetJob::succeeded, [response, args, &ver] { QJsonParseError parse_error{}; @@ -137,7 +135,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe for (auto file : arr) { auto file_obj = Json::requireObject(file); auto file_tmp = FlameMod::loadIndexedPackVersion(file_obj); - if(file_tmp.date > ver_tmp.date) { + if (file_tmp.date > ver_tmp.date) { ver_tmp = file_tmp; latest_file_obj = file_obj; } @@ -160,7 +158,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe return ver; } -Task::Ptr FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const +Task::Ptr FlameAPI::getProjects(QStringList addonIds, std::shared_ptr response) const { auto netJob = makeShared(QString("Flame::GetProjects"), APPLICATION->network()); @@ -177,13 +175,12 @@ Task::Ptr FlameAPI::getProjects(QStringList addonIds, QByteArray* response) cons netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods"), response, body_raw)); - QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; }); QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; }); return netJob; } -Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) const +Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, std::shared_ptr response) const { auto netJob = makeShared(QString("Flame::GetFiles"), APPLICATION->network()); @@ -200,7 +197,6 @@ Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) c netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods/files"), response, body_raw)); - QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; }); QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; }); return netJob; diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 5811d7175..49bc316f2 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -4,7 +4,10 @@ #pragma once +#include +#include #include "modplatform/ModIndex.h" +#include "modplatform/ResourceAPI.h" #include "modplatform/helpers/NetworkResourceAPI.h" class FlameAPI : public NetworkResourceAPI { @@ -14,12 +17,14 @@ class FlameAPI : public NetworkResourceAPI { auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion; - Task::Ptr getProjects(QStringList addonIds, QByteArray* response) const override; - Task::Ptr matchFingerprints(const QList& fingerprints, QByteArray* response); - Task::Ptr getFiles(const QStringList& fileIds, QByteArray* response) const; + Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const override; + Task::Ptr matchFingerprints(const QList& fingerprints, std::shared_ptr response); + Task::Ptr getFiles(const QStringList& fileIds, std::shared_ptr response) const; [[nodiscard]] auto getSortingMethods() const -> QList override; + static inline auto validateModLoaders(ModLoaderTypes loaders) -> bool { return loaders & (Forge | Fabric | Quilt); } + private: static int getClassId(ModPlatform::ResourceType type) { @@ -41,14 +46,15 @@ class FlameAPI : public NetworkResourceAPI { return 4; // TODO: remove this once Quilt drops official Fabric support if (loaders & Quilt) // NOTE: Most if not all Fabric mods should work *currently* - return 4; // Quilt would probably be 5 + return 4; // Quilt would probably be 5 return 0; } private: [[nodiscard]] std::optional getSearchURL(SearchArgs const& args) const override { - auto gameVersionStr = args.versions.has_value() ? QString("gameVersion=%1").arg(args.versions.value().front().toString()) : QString(); + auto gameVersionStr = + args.versions.has_value() ? QString("gameVersion=%1").arg(args.versions.value().front().toString()) : QString(); QStringList get_arguments; get_arguments.append(QString("classId=%1").arg(getClassId(args.type))); @@ -73,14 +79,48 @@ class FlameAPI : public NetworkResourceAPI { [[nodiscard]] std::optional getVersionsURL(VersionSearchArgs const& args) const override { - QString url{QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&").arg(args.pack.addonId.toString())}; + auto addonId = args.pack.addonId.toString(); + QString url{ QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&").arg(addonId) }; QStringList get_parameters; if (args.mcVersions.has_value()) get_parameters.append(QString("gameVersion=%1").arg(args.mcVersions.value().front().toString())); - if (args.loaders.has_value()) - get_parameters.append(QString("modLoaderType=%1").arg(getMappedModLoader(args.loaders.value()))); + + if (args.loaders.has_value()) { + int mappedModLoader = getMappedModLoader(args.loaders.value()); + + if (args.loaders.value() & Quilt) { + auto overide = ModPlatform::getOverrideDeps(); + auto over = std::find_if(overide.cbegin(), overide.cend(), [addonId](auto dep) { + return dep.provider == ModPlatform::ResourceProvider::FLAME && addonId == dep.quilt; + }); + if (over != overide.cend()) { + mappedModLoader = 5; + } + } + + get_parameters.append(QString("modLoaderType=%1").arg(mappedModLoader)); + } return url + get_parameters.join('&'); }; + + [[nodiscard]] std::optional getDependencyURL(DependencySearchArgs const& args) const override + { + auto mappedModLoader = getMappedModLoader(args.loader); + auto addonId = args.dependency.addonId.toString(); + if (args.loader & Quilt) { + auto overide = ModPlatform::getOverrideDeps(); + auto over = std::find_if(overide.cbegin(), overide.cend(), [addonId](auto dep) { + return dep.provider == ModPlatform::ResourceProvider::FLAME && addonId == dep.quilt; + }); + if (over != overide.cend()) { + mappedModLoader = 5; + } + } + return QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&gameVersion=%2&modLoaderType=%3") + .arg(addonId) + .arg(args.mcVersion.toString()) + .arg(mappedModLoader); + }; }; diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp index e09aeb3d9..a2628e34c 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.cpp +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -31,7 +31,7 @@ ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info) auto get_project_job = new NetJob("Flame::GetProjectJob", APPLICATION->network()); - auto response = new QByteArray(); + auto response = std::make_shared(); auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(ver_info.addonId.toString()); auto dl = Net::Download::makeByteArray(url, response); get_project_job->addNetAction(dl); @@ -75,7 +75,7 @@ ModPlatform::IndexedVersion getFileInfo(int addonId, int fileId) auto get_file_info_job = new NetJob("Flame::GetFileInfoJob", APPLICATION->network()); - auto response = new QByteArray(); + auto response = std::make_shared(); auto url = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(QString::number(addonId), QString::number(fileId)); auto dl = Net::Download::makeByteArray(url, response); get_file_info_job->addNetAction(dl); diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index dae93d1c6..b57db288a 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -153,6 +153,9 @@ bool FlameCreationTask::updateInstance() old_files.remove(file.key()); files_iterator = files.erase(files_iterator); + + if (files_iterator != files.begin()) + files_iterator--; } } @@ -179,7 +182,7 @@ bool FlameCreationTask::updateInstance() fileIds.append(QString::number(file.fileId)); } - auto* raw_response = new QByteArray; + auto raw_response = std::make_shared(); auto job = api.getFiles(fileIds, raw_response); QEventLoop loop; @@ -467,8 +470,9 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop) switch (result.type) { case Flame::File::Type::Folder: { logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath)); - // fall-through intentional, we treat these as plain old mods and dump them wherever. + // fallthrough intentional, we treat these as plain old mods and dump them wherever. } + /* fallthrough */ case Flame::File::Type::SingleFile: case Flame::File::Type::Mod: { if (!result.url.isEmpty()) { @@ -559,6 +563,8 @@ void FlameCreationTask::validateZIPResouces() if (FS::move(localPath, destPath)) { return destPath; } + } else { + qDebug() << "Target folder of" << fileName << "is correct at" << targetFolder; } return localPath; }; @@ -580,6 +586,9 @@ void FlameCreationTask::validateZIPResouces() QString worldPath; switch (type) { + case PackedResourceType::Mod : + validatePath(fileName, targetFolder, "mods"); + break; case PackedResourceType::ResourcePack : validatePath(fileName, targetFolder, "resourcepacks"); break; @@ -589,9 +598,6 @@ void FlameCreationTask::validateZIPResouces() case PackedResourceType::DataPack : validatePath(fileName, targetFolder, "datapacks"); break; - case PackedResourceType::Mod : - validatePath(fileName, targetFolder, "mods"); - break; case PackedResourceType::ShaderPack : // in theroy flame API can't do this but who knows, that *may* change ? // better to handle it if it *does* occure in the future diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 7498e8302..227ce4898 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -39,15 +39,15 @@ void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj) auto links_obj = Json::ensureObject(obj, "links"); pack.extraData.issuesUrl = Json::ensureString(links_obj, "issuesUrl"); - if(pack.extraData.issuesUrl.endsWith('/')) + if (pack.extraData.issuesUrl.endsWith('/')) pack.extraData.issuesUrl.chop(1); pack.extraData.sourceUrl = Json::ensureString(links_obj, "sourceUrl"); - if(pack.extraData.sourceUrl.endsWith('/')) + if (pack.extraData.sourceUrl.endsWith('/')) pack.extraData.sourceUrl.chop(1); pack.extraData.wikiUrl = Json::ensureString(links_obj, "wikiUrl"); - if(pack.extraData.wikiUrl.endsWith('/')) + if (pack.extraData.wikiUrl.endsWith('/')) pack.extraData.wikiUrl.chop(1); if (!pack.extraData.body.isEmpty()) @@ -56,7 +56,7 @@ void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj) void FlameMod::loadBody(ModPlatform::IndexedPack& pack, QJsonObject& obj) { - pack.extraData.body = api.getModDescription(pack.addonId.toInt()); + pack.extraData.body = api.getModDescription(pack.addonId.toInt()); if (!pack.extraData.issuesUrl.isEmpty() || !pack.extraData.sourceUrl.isEmpty() || !pack.extraData.wikiUrl.isEmpty()) pack.extraDataLoaded = true; @@ -64,12 +64,12 @@ void FlameMod::loadBody(ModPlatform::IndexedPack& pack, QJsonObject& obj) static QString enumToString(int hash_algorithm) { - switch(hash_algorithm){ - default: - case 1: - return "sha1"; - case 2: - return "md5"; + switch (hash_algorithm) { + default: + case 1: + return "sha1"; + case 2: + return "md5"; } } @@ -84,12 +84,12 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, for (auto versionIter : arr) { auto obj = versionIter.toObject(); - + auto file = loadIndexedPackVersion(obj); - if(!file.addonId.isValid()) + if (!file.addonId.isValid()) file.addonId = pack.addonId; - if(file.fileId.isValid()) // Heuristic to check if the returned value is valid + if (file.fileId.isValid()) // Heuristic to check if the returned value is valid unsortedVersions.append(file); } @@ -136,8 +136,61 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> } } - if(load_changelog) + auto dependencies = Json::ensureArray(obj, "dependencies"); + for (auto d : dependencies) { + auto dep = Json::ensureObject(d); + ModPlatform::Dependency dependency; + dependency.addonId = Json::requireInteger(dep, "modId"); + switch (Json::requireInteger(dep, "relationType")) { + case 1: // EmbeddedLibrary + dependency.type = ModPlatform::DependencyType::EMBEDDED; + break; + case 2: // OptionalDependency + dependency.type = ModPlatform::DependencyType::OPTIONAL; + break; + case 3: // RequiredDependency + dependency.type = ModPlatform::DependencyType::REQUIRED; + break; + case 4: // Tool + dependency.type = ModPlatform::DependencyType::TOOL; + break; + case 5: // Incompatible + dependency.type = ModPlatform::DependencyType::INCOMPATIBLE; + break; + case 6: // Include + dependency.type = ModPlatform::DependencyType::INCLUDE; + break; + default: + dependency.type = ModPlatform::DependencyType::UNKNOWN; + break; + } + file.dependencies.append(dependency); + } + + if (load_changelog) file.changelog = api.getModFileChangelog(file.addonId.toInt(), file.fileId.toInt()); return file; } + +ModPlatform::IndexedVersion FlameMod::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) +{ + QVector versions; + for (auto versionIter : arr) { + auto obj = versionIter.toObject(); + + auto file = loadIndexedPackVersion(obj); + if (!file.addonId.isValid()) + file.addonId = m.addonId; + + if (file.fileId.isValid()) // Heuristic to check if the returned value is valid + versions.append(file); + } + + auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { + // dates are in RFC 3339 format + return a.date > b.date; + }; + std::sort(versions.begin(), versions.end(), orderSortPredicate); + return versions.front(); +}; diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h index 33c4a5298..aa0d6f812 100644 --- a/launcher/modplatform/flame/FlameModIndex.h +++ b/launcher/modplatform/flame/FlameModIndex.h @@ -6,8 +6,8 @@ #include "modplatform/ModIndex.h" -#include "BaseInstance.h" #include +#include "BaseInstance.h" namespace FlameMod { @@ -19,5 +19,5 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, const shared_qobject_ptr& network, const BaseInstance* inst); auto loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false) -> ModPlatform::IndexedVersion; - -} // namespace FlameMod +auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion; +} // namespace FlameMod \ No newline at end of file diff --git a/launcher/modplatform/flame/FlamePackExportTask.cpp b/launcher/modplatform/flame/FlamePackExportTask.cpp new file mode 100644 index 000000000..ac0da2142 --- /dev/null +++ b/launcher/modplatform/flame/FlamePackExportTask.cpp @@ -0,0 +1,473 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "FlamePackExportTask.h" +#include +#include + +#include +#include +#include +#include +#include +#include +#include "Json.h" +#include "MMCZip.h" +#include "minecraft/PackProfile.h" +#include "minecraft/mod/ModFolderModel.h" +#include "modplatform/ModIndex.h" +#include "modplatform/flame/FlameModIndex.h" +#include "modplatform/helpers/HashUtils.h" +#include "tasks/Task.h" + +const QString FlamePackExportTask::TEMPLATE = "
  • {name}{authors}
  • \n"; +const QStringList FlamePackExportTask::FILE_EXTENSIONS({ "jar", "zip" }); + +FlamePackExportTask::FlamePackExportTask(const QString& name, + const QString& version, + const QString& author, + InstancePtr instance, + const QString& output, + MMCZip::FilterFunction filter) + : name(name) + , version(version) + , author(author) + , instance(instance) + , mcInstance(dynamic_cast(instance.get())) + , gameRoot(instance->gameRoot()) + , output(output) + , filter(filter) +{} + +void FlamePackExportTask::executeTask() +{ + setStatus(tr("Searching for files...")); + setProgress(0, 5); + collectFiles(); +} + +bool FlamePackExportTask::abort() +{ + if (task != nullptr) { + task->abort(); + task = nullptr; + emitAborted(); + return true; + } + + if (buildZipFuture.isRunning()) { + buildZipFuture.cancel(); + // NOTE: Here we don't do `emitAborted()` because it will be done when `buildZipFuture` actually cancels, which may not occur + // immediately. + return true; + } + + return false; +} + +void FlamePackExportTask::collectFiles() +{ + setAbortable(false); + QCoreApplication::processEvents(); + + files.clear(); + if (!MMCZip::collectFileListRecursively(instance->gameRoot(), nullptr, &files, filter)) { + emitFailed(tr("Could not search for files")); + return; + } + + pendingHashes.clear(); + resolvedFiles.clear(); + + if (mcInstance != nullptr) { + mcInstance->loaderModList()->update(); + connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, &FlamePackExportTask::collectHashes); + } else + collectHashes(); +} + +void FlamePackExportTask::collectHashes() +{ + setAbortable(true); + setStatus(tr("Finding file hashes...")); + setProgress(1, 5); + auto allMods = mcInstance->loaderModList()->allMods(); + ConcurrentTask::Ptr hashingTask(new ConcurrentTask(this, "MakeHashesTask", 10)); + task.reset(hashingTask); + for (const QFileInfo& file : files) { + const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath()); + // require sensible file types + if (!std::any_of(FILE_EXTENSIONS.begin(), FILE_EXTENSIONS.end(), [&relative](const QString& extension) { + return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled"); + })) + continue; + + if (relative.startsWith("resourcepacks/") && + (relative.endsWith(".zip") || relative.endsWith(".zip.disabled"))) { // is resourcepack + auto hashTask = Hashing::createFlameHasher(file.absoluteFilePath()); + connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, relative, file](QString hash) { + if (m_state == Task::State::Running) { + pendingHashes.insert(hash, { relative, file.absoluteFilePath(), relative.endsWith(".zip") }); + } + }); + connect(hashTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed); + hashingTask->addTask(hashTask); + continue; + } + + if (auto modIter = std::find_if(allMods.begin(), allMods.end(), [&file](Mod* mod) { return mod->fileinfo() == file; }); + modIter != allMods.end()) { + const Mod* mod = *modIter; + if (!mod || mod->type() == ResourceType::FOLDER) { + continue; + } + if (mod->metadata() && mod->metadata()->provider == ModPlatform::ResourceProvider::FLAME) { + resolvedFiles.insert(mod->fileinfo().absoluteFilePath(), + { mod->metadata()->project_id.toInt(), mod->metadata()->file_id.toInt(), mod->enabled(), true, + mod->metadata()->name, mod->metadata()->slug, mod->authors().join(", ") }); + continue; + } + + auto hashTask = Hashing::createFlameHasher(mod->fileinfo().absoluteFilePath()); + connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) { + if (m_state == Task::State::Running) { + pendingHashes.insert(hash, { mod->name(), mod->fileinfo().absoluteFilePath(), mod->enabled(), true }); + } + }); + connect(hashTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed); + hashingTask->addTask(hashTask); + } + } + auto progressStep = std::make_shared(); + connect(hashingTask.get(), &Task::finished, this, [this, progressStep] { + progressStep->state = TaskStepState::Succeeded; + stepProgress(*progressStep); + }); + + connect(hashingTask.get(), &Task::succeeded, this, &FlamePackExportTask::makeApiRequest); + connect(hashingTask.get(), &Task::failed, this, [this, progressStep](QString reason) { + progressStep->state = TaskStepState::Failed; + stepProgress(*progressStep); + emitFailed(reason); + }); + connect(hashingTask.get(), &Task::stepProgress, this, &FlamePackExportTask::propogateStepProgress); + + connect(hashingTask.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) { + progressStep->update(current, total); + stepProgress(*progressStep); + }); + connect(hashingTask.get(), &Task::status, this, [this, progressStep](QString status) { + progressStep->status = status; + stepProgress(*progressStep); + }); + hashingTask->start(); +} + +void FlamePackExportTask::makeApiRequest() +{ + if (pendingHashes.isEmpty()) { + buildZip(); + return; + } + + setStatus(tr("Finding versions for hashes...")); + setProgress(2, 5); + auto response = std::make_shared(); + + QList fingerprints; + for (auto& murmur : pendingHashes.keys()) { + fingerprints.push_back(murmur.toUInt()); + } + + task.reset(api.matchFingerprints(fingerprints, response)); + + connect(task.get(), &Task::succeeded, this, [this, response] { + QJsonParseError parseError{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parseError); + if (parseError.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from CurseForge::CurrentVersions at " << parseError.offset + << " reason: " << parseError.errorString(); + qWarning() << *response; + + failed(parseError.errorString()); + return; + } + + try { + auto docObj = Json::requireObject(doc); + auto dataObj = Json::requireObject(docObj, "data"); + auto dataArr = Json::requireArray(dataObj, "exactMatches"); + + if (dataArr.isEmpty()) { + qWarning() << "No matches found for fingerprint search!"; + + return; + } + for (auto match : dataArr) { + auto matchObj = Json::ensureObject(match, {}); + auto fileObj = Json::ensureObject(matchObj, "file", {}); + + if (matchObj.isEmpty() || fileObj.isEmpty()) { + qWarning() << "Fingerprint match is empty!"; + + return; + } + + auto fingerprint = QString::number(Json::ensureVariant(fileObj, "fileFingerprint").toUInt()); + auto mod = pendingHashes.find(fingerprint); + if (mod == pendingHashes.end()) { + qWarning() << "Invalid fingerprint from the API response."; + continue; + } + + setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name)); + if (Json::ensureBoolean(fileObj, "isAvailable", false, "isAvailable")) + resolvedFiles.insert(mod->path, { Json::requireInteger(fileObj, "modId"), Json::requireInteger(fileObj, "id"), + mod->enabled, mod->isMod }); + } + + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << doc; + } + pendingHashes.clear(); + }); + connect(task.get(), &Task::finished, this, &FlamePackExportTask::getProjectsInfo); + connect(task.get(), &NetJob::failed, this, &FlamePackExportTask::emitFailed); + task->start(); +} + +void FlamePackExportTask::getProjectsInfo() +{ + setStatus(tr("Finding project info from CurseForge...")); + setProgress(3, 5); + QStringList addonIds; + for (const auto& resolved : resolvedFiles) { + if (resolved.slug.isEmpty()) { + addonIds << QString::number(resolved.addonId); + } + } + + auto response = std::make_shared(); + Task::Ptr projTask; + + if (addonIds.isEmpty()) { + buildZip(); + return; + } else if (addonIds.size() == 1) { + projTask = api.getProject(*addonIds.begin(), response); + } else { + projTask = api.getProjects(addonIds, response); + } + + connect(projTask.get(), &Task::succeeded, this, [this, response, addonIds] { + QJsonParseError parseError{}; + auto doc = QJsonDocument::fromJson(*response, &parseError); + if (parseError.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from CurseForge projects task at " << parseError.offset + << " reason: " << parseError.errorString(); + qWarning() << *response; + failed(parseError.errorString()); + return; + } + + try { + QJsonArray entries; + if (addonIds.size() == 1) + entries = { Json::requireObject(Json::requireObject(doc), "data") }; + else + entries = Json::requireArray(Json::requireObject(doc), "data"); + + for (auto entry : entries) { + auto entryObj = Json::requireObject(entry); + + try { + setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(Json::requireString(entryObj, "name"))); + + ModPlatform::IndexedPack pack; + FlameMod::loadIndexedPack(pack, entryObj); + for (auto key : resolvedFiles.keys()) { + auto val = resolvedFiles.value(key); + if (val.addonId == pack.addonId) { + val.name = pack.name; + val.slug = pack.slug; + QStringList authors; + for (auto author : pack.authors) + authors << author.name; + + val.authors = authors.join(", "); + resolvedFiles[key] = val; + } + } + + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << entries; + } + } + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << doc; + } + buildZip(); + }); + task.reset(projTask); + task->start(); +} + +void FlamePackExportTask::buildZip() +{ + setStatus(tr("Adding files...")); + setProgress(4, 5); + + buildZipFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { + QuaZip zip(output); + if (!zip.open(QuaZip::mdCreate)) { + QFile::remove(output); + return BuildZipResult(tr("Could not create file")); + } + + if (buildZipFuture.isCanceled()) + return BuildZipResult(); + + QuaZipFile indexFile(&zip); + if (!indexFile.open(QIODevice::WriteOnly, QuaZipNewInfo("manifest.json"))) { + QFile::remove(output); + return BuildZipResult(tr("Could not create index")); + } + indexFile.write(generateIndex()); + + QuaZipFile modlist(&zip); + if (!modlist.open(QIODevice::WriteOnly, QuaZipNewInfo("modlist.html"))) { + QFile::remove(output); + return BuildZipResult(tr("Could not create index")); + } + QString content = ""; + for (auto mod : resolvedFiles) { + if (mod.isMod) { + content += QString(TEMPLATE) + .replace("{name}", mod.name.toHtmlEscaped()) + .replace("{url}", ModPlatform::getMetaURL(ModPlatform::ResourceProvider::FLAME, mod.addonId).toHtmlEscaped()) + .replace("{authors}", !mod.authors.isEmpty() ? QString(" (by %1)").arg(mod.authors).toHtmlEscaped() : ""); + } + } + content = "
      " + content + "
    "; + modlist.write(content.toUtf8()); + + auto progressStep = std::make_shared(); + + size_t progress = 0; + for (const QFileInfo& file : files) { + if (buildZipFuture.isCanceled()) { + QFile::remove(output); + progressStep->state = TaskStepState::Failed; + stepProgress(*progressStep); + return BuildZipResult(); + } + progressStep->update(progress, files.length()); + stepProgress(*progressStep); + + const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath()); + if (!resolvedFiles.contains(file.absoluteFilePath()) && + !JlCompress::compressFile(&zip, file.absoluteFilePath(), "overrides/" + relative)) { + QFile::remove(output); + return BuildZipResult(tr("Could not read and compress %1").arg(relative)); + } + progress++; + } + + zip.close(); + + if (zip.getZipError() != 0) { + QFile::remove(output); + progressStep->state = TaskStepState::Failed; + stepProgress(*progressStep); + return BuildZipResult(tr("A zip error occurred")); + } + progressStep->state = TaskStepState::Succeeded; + stepProgress(*progressStep); + return BuildZipResult(); + }); + connect(&buildZipWatcher, &QFutureWatcher::finished, this, &FlamePackExportTask::finish); + buildZipWatcher.setFuture(buildZipFuture); +} + +void FlamePackExportTask::finish() +{ + if (buildZipFuture.isCanceled()) + emitAborted(); + else { + const BuildZipResult result = buildZipFuture.result(); + if (result.has_value()) + emitFailed(result.value()); + else + emitSucceeded(); + } +} + +QByteArray FlamePackExportTask::generateIndex() +{ + QJsonObject obj; + obj["manifestType"] = "minecraftModpack"; + obj["manifestVersion"] = 1; + obj["name"] = name; + obj["version"] = version; + obj["author"] = author; + obj["overrides"] = "overrides"; + if (mcInstance) { + QJsonObject version; + auto profile = mcInstance->getPackProfile(); + // collect all supported components + const ComponentPtr minecraft = profile->getComponent("net.minecraft"); + const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader"); + const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader"); + const ComponentPtr forge = profile->getComponent("net.minecraftforge"); + + // convert all available components to mrpack dependencies + if (minecraft != nullptr) + version["version"] = minecraft->m_version; + QString id; + if (quilt != nullptr) + id = "quilt-" + quilt->getVersion(); + else if (fabric != nullptr) + id = "fabric-" + fabric->getVersion(); + else if (forge != nullptr) + id = "forge-" + forge->getVersion(); + version["modLoaders"] = QJsonArray(); + if (!id.isEmpty()) { + QJsonObject loader; + loader["id"] = id; + loader["primary"] = true; + version["modLoaders"] = QJsonArray({ loader }); + } + obj["minecraft"] = version; + } + + QJsonArray files; + for (auto mod : resolvedFiles) { + QJsonObject file; + file["projectID"] = mod.addonId; + file["fileID"] = mod.version; + file["required"] = mod.enabled; + files << file; + } + obj["files"] = files; + + return QJsonDocument(obj).toJson(QJsonDocument::Compact); +} diff --git a/launcher/modplatform/flame/FlamePackExportTask.h b/launcher/modplatform/flame/FlamePackExportTask.h new file mode 100644 index 000000000..3dee0a7ea --- /dev/null +++ b/launcher/modplatform/flame/FlamePackExportTask.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include "BaseInstance.h" +#include "MMCZip.h" +#include "minecraft/MinecraftInstance.h" +#include "modplatform/flame/FlameAPI.h" +#include "tasks/Task.h" + +class FlamePackExportTask : public Task { + public: + FlamePackExportTask(const QString& name, + const QString& version, + const QString& author, + InstancePtr instance, + const QString& output, + MMCZip::FilterFunction filter); + + protected: + void executeTask() override; + bool abort() override; + + private: + static const QString TEMPLATE; + static const QStringList FILE_EXTENSIONS; + + // inputs + const QString name, version, author; + const InstancePtr instance; + MinecraftInstance* mcInstance; + const QDir gameRoot; + const QString output; + const MMCZip::FilterFunction filter; + + typedef std::optional BuildZipResult; + struct ResolvedFile { + int addonId; + int version; + bool enabled; + bool isMod; + + QString name; + QString slug; + QString authors; + }; + struct HashInfo { + QString name; + QString path; + bool enabled; + bool isMod; + }; + + FlameAPI api; + + QFileInfoList files; + QMap pendingHashes{}; + QMap resolvedFiles{}; + Task::Ptr task; + QFuture buildZipFuture; + QFutureWatcher buildZipWatcher; + + void collectFiles(); + void collectHashes(); + void makeApiRequest(); + void getProjectsInfo(); + void buildZip(); + void finish(); + + QByteArray generateIndex(); +}; diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h index 1ca0fc0e5..b089b722c 100644 --- a/launcher/modplatform/flame/FlamePackIndex.h +++ b/launcher/modplatform/flame/FlamePackIndex.h @@ -4,6 +4,7 @@ #include #include #include +#include "modplatform/ModIndex.h" namespace Flame { @@ -27,8 +28,7 @@ struct ModpackExtra { QString sourceUrl; }; -struct IndexedPack -{ +struct IndexedPack { int addonId; QString name; QString description; @@ -43,9 +43,9 @@ struct IndexedPack ModpackExtra extra; }; -void loadIndexedPack(IndexedPack & m, QJsonObject & obj); +void loadIndexedPack(IndexedPack& m, QJsonObject& obj); void loadIndexedInfo(IndexedPack&, QJsonObject&); -void loadIndexedPackVersions(IndexedPack & m, QJsonArray & arr); -} +void loadIndexedPackVersions(IndexedPack& m, QJsonArray& arr); +} // namespace Flame Q_DECLARE_METATYPE(Flame::IndexedPack) diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp index 22008297f..ee4d07662 100644 --- a/launcher/modplatform/flame/PackManifest.cpp +++ b/launcher/modplatform/flame/PackManifest.cpp @@ -76,13 +76,8 @@ bool Flame::File::parseFromObject(const QJsonObject& obj, bool throw_on_blocked // It is also optional type = File::Type::SingleFile; - if (fileName.endsWith(".zip")) { - // this is probably a resource pack - targetFolder = "resourcepacks"; - } else { - // this is probably a mod, dunno what else could modpacks download - targetFolder = "mods"; - } + targetFolder = "mods"; + // get the hash hash = QString(); auto hashes = Json::ensureArray(obj, "hashes"); diff --git a/launcher/modplatform/helpers/ExportToModList.cpp b/launcher/modplatform/helpers/ExportToModList.cpp new file mode 100644 index 000000000..1f01c4a89 --- /dev/null +++ b/launcher/modplatform/helpers/ExportToModList.cpp @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "ExportToModList.h" +#include +#include +#include + +namespace ExportToModList { +QString toHTML(QList mods, OptionalData extraData) +{ + QStringList lines; + for (auto mod : mods) { + auto meta = mod->metadata(); + auto modName = mod->name().toHtmlEscaped(); + if (extraData & Url) { + auto url = mod->metaurl().toHtmlEscaped(); + if (!url.isEmpty()) + modName = QString("%2").arg(url, modName); + } + auto line = modName; + if (extraData & Version) { + auto ver = mod->version(); + if (ver.isEmpty() && meta != nullptr) + ver = meta->version().toString(); + if (!ver.isEmpty()) + line += QString(" [%1]").arg(ver.toHtmlEscaped()); + } + if (extraData & Authors && !mod->authors().isEmpty()) + line += " by " + mod->authors().join(", ").toHtmlEscaped(); + lines.append(QString("
  • %1
  • ").arg(line)); + } + return QString("
      \n\t%1\n
    ").arg(lines.join("\n\t")); +} + +QString toMarkdown(QList mods, OptionalData extraData) +{ + QStringList lines; + for (auto mod : mods) { + auto meta = mod->metadata(); + auto modName = mod->name(); + if (extraData & Url) { + auto url = mod->metaurl(); + if (!url.isEmpty()) + modName = QString("[%1](%2)").arg(modName, url); + } + auto line = modName; + if (extraData & Version) { + auto ver = mod->version(); + if (ver.isEmpty() && meta != nullptr) + ver = meta->version().toString(); + if (!ver.isEmpty()) + line += QString(" [%1]").arg(ver); + } + if (extraData & Authors && !mod->authors().isEmpty()) + line += " by " + mod->authors().join(", "); + lines << "- " + line; + } + return lines.join("\n"); +} + +QString toPlainTXT(QList mods, OptionalData extraData) +{ + QStringList lines; + for (auto mod : mods) { + auto meta = mod->metadata(); + auto modName = mod->name(); + + auto line = modName; + if (extraData & Url) { + auto url = mod->metaurl(); + if (!url.isEmpty()) + line += QString(" (%1)").arg(url); + } + if (extraData & Version) { + auto ver = mod->version(); + if (ver.isEmpty() && meta != nullptr) + ver = meta->version().toString(); + if (!ver.isEmpty()) + line += QString(" [%1]").arg(ver); + } + if (extraData & Authors && !mod->authors().isEmpty()) + line += " by " + mod->authors().join(", "); + lines << line; + } + return lines.join("\n"); +} + +QString toJSON(QList mods, OptionalData extraData) +{ + QJsonArray lines; + for (auto mod : mods) { + auto meta = mod->metadata(); + auto modName = mod->name(); + QJsonObject line; + line["name"] = modName; + if (extraData & Url) { + auto url = mod->metaurl(); + if (!url.isEmpty()) + line["url"] = url; + } + if (extraData & Version) { + auto ver = mod->version(); + if (ver.isEmpty() && meta != nullptr) + ver = meta->version().toString(); + if (!ver.isEmpty()) + line["version"] = ver; + } + if (extraData & Authors && !mod->authors().isEmpty()) + line["authors"] = QJsonArray::fromStringList(mod->authors()); + lines << line; + } + QJsonDocument doc; + doc.setArray(lines); + return doc.toJson(); +} + +QString toCSV(QList mods, OptionalData extraData) +{ + QStringList lines; + for (auto mod : mods) { + QStringList data; + auto meta = mod->metadata(); + auto modName = mod->name(); + + data << modName; + if (extraData & Url) + data << mod->metaurl(); + if (extraData & Version) { + auto ver = mod->version(); + if (ver.isEmpty() && meta != nullptr) + ver = meta->version().toString(); + data << ver; + } + if (extraData & Authors) { + QString authors; + if (mod->authors().length() == 1) + authors = mod->authors().back(); + else if (mod->authors().length() > 1) + authors = QString("\"%1\"").arg(mod->authors().join(",")); + data << authors; + } + lines << data.join(","); + } + return lines.join("\n"); +} + +QString exportToModList(QList mods, Formats format, OptionalData extraData) +{ + switch (format) { + case HTML: + return toHTML(mods, extraData); + case MARKDOWN: + return toMarkdown(mods, extraData); + case PLAINTXT: + return toPlainTXT(mods, extraData); + case JSON: + return toJSON(mods, extraData); + case CSV: + return toCSV(mods, extraData); + default: { + return QString("unknown format:%1").arg(format); + } + } +} + +QString exportToModList(QList mods, QString lineTemplate) +{ + QStringList lines; + for (auto mod : mods) { + auto meta = mod->metadata(); + auto modName = mod->name(); + auto url = mod->metaurl(); + auto ver = mod->version(); + if (ver.isEmpty() && meta != nullptr) + ver = meta->version().toString(); + auto authors = mod->authors().join(", "); + lines << QString(lineTemplate) + .replace("{name}", modName) + .replace("{url}", url) + .replace("{version}", ver) + .replace("{authors}", authors); + } + return lines.join("\n"); +} +} // namespace ExportToModList \ No newline at end of file diff --git a/launcher/modplatform/helpers/ExportToModList.h b/launcher/modplatform/helpers/ExportToModList.h new file mode 100644 index 000000000..7ea4ba9c2 --- /dev/null +++ b/launcher/modplatform/helpers/ExportToModList.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once +#include +#include +#include "minecraft/mod/Mod.h" + +namespace ExportToModList { + +enum Formats { HTML, MARKDOWN, PLAINTXT, JSON, CSV, CUSTOM }; +enum OptionalData { + Authors = 1 << 0, + Url = 1 << 1, + Version = 1 << 2, +}; +QString exportToModList(QList mods, Formats format, OptionalData extraData); +QString exportToModList(QList mods, QString lineTemplate); +} // namespace ExportToModList diff --git a/launcher/modplatform/helpers/HashUtils.cpp b/launcher/modplatform/helpers/HashUtils.cpp index 81c94e1b5..6ff1d1710 100644 --- a/launcher/modplatform/helpers/HashUtils.cpp +++ b/launcher/modplatform/helpers/HashUtils.cpp @@ -71,6 +71,7 @@ void ModrinthHasher::executeTask() emitFailed("Empty hash!"); } else { emitSucceeded(); + emit resultsReady(m_hash); } } @@ -88,13 +89,13 @@ void FlameHasher::executeTask() emitFailed("Empty hash!"); } else { emitSucceeded(); + emit resultsReady(m_hash); } } - -BlockedModHasher::BlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider) - : Hasher(file_path), provider(provider) { - setObjectName(QString("BlockedModHasher: %1").arg(file_path)); +BlockedModHasher::BlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider) : Hasher(file_path), provider(provider) +{ + setObjectName(QString("BlockedModHasher: %1").arg(file_path)); hash_type = ProviderCaps.hashType(provider).first(); } @@ -120,14 +121,17 @@ void BlockedModHasher::executeTask() emitFailed("Empty hash!"); } else { emitSucceeded(); + emit resultsReady(m_hash); } } -QStringList BlockedModHasher::getHashTypes() { +QStringList BlockedModHasher::getHashTypes() +{ return ProviderCaps.hashType(provider); } -bool BlockedModHasher::useHashType(QString type) { +bool BlockedModHasher::useHashType(QString type) +{ auto types = ProviderCaps.hashType(provider); if (types.contains(type)) { hash_type = type; diff --git a/launcher/modplatform/helpers/HashUtils.h b/launcher/modplatform/helpers/HashUtils.h index 91146a525..73a2435a2 100644 --- a/launcher/modplatform/helpers/HashUtils.h +++ b/launcher/modplatform/helpers/HashUtils.h @@ -8,6 +8,7 @@ namespace Hashing { class Hasher : public Task { + Q_OBJECT public: using Ptr = shared_qobject_ptr; @@ -21,6 +22,9 @@ class Hasher : public Task { QString getResult() const { return m_hash; }; QString getPath() const { return m_path; }; + signals: + void resultsReady(QString hash); + protected: QString m_hash; QString m_path; @@ -48,6 +52,7 @@ class BlockedModHasher : public Hasher { QStringList getHashTypes(); bool useHashType(QString type); + private: ModPlatform::ResourceProvider provider; QString hash_type; diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.cpp b/launcher/modplatform/helpers/NetworkResourceAPI.cpp index a3c592fdc..c278f800d 100644 --- a/launcher/modplatform/helpers/NetworkResourceAPI.cpp +++ b/launcher/modplatform/helpers/NetworkResourceAPI.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-3.0-only #include "NetworkResourceAPI.h" +#include #include "Application.h" #include "net/NetJob.h" @@ -19,12 +20,12 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks& auto search_url = search_url_optional.value(); - auto response = new QByteArray(); + auto response = std::make_shared(); auto netJob = makeShared(QString("%1::Search").arg(debugName()), APPLICATION->network()); netJob->addNetAction(Net::Download::makeByteArray(QUrl(search_url), response)); - QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks]{ + QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { @@ -40,27 +41,21 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks& callbacks.on_succeed(doc); }); - QObject::connect(netJob.get(), &NetJob::failed, [&netJob, callbacks](QString reason){ + QObject::connect(netJob.get(), &NetJob::failed, [&netJob, callbacks](QString reason) { int network_error_code = -1; if (auto* failed_action = netJob->getFailedActions().at(0); failed_action && failed_action->m_reply) network_error_code = failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - callbacks.on_fail(reason, network_error_code); + callbacks.on_fail(reason, network_error_code); }); - QObject::connect(netJob.get(), &NetJob::aborted, [callbacks]{ - callbacks.on_abort(); - }); - QObject::connect(netJob.get(), &NetJob::finished, [response] { - delete response; - }); - + QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); }); return netJob; } Task::Ptr NetworkResourceAPI::getProjectInfo(ProjectInfoArgs&& args, ProjectInfoCallbacks&& callbacks) const { - auto response = new QByteArray(); + auto response = std::make_shared(); auto job = getProject(args.pack.addonId.toString(), response); QObject::connect(job.get(), &NetJob::succeeded, [response, callbacks, args] { @@ -88,7 +83,7 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi auto versions_url = versions_url_optional.value(); auto netJob = makeShared(QString("%1::Versions").arg(args.pack.name), APPLICATION->network()); - auto response = new QByteArray(); + auto response = std::make_shared(); netJob->addNetAction(Net::Download::makeByteArray(versions_url, response)); @@ -105,14 +100,10 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi callbacks.on_succeed(doc, args.pack); }); - QObject::connect(netJob.get(), &NetJob::finished, [response] { - delete response; - }); - return netJob; } -Task::Ptr NetworkResourceAPI::getProject(QString addonId, QByteArray* response) const +Task::Ptr NetworkResourceAPI::getProject(QString addonId, std::shared_ptr response) const { auto project_url_optional = getInfoURL(addonId); if (!project_url_optional.has_value()) @@ -124,9 +115,34 @@ Task::Ptr NetworkResourceAPI::getProject(QString addonId, QByteArray* response) netJob->addNetAction(Net::Download::makeByteArray(QUrl(project_url), response)); - QObject::connect(netJob.get(), &NetJob::finished, [response] { - delete response; + return netJob; +} + +Task::Ptr NetworkResourceAPI::getDependencyVersion(DependencySearchArgs&& args, DependencySearchCallbacks&& callbacks) const +{ + auto versions_url_optional = getDependencyURL(args); + if (!versions_url_optional.has_value()) + return nullptr; + + auto versions_url = versions_url_optional.value(); + + auto netJob = makeShared(QString("%1::Dependency").arg(args.dependency.addonId.toString()), APPLICATION->network()); + auto response = std::make_shared(); + + netJob->addNetAction(Net::Download::makeByteArray(versions_url, response)); + + QObject::connect(netJob.get(), &NetJob::succeeded, [=] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response for getting versions at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + callbacks.on_succeed(doc, args.dependency); }); return netJob; -} +}; diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.h b/launcher/modplatform/helpers/NetworkResourceAPI.h index 94813bec8..b72e82533 100644 --- a/launcher/modplatform/helpers/NetworkResourceAPI.h +++ b/launcher/modplatform/helpers/NetworkResourceAPI.h @@ -4,19 +4,22 @@ #pragma once +#include #include "modplatform/ResourceAPI.h" class NetworkResourceAPI : public ResourceAPI { public: Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const override; - Task::Ptr getProject(QString addonId, QByteArray* response) const override; + Task::Ptr getProject(QString addonId, std::shared_ptr response) const override; Task::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const override; Task::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const override; + Task::Ptr getDependencyVersion(DependencySearchArgs&&, DependencySearchCallbacks&&) const override; protected: [[nodiscard]] virtual auto getSearchURL(SearchArgs const& args) const -> std::optional = 0; [[nodiscard]] virtual auto getInfoURL(QString const& id) const -> std::optional = 0; [[nodiscard]] virtual auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional = 0; + [[nodiscard]] virtual auto getDependencyURL(DependencySearchArgs const& args) const -> std::optional = 0; }; diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp index e8768c5cd..a8a0fc2c2 100644 --- a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp @@ -51,11 +51,11 @@ void PackFetchTask::fetch() QUrl publicPacksUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/modpacks.xml"); qDebug() << "Downloading public version info from" << publicPacksUrl.toString(); - jobPtr->addNetAction(Net::Download::makeByteArray(publicPacksUrl, &publicModpacksXmlFileData)); + jobPtr->addNetAction(Net::Download::makeByteArray(publicPacksUrl, publicModpacksXmlFileData)); QUrl thirdPartyUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/thirdparty.xml"); qDebug() << "Downloading thirdparty version info from" << thirdPartyUrl.toString(); - jobPtr->addNetAction(Net::Download::makeByteArray(thirdPartyUrl, &thirdPartyModpacksXmlFileData)); + jobPtr->addNetAction(Net::Download::makeByteArray(thirdPartyUrl, thirdPartyModpacksXmlFileData)); QObject::connect(jobPtr.get(), &NetJob::succeeded, this, &PackFetchTask::fileDownloadFinished); QObject::connect(jobPtr.get(), &NetJob::failed, this, &PackFetchTask::fileDownloadFailed); @@ -64,22 +64,19 @@ void PackFetchTask::fetch() jobPtr->start(); } -void PackFetchTask::fetchPrivate(const QStringList & toFetch) +void PackFetchTask::fetchPrivate(const QStringList& toFetch) { QString privatePackBaseUrl = BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1.xml"; - for (auto &packCode: toFetch) - { - QByteArray *data = new QByteArray(); - NetJob *job = new NetJob("Fetching private pack", m_network); + for (auto& packCode : toFetch) { + auto data = std::make_shared(); + NetJob* job = new NetJob("Fetching private pack", m_network); job->addNetAction(Net::Download::makeByteArray(privatePackBaseUrl.arg(packCode), data)); - QObject::connect(job, &NetJob::succeeded, this, [this, job, data, packCode] - { + QObject::connect(job, &NetJob::succeeded, this, [this, job, data, packCode] { ModpackList packs; parseAndAddPacks(*data, PackType::Private, packs); - foreach(Modpack currentPack, packs) - { + foreach (Modpack currentPack, packs) { currentPack.packCode = packCode; emit privateFileDownloadFinished(currentPack); } @@ -87,24 +84,20 @@ void PackFetchTask::fetchPrivate(const QStringList & toFetch) job->deleteLater(); data->clear(); - delete data; }); - QObject::connect(job, &NetJob::failed, this, [this, job, packCode, data](QString reason) - { + QObject::connect(job, &NetJob::failed, this, [this, job, packCode, data](QString reason) { emit privateFileDownloadFailed(reason, packCode); job->deleteLater(); data->clear(); - delete data; }); - QObject::connect(job, &NetJob::aborted, this, [this, job, data]{ + QObject::connect(job, &NetJob::aborted, this, [this, job, data] { emit aborted(); job->deleteLater(); data->clear(); - delete data; }); job->start(); @@ -117,27 +110,22 @@ void PackFetchTask::fileDownloadFinished() QStringList failedLists; - if(!parseAndAddPacks(publicModpacksXmlFileData, PackType::Public, publicPacks)) - { + if (!parseAndAddPacks(*publicModpacksXmlFileData, PackType::Public, publicPacks)) { failedLists.append(tr("Public Packs")); } - if(!parseAndAddPacks(thirdPartyModpacksXmlFileData, PackType::ThirdParty, thirdPartyPacks)) - { + if (!parseAndAddPacks(*thirdPartyModpacksXmlFileData, PackType::ThirdParty, thirdPartyPacks)) { failedLists.append(tr("Third Party Packs")); } - if(failedLists.size() > 0) - { + if (failedLists.size() > 0) { emit failed(tr("Failed to download some pack lists: %1").arg(failedLists.join("\n- "))); - } - else - { + } else { emit finished(publicPacks, thirdPartyPacks); } } -bool PackFetchTask::parseAndAddPacks(QByteArray &data, PackType packType, ModpackList &list) +bool PackFetchTask::parseAndAddPacks(QByteArray& data, PackType packType, ModpackList& list) { QDomDocument doc; @@ -145,8 +133,7 @@ bool PackFetchTask::parseAndAddPacks(QByteArray &data, PackType packType, Modpac int errorLine = -1; int errorCol = -1; - if(!doc.setContent(data, false, &errorMsg, &errorLine, &errorCol)) - { + if (!doc.setContent(data, false, &errorMsg, &errorLine, &errorCol)) { auto fullErrMsg = QString("Failed to fetch modpack data: %1 %2:%3!").arg(errorMsg).arg(errorLine).arg(errorCol); qWarning() << fullErrMsg; data.clear(); @@ -154,8 +141,7 @@ bool PackFetchTask::parseAndAddPacks(QByteArray &data, PackType packType, Modpac } QDomNodeList nodes = doc.elementsByTagName("modpack"); - for(int i = 0; i < nodes.length(); i++) - { + for (int i = 0; i < nodes.length(); i++) { QDomElement element = nodes.at(i).toElement(); Modpack modpack; @@ -169,26 +155,20 @@ bool PackFetchTask::parseAndAddPacks(QByteArray &data, PackType packType, Modpac modpack.broken = false; modpack.bugged = false; - //remove empty if the xml is bugged - for(QString curr : modpack.oldVersions) - { - if(curr.isNull() || curr.isEmpty()) - { + // remove empty if the xml is bugged + for (QString curr : modpack.oldVersions) { + if (curr.isNull() || curr.isEmpty()) { modpack.oldVersions.removeAll(curr); modpack.bugged = true; qWarning() << "Removed some empty versions from" << modpack.name; } } - if(modpack.oldVersions.size() < 1) - { - if(!modpack.currentVersion.isNull() && !modpack.currentVersion.isEmpty()) - { + if (modpack.oldVersions.size() < 1) { + if (!modpack.currentVersion.isNull() && !modpack.currentVersion.isEmpty()) { modpack.oldVersions.append(modpack.currentVersion); qWarning() << "Added current version to oldVersions because oldVersions was empty! (" + modpack.name + ")"; - } - else - { + } else { modpack.broken = true; qWarning() << "Broken pack:" << modpack.name << " => No valid version!"; } @@ -218,4 +198,4 @@ void PackFetchTask::fileDownloadAborted() emit aborted(); } -} +} // namespace LegacyFTB diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.h b/launcher/modplatform/legacy_ftb/PackFetchTask.h index 8f3c4f3ba..f2116ce99 100644 --- a/launcher/modplatform/legacy_ftb/PackFetchTask.h +++ b/launcher/modplatform/legacy_ftb/PackFetchTask.h @@ -1,41 +1,41 @@ #pragma once -#include "net/NetJob.h" -#include #include #include +#include +#include #include "PackHelpers.h" +#include "net/NetJob.h" namespace LegacyFTB { class PackFetchTask : public QObject { - Q_OBJECT -public: - PackFetchTask(shared_qobject_ptr network) : QObject(nullptr), m_network(network) {}; + public: + PackFetchTask(shared_qobject_ptr network) : QObject(nullptr), m_network(network){}; virtual ~PackFetchTask() = default; void fetch(); - void fetchPrivate(const QStringList &toFetch); + void fetchPrivate(const QStringList& toFetch); -private: + private: shared_qobject_ptr m_network; NetJob::Ptr jobPtr; - QByteArray publicModpacksXmlFileData; - QByteArray thirdPartyModpacksXmlFileData; + std::shared_ptr publicModpacksXmlFileData = std::make_shared(); + std::shared_ptr thirdPartyModpacksXmlFileData = std::make_shared(); - bool parseAndAddPacks(QByteArray &data, PackType packType, ModpackList &list); + bool parseAndAddPacks(QByteArray& data, PackType packType, ModpackList& list); ModpackList publicPacks; ModpackList thirdPartyPacks; -protected slots: + protected slots: void fileDownloadFinished(); void fileDownloadFailed(QString reason); void fileDownloadAborted(); -signals: + signals: void finished(ModpackList publicPacks, ModpackList thirdPartyPacks); void failed(QString reason); void aborted(); @@ -44,4 +44,4 @@ signals: void privateFileDownloadFailed(QString reason, QString packCode); }; -} +} // namespace LegacyFTB diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp index 36c142acb..a4c78397b 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -37,16 +37,16 @@ #include -#include "MMCZip.h" #include "BaseInstance.h" #include "FileSystem.h" -#include "settings/INISettingsObject.h" +#include "MMCZip.h" +#include "minecraft/GradleSpecifier.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" -#include "minecraft/GradleSpecifier.h" +#include "settings/INISettingsObject.h" -#include "BuildConfig.h" #include "Application.h" +#include "BuildConfig.h" namespace LegacyFTB { @@ -65,6 +65,7 @@ void PackInstallTask::executeTask() void PackInstallTask::downloadPack() { setStatus(tr("Downloading zip for %1").arg(m_pack.name)); + setProgress(1, 4); setAbortable(false); archivePath = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file); @@ -78,11 +79,10 @@ void PackInstallTask::downloadPack() } netJobContainer->addNetAction(Net::Download::makeFile(url, archivePath)); - connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); - connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed); - connect(netJobContainer.get(), &NetJob::progress, this, &PackInstallTask::onDownloadProgress); + connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::unzip); + connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::emitFailed); connect(netJobContainer.get(), &NetJob::stepProgress, this, &PackInstallTask::propogateStepProgress); - connect(netJobContainer.get(), &NetJob::aborted, this, &PackInstallTask::onDownloadAborted); + connect(netJobContainer.get(), &NetJob::aborted, this, &PackInstallTask::emitAborted); netJobContainer->start(); @@ -90,27 +90,6 @@ void PackInstallTask::downloadPack() progress(1, 4); } -void PackInstallTask::onDownloadSucceeded() -{ - unzip(); -} - -void PackInstallTask::onDownloadFailed(QString reason) -{ - emitFailed(reason); -} - -void PackInstallTask::onDownloadProgress(qint64 current, qint64 total) -{ - progress(current, total * 4); - setStatus(tr("Downloading zip for %1 (%2%)").arg(m_pack.name).arg(current / 10)); -} - -void PackInstallTask::onDownloadAborted() -{ - emitAborted(); -} - void PackInstallTask::unzip() { setStatus(tr("Extracting modpack")); @@ -120,16 +99,17 @@ void PackInstallTask::unzip() QDir extractDir(m_stagingPath); m_packZip.reset(new QuaZip(archivePath)); - if(!m_packZip->open(QuaZip::mdUnzip)) - { + if (!m_packZip->open(QuaZip::mdUnzip)) { emitFailed(tr("Failed to open modpack file %1!").arg(archivePath)); return; } #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload::of(MMCZip::extractDir), archivePath, extractDir.absolutePath() + "/unzip"); + m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload::of(MMCZip::extractDir), archivePath, + extractDir.absolutePath() + "/unzip"); #else - m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/unzip"); + m_extractFuture = + QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/unzip"); #endif connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, &PackInstallTask::onUnzipFinished); connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, &PackInstallTask::onUnzipCanceled); @@ -151,11 +131,9 @@ void PackInstallTask::install() setStatus(tr("Installing modpack")); progress(3, 4); QDir unzipMcDir(m_stagingPath + "/unzip/minecraft"); - if(unzipMcDir.exists()) - { - //ok, found minecraft dir, move contents to instance dir - if(!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/.minecraft")) - { + if (unzipMcDir.exists()) { + // ok, found minecraft dir, move contents to instance dir + if (!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/.minecraft")) { emitFailed(tr("Failed to move unzipped Minecraft!")); return; } @@ -172,23 +150,20 @@ void PackInstallTask::install() bool fallback = true; - //handle different versions + // handle different versions QFile packJson(m_stagingPath + "/.minecraft/pack.json"); QDir jarmodDir = QDir(m_stagingPath + "/unzip/instMods"); - if(packJson.exists()) - { + if (packJson.exists()) { packJson.open(QIODevice::ReadOnly | QIODevice::Text); QJsonDocument doc = QJsonDocument::fromJson(packJson.readAll()); packJson.close(); - //we only care about the libs + // we only care about the libs QJsonArray libs = doc.object().value("libraries").toArray(); - foreach (const QJsonValue &value, libs) - { + foreach (const QJsonValue& value, libs) { QString nameValue = value.toObject().value("name").toString(); - if(!nameValue.startsWith("net.minecraftforge")) - { + if (!nameValue.startsWith("net.minecraftforge")) { continue; } @@ -199,16 +174,13 @@ void PackInstallTask::install() fallback = false; break; } - } - if(jarmodDir.exists()) - { + if (jarmodDir.exists()) { qDebug() << "Found jarmods, installing..."; QStringList jarmods; - for (auto info: jarmodDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) - { + for (auto info : jarmodDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) { qDebug() << "Jarmod:" << info.fileName(); jarmods.push_back(info.absoluteFilePath()); } @@ -217,12 +189,11 @@ void PackInstallTask::install() fallback = false; } - //just nuke unzip directory, it s not needed anymore + // just nuke unzip directory, it s not needed anymore FS::deletePath(m_stagingPath + "/unzip"); - if(fallback) - { - //TODO: Some fallback mechanism... or just keep failing! + if (fallback) { + // TODO: Some fallback mechanism... or just keep failing! emitFailed(tr("No installation method found!")); return; } @@ -232,8 +203,7 @@ void PackInstallTask::install() progress(4, 4); instance.setName(name()); - if(m_instIcon == "default") - { + if (m_instIcon == "default") { m_instIcon = "ftb_logo"; } instance.setIconKey(m_instIcon); @@ -252,4 +222,4 @@ bool PackInstallTask::abort() return InstanceTask::abort(); } -} +} // namespace LegacyFTB diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.h b/launcher/modplatform/legacy_ftb/PackInstallTask.h index da791e065..30ff48597 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.h +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.h @@ -1,12 +1,12 @@ #pragma once -#include "InstanceTask.h" -#include "net/NetJob.h" #include #include +#include "InstanceTask.h" +#include "PackHelpers.h" #include "meta/Index.h" #include "meta/Version.h" #include "meta/VersionList.h" -#include "PackHelpers.h" +#include "net/NetJob.h" #include "net/NetJob.h" @@ -14,36 +14,31 @@ namespace LegacyFTB { -class PackInstallTask : public InstanceTask -{ +class PackInstallTask : public InstanceTask { Q_OBJECT -public: + public: explicit PackInstallTask(shared_qobject_ptr network, Modpack pack, QString version); - virtual ~PackInstallTask(){} + virtual ~PackInstallTask() {} bool canAbort() const override { return true; } bool abort() override; -protected: + protected: //! Entry point for tasks. virtual void executeTask() override; -private: + private: void downloadPack(); void unzip(); void install(); -private slots: - void onDownloadSucceeded(); - void onDownloadFailed(QString reason); - void onDownloadProgress(qint64 current, qint64 total); - void onDownloadAborted(); + private slots: void onUnzipFinished(); void onUnzipCanceled(); -private: /* data */ + private: /* data */ shared_qobject_ptr m_network; bool abortable = false; std::unique_ptr m_packZip; @@ -56,4 +51,4 @@ private: /* data */ QString m_version; }; -} +} // namespace LegacyFTB diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp index 29e3d129d..364cf3f30 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.cpp +++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp @@ -9,19 +9,17 @@ #include "net/NetJob.h" #include "net/Upload.h" -Task::Ptr ModrinthAPI::currentVersion(QString hash, QString hash_format, QByteArray* response) +Task::Ptr ModrinthAPI::currentVersion(QString hash, QString hash_format, std::shared_ptr response) { auto netJob = makeShared(QString("Modrinth::GetCurrentVersion"), APPLICATION->network()); netJob->addNetAction(Net::Download::makeByteArray( QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1?algorithm=%2").arg(hash, hash_format), response)); - QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; }); - return netJob; } -Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, QByteArray* response) +Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, std::shared_ptr response) { auto netJob = makeShared(QString("Modrinth::GetCurrentVersions"), APPLICATION->network()); @@ -35,8 +33,6 @@ Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_f netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files"), response, body_raw)); - QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; }); - return netJob; } @@ -44,7 +40,7 @@ Task::Ptr ModrinthAPI::latestVersion(QString hash, QString hash_format, std::optional> mcVersions, std::optional loaders, - QByteArray* response) + std::shared_ptr response) { auto netJob = makeShared(QString("Modrinth::GetLatestVersion"), APPLICATION->network()); @@ -67,8 +63,6 @@ Task::Ptr ModrinthAPI::latestVersion(QString hash, netJob->addNetAction(Net::Upload::makeByteArray( QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1/update?algorithm=%2").arg(hash, hash_format), response, body_raw)); - QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; }); - return netJob; } @@ -76,7 +70,7 @@ Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes, QString hash_format, std::optional> mcVersions, std::optional loaders, - QByteArray* response) + std::shared_ptr response) { auto netJob = makeShared(QString("Modrinth::GetLatestVersions"), APPLICATION->network()); @@ -101,22 +95,16 @@ Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes, netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files/update"), response, body_raw)); - QObject::connect(netJob.get(), &NetJob::finished, [response] { delete response; }); - return netJob; } -Task::Ptr ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const +Task::Ptr ModrinthAPI::getProjects(QStringList addonIds, std::shared_ptr response) const { auto netJob = makeShared(QString("Modrinth::GetProjects"), APPLICATION->network()); auto searchUrl = getMultipleModInfoURL(addonIds); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); - QObject::connect(netJob.get(), &NetJob::finished, [response, netJob] { - delete response; - }); - return netJob; } diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index b91ac5c14..58af14cc7 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -12,27 +12,23 @@ class ModrinthAPI : public NetworkResourceAPI { public: - auto currentVersion(QString hash, - QString hash_format, - QByteArray* response) -> Task::Ptr; + auto currentVersion(QString hash, QString hash_format, std::shared_ptr response) -> Task::Ptr; - auto currentVersions(const QStringList& hashes, - QString hash_format, - QByteArray* response) -> Task::Ptr; + auto currentVersions(const QStringList& hashes, QString hash_format, std::shared_ptr response) -> Task::Ptr; auto latestVersion(QString hash, QString hash_format, std::optional> mcVersions, std::optional loaders, - QByteArray* response) -> Task::Ptr; + std::shared_ptr response) -> Task::Ptr; auto latestVersions(const QStringList& hashes, QString hash_format, - std::optional> mcVersions, - std::optional loaders, - QByteArray* response) -> Task::Ptr; + std::optional> mcVersions, + std::optional loaders, + std::shared_ptr response) -> Task::Ptr; - Task::Ptr getProjects(QStringList addonIds, QByteArray* response) const override; + Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const override; public: [[nodiscard]] auto getSortingMethods() const -> QList override; @@ -42,7 +38,7 @@ class ModrinthAPI : public NetworkResourceAPI { static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList { QStringList l; - for (auto loader : {Forge, Fabric, Quilt}) { + for (auto loader : { Forge, Fabric, Quilt, LiteLoader }) { if (types & loader) { l << getModLoaderString(loader); } @@ -55,8 +51,7 @@ class ModrinthAPI : public NetworkResourceAPI { static auto getModLoaderFilters(ModLoaderTypes types) -> const QString { QStringList l; - for (auto loader : getModLoaderStrings(types)) - { + for (auto loader : getModLoaderStrings(types)) { l << QString("\"categories:%1\"").arg(loader); } return l.join(','); @@ -97,7 +92,7 @@ class ModrinthAPI : public NetworkResourceAPI { { if (args.loaders.has_value()) { if (!validateModLoaders(args.loaders.value())) { - qWarning() << "Modrinth only have Forge and Fabric-compatible mods!"; + qWarning() << "Modrinth - or our interface - does not support any the provided mod loaders!"; return {}; } } @@ -139,16 +134,22 @@ class ModrinthAPI : public NetworkResourceAPI { auto getGameVersionsArray(std::list mcVersions) const -> QString { QString s; - for(auto& ver : mcVersions){ + for (auto& ver : mcVersions) { s += QString("\"versions:%1\",").arg(ver.toString()); } - s.remove(s.length() - 1, 1); //remove last comma + s.remove(s.length() - 1, 1); // remove last comma return s.isEmpty() ? QString() : s; } - inline auto validateModLoaders(ModLoaderTypes loaders) const -> bool - { - return loaders & (Forge | Fabric | Quilt); - } + static inline auto validateModLoaders(ModLoaderTypes loaders) -> bool { return loaders & (Forge | Fabric | Quilt | LiteLoader); } + [[nodiscard]] std::optional getDependencyURL(DependencySearchArgs const& args) const override + { + return args.dependency.version.length() != 0 ? QString("%1/version/%2").arg(BuildConfig.MODRINTH_PROD_URL, args.dependency.version) + : QString("%1/project/%2/version?game_versions=[\"%3\"]&loaders=[\"%4\"]") + .arg(BuildConfig.MODRINTH_PROD_URL) + .arg(args.dependency.addonId.toString()) + .arg(args.mcVersion.toString()) + .arg(getModLoaderStrings(args.loader).join("\",\"")); + }; }; diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index 4fe91ce78..a7c22832a 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -53,12 +53,11 @@ void ModrinthCheckUpdate::executeTask() // (though it will rarely happen, if at all) if (mod->metadata()->hash_format != best_hash_type) { auto hash_task = Hashing::createModrinthHasher(mod->fileinfo().absoluteFilePath()); - connect(hash_task.get(), &Task::succeeded, [&] { - QString hash(hash_task->getResult()); + connect(hash_task.get(), &Hashing::Hasher::resultsReady, [&hashes, &mappings, mod](QString hash) { hashes.append(hash); mappings.insert(hash, mod); }); - connect(hash_task.get(), &Task::failed, [this, hash_task] { failed("Failed to generate hash"); }); + connect(hash_task.get(), &Task::failed, [this] { failed("Failed to generate hash"); }); hashing_task.addTask(hash_task); } else { hashes.append(hash); @@ -71,7 +70,7 @@ void ModrinthCheckUpdate::executeTask() hashing_task.start(); loop.exec(); - auto* response = new QByteArray(); + auto response = std::make_shared(); auto job = api.latestVersions(hashes, best_hash_type, m_game_versions, m_loaders, response); QEventLoop lock; diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index bb8227aa2..76f072773 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -214,7 +214,7 @@ bool ModrinthCreationTask::createInstance() if (m_instIcon != "default") { instance.setIconKey(m_instIcon); - } else { + } else if (!m_managed_id.isEmpty()) { instance.setIconKey("modrinth"); } diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp index 29df90ddf..30fe566da 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp @@ -27,7 +27,7 @@ #include "minecraft/PackProfile.h" #include "minecraft/mod/ModFolderModel.h" -const QStringList ModrinthPackExportTask::PREFIXES({ "mods", "coremods", "resourcepacks", "texturepacks", "shaderpacks" }); +const QStringList ModrinthPackExportTask::PREFIXES({ "mods/", "coremods/", "resourcepacks/", "texturepacks/", "shaderpacks/" }); const QStringList ModrinthPackExportTask::FILE_EXTENSIONS({ "jar", "litemod", "zip" }); ModrinthPackExportTask::ModrinthPackExportTask(const QString& name, @@ -64,7 +64,8 @@ bool ModrinthPackExportTask::abort() if (buildZipFuture.isRunning()) { buildZipFuture.cancel(); - // NOTE: Here we don't do `emitAborted()` because it will be done when `buildZipFuture` actually cancels, which may not occur immediately. + // NOTE: Here we don't do `emitAborted()` because it will be done when `buildZipFuture` actually cancels, which may not occur + // immediately. return true; } @@ -94,19 +95,18 @@ void ModrinthPackExportTask::collectFiles() void ModrinthPackExportTask::collectHashes() { + setStatus(tr("Finding file hashes...")); for (const QFileInfo& file : files) { QCoreApplication::processEvents(); const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath()); // require sensible file types - if (!std::any_of(PREFIXES.begin(), PREFIXES.end(), - [&relative](const QString& prefix) { return relative.startsWith(prefix + QDir::separator()); })) + if (!std::any_of(PREFIXES.begin(), PREFIXES.end(), [&relative](const QString& prefix) { return relative.startsWith(prefix); })) continue; if (!std::any_of(FILE_EXTENSIONS.begin(), FILE_EXTENSIONS.end(), [&relative](const QString& extension) { return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled"); - })) { + })) continue; - } QCryptographicHash sha512(QCryptographicHash::Algorithm::Sha512); @@ -136,8 +136,8 @@ void ModrinthPackExportTask::collectHashes() QCryptographicHash sha1(QCryptographicHash::Algorithm::Sha1); sha1.addData(data); - ResolvedFile file{ sha1.result().toHex(), sha512.result().toHex(), url.toString(), openFile.size() }; - resolvedFiles[relative] = file; + ResolvedFile resolvedFile{ sha1.result().toHex(), sha512.result().toHex(), url.toEncoded(), openFile.size() }; + resolvedFiles[relative] = resolvedFile; // nice! we've managed to resolve based on local metadata! // no need to enqueue it @@ -159,7 +159,8 @@ void ModrinthPackExportTask::makeApiRequest() if (pendingHashes.isEmpty()) buildZip(); else { - QByteArray* response = new QByteArray; + setStatus(tr("Finding versions for hashes...")); + auto response = std::make_shared(); task = api.currentVersions(pendingHashes.values(), "sha512", response); connect(task.get(), &NetJob::succeeded, [this, response]() { parseApiResponse(response); }); connect(task.get(), &NetJob::failed, this, &ModrinthPackExportTask::emitFailed); @@ -167,7 +168,7 @@ void ModrinthPackExportTask::makeApiRequest() } } -void ModrinthPackExportTask::parseApiResponse(const QByteArray* response) +void ModrinthPackExportTask::parseApiResponse(const std::shared_ptr response) { task = nullptr; @@ -265,13 +266,13 @@ void ModrinthPackExportTask::finish() QByteArray ModrinthPackExportTask::generateIndex() { - QJsonObject obj; - obj["formatVersion"] = 1; - obj["game"] = "minecraft"; - obj["name"] = name; - obj["versionId"] = version; + QJsonObject out; + out["formatVersion"] = 1; + out["game"] = "minecraft"; + out["name"] = name; + out["versionId"] = version; if (!summary.isEmpty()) - obj["summary"] = summary; + out["summary"] = summary; if (mcInstance) { auto profile = mcInstance->getPackProfile(); @@ -292,32 +293,40 @@ QByteArray ModrinthPackExportTask::generateIndex() if (forge != nullptr) dependencies["forge"] = forge->m_version; - obj["dependencies"] = dependencies; + out["dependencies"] = dependencies; } - QJsonArray files; - QMapIterator iterator(resolvedFiles); - while (iterator.hasNext()) { - iterator.next(); + QJsonArray filesOut; + for (auto iterator = resolvedFiles.constBegin(); iterator != resolvedFiles.constEnd(); iterator++) { + QJsonObject fileOut; + QString path = iterator.key(); const ResolvedFile& value = iterator.value(); - QJsonObject file; - QString path = iterator.key(); - path.replace(QDir::separator(), "/"); - file["path"] = path; - file["downloads"] = QJsonArray({ iterator.value().url }); + // detect disabled mod + const QFileInfo pathInfo(path); + if (pathInfo.suffix() == "disabled") { + // rename it + path = pathInfo.dir().filePath(pathInfo.completeBaseName()); + // ...and make it optional + QJsonObject env; + env["client"] = "optional"; + env["server"] = "optional"; + fileOut["env"] = env; + } + + fileOut["path"] = path; + fileOut["downloads"] = QJsonArray{ iterator.value().url }; QJsonObject hashes; hashes["sha1"] = value.sha1; hashes["sha512"] = value.sha512; + fileOut["hashes"] = hashes; - file["hashes"] = hashes; - file["fileSize"] = value.size; - - files << file; + fileOut["fileSize"] = value.size; + filesOut << fileOut; } - obj["files"] = files; + out["files"] = filesOut; - return QJsonDocument(obj).toJson(QJsonDocument::Compact); + return QJsonDocument(out).toJson(QJsonDocument::Compact); } diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.h b/launcher/modplatform/modrinth/ModrinthPackExportTask.h index af00ffaab..96f292c1b 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.h +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.h @@ -69,7 +69,7 @@ class ModrinthPackExportTask : public Task { void collectFiles(); void collectHashes(); void makeApiRequest(); - void parseApiResponse(const QByteArray* response); + void parseApiResponse(const std::shared_ptr response); void buildZip(); void finish(); diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 7ade131e4..b40373496 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -22,7 +22,7 @@ #include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" -#include "net/NetJob.h" +#include "modplatform/ModIndex.h" static ModrinthAPI api; static ModPlatform::ProviderCapabilities ProviderCaps; @@ -140,6 +140,28 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t file.version_number = Json::requireString(obj, "version_number"); file.changelog = Json::requireString(obj, "changelog"); + auto dependencies = Json::ensureArray(obj, "dependencies"); + for (auto d : dependencies) { + auto dep = Json::ensureObject(d); + ModPlatform::Dependency dependency; + dependency.addonId = Json::ensureString(dep, "project_id"); + dependency.version = Json::ensureString(dep, "version_id"); + auto depType = Json::requireString(dep, "dependency_type"); + + if (depType == "required") + dependency.type = ModPlatform::DependencyType::REQUIRED; + else if (depType == "optional") + dependency.type = ModPlatform::DependencyType::OPTIONAL; + else if (depType == "incompatible") + dependency.type = ModPlatform::DependencyType::INCOMPATIBLE; + else if (depType == "embedded") + dependency.type = ModPlatform::DependencyType::EMBEDDED; + else + dependency.type = ModPlatform::DependencyType::UNKNOWN; + + file.dependencies.append(dependency); + } + auto files = Json::requireArray(obj, "files"); int i = 0; @@ -195,3 +217,22 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t return {}; } + +auto Modrinth::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion +{ + QVector versions; + + for (auto versionIter : arr) { + auto obj = versionIter.toObject(); + auto file = loadIndexedPackVersion(obj); + + if (file.fileId.isValid()) // Heuristic to check if the returned value is valid + versions.append(file); + } + auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { + // dates are in RFC 3339 format + return a.date > b.date; + }; + std::sort(versions.begin(), versions.end(), orderSortPredicate); + return versions.length() != 0 ? versions.front() : ModPlatform::IndexedVersion(); +} \ No newline at end of file diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h index e73e4b186..a8d986c57 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.h +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -19,8 +19,8 @@ #include "modplatform/ModIndex.h" -#include "BaseInstance.h" #include +#include "BaseInstance.h" namespace Modrinth { @@ -31,5 +31,6 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, const shared_qobject_ptr& network, const BaseInstance* inst); auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion; +auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion; } // namespace Modrinth diff --git a/launcher/modplatform/technic/SolderPackInstallTask.cpp b/launcher/modplatform/technic/SolderPackInstallTask.cpp index c26d6a5a6..6a05d17ae 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.cpp +++ b/launcher/modplatform/technic/SolderPackInstallTask.cpp @@ -37,20 +37,19 @@ #include #include -#include #include +#include -#include "TechnicPackProcessor.h" #include "SolderPackManifest.h" +#include "TechnicPackProcessor.h" #include "net/ChecksumValidator.h" -Technic::SolderPackInstallTask::SolderPackInstallTask( - shared_qobject_ptr network, - const QUrl &solderUrl, - const QString &pack, - const QString &version, - const QString &minecraftVersion -) { +Technic::SolderPackInstallTask::SolderPackInstallTask(shared_qobject_ptr network, + const QUrl& solderUrl, + const QString& pack, + const QString& version, + const QString& minecraftVersion) +{ m_solderUrl = solderUrl; m_pack = pack; m_version = version; @@ -58,9 +57,9 @@ Technic::SolderPackInstallTask::SolderPackInstallTask( m_minecraftVersion = minecraftVersion; } -bool Technic::SolderPackInstallTask::abort() { - if(m_abortable) - { +bool Technic::SolderPackInstallTask::abort() +{ + if (m_abortable) { return m_filesNetJob->abort(); } return false; @@ -72,7 +71,7 @@ void Technic::SolderPackInstallTask::executeTask() m_filesNetJob.reset(new NetJob(tr("Resolving modpack files"), m_network)); auto sourceUrl = QString("%1/modpack/%2/%3").arg(m_solderUrl.toString(), m_pack, m_version); - m_filesNetJob->addNetAction(Net::Download::makeByteArray(sourceUrl, &m_response)); + m_filesNetJob->addNetAction(Net::Download::makeByteArray(sourceUrl, m_response)); auto job = m_filesNetJob.get(); connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded); @@ -85,11 +84,11 @@ void Technic::SolderPackInstallTask::fileListSucceeded() { setStatus(tr("Downloading modpack")); - QJsonParseError parse_error {}; - QJsonDocument doc = QJsonDocument::fromJson(m_response, &parse_error); + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*m_response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << m_response; + qWarning() << *m_response; return; } auto obj = doc.object(); @@ -110,7 +109,7 @@ void Technic::SolderPackInstallTask::fileListSucceeded() m_filesNetJob.reset(new NetJob(tr("Downloading modpack"), m_network)); int i = 0; - for (const auto &mod : build.mods) { + for (const auto& mod : build.mods) { auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i)); auto dl = Net::Download::makeFile(mod.url, path); diff --git a/launcher/modplatform/technic/SolderPackInstallTask.h b/launcher/modplatform/technic/SolderPackInstallTask.h index aa14ce88b..f2c6a83a4 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.h +++ b/launcher/modplatform/technic/SolderPackInstallTask.h @@ -40,45 +40,48 @@ #include #include +#include -namespace Technic -{ - class SolderPackInstallTask : public InstanceTask - { - Q_OBJECT - public: - explicit SolderPackInstallTask(shared_qobject_ptr network, const QUrl &solderUrl, const QString& pack, const QString& version, const QString &minecraftVersion); +namespace Technic { +class SolderPackInstallTask : public InstanceTask { + Q_OBJECT + public: + explicit SolderPackInstallTask(shared_qobject_ptr network, + const QUrl& solderUrl, + const QString& pack, + const QString& version, + const QString& minecraftVersion); - bool canAbort() const override { return true; } - bool abort() override; + bool canAbort() const override { return true; } + bool abort() override; - protected: - //! Entry point for tasks. - virtual void executeTask() override; + protected: + //! Entry point for tasks. + virtual void executeTask() override; - private slots: - void fileListSucceeded(); - void downloadSucceeded(); - void downloadFailed(QString reason); - void downloadProgressChanged(qint64 current, qint64 total); - void downloadAborted(); - void extractFinished(); - void extractAborted(); + private slots: + void fileListSucceeded(); + void downloadSucceeded(); + void downloadFailed(QString reason); + void downloadProgressChanged(qint64 current, qint64 total); + void downloadAborted(); + void extractFinished(); + void extractAborted(); - private: - bool m_abortable = false; + private: + bool m_abortable = false; - shared_qobject_ptr m_network; + shared_qobject_ptr m_network; - NetJob::Ptr m_filesNetJob; - QUrl m_solderUrl; - QString m_pack; - QString m_version; - QString m_minecraftVersion; - QByteArray m_response; - QTemporaryDir m_outputDir; - int m_modCount; - QFuture m_extractFuture; - QFutureWatcher m_extractFutureWatcher; - }; -} + NetJob::Ptr m_filesNetJob; + QUrl m_solderUrl; + QString m_pack; + QString m_version; + QString m_minecraftVersion; + std::shared_ptr m_response = std::make_shared(); + QTemporaryDir m_outputDir; + int m_modCount; + QFuture m_extractFuture; + QFutureWatcher m_extractFutureWatcher; +}; +} // namespace Technic diff --git a/launcher/mojang/PackageManifest.cpp b/launcher/mojang/PackageManifest.cpp deleted file mode 100644 index b3dfd7fc1..000000000 --- a/launcher/mojang/PackageManifest.cpp +++ /dev/null @@ -1,427 +0,0 @@ -#include "PackageManifest.h" -#include -#include -#include -#include -#include - -#ifndef Q_OS_WIN32 -#include -#include -#include -#endif - -namespace mojang_files { - -const Hash hash_of_empty_string = "da39a3ee5e6b4b0d3255bfef95601890afd80709"; - -int Path::compare(const Path& rhs) const -{ - auto left_cursor = begin(); - auto left_end = end(); - auto right_cursor = rhs.begin(); - auto right_end = rhs.end(); - - while (left_cursor != left_end && right_cursor != right_end) - { - if(*left_cursor < *right_cursor) - { - return -1; - } - else if(*left_cursor > *right_cursor) - { - return 1; - } - left_cursor++; - right_cursor++; - } - - if(left_cursor == left_end) - { - if(right_cursor == right_end) - { - return 0; - } - return -1; - } - return 1; -} - -void Package::addFile(const Path& path, const File& file) { - addFolder(path.parent_path()); - files[path] = file; -} - -void Package::addFolder(Path folder) { - if(!folder.has_parent_path()) { - return; - } - do { - folders.insert(folder); - folder = folder.parent_path(); - } while(folder.has_parent_path()); -} - -void Package::addLink(const Path& path, const Path& target) { - addFolder(path.parent_path()); - symlinks[path] = target; -} - -void Package::addSource(const FileSource& source) { - sources[source.hash] = source; -} - - -namespace { -void fromJson(QJsonDocument & doc, Package & out) { - std::set seen_paths; - if (!doc.isObject()) - { - throw JSONValidationError("file manifest is not an object"); - } - QJsonObject root = doc.object(); - - auto filesObj = Json::ensureObject(root, "files"); - auto iter = filesObj.begin(); - while (iter != filesObj.end()) - { - Path objectPath = Path(iter.key()); - auto value = iter.value(); - iter++; - if(seen_paths.count(objectPath)) { - throw JSONValidationError("duplicate path inside manifest, the manifest is invalid"); - } - if (!value.isObject()) - { - throw JSONValidationError("file entry inside manifest is not an an object"); - } - seen_paths.insert(objectPath); - - auto fileObject = value.toObject(); - auto type = Json::requireString(fileObject, "type"); - if(type == "directory") { - out.addFolder(objectPath); - continue; - } - else if(type == "file") { - FileSource bestSource; - File file; - file.executable = Json::ensureBoolean(fileObject, QString("executable"), false); - auto downloads = Json::requireObject(fileObject, "downloads"); - for(auto iter2 = downloads.begin(); iter2 != downloads.end(); iter2++) { - FileSource source; - - auto downloadObject = Json::requireObject(iter2.value()); - source.hash = Json::requireString(downloadObject, "sha1"); - source.size = Json::requireInteger(downloadObject, "size"); - source.url = Json::requireString(downloadObject, "url"); - - auto compression = iter2.key(); - if(compression == "raw") { - file.hash = source.hash; - file.size = source.size; - source.compression = Compression::Raw; - } - else if (compression == "lzma") { - source.compression = Compression::Lzma; - } - else { - continue; - } - bestSource.upgrade(source); - } - if(bestSource.isBad()) { - throw JSONValidationError("No valid compression method for file " + iter.key()); - } - out.addFile(objectPath, file); - out.addSource(bestSource); - } - else if(type == "link") { - auto target = Json::requireString(fileObject, "target"); - out.symlinks[objectPath] = target; - out.addLink(objectPath, target); - } - else { - throw JSONValidationError("Invalid item type in manifest: " + type); - } - } - // make sure the containing folder exists - out.folders.insert(Path()); -} -} - -Package Package::fromManifestContents(const QByteArray& contents) -{ - Package out; - try - { - auto doc = Json::requireDocument(contents, "Manifest"); - fromJson(doc, out); - return out; - } - catch (const Exception &e) - { - qDebug() << QString("Unable to parse manifest: %1").arg(e.cause()); - out.valid = false; - return out; - } -} - -Package Package::fromManifestFile(const QString & filename) { - Package out; - try - { - auto doc = Json::requireDocument(filename, filename); - fromJson(doc, out); - return out; - } - catch (const Exception &e) - { - qDebug() << QString("Unable to parse manifest file %1: %2").arg(filename, e.cause()); - out.valid = false; - return out; - } -} - -#ifndef Q_OS_WIN32 - -#include -#include -#include - -namespace { -// FIXME: Qt obscures symlink targets by making them absolute. that is useless. this is the workaround - we do it ourselves -bool actually_read_symlink_target(const QString & filepath, Path & out) -{ - struct ::stat st; - // FIXME: here, we assume the native filesystem encoding. May the Gods have mercy upon our Souls. - QByteArray nativePath = filepath.toUtf8(); - const char * filepath_cstr = nativePath.data(); - - if (lstat(filepath_cstr, &st) != 0) - { - return false; - } - - auto size = st.st_size ? st.st_size + 1 : PATH_MAX; - std::string temp(size, '\0'); - // because we don't realiably know how long the damn thing actually is, we loop and expand. POSIX is naff - do - { - auto link_length = ::readlink(filepath_cstr, &temp[0], temp.size()); - if(link_length == -1) - { - return false; - } - if(std::string::size_type(link_length) < temp.size()) - { - // buffer was long enough and we managed to read the link target. RETURN here. - temp.resize(link_length); - out = Path(QString::fromUtf8(temp.c_str())); - return true; - } - temp.resize(temp.size() * 2); - } while (true); -} -} -#endif - -// FIXME: Qt filesystem abstraction is bad, but ... let's hope it doesn't break too much? -// FIXME: The error handling is just DEFICIENT -Package Package::fromInspectedFolder(const QString& folderPath) -{ - QDir root(folderPath); - - Package out; - QDirIterator iterator(folderPath, QDir::NoDotAndDotDot | QDir::AllEntries | QDir::System | QDir::Hidden, QDirIterator::Subdirectories); - while(iterator.hasNext()) { - iterator.next(); - - auto fileInfo = iterator.fileInfo(); - auto relPath = root.relativeFilePath(fileInfo.filePath()); - // FIXME: this is probably completely busted on Windows anyway, so just disable it. - // Qt makes shit up and doesn't understand the platform details - // TODO: Actually use a filesystem library that isn't terrible and has decen license. - // I only know one, and I wrote it. Sadly, currently proprietary. PAIN. -#ifndef Q_OS_WIN32 - if(fileInfo.isSymLink()) { - Path targetPath; - if(!actually_read_symlink_target(fileInfo.filePath(), targetPath)) { - qCritical() << "Folder inspection: Unknown filesystem object:" << fileInfo.absoluteFilePath(); - out.valid = false; - } - out.addLink(relPath, targetPath); - } - else -#endif - if(fileInfo.isDir()) { - out.addFolder(relPath); - } - else if(fileInfo.isFile()) { - File f; - f.executable = fileInfo.isExecutable(); - f.size = fileInfo.size(); - // FIXME: async / optimize the hashing - QFile input(fileInfo.absoluteFilePath()); - if(!input.open(QIODevice::ReadOnly)) { - qCritical() << "Folder inspection: Failed to open file:" << fileInfo.absoluteFilePath(); - out.valid = false; - break; - } - f.hash = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Sha1).toHex().constData(); - out.addFile(relPath, f); - } - else { - // Something else... oh my - qCritical() << "Folder inspection: Unknown filesystem object:" << fileInfo.absoluteFilePath(); - out.valid = false; - break; - } - } - out.folders.insert(Path(".")); - out.valid = true; - return out; -} - -namespace { -struct shallow_first_sort -{ - bool operator()(const Path &lhs, const Path &rhs) const - { - auto lhs_depth = lhs.length(); - auto rhs_depth = rhs.length(); - if(lhs_depth < rhs_depth) - { - return true; - } - else if(lhs_depth == rhs_depth) - { - if(lhs < rhs) - { - return true; - } - } - return false; - } -}; - -struct deep_first_sort -{ - bool operator()(const Path &lhs, const Path &rhs) const - { - auto lhs_depth = lhs.length(); - auto rhs_depth = rhs.length(); - if(lhs_depth > rhs_depth) - { - return true; - } - else if(lhs_depth == rhs_depth) - { - if(lhs < rhs) - { - return true; - } - } - return false; - } -}; -} - -UpdateOperations UpdateOperations::resolve(const Package& from, const Package& to) -{ - UpdateOperations out; - - if(!from.valid || !to.valid) { - out.valid = false; - return out; - } - - // Files - for(auto iter = from.files.begin(); iter != from.files.end(); iter++) { - const auto ¤t_hash = iter->second.hash; - const auto ¤t_executable = iter->second.executable; - const auto &path = iter->first; - - auto iter2 = to.files.find(path); - if(iter2 == to.files.end()) { - // removed - out.deletes.push_back(path); - continue; - } - auto new_hash = iter2->second.hash; - auto new_executable = iter2->second.executable; - if (current_hash != new_hash) { - out.deletes.push_back(path); - out.downloads.emplace( - std::pair{ - path, - FileDownload(to.sources.at(iter2->second.hash), iter2->second.executable) - } - ); - } - else if (current_executable != new_executable) { - out.executable_fixes[path] = new_executable; - } - } - for(auto iter = to.files.begin(); iter != to.files.end(); iter++) { - auto path = iter->first; - if(!from.files.count(path)) { - out.downloads.emplace( - std::pair{ - path, - FileDownload(to.sources.at(iter->second.hash), iter->second.executable) - } - ); - } - } - - // Folders - std::set remove_folders; - std::set make_folders; - for(auto from_path: from.folders) { - auto iter = to.folders.find(from_path); - if(iter == to.folders.end()) { - remove_folders.insert(from_path); - } - } - for(auto & rmdir: remove_folders) { - out.rmdirs.push_back(rmdir); - } - for(auto to_path: to.folders) { - auto iter = from.folders.find(to_path); - if(iter == from.folders.end()) { - make_folders.insert(to_path); - } - } - for(auto & mkdir: make_folders) { - out.mkdirs.push_back(mkdir); - } - - // Symlinks - for(auto iter = from.symlinks.begin(); iter != from.symlinks.end(); iter++) { - const auto ¤t_target = iter->second; - const auto &path = iter->first; - - auto iter2 = to.symlinks.find(path); - if(iter2 == to.symlinks.end()) { - // removed - out.deletes.push_back(path); - continue; - } - const auto &new_target = iter2->second; - if (current_target != new_target) { - out.deletes.push_back(path); - out.mklinks[path] = iter2->second; - } - } - for(auto iter = to.symlinks.begin(); iter != to.symlinks.end(); iter++) { - auto path = iter->first; - if(!from.symlinks.count(path)) { - out.mklinks[path] = iter->second; - } - } - out.valid = true; - return out; -} - -} diff --git a/launcher/mojang/PackageManifest.h b/launcher/mojang/PackageManifest.h deleted file mode 100644 index fd7ab0ad4..000000000 --- a/launcher/mojang/PackageManifest.h +++ /dev/null @@ -1,171 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include "tasks/Task.h" - -namespace mojang_files { - -using Hash = QString; -extern const Hash empty_hash; - -// simple-ish path implementation. assumes always relative and does not allow '..' entries -class Path -{ -public: - using parts_type = QStringList; - - Path() = default; - Path(QString string) { - auto parts_in = string.split('/'); - for(auto & part: parts_in) { - if(part.isEmpty() || part == ".") { - continue; - } - if(part == "..") { - if(parts.size()) { - parts.pop_back(); - } - continue; - } - parts.push_back(part); - } - } - - bool has_parent_path() const - { - return parts.size() > 0; - } - - Path parent_path() const - { - if (parts.empty()) - return Path(); - return Path(parts.begin(), std::prev(parts.end())); - } - - bool empty() const - { - return parts.empty(); - } - - int length() const - { - return parts.length(); - } - - bool operator==(const Path & rhs) const { - return parts == rhs.parts; - } - - bool operator!=(const Path & rhs) const { - return parts != rhs.parts; - } - - inline bool operator<(const Path& rhs) const - { - return compare(rhs) < 0; - } - - parts_type::const_iterator begin() const - { - return parts.begin(); - } - - parts_type::const_iterator end() const - { - return parts.end(); - } - - QString toString() const { - return parts.join("/"); - } - -private: - Path(const parts_type::const_iterator & start, const parts_type::const_iterator & end) { - auto cursor = start; - while(cursor != end) { - parts.push_back(*cursor); - cursor++; - } - } - int compare(const Path& p) const; - - parts_type parts; -}; - - -enum class Compression { - Raw, - Lzma, - Unknown -}; - - -struct FileSource -{ - Compression compression = Compression::Unknown; - Hash hash; - QString url; - std::size_t size = 0; - void upgrade(const FileSource & other) { - if(compression == Compression::Unknown || other.size < size) { - *this = other; - } - } - bool isBad() const { - return compression == Compression::Unknown; - } -}; - -struct File -{ - Hash hash; - bool executable; - std::uint64_t size = 0; -}; - -struct Package { - static Package fromInspectedFolder(const QString &folderPath); - static Package fromManifestFile(const QString &path); - static Package fromManifestContents(const QByteArray& contents); - - explicit operator bool() const - { - return valid; - } - void addFolder(Path folder); - void addFile(const Path & path, const File & file); - void addLink(const Path & path, const Path & target); - void addSource(const FileSource & source); - - std::map sources; - bool valid = true; - std::set folders; - std::map files; - std::map symlinks; -}; - -struct FileDownload : FileSource -{ - FileDownload(const FileSource& source, bool executable) { - static_cast (*this) = source; - this->executable = executable; - } - bool executable = false; -}; - -struct UpdateOperations { - static UpdateOperations resolve(const Package & from, const Package & to); - bool valid = false; - std::vector deletes; - std::vector rmdirs; - std::vector mkdirs; - std::map downloads; - std::map mklinks; - std::map executable_fixes; -}; - -} diff --git a/launcher/net/ByteArraySink.h b/launcher/net/ByteArraySink.h index 501318a11..d6b17d605 100644 --- a/launcher/net/ByteArraySink.h +++ b/launcher/net/ByteArraySink.h @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln + * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -46,14 +47,17 @@ namespace Net { */ class ByteArraySink : public Sink { public: - ByteArraySink(QByteArray* output) : m_output(output){}; + ByteArraySink(std::shared_ptr output) : m_output(output){}; virtual ~ByteArraySink() = default; public: auto init(QNetworkRequest& request) -> Task::State override { - m_output->clear(); + if (m_output) + m_output->clear(); + else + qWarning() << "ByteArraySink did not initialize the buffer because it's not addressable"; if (initAllValidators(request)) return Task::State::Running; return Task::State::Failed; @@ -61,7 +65,10 @@ class ByteArraySink : public Sink { auto write(QByteArray& data) -> Task::State override { - m_output->append(data); + if (m_output) + m_output->append(data); + else + qWarning() << "ByteArraySink did not write the buffer because it's not addressable"; if (writeAllValidators(data)) return Task::State::Running; return Task::State::Failed; @@ -69,7 +76,10 @@ class ByteArraySink : public Sink { auto abort() -> Task::State override { - m_output->clear(); + if (m_output) + m_output->clear(); + else + qWarning() << "ByteArraySink did not clear the buffer because it's not addressable"; failAllValidators(); return Task::State::Failed; } @@ -84,6 +94,6 @@ class ByteArraySink : public Sink { auto hasLocalData() -> bool override { return false; } private: - QByteArray* m_output; + std::shared_ptr m_output; }; } // namespace Net diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index 7f8d3a067..4ea45c635 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -41,6 +41,7 @@ #include #include +#include #include "ByteArraySink.h" #include "ChecksumValidator.h" @@ -69,7 +70,7 @@ auto Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Down return dl; } -auto Download::makeByteArray(QUrl url, QByteArray* output, Options options) -> Download::Ptr +auto Download::makeByteArray(QUrl url, std::shared_ptr output, Options options) -> Download::Ptr { auto dl = makeShared(); dl->m_url = url; diff --git a/launcher/net/Download.h b/launcher/net/Download.h index 920164a30..2e861732d 100644 --- a/launcher/net/Download.h +++ b/launcher/net/Download.h @@ -60,7 +60,7 @@ class Download : public NetAction { ~Download() override = default; static auto makeCached(QUrl url, MetaEntryPtr entry, Options options = Option::NoOptions) -> Download::Ptr; - static auto makeByteArray(QUrl url, QByteArray* output, Options options = Option::NoOptions) -> Download::Ptr; + static auto makeByteArray(QUrl url, std::shared_ptr output, Options options = Option::NoOptions) -> Download::Ptr; static auto makeFile(QUrl url, QString path, Options options = Option::NoOptions) -> Download::Ptr; public: diff --git a/launcher/net/Upload.cpp b/launcher/net/Upload.cpp index 4f9553edf..3f6f58290 100644 --- a/launcher/net/Upload.cpp +++ b/launcher/net/Upload.cpp @@ -39,218 +39,226 @@ #include "Upload.h" #include -#include "ByteArraySink.h" -#include "BuildConfig.h" #include "Application.h" +#include "BuildConfig.h" +#include "ByteArraySink.h" #include "net/Logging.h" namespace Net { - bool Upload::abort() - { - if (m_reply) { - m_reply->abort(); - } else { - m_state = State::AbortedByUser; +bool Upload::abort() +{ + if (m_reply) { + m_reply->abort(); + } else { + m_state = State::AbortedByUser; + } + return true; +} + +void Upload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) +{ + setProgress(bytesReceived, bytesTotal); +} + +void Upload::downloadError(QNetworkReply::NetworkError error) +{ + if (error == QNetworkReply::OperationCanceledError) { + qCCritical(taskUploadLogC) << getUid().toString() << "Aborted " << m_url.toString(); + m_state = State::AbortedByUser; + } else { + // error happened during download. + qCCritical(taskUploadLogC) << getUid().toString() << "Failed " << m_url.toString() << " with reason " << error; + m_state = State::Failed; + } +} + +void Upload::sslErrors(const QList& errors) +{ + int i = 1; + for (const auto& error : errors) { + qCCritical(taskUploadLogC) << getUid().toString() << "Upload" << m_url.toString() << "SSL Error #" << i << " : " + << error.errorString(); + auto cert = error.certificate(); + qCCritical(taskUploadLogC) << getUid().toString() << "Certificate in question:\n" << cert.toText(); + i++; + } +} + +bool Upload::handleRedirect() +{ + QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl(); + if (!redirect.isValid()) { + if (!m_reply->hasRawHeader("Location")) { + // no redirect -> it's fine to continue + return false; } - return true; - } - - void Upload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { - setProgress(bytesReceived, bytesTotal); - } - - void Upload::downloadError(QNetworkReply::NetworkError error) { - if (error == QNetworkReply::OperationCanceledError) { - qCCritical(taskUploadLogC) << getUid().toString() << "Aborted " << m_url.toString(); - m_state = State::AbortedByUser; - } else { - // error happened during download. - qCCritical(taskUploadLogC) << getUid().toString() << "Failed " << m_url.toString() << " with reason " << error; - m_state = State::Failed; + // there is a Location header, but it's not correct. we need to apply some workarounds... + QByteArray redirectBA = m_reply->rawHeader("Location"); + if (redirectBA.size() == 0) { + // empty, yet present redirect header? WTF? + return false; } - } - - void Upload::sslErrors(const QList &errors) { - int i = 1; - for (const auto& error : errors) { - qCCritical(taskUploadLogC) << getUid().toString() << "Upload" << m_url.toString() << "SSL Error #" << i << " : " << error.errorString(); - auto cert = error.certificate(); - qCCritical(taskUploadLogC) << getUid().toString() << "Certificate in question:\n" << cert.toText(); - i++; - } - } - - bool Upload::handleRedirect() - { - QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl(); - if (!redirect.isValid()) { - if (!m_reply->hasRawHeader("Location")) { - // no redirect -> it's fine to continue - return false; - } - // there is a Location header, but it's not correct. we need to apply some workarounds... - QByteArray redirectBA = m_reply->rawHeader("Location"); - if (redirectBA.size() == 0) { - // empty, yet present redirect header? WTF? - return false; - } - QString redirectStr = QString::fromUtf8(redirectBA); - - if (redirectStr.startsWith("//")) { - /* - * IF the URL begins with //, we need to insert the URL scheme. - * See: https://bugreports.qt.io/browse/QTBUG-41061 - * See: http://tools.ietf.org/html/rfc3986#section-4.2 - */ - redirectStr = m_reply->url().scheme() + ":" + redirectStr; - } else if (redirectStr.startsWith("/")) { - /* - * IF the URL begins with /, we need to process it as a relative URL - */ - auto url = m_reply->url(); - url.setPath(redirectStr, QUrl::TolerantMode); - redirectStr = url.toString(); - } + QString redirectStr = QString::fromUtf8(redirectBA); + if (redirectStr.startsWith("//")) { /* - * Next, make sure the URL is parsed in tolerant mode. Qt doesn't parse the location header in tolerant mode, which causes issues. - * FIXME: report Qt bug for this + * IF the URL begins with //, we need to insert the URL scheme. + * See: https://bugreports.qt.io/browse/QTBUG-41061 + * See: http://tools.ietf.org/html/rfc3986#section-4.2 */ - redirect = QUrl(redirectStr, QUrl::TolerantMode); - if (!redirect.isValid()) { - qCWarning(taskUploadLogC) << getUid().toString() << "Failed to parse redirect URL:" << redirectStr; - downloadError(QNetworkReply::ProtocolFailure); - return false; - } - qCDebug(taskUploadLogC) << getUid().toString() << "Fixed location header:" << redirect; - } else { - qCDebug(taskUploadLogC) << getUid().toString() << "Location header:" << redirect; + redirectStr = m_reply->url().scheme() + ":" + redirectStr; + } else if (redirectStr.startsWith("/")) { + /* + * IF the URL begins with /, we need to process it as a relative URL + */ + auto url = m_reply->url(); + url.setPath(redirectStr, QUrl::TolerantMode); + redirectStr = url.toString(); } - m_url = QUrl(redirect.toString()); - qCDebug(taskUploadLogC) << getUid().toString() << "Following redirect to " << m_url.toString(); - startAction(m_network); - return true; + /* + * Next, make sure the URL is parsed in tolerant mode. Qt doesn't parse the location header in tolerant mode, which causes issues. + * FIXME: report Qt bug for this + */ + redirect = QUrl(redirectStr, QUrl::TolerantMode); + if (!redirect.isValid()) { + qCWarning(taskUploadLogC) << getUid().toString() << "Failed to parse redirect URL:" << redirectStr; + downloadError(QNetworkReply::ProtocolFailure); + return false; + } + qCDebug(taskUploadLogC) << getUid().toString() << "Fixed location header:" << redirect; + } else { + qCDebug(taskUploadLogC) << getUid().toString() << "Location header:" << redirect; } - void Upload::downloadFinished() { - // handle HTTP redirection first - // very unlikely for post requests, still can happen - if (handleRedirect()) { - qCDebug(taskUploadLogC) << getUid().toString() << "Upload redirected:" << m_url.toString(); - return; - } + m_url = QUrl(redirect.toString()); + qCDebug(taskUploadLogC) << getUid().toString() << "Following redirect to " << m_url.toString(); + startAction(m_network); + return true; +} - // if the download failed before this point ... - if (m_state == State::Succeeded) { - qCDebug(taskUploadLogC) << getUid().toString() << "Upload failed but we are allowed to proceed:" << m_url.toString(); - m_sink->abort(); - m_reply.reset(); - emit succeeded(); - return; - } else if (m_state == State::Failed) { - qCDebug(taskUploadLogC) << getUid().toString() << "Upload failed in previous step:" << m_url.toString(); - m_sink->abort(); - m_reply.reset(); - emit failed(""); - return; - } else if (m_state == State::AbortedByUser) { - qCDebug(taskUploadLogC) << getUid().toString() << "Upload aborted in previous step:" << m_url.toString(); - m_sink->abort(); - m_reply.reset(); - emit aborted(); - return; - } +void Upload::downloadFinished() +{ + // handle HTTP redirection first + // very unlikely for post requests, still can happen + if (handleRedirect()) { + qCDebug(taskUploadLogC) << getUid().toString() << "Upload redirected:" << m_url.toString(); + return; + } - // make sure we got all the remaining data, if any - auto data = m_reply->readAll(); - if (data.size()) { - qCDebug(taskUploadLogC) << getUid().toString() << "Writing extra" << data.size() << "bytes"; - m_state = m_sink->write(data); - } - - // otherwise, finalize the whole graph - m_state = m_sink->finalize(*m_reply.get()); - if (m_state != State::Succeeded) { - qCDebug(taskUploadLogC) << getUid().toString() << "Upload failed to finalize:" << m_url.toString(); - m_sink->abort(); - m_reply.reset(); - emit failed(""); - return; - } + // if the download failed before this point ... + if (m_state == State::Succeeded) { + qCDebug(taskUploadLogC) << getUid().toString() << "Upload failed but we are allowed to proceed:" << m_url.toString(); + m_sink->abort(); m_reply.reset(); - qCDebug(taskUploadLogC) << getUid().toString() << "Upload succeeded:" << m_url.toString(); emit succeeded(); + return; + } else if (m_state == State::Failed) { + qCDebug(taskUploadLogC) << getUid().toString() << "Upload failed in previous step:" << m_url.toString(); + m_sink->abort(); + m_reply.reset(); + emit failed(""); + return; + } else if (m_state == State::AbortedByUser) { + qCDebug(taskUploadLogC) << getUid().toString() << "Upload aborted in previous step:" << m_url.toString(); + m_sink->abort(); + m_reply.reset(); + emit aborted(); + return; } - void Upload::downloadReadyRead() { - if (m_state == State::Running) { - auto data = m_reply->readAll(); - m_state = m_sink->write(data); - } + // make sure we got all the remaining data, if any + auto data = m_reply->readAll(); + if (data.size()) { + qCDebug(taskUploadLogC) << getUid().toString() << "Writing extra" << data.size() << "bytes"; + m_state = m_sink->write(data); } - void Upload::executeTask() { - setStatus(tr("Uploading %1").arg(m_url.toString())); + // otherwise, finalize the whole graph + m_state = m_sink->finalize(*m_reply.get()); + if (m_state != State::Succeeded) { + qCDebug(taskUploadLogC) << getUid().toString() << "Upload failed to finalize:" << m_url.toString(); + m_sink->abort(); + m_reply.reset(); + emit failed(""); + return; + } + m_reply.reset(); + qCDebug(taskUploadLogC) << getUid().toString() << "Upload succeeded:" << m_url.toString(); + emit succeeded(); +} - if (m_state == State::AbortedByUser) { - qCWarning(taskUploadLogC) << getUid().toString() << "Attempt to start an aborted Upload:" << m_url.toString(); - emit aborted(); +void Upload::downloadReadyRead() +{ + if (m_state == State::Running) { + auto data = m_reply->readAll(); + m_state = m_sink->write(data); + } +} + +void Upload::executeTask() +{ + setStatus(tr("Uploading %1").arg(m_url.toString())); + + if (m_state == State::AbortedByUser) { + qCWarning(taskUploadLogC) << getUid().toString() << "Attempt to start an aborted Upload:" << m_url.toString(); + emit aborted(); + return; + } + QNetworkRequest request(m_url); + m_state = m_sink->init(request); + switch (m_state) { + case State::Succeeded: + emitSucceeded(); + qCDebug(taskUploadLogC) << getUid().toString() << "Upload cache hit " << m_url.toString(); return; - } - QNetworkRequest request(m_url); - m_state = m_sink->init(request); - switch (m_state) { - case State::Succeeded: - emitSucceeded(); - qCDebug(taskUploadLogC) << getUid().toString() << "Upload cache hit " << m_url.toString(); - return; - case State::Running: - qCDebug(taskUploadLogC) << getUid().toString() << "Uploading " << m_url.toString(); - break; - case State::Inactive: - case State::Failed: - emitFailed(""); - return; - case State::AbortedByUser: - emitAborted(); - return; - } + case State::Running: + qCDebug(taskUploadLogC) << getUid().toString() << "Uploading " << m_url.toString(); + break; + case State::Inactive: + case State::Failed: + emitFailed(""); + return; + case State::AbortedByUser: + emitAborted(); + return; + } - request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8()); - // TODO remove duplication - if (APPLICATION->capabilities() & Application::SupportsFlame && request.url().host() == QUrl(BuildConfig.FLAME_BASE_URL).host()) { - request.setRawHeader("x-api-key", APPLICATION->getFlameAPIKey().toUtf8()); - } else if (request.url().host() == QUrl(BuildConfig.MODRINTH_PROD_URL).host() || - request.url().host() == QUrl(BuildConfig.MODRINTH_STAGING_URL).host()) { - QString token = APPLICATION->getModrinthAPIToken(); - if (!token.isNull()) - request.setRawHeader("Authorization", token.toUtf8()); - } + request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8()); + // TODO remove duplication + if (APPLICATION->capabilities() & Application::SupportsFlame && request.url().host() == QUrl(BuildConfig.FLAME_BASE_URL).host()) { + request.setRawHeader("x-api-key", APPLICATION->getFlameAPIKey().toUtf8()); + } else if (request.url().host() == QUrl(BuildConfig.MODRINTH_PROD_URL).host() || + request.url().host() == QUrl(BuildConfig.MODRINTH_STAGING_URL).host()) { + QString token = APPLICATION->getModrinthAPIToken(); + if (!token.isNull()) + request.setRawHeader("Authorization", token.toUtf8()); + } - //TODO other types of post requests ? - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QNetworkReply* rep = m_network->post(request, m_post_data); + // TODO other types of post requests ? + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + QNetworkReply* rep = m_network->post(request, m_post_data); - m_reply.reset(rep); - connect(rep, &QNetworkReply::downloadProgress, this, &Upload::downloadProgress); - connect(rep, &QNetworkReply::finished, this, &Upload::downloadFinished); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 - connect(rep, &QNetworkReply::errorOccurred, this, &Upload::downloadError); + m_reply.reset(rep); + connect(rep, &QNetworkReply::downloadProgress, this, &Upload::downloadProgress); + connect(rep, &QNetworkReply::finished, this, &Upload::downloadFinished); +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 + connect(rep, &QNetworkReply::errorOccurred, this, &Upload::downloadError); #else - connect(rep, QOverload::of(&QNetworkReply::error), this, &Upload::downloadError); + connect(rep, QOverload::of(&QNetworkReply::error), this, &Upload::downloadError); #endif - connect(rep, &QNetworkReply::sslErrors, this, &Upload::sslErrors); - connect(rep, &QNetworkReply::readyRead, this, &Upload::downloadReadyRead); - } + connect(rep, &QNetworkReply::sslErrors, this, &Upload::sslErrors); + connect(rep, &QNetworkReply::readyRead, this, &Upload::downloadReadyRead); +} - Upload::Ptr Upload::makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data) { - auto up = makeShared(); - up->m_url = std::move(url); - up->m_sink.reset(new ByteArraySink(output)); - up->m_post_data = std::move(m_post_data); - return up; - } -} // Net +Upload::Ptr Upload::makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data) +{ + auto up = makeShared(); + up->m_url = std::move(url); + up->m_sink.reset(new ByteArraySink(output)); + up->m_post_data = std::move(m_post_data); + return up; +} +} // namespace Net diff --git a/launcher/net/Upload.h b/launcher/net/Upload.h index e8f0ea40d..0b0c94976 100644 --- a/launcher/net/Upload.h +++ b/launcher/net/Upload.h @@ -42,31 +42,31 @@ namespace Net { - class Upload : public NetAction { - Q_OBJECT +class Upload : public NetAction { + Q_OBJECT - public: - using Ptr = shared_qobject_ptr; + public: + using Ptr = shared_qobject_ptr; - static Upload::Ptr makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data); - auto abort() -> bool override; - auto canAbort() const -> bool override { return true; }; + static Upload::Ptr makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data); + auto abort() -> bool override; + auto canAbort() const -> bool override { return true; }; - protected slots: - void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; - void downloadError(QNetworkReply::NetworkError error) override; - void sslErrors(const QList & errors) override; - void downloadFinished() override; - void downloadReadyRead() override; + protected slots: + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; + void downloadError(QNetworkReply::NetworkError error) override; + void sslErrors(const QList& errors) override; + void downloadFinished() override; + void downloadReadyRead() override; - public slots: - void executeTask() override; - private: - std::unique_ptr m_sink; - QByteArray m_post_data; + public slots: + void executeTask() override; - bool handleRedirect(); - }; + private: + std::unique_ptr m_sink; + QByteArray m_post_data; -} // Net + bool handleRedirect(); +}; +} // namespace Net diff --git a/launcher/news/NewsChecker.cpp b/launcher/news/NewsChecker.cpp index 1f1520d0a..4f02bf5e0 100644 --- a/launcher/news/NewsChecker.cpp +++ b/launcher/news/NewsChecker.cpp @@ -58,7 +58,7 @@ void NewsChecker::reloadNews() qDebug() << "Reloading news."; NetJob::Ptr job{ new NetJob("News RSS Feed", m_network) }; - job->addNetAction(Net::Download::makeByteArray(m_feedUrl, &newsData)); + job->addNetAction(Net::Download::makeByteArray(m_feedUrl, newsData)); QObject::connect(job.get(), &NetJob::succeeded, this, &NewsChecker::rssDownloadFinished); QObject::connect(job.get(), &NetJob::failed, this, &NewsChecker::rssDownloadFailed); m_newsNetJob.reset(job); @@ -79,32 +79,27 @@ void NewsChecker::rssDownloadFinished() int errorCol = -1; // Parse the XML. - if (!doc.setContent(newsData, false, &errorMsg, &errorLine, &errorCol)) - { + if (!doc.setContent(*newsData, false, &errorMsg, &errorLine, &errorCol)) { QString fullErrorMsg = QString("Error parsing RSS feed XML. %1 at %2:%3.").arg(errorMsg).arg(errorLine).arg(errorCol); fail(fullErrorMsg); - newsData.clear(); + newsData->clear(); return; } - newsData.clear(); + newsData->clear(); } // If the parsing succeeded, read it. QDomNodeList items = doc.elementsByTagName("entry"); m_newsEntries.clear(); - for (int i = 0; i < items.length(); i++) - { + for (int i = 0; i < items.length(); i++) { QDomElement element = items.at(i).toElement(); NewsEntryPtr entry; entry.reset(new NewsEntry()); QString errorMsg = "An unknown error occurred."; - if (NewsEntry::fromXmlElement(element, entry.get(), &errorMsg)) - { + if (NewsEntry::fromXmlElement(element, entry.get(), &errorMsg)) { qDebug() << "Loaded news entry" << entry->title; m_newsEntries.append(entry); - } - else - { + } else { qWarning() << "Failed to load news entry at index" << i << ":" << errorMsg; } } diff --git a/launcher/news/NewsChecker.h b/launcher/news/NewsChecker.h index 8467a5412..41babfff7 100644 --- a/launcher/news/NewsChecker.h +++ b/launcher/news/NewsChecker.h @@ -85,7 +85,7 @@ protected: /* data */ //! True if news has been loaded. bool m_loadedNews; - QByteArray newsData; + std::shared_ptr newsData = std::make_shared(); /*! * Gets the error message that was given last time the news was loaded. diff --git a/launcher/resources/OSX/OSX.qrc b/launcher/resources/OSX/OSX.qrc index 9d4511d1a..49f56b0c1 100644 --- a/launcher/resources/OSX/OSX.qrc +++ b/launcher/resources/OSX/OSX.qrc @@ -16,7 +16,6 @@ scalable/jarmods.svg scalable/java.svg scalable/language.svg - scalable/launcher.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg diff --git a/launcher/resources/OSX/scalable/launcher.svg b/launcher/resources/OSX/scalable/launcher.svg deleted file mode 100644 index aeee84338..000000000 --- a/launcher/resources/OSX/scalable/launcher.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - Prism Launcher Logo - - - - - - - - - - - - - - - - - - - - - - - Prism Launcher Logo - 19/10/2022 - - - Prism Launcher - - - - - AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke - - - https://github.com/PrismLauncher/PrismLauncher - - - Prism Launcher - - - - - - - - - - - - - - - diff --git a/launcher/resources/breeze_dark/scalable/launcher.svg b/launcher/resources/breeze_dark/scalable/launcher.svg deleted file mode 100644 index aeee84338..000000000 --- a/launcher/resources/breeze_dark/scalable/launcher.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - Prism Launcher Logo - - - - - - - - - - - - - - - - - - - - - - - Prism Launcher Logo - 19/10/2022 - - - Prism Launcher - - - - - AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke - - - https://github.com/PrismLauncher/PrismLauncher - - - Prism Launcher - - - - - - - - - - - - - - - diff --git a/launcher/resources/flat/flat.qrc b/launcher/resources/flat/flat.qrc index cadf87364..2fd5daefe 100644 --- a/launcher/resources/flat/flat.qrc +++ b/launcher/resources/flat/flat.qrc @@ -18,7 +18,6 @@ scalable/jarmods.svg scalable/java.svg scalable/language.svg - scalable/launcher.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg diff --git a/launcher/resources/flat/scalable/launcher.svg b/launcher/resources/flat/scalable/launcher.svg deleted file mode 100644 index aeee84338..000000000 --- a/launcher/resources/flat/scalable/launcher.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - Prism Launcher Logo - - - - - - - - - - - - - - - - - - - - - - - Prism Launcher Logo - 19/10/2022 - - - Prism Launcher - - - - - AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke - - - https://github.com/PrismLauncher/PrismLauncher - - - Prism Launcher - - - - - - - - - - - - - - - diff --git a/launcher/resources/flat_white/flat_white.qrc b/launcher/resources/flat_white/flat_white.qrc index 2701462fe..a1c940da0 100644 --- a/launcher/resources/flat_white/flat_white.qrc +++ b/launcher/resources/flat_white/flat_white.qrc @@ -18,7 +18,6 @@ scalable/jarmods.svg scalable/java.svg scalable/language.svg - scalable/launcher.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg diff --git a/launcher/resources/flat_white/scalable/launcher.svg b/launcher/resources/flat_white/scalable/launcher.svg deleted file mode 100644 index 54131b657..000000000 --- a/launcher/resources/flat_white/scalable/launcher.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/launcher/resources/iOS/iOS.qrc b/launcher/resources/iOS/iOS.qrc index 0b79efb2a..9b8d84f50 100644 --- a/launcher/resources/iOS/iOS.qrc +++ b/launcher/resources/iOS/iOS.qrc @@ -16,7 +16,6 @@ scalable/jarmods.svg scalable/java.svg scalable/language.svg - scalable/launcher.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg diff --git a/launcher/resources/iOS/scalable/launcher.svg b/launcher/resources/iOS/scalable/launcher.svg deleted file mode 100644 index aeee84338..000000000 --- a/launcher/resources/iOS/scalable/launcher.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - Prism Launcher Logo - - - - - - - - - - - - - - - - - - - - - - - Prism Launcher Logo - 19/10/2022 - - - Prism Launcher - - - - - AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke - - - https://github.com/PrismLauncher/PrismLauncher - - - Prism Launcher - - - - - - - - - - - - - - - diff --git a/launcher/resources/pe_blue/pe_blue.qrc b/launcher/resources/pe_blue/pe_blue.qrc index 1b2b62915..da45ef9a1 100644 --- a/launcher/resources/pe_blue/pe_blue.qrc +++ b/launcher/resources/pe_blue/pe_blue.qrc @@ -16,7 +16,6 @@ scalable/jarmods.svg scalable/java.svg scalable/language.svg - scalable/launcher.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg diff --git a/launcher/resources/pe_blue/scalable/launcher.svg b/launcher/resources/pe_blue/scalable/launcher.svg deleted file mode 100644 index aeee84338..000000000 --- a/launcher/resources/pe_blue/scalable/launcher.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - Prism Launcher Logo - - - - - - - - - - - - - - - - - - - - - - - Prism Launcher Logo - 19/10/2022 - - - Prism Launcher - - - - - AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke - - - https://github.com/PrismLauncher/PrismLauncher - - - Prism Launcher - - - - - - - - - - - - - - - diff --git a/launcher/resources/pe_colored/pe_colored.qrc b/launcher/resources/pe_colored/pe_colored.qrc index 084fca930..ba5bd44f9 100644 --- a/launcher/resources/pe_colored/pe_colored.qrc +++ b/launcher/resources/pe_colored/pe_colored.qrc @@ -16,7 +16,6 @@ scalable/jarmods.svg scalable/java.svg scalable/language.svg - scalable/launcher.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg diff --git a/launcher/resources/pe_colored/scalable/launcher.svg b/launcher/resources/pe_colored/scalable/launcher.svg deleted file mode 100644 index aeee84338..000000000 --- a/launcher/resources/pe_colored/scalable/launcher.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - Prism Launcher Logo - - - - - - - - - - - - - - - - - - - - - - - Prism Launcher Logo - 19/10/2022 - - - Prism Launcher - - - - - AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke - - - https://github.com/PrismLauncher/PrismLauncher - - - Prism Launcher - - - - - - - - - - - - - - - diff --git a/launcher/resources/pe_dark/pe_dark.qrc b/launcher/resources/pe_dark/pe_dark.qrc index 5c49b75a3..2bfec42cb 100644 --- a/launcher/resources/pe_dark/pe_dark.qrc +++ b/launcher/resources/pe_dark/pe_dark.qrc @@ -16,7 +16,6 @@ scalable/jarmods.svg scalable/java.svg scalable/language.svg - scalable/launcher.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg diff --git a/launcher/resources/pe_dark/scalable/launcher.svg b/launcher/resources/pe_dark/scalable/launcher.svg deleted file mode 100644 index aeee84338..000000000 --- a/launcher/resources/pe_dark/scalable/launcher.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - Prism Launcher Logo - - - - - - - - - - - - - - - - - - - - - - - Prism Launcher Logo - 19/10/2022 - - - Prism Launcher - - - - - AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke - - - https://github.com/PrismLauncher/PrismLauncher - - - Prism Launcher - - - - - - - - - - - - - - - diff --git a/launcher/resources/pe_light/pe_light.qrc b/launcher/resources/pe_light/pe_light.qrc index a8e3f1572..25d5da73b 100644 --- a/launcher/resources/pe_light/pe_light.qrc +++ b/launcher/resources/pe_light/pe_light.qrc @@ -16,7 +16,6 @@ scalable/jarmods.svg scalable/java.svg scalable/language.svg - scalable/launcher.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg diff --git a/launcher/resources/pe_light/scalable/launcher.svg b/launcher/resources/pe_light/scalable/launcher.svg deleted file mode 100644 index aeee84338..000000000 --- a/launcher/resources/pe_light/scalable/launcher.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - Prism Launcher Logo - - - - - - - - - - - - - - - - - - - - - - - Prism Launcher Logo - 19/10/2022 - - - Prism Launcher - - - - - AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke - - - https://github.com/PrismLauncher/PrismLauncher - - - Prism Launcher - - - - - - - - - - - - - - - diff --git a/launcher/settings/INIFile.cpp b/launcher/settings/INIFile.cpp index f0347cabf..d16256b96 100644 --- a/launcher/settings/INIFile.cpp +++ b/launcher/settings/INIFile.cpp @@ -45,12 +45,12 @@ #include -INIFile::INIFile() -{ -} +INIFile::INIFile() {} bool INIFile::saveFile(QString fileName) { + if (!contains("ConfigVersion")) + insert("ConfigVersion", "1.2"); QSettings _settings_obj{ fileName, QSettings::Format::IniFormat }; _settings_obj.setFallbacksEnabled(false); @@ -71,6 +71,81 @@ bool INIFile::saveFile(QString fileName) return true; } +QString unescape(QString orig) +{ + QString out; + QChar prev = QChar::Null; + for (auto c : orig) { + if (prev == '\\') { + if (c == 'n') + out += '\n'; + else if (c == 't') + out += '\t'; + else if (c == '#') + out += '#'; + else + out += c; + prev = QChar::Null; + } else { + if (c == '\\') { + prev = c; + continue; + } + out += c; + prev = QChar::Null; + } + } + return out; +} + +QString unquote(QString str) +{ + if ((str.contains(QChar(';')) || str.contains(QChar('=')) || str.contains(QChar(','))) && str.endsWith("\"") && str.startsWith("\"")) { +#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0) + str = str.remove(0, 1); + str = str.remove(str.size() - 1, 1); +#else + str = str.removeFirst().removeLast(); +#endif + } + return str; +} + +bool parseOldFileFormat(QIODevice& device, QSettings::SettingsMap& map) +{ + QTextStream in(device.readAll()); +#if QT_VERSION <= QT_VERSION_CHECK(6, 0, 0) + in.setCodec("UTF-8"); +#endif + + QStringList lines = in.readAll().split('\n'); + for (int i = 0; i < lines.count(); i++) { + QString& lineRaw = lines[i]; + // Ignore comments. + int commentIndex = 0; + QString line = lineRaw; + // Search for comments until no more escaped # are available + while ((commentIndex = line.indexOf('#', commentIndex + 1)) != -1) { + if (commentIndex > 0 && line.at(commentIndex - 1) == '\\') { + continue; + } + line = line.left(lineRaw.indexOf('#')).trimmed(); + } + + int eqPos = line.indexOf('='); + if (eqPos == -1) + continue; + QString key = line.left(eqPos).trimmed(); + QString valueStr = line.right(line.length() - eqPos - 1).trimmed(); + + valueStr = unquote(unescape(valueStr)); + + QVariant value(valueStr); + map.insert(key, value); + } + + return true; +} bool INIFile::loadFile(QString fileName) { @@ -84,10 +159,29 @@ bool INIFile::loadFile(QString fileName) qCritical() << "A format error occurred (e.g. loading a malformed INI file)."; return false; } - - for (auto&& key : _settings_obj.allKeys()) - insert(key, _settings_obj.value(key)); - + if (!_settings_obj.value("ConfigVersion").isValid()) { + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) + return false; + QSettings::SettingsMap map; + parseOldFileFormat(file, map); + file.close(); + for (auto&& key : map.keys()) + insert(key, map.value(key)); + insert("ConfigVersion", "1.2"); + } else if (_settings_obj.value("ConfigVersion").toString() == "1.1") { + for (auto&& key : _settings_obj.allKeys()) { + if (auto valueStr = _settings_obj.value(key).toString(); + (valueStr.contains(QChar(';')) || valueStr.contains(QChar('=')) || valueStr.contains(QChar(','))) && + valueStr.endsWith("\"") && valueStr.startsWith("\"")) { + insert(key, unquote(valueStr)); + } else + insert(key, _settings_obj.value(key)); + } + insert("ConfigVersion", "1.2"); + } else + for (auto&& key : _settings_obj.allKeys()) + insert(key, _settings_obj.value(key)); return true; } @@ -103,4 +197,3 @@ void INIFile::set(QString key, QVariant val) { this->operator[](key) = val; } - diff --git a/launcher/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp index 5ee145055..9aada5e69 100644 --- a/launcher/tasks/ConcurrentTask.cpp +++ b/launcher/tasks/ConcurrentTask.cpp @@ -138,19 +138,18 @@ void ConcurrentTask::startNext() connect(next.get(), &Task::progress, this, [this, next](qint64 current, qint64 total) { subTaskProgress(next, current, total); }); m_doing.insert(next.get(), next); + qsizetype num_starts = qMin(m_queue.size(), m_total_max_size - m_doing.size()); auto task_progress = std::make_shared(next->getUid()); m_task_progress.insert(next->getUid(), task_progress); updateState(); updateStepProgress(*task_progress.get(), Operation::ADDED); - QCoreApplication::processEvents(); QMetaObject::invokeMethod(next.get(), &Task::start, Qt::QueuedConnection); // Allow going up the number of concurrent tasks in case of tasks being added in the middle of a running task. - int num_starts = qMin(m_queue.size(), m_total_max_size - m_doing.size()); for (int i = 0; i < num_starts; i++) QMetaObject::invokeMethod(this, &ConcurrentTask::startNext, Qt::QueuedConnection); } diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index 23e55c51d..2763cca26 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -42,6 +42,7 @@ #include #include #include +#include #include "FileSystem.h" #include "net/NetJob.h" @@ -454,6 +455,7 @@ QVariant TranslationsModel::data(const QModelIndex& index, int role) const return QString("%1%").arg(lang.percentTranslated(), 3, 'f', 1); } } + qWarning("TranslationModel::data not implemented when role is DisplayRole"); } case Qt::ToolTipRole: { @@ -526,34 +528,34 @@ Language * TranslationsModel::findLanguage(const QString& key) } } +void TranslationsModel::setUseSystemLocale(bool useSystemLocale) +{ + APPLICATION->settings()->set("UseSystemLocale", useSystemLocale); + QLocale::setDefault(QLocale(useSystemLocale ? QString::fromStdString(std::locale().name()) : defaultLangCode)); +} + bool TranslationsModel::selectLanguage(QString key) { - QString &langCode = key; + QString& langCode = key; auto langPtr = findLanguage(key); - if (langCode.isEmpty()) - { + if (langCode.isEmpty()) { d->no_language_set = true; } - if(!langPtr) - { + if (!langPtr) { qWarning() << "Selected invalid language" << key << ", defaulting to" << defaultLangCode; langCode = defaultLangCode; - } - else - { + } else { langCode = langPtr->key; } // uninstall existing translators if there are any - if (d->m_app_translator) - { + if (d->m_app_translator) { QCoreApplication::removeTranslator(d->m_app_translator.get()); d->m_app_translator.reset(); } - if (d->m_qt_translator) - { + if (d->m_qt_translator) { QCoreApplication::removeTranslator(d->m_qt_translator.get()); d->m_qt_translator.reset(); } @@ -563,8 +565,9 @@ bool TranslationsModel::selectLanguage(QString key) * In a multithreaded application, the default locale should be set at application startup, before any non-GUI threads are created. * This function is not reentrant. */ - QLocale locale = QLocale(langCode); - QLocale::setDefault(locale); + QLocale::setDefault( + QLocale(APPLICATION->settings()->get("UseSystemLocale").toBool() ? QString::fromStdString(std::locale().name()) : langCode)); + // if it's the default UI language, finish if(langCode == defaultLangCode) diff --git a/launcher/translations/TranslationsModel.h b/launcher/translations/TranslationsModel.h index 3abf84e6e..cff23ce74 100644 --- a/launcher/translations/TranslationsModel.h +++ b/launcher/translations/TranslationsModel.h @@ -20,17 +20,16 @@ struct Language; -class TranslationsModel : public QAbstractListModel -{ +class TranslationsModel : public QAbstractListModel { Q_OBJECT -public: - explicit TranslationsModel(QString path, QObject *parent = 0); + public: + explicit TranslationsModel(QString path, QObject* parent = 0); virtual ~TranslationsModel(); - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - int columnCount(const QModelIndex & parent) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + int columnCount(const QModelIndex& parent) const override; bool selectLanguage(QString key); void updateLanguage(QString key); @@ -38,27 +37,27 @@ public: QString selectedLanguage(); void downloadIndex(); + void setUseSystemLocale(bool useSystemLocale); -private: - Language *findLanguage(const QString & key); + private: + Language* findLanguage(const QString& key); void reloadLocalFiles(); void downloadTranslation(QString key); void downloadNext(); // hide copy constructor - TranslationsModel(const TranslationsModel &) = delete; + TranslationsModel(const TranslationsModel&) = delete; // hide assign op - TranslationsModel &operator=(const TranslationsModel &) = delete; + TranslationsModel& operator=(const TranslationsModel&) = delete; -private slots: + private slots: void indexReceived(); void indexFailed(QString reason); void dlFailed(QString reason); void dlGood(); - void translationDirChanged(const QString &path); + void translationDirChanged(const QString& path); - -private: /* data */ + private: /* data */ struct Private; std::unique_ptr d; }; diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 1addfe216..da572fc34 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -43,102 +43,100 @@ #include "FileSystem.h" #include "MainWindow.h" +#include "ui/dialogs/ExportToModListDialog.h" #include "ui_MainWindow.h" -#include -#include #include #include +#include +#include -#include #include #include #include #include +#include #include #include +#include +#include +#include #include -#include -#include -#include #include #include #include -#include -#include -#include -#include -#include #include #include +#include +#include +#include +#include +#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include #include #include #include -#include -#include "InstanceWindow.h" #include "InstancePageProvider.h" +#include "InstanceWindow.h" #include "JavaCommon.h" #include "LaunchController.h" -#include "ui/instanceview/InstanceProxyModel.h" -#include "ui/instanceview/InstanceView.h" -#include "ui/instanceview/InstanceDelegate.h" -#include "ui/widgets/LabeledToolButton.h" +#include "ui/dialogs/AboutDialog.h" +#include "ui/dialogs/CopyInstanceDialog.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/EditAccountDialog.h" +#include "ui/dialogs/ExportInstanceDialog.h" +#include "ui/dialogs/ExportPackDialog.h" +#include "ui/dialogs/IconPickerDialog.h" +#include "ui/dialogs/ImportResourceDialog.h" #include "ui/dialogs/NewInstanceDialog.h" #include "ui/dialogs/NewsDialog.h" #include "ui/dialogs/ProgressDialog.h" -#include "ui/dialogs/AboutDialog.h" -#include "ui/dialogs/CustomMessageBox.h" -#include "ui/dialogs/IconPickerDialog.h" -#include "ui/dialogs/CopyInstanceDialog.h" -#include "ui/dialogs/EditAccountDialog.h" -#include "ui/dialogs/ExportInstanceDialog.h" -#include "ui/dialogs/ExportMrPackDialog.h" -#include "ui/dialogs/ImportResourceDialog.h" +#include "ui/instanceview/InstanceDelegate.h" +#include "ui/instanceview/InstanceProxyModel.h" +#include "ui/instanceview/InstanceView.h" #include "ui/themes/ITheme.h" #include "ui/themes/ThemeManager.h" +#include "ui/widgets/LabeledToolButton.h" -#include "minecraft/mod/tasks/LocalResourceParse.h" +#include "minecraft/WorldList.h" #include "minecraft/mod/ModFolderModel.h" #include "minecraft/mod/ShaderPackFolderModel.h" -#include "minecraft/WorldList.h" +#include "minecraft/mod/tasks/LocalResourceParse.h" #include "KonamiCode.h" -#include "InstanceImportTask.h" #include "InstanceCopyTask.h" +#include "InstanceImportTask.h" #include "MMCTime.h" namespace { -QString profileInUseFilter(const QString & profile, bool used) +QString profileInUseFilter(const QString& profile, bool used) { - if(used) - { + if (used) { return QObject::tr("%1 (in use)").arg(profile); - } - else - { + } else { return profile; } } -} +} // namespace -MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) +MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); @@ -184,7 +182,6 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi ui->instanceToolBar->addContextMenuAction(ui->newsToolBar->toggleViewAction()); ui->instanceToolBar->addContextMenuAction(ui->instanceToolBar->toggleViewAction()); ui->instanceToolBar->addContextMenuAction(ui->actionLockToolbars); - } // set the menu for the folders help, accounts, and export tool buttons @@ -205,6 +202,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi auto exportInstanceMenu = new QMenu(this); exportInstanceMenu->addAction(ui->actionExportInstanceZip); exportInstanceMenu->addAction(ui->actionExportInstanceMrPack); + exportInstanceMenu->addAction(ui->actionExportInstanceFlamePack); + exportInstanceMenu->addAction(ui->actionExportInstanceToModList); ui->actionExportInstance->setMenu(exportInstanceMenu); } @@ -224,6 +223,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi // disabled until we have an instance selected ui->instanceToolBar->setEnabled(false); setInstanceActionsEnabled(false); + + // add a close button at the end of the main toolbar when running on gamescope / steam deck + // FIXME: detect if we don't have server side decorations instead + if (qgetenv("XDG_CURRENT_DESKTOP") == "gamescope") { + ui->mainToolBar->addAction(ui->actionCloseWindow); + } } // add the toolbar toggles to the view menu @@ -293,9 +298,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi connect(proxymodel, &InstanceProxyModel::dataChanged, this, &MainWindow::instanceDataChanged); view->setModel(proxymodel); - view->setSourceOfGroupCollapseStatus([](const QString & groupName)->bool { - return APPLICATION->instances()->isGroupCollapsed(groupName); - }); + view->setSourceOfGroupCollapseStatus( + [](const QString& groupName) -> bool { return APPLICATION->instances()->isGroupCollapsed(groupName); }); connect(view, &InstanceView::groupStateChanged, APPLICATION->instances().get(), &InstanceList::on_GroupStateChanged); ui->horizontalLayout->addWidget(view); } @@ -341,7 +345,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi statusBar()->addPermanentWidget(m_statusCenter, 0); // Add "manage accounts" button, right align - QWidget *spacer = new QWidget(); + QWidget* spacer = new QWidget(); spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); ui->mainToolBar->insertWidget(ui->actionAccountsButton, spacer); @@ -353,21 +357,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi // Update the menu when the active account changes. // Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit. // Template hell sucks... - connect( - APPLICATION->accounts().get(), - &AccountList::defaultAccountChanged, - [this] { - defaultAccountChanged(); - } - ); - connect( - APPLICATION->accounts().get(), - &AccountList::listChanged, - [this] - { - repopulateAccountsMenu(); - } - ); + connect(APPLICATION->accounts().get(), &AccountList::defaultAccountChanged, [this] { defaultAccountChanged(); }); + connect(APPLICATION->accounts().get(), &AccountList::listChanged, [this] { repopulateAccountsMenu(); }); // Show initial account defaultAccountChanged(); @@ -408,9 +399,9 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi // macOS always has a native menu bar, so these fixes are not applicable // Other systems may or may not have a native menu bar (most do not - it seems like only Ubuntu Unity does) #ifndef Q_OS_MAC -void MainWindow::keyReleaseEvent(QKeyEvent *event) +void MainWindow::keyReleaseEvent(QKeyEvent* event) { - if(event->key()==Qt::Key_Alt && !APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool()) + if (event->key() == Qt::Key_Alt && !APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool()) ui->menuBar->setVisible(!ui->menuBar->isVisible()); else QMainWindow::keyReleaseEvent(event); @@ -419,7 +410,6 @@ void MainWindow::keyReleaseEvent(QKeyEvent *event) void MainWindow::retranslateUi() { - if (m_selectedInstance) { m_statusLeft->setText(m_selectedInstance->getStatusbarDescription()); } else { @@ -429,7 +419,7 @@ void MainWindow::retranslateUi() ui->retranslateUi(this); MinecraftAccountPtr defaultAccount = APPLICATION->accounts()->defaultAccount(); - if(defaultAccount) { + if (defaultAccount) { auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse()); ui->actionAccountsButton->setText(profileLabel); } @@ -449,14 +439,12 @@ void MainWindow::retranslateUi() } } -MainWindow::~MainWindow() -{ -} +MainWindow::~MainWindow() {} -QMenu * MainWindow::createPopupMenu() +QMenu* MainWindow::createPopupMenu() { QMenu* filteredMenu = QMainWindow::createPopupMenu(); - filteredMenu->removeAction( ui->mainToolBar->toggleViewAction() ); + filteredMenu->removeAction(ui->mainToolBar->toggleViewAction()); filteredMenu->addAction(ui->actionLockToolbars); @@ -470,10 +458,11 @@ void MainWindow::lockToolbars(bool state) APPLICATION->settings()->set("ToolbarsLocked", state); } - void MainWindow::konamiTriggered() { - QString gradient = " stop:0 rgba(125, 0, 0, 255), stop:0.166 rgba(125, 125, 0, 255), stop:0.333 rgba(0, 125, 0, 255), stop:0.5 rgba(0, 125, 125, 255), stop:0.666 rgba(0, 0, 125, 255), stop:0.833 rgba(125, 0, 125, 255), stop:1 rgba(125, 0, 0, 255));"; + QString gradient = + " stop:0 rgba(125, 0, 0, 255), stop:0.166 rgba(125, 125, 0, 255), stop:0.333 rgba(0, 125, 0, 255), stop:0.5 rgba(0, 125, 125, " + "255), stop:0.666 rgba(0, 0, 125, 255), stop:0.833 rgba(125, 0, 125, 255), stop:1 rgba(125, 0, 0, 255));"; QString stylesheet = "background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0," + gradient; if (ui->mainToolBar->styleSheet() == stylesheet) { ui->mainToolBar->setStyleSheet(""); @@ -492,16 +481,15 @@ void MainWindow::konamiTriggered() } } -void MainWindow::showInstanceContextMenu(const QPoint &pos) +void MainWindow::showInstanceContextMenu(const QPoint& pos) { - QList actions; + QList actions; - QAction *actionSep = new QAction("", this); + QAction* actionSep = new QAction("", this); actionSep->setSeparator(true); bool onInstance = view->indexAt(pos).isValid(); - if (onInstance) - { + if (onInstance) { // reuse the file menu actions actions = ui->fileMenu->actions(); @@ -515,21 +503,18 @@ void MainWindow::showInstanceContextMenu(const QPoint &pos) // add header actions.prepend(actionSep); - QAction *actionVoid = new QAction(m_selectedInstance->name(), this); + QAction* actionVoid = new QAction(m_selectedInstance->name(), this); actionVoid->setEnabled(false); actions.prepend(actionVoid); - } - else - { + } else { auto group = view->groupNameAt(pos); - QAction *actionVoid = new QAction(BuildConfig.LAUNCHER_DISPLAYNAME, this); + QAction* actionVoid = new QAction(BuildConfig.LAUNCHER_DISPLAYNAME, this); actionVoid->setEnabled(false); - QAction *actionCreateInstance = new QAction(tr("Create instance"), this); + QAction* actionCreateInstance = new QAction(tr("Create instance"), this); actionCreateInstance->setToolTip(ui->actionAddInstance->toolTip()); - if(!group.isNull()) - { + if (!group.isNull()) { QVariantMap data; data["group"] = group; actionCreateInstance->setData(data); @@ -540,9 +525,8 @@ void MainWindow::showInstanceContextMenu(const QPoint &pos) actions.prepend(actionSep); actions.prepend(actionVoid); actions.append(actionCreateInstance); - if(!group.isNull()) - { - QAction *actionDeleteGroup = new QAction(tr("Delete group '%1'").arg(group), this); + if (!group.isNull()) { + QAction* actionDeleteGroup = new QAction(tr("Delete group '%1'").arg(group), this); QVariantMap data; data["group"] = group; actionDeleteGroup->setData(data); @@ -573,39 +557,27 @@ void MainWindow::updateToolsMenu() ui->actionLaunchInstanceOffline->setDisabled(!m_selectedInstance || currentInstanceRunning); ui->actionLaunchInstanceDemo->setDisabled(!m_selectedInstance || currentInstanceRunning); - QMenu *launchMenu = ui->actionLaunchInstance->menu(); - if (launchMenu) - { + QMenu* launchMenu = ui->actionLaunchInstance->menu(); + if (launchMenu) { launchMenu->clear(); - } - else - { + } else { launchMenu = new QMenu(this); } - QAction *normalLaunch = launchMenu->addAction(tr("Launch")); + QAction* normalLaunch = launchMenu->addAction(tr("Launch")); normalLaunch->setShortcut(QKeySequence::Open); - QAction *normalLaunchOffline = launchMenu->addAction(tr("Launch Offline")); + QAction* normalLaunchOffline = launchMenu->addAction(tr("Launch Offline")); normalLaunchOffline->setShortcut(QKeySequence(tr("Ctrl+Shift+O"))); - QAction *normalLaunchDemo = launchMenu->addAction(tr("Launch Demo")); + QAction* normalLaunchDemo = launchMenu->addAction(tr("Launch Demo")); normalLaunchDemo->setShortcut(QKeySequence(tr("Ctrl+Alt+O"))); - if (m_selectedInstance) - { + if (m_selectedInstance) { normalLaunch->setEnabled(m_selectedInstance->canLaunch()); normalLaunchOffline->setEnabled(m_selectedInstance->canLaunch()); normalLaunchDemo->setEnabled(m_selectedInstance->canLaunch()); - connect(normalLaunch, &QAction::triggered, [this]() { - APPLICATION->launch(m_selectedInstance, true, false); - }); - connect(normalLaunchOffline, &QAction::triggered, [this]() { - APPLICATION->launch(m_selectedInstance, false, false); - }); - connect(normalLaunchDemo, &QAction::triggered, [this]() { - APPLICATION->launch(m_selectedInstance, false, true); - }); - } - else - { + connect(normalLaunch, &QAction::triggered, [this]() { APPLICATION->launch(m_selectedInstance, true, false); }); + connect(normalLaunchOffline, &QAction::triggered, [this]() { APPLICATION->launch(m_selectedInstance, false, false); }); + connect(normalLaunchDemo, &QAction::triggered, [this]() { APPLICATION->launch(m_selectedInstance, false, true); }); + } else { normalLaunch->setDisabled(true); normalLaunchOffline->setDisabled(true); normalLaunchDemo->setDisabled(true); @@ -619,35 +591,25 @@ void MainWindow::updateToolsMenu() QString profilersTitle = tr("Profilers"); launchMenu->addSeparator()->setText(profilersTitle); - for (auto profiler : APPLICATION->profilers().values()) - { - QAction *profilerAction = launchMenu->addAction(profiler->name()); - QAction *profilerOfflineAction = launchMenu->addAction(tr("%1 Offline").arg(profiler->name())); + for (auto profiler : APPLICATION->profilers().values()) { + QAction* profilerAction = launchMenu->addAction(profiler->name()); + QAction* profilerOfflineAction = launchMenu->addAction(tr("%1 Offline").arg(profiler->name())); QString error; - if (!profiler->check(&error)) - { + if (!profiler->check(&error)) { profilerAction->setDisabled(true); profilerOfflineAction->setDisabled(true); QString profilerToolTip = tr("Profiler not setup correctly. Go into settings, \"External Tools\"."); profilerAction->setToolTip(profilerToolTip); profilerOfflineAction->setToolTip(profilerToolTip); - } - else if (m_selectedInstance) - { + } else if (m_selectedInstance) { profilerAction->setEnabled(m_selectedInstance->canLaunch()); profilerOfflineAction->setEnabled(m_selectedInstance->canLaunch()); - connect(profilerAction, &QAction::triggered, [this, profiler]() - { - APPLICATION->launch(m_selectedInstance, true, false, profiler.get()); - }); - connect(profilerOfflineAction, &QAction::triggered, [this, profiler]() - { - APPLICATION->launch(m_selectedInstance, false, false, profiler.get()); - }); - } - else - { + connect(profilerAction, &QAction::triggered, + [this, profiler]() { APPLICATION->launch(m_selectedInstance, true, false, profiler.get()); }); + connect(profilerOfflineAction, &QAction::triggered, + [this, profiler]() { APPLICATION->launch(m_selectedInstance, false, false, profiler.get()); }); + } else { profilerAction->setDisabled(true); profilerOfflineAction->setDisabled(true); } @@ -657,7 +619,7 @@ void MainWindow::updateToolsMenu() void MainWindow::updateThemeMenu() { - QMenu *themeMenu = ui->actionChangeTheme->menu(); + QMenu* themeMenu = ui->actionChangeTheme->menu(); if (themeMenu) { themeMenu->clear(); @@ -667,10 +629,10 @@ void MainWindow::updateThemeMenu() auto themes = APPLICATION->getValidApplicationThemes(); - QActionGroup* themesGroup = new QActionGroup( this ); + QActionGroup* themesGroup = new QActionGroup(this); for (auto* theme : themes) { - QAction * themeAction = themeMenu->addAction(theme->name()); + QAction* themeAction = themeMenu->addAction(theme->name()); themeAction->setCheckable(true); if (APPLICATION->settings()->get("ApplicationTheme").toString() == theme->id()) { @@ -692,7 +654,7 @@ void MainWindow::repopulateAccountsMenu() ui->accountsMenu->clear(); // NOTE: this is done so the accounts button text is not set to the accounts menu title - QMenu *accountsButtonMenu = ui->actionAccountsButton->menu(); + QMenu* accountsButtonMenu = ui->actionAccountsButton->menu(); if (accountsButtonMenu) { accountsButtonMenu->clear(); } else { @@ -704,11 +666,9 @@ void MainWindow::repopulateAccountsMenu() MinecraftAccountPtr defaultAccount = accounts->defaultAccount(); QString active_profileId = ""; - if (defaultAccount) - { + if (defaultAccount) { // this can be called before accountMenuButton exists - if (ui->actionAccountsButton) - { + if (ui->actionAccountsButton) { auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse()); ui->actionAccountsButton->setText(profileLabel); } @@ -716,38 +676,31 @@ void MainWindow::repopulateAccountsMenu() QActionGroup* accountsGroup = new QActionGroup(this); - if (accounts->count() <= 0) - { + if (accounts->count() <= 0) { ui->actionNoAccountsAdded->setEnabled(false); ui->accountsMenu->addAction(ui->actionNoAccountsAdded); - } - else - { + } else { // TODO: Nicer way to iterate? - for (int i = 0; i < accounts->count(); i++) - { + for (int i = 0; i < accounts->count(); i++) { MinecraftAccountPtr account = accounts->at(i); auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse()); - QAction *action = new QAction(profileLabel, this); + QAction* action = new QAction(profileLabel, this); action->setData(i); action->setCheckable(true); action->setActionGroup(accountsGroup); - if (defaultAccount == account) - { + if (defaultAccount == account) { action->setChecked(true); } auto face = account->getFace(); - if(!face.isNull()) { + if (!face.isNull()) { action->setIcon(face); - } - else { + } else { action->setIcon(APPLICATION->getThemedIcon("noaccount")); } const int highestNumberKey = 9; - if(isetShortcut(QKeySequence(tr("Ctrl+%1").arg(i + 1))); } @@ -774,8 +727,7 @@ void MainWindow::repopulateAccountsMenu() void MainWindow::updatesAllowedChanged(bool allowed) { - if(!BuildConfig.UPDATER_ENABLED) - { + if (!BuildConfig.UPDATER_ENABLED) { return; } ui->actionCheckUpdate->setEnabled(allowed); @@ -786,7 +738,7 @@ void MainWindow::updatesAllowedChanged(bool allowed) */ void MainWindow::changeActiveAccount() { - QAction *sAction = (QAction *)sender(); + QAction* sAction = (QAction*)sender(); // Profile's associated Mojang username if (sAction->data().type() != QVariant::Type::Int) @@ -795,7 +747,7 @@ void MainWindow::changeActiveAccount() QVariant data = sAction->data(); bool valid = false; int index = data.toInt(&valid); - if(!valid) { + if (!valid) { index = -1; } auto accounts = APPLICATION->accounts(); @@ -810,15 +762,13 @@ void MainWindow::defaultAccountChanged() MinecraftAccountPtr account = APPLICATION->accounts()->defaultAccount(); // FIXME: this needs adjustment for MSA - if (account && account->profileName() != "") - { + if (account && account->profileName() != "") { auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse()); ui->actionAccountsButton->setText(profileLabel); auto face = account->getFace(); - if(face.isNull()) { + if (face.isNull()) { ui->actionAccountsButton->setIcon(APPLICATION->getThemedIcon("noaccount")); - } - else { + } else { ui->actionAccountsButton->setIcon(face); } return; @@ -829,33 +779,30 @@ void MainWindow::defaultAccountChanged() ui->actionAccountsButton->setText(tr("Accounts")); } -bool MainWindow::eventFilter(QObject *obj, QEvent *ev) +bool MainWindow::eventFilter(QObject* obj, QEvent* ev) { - if (obj == view) - { - if (ev->type() == QEvent::KeyPress) - { + if (obj == view) { + if (ev->type() == QEvent::KeyPress) { secretEventFilter->input(ev); - QKeyEvent *keyEvent = static_cast(ev); - switch (keyEvent->key()) - { - /* - case Qt::Key_Enter: - case Qt::Key_Return: - activateInstance(m_selectedInstance); - return true; - */ - case Qt::Key_Delete: - on_actionDeleteInstance_triggered(); - return true; - case Qt::Key_F5: - refreshInstances(); - return true; - case Qt::Key_F2: - on_actionRenameInstance_triggered(); - return true; - default: - break; + QKeyEvent* keyEvent = static_cast(ev); + switch (keyEvent->key()) { + /* + case Qt::Key_Enter: + case Qt::Key_Return: + activateInstance(m_selectedInstance); + return true; + */ + case Qt::Key_Delete: + on_actionDeleteInstance_triggered(); + return true; + case Qt::Key_F5: + refreshInstances(); + return true; + case Qt::Key_F2: + on_actionRenameInstance_triggered(); + return true; + default: + break; } } } @@ -864,23 +811,17 @@ bool MainWindow::eventFilter(QObject *obj, QEvent *ev) void MainWindow::updateNewsLabel() { - if (m_newsChecker->isLoadingNews()) - { + if (m_newsChecker->isLoadingNews()) { newsLabel->setText(tr("Loading news...")); newsLabel->setEnabled(false); ui->actionMoreNews->setVisible(false); - } - else - { + } else { QList entries = m_newsChecker->getNewsEntries(); - if (entries.length() > 0) - { + if (entries.length() > 0) { newsLabel->setText(entries[0]->title); newsLabel->setEnabled(true); ui->actionMoreNews->setVisible(true); - } - else - { + } else { newsLabel->setText(tr("No news available.")); newsLabel->setEnabled(false); ui->actionMoreNews->setVisible(false); @@ -888,7 +829,7 @@ void MainWindow::updateNewsLabel() } } -QList stringToIntList(const QString &string) +QList stringToIntList(const QString& string) { #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) QStringList split = string.split(',', Qt::SkipEmptyParts); @@ -896,17 +837,15 @@ QList stringToIntList(const QString &string) QStringList split = string.split(',', QString::SkipEmptyParts); #endif QList out; - for (int i = 0; i < split.size(); ++i) - { + for (int i = 0; i < split.size(); ++i) { out.append(split.at(i).toInt()); } return out; } -QString intListToString(const QList &list) +QString intListToString(const QList& list) { QStringList slist; - for (int i = 0; i < list.size(); ++i) - { + for (int i = 0; i < list.size(); ++i) { slist.append(QString::number(list.at(i))); } return slist.join(','); @@ -920,47 +859,30 @@ void MainWindow::onCatToggled(bool state) void MainWindow::setCatBackground(bool enabled) { - if (enabled) { - view->setStyleSheet(QString(R"( -InstanceView -{ - background-image: url(:/backgrounds/%1); - background-attachment: fixed; - background-clip: padding; - background-position: bottom right; - background-repeat: none; - background-color:palette(base); -})") - .arg(ThemeManager::getCatImage())); - } else { - view->setStyleSheet(QString()); - } + view->setPaintCat(enabled); + view->viewport()->repaint(); } -void MainWindow::runModalTask(Task *task) +void MainWindow::runModalTask(Task* task) { - connect(task, &Task::failed, [this](QString reason) - { - CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); - }); - connect(task, &Task::succeeded, [this, task]() - { - QStringList warnings = task->warnings(); - if(warnings.count()) - { - CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); - } - }); - connect(task, &Task::aborted, [this] - { - CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)->show(); - }); + connect(task, &Task::failed, + [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); + connect(task, &Task::succeeded, [this, task]() { + QStringList warnings = task->warnings(); + if (warnings.count()) { + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + } + }); + connect(task, &Task::aborted, [this] { + CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information) + ->show(); + }); ProgressDialog loadDialog(this); loadDialog.setSkipButton(true, tr("Abort")); loadDialog.execWithTask(task); } -void MainWindow::instanceFromInstanceTask(InstanceTask *rawTask) +void MainWindow::instanceFromInstanceTask(InstanceTask* rawTask) { unique_qobject_ptr task(APPLICATION->instances()->wrapInstanceTask(rawTask)); runModalTask(task.get()); @@ -987,52 +909,43 @@ void MainWindow::finalizeInstance(InstancePtr inst) { view->updateGeometries(); setSelectedInstanceById(inst->id()); - if (APPLICATION->accounts()->anyAccountIsValid()) - { + if (APPLICATION->accounts()->anyAccountIsValid()) { ProgressDialog loadDialog(this); auto update = inst->createUpdateTask(Net::Mode::Online); - connect(update.get(), &Task::failed, [this](QString reason) - { - QString error = QString("Instance load failed: %1").arg(reason); - CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show(); - }); - if(update) - { + connect(update.get(), &Task::failed, [this](QString reason) { + QString error = QString("Instance load failed: %1").arg(reason); + CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show(); + }); + if (update) { loadDialog.setSkipButton(true, tr("Abort")); loadDialog.execWithTask(update.get()); } - } - else - { - CustomMessageBox::selectable( - this, - tr("Error"), - tr("The launcher cannot download Minecraft or update instances unless you have at least " - "one account added.\nPlease add your Mojang or Minecraft account."), - QMessageBox::Warning - )->show(); + } else { + CustomMessageBox::selectable(this, tr("Error"), + tr("The launcher cannot download Minecraft or update instances unless you have at least " + "one account added.\nPlease add your Mojang or Minecraft account."), + QMessageBox::Warning) + ->show(); } } void MainWindow::addInstance(QString url) { QString groupName; - do - { + do { QObject* obj = sender(); - if(!obj) + if (!obj) break; - QAction *action = qobject_cast(obj); - if(!action) + QAction* action = qobject_cast(obj); + if (!action) break; auto map = action->data().toMap(); - if(!map.contains("group")) + if (!map.contains("group")) break; groupName = map["group"].toString(); - } while(0); + } while (0); - if(groupName.isEmpty()) - { + if (groupName.isEmpty()) { groupName = APPLICATION->settings()->get("LastUsedGroupForNewInstance").toString(); } @@ -1042,9 +955,8 @@ void MainWindow::addInstance(QString url) APPLICATION->settings()->set("LastUsedGroupForNewInstance", newInstDlg.instGroup()); - InstanceTask * creationTask = newInstDlg.extractTask(); - if(creationTask) - { + InstanceTask* creationTask = newInstDlg.extractTask(); + if (creationTask) { instanceFromInstanceTask(creationTask); } } @@ -1069,7 +981,7 @@ void MainWindow::processURLs(QList urls) break; } - auto localFileName = QDir::toNativeSeparators(url.toLocalFile()) ; + auto localFileName = QDir::toNativeSeparators(url.toLocalFile()); QFileInfo localFileInfo(localFileName); auto type = ResourceUtils::identify(localFileInfo); @@ -1138,8 +1050,7 @@ void MainWindow::on_actionChangeInstIcon_triggered() IconPickerDialog dlg(this); dlg.execWithSelection(m_selectedInstance->iconKey()); - if (dlg.result() == QDialog::Accepted) - { + if (dlg.result() == QDialog::Accepted) { m_selectedInstance->setIconKey(dlg.selectedIconKey); auto icon = APPLICATION->icons()->getIcon(dlg.selectedIconKey); ui->actionChangeInstIcon->setIcon(icon); @@ -1149,8 +1060,7 @@ void MainWindow::on_actionChangeInstIcon_triggered() void MainWindow::iconUpdated(QString icon) { - if (icon == m_currentInstIcon) - { + if (icon == m_currentInstIcon) { auto icon = APPLICATION->icons()->getIcon(m_currentInstIcon); ui->actionChangeInstIcon->setIcon(icon); changeIconButton->setIcon(icon); @@ -1165,13 +1075,12 @@ void MainWindow::updateInstanceToolIcon(QString new_icon) changeIconButton->setIcon(icon); } -void MainWindow::setSelectedInstanceById(const QString &id) +void MainWindow::setSelectedInstanceById(const QString& id) { if (id.isNull()) return; const QModelIndex index = APPLICATION->instances()->getInstanceIndexById(id); - if (index.isValid()) - { + if (index.isValid()) { QModelIndex selectionIndex = proxymodel->mapFromSource(index); view->selectionModel()->setCurrentIndex(selectionIndex, QItemSelectionModel::ClearAndSelect); updateStatusCenter(); @@ -1193,8 +1102,7 @@ void MainWindow::on_actionChangeInstGroup_triggered() name = QInputDialog::getItem(this, tr("Group name"), tr("Enter a new group name."), groups, foo, true, &ok); name = name.simplified(); - if (ok) - { + if (ok) { APPLICATION->instances()->setInstanceGroup(instId, name); } } @@ -1202,21 +1110,19 @@ void MainWindow::on_actionChangeInstGroup_triggered() void MainWindow::deleteGroup() { QObject* obj = sender(); - if(!obj) + if (!obj) return; - QAction *action = qobject_cast(obj); - if(!action) + QAction* action = qobject_cast(obj); + if (!action) return; auto map = action->data().toMap(); - if(!map.contains("group")) + if (!map.contains("group")) return; QString groupName = map["group"].toString(); - if(!groupName.isEmpty()) - { - auto reply = QMessageBox::question(this, tr("Delete group"), tr("Are you sure you want to delete the group %1?") - .arg(groupName), QMessageBox::Yes | QMessageBox::No); - if(reply == QMessageBox::Yes) - { + if (!groupName.isEmpty()) { + auto reply = QMessageBox::question(this, tr("Delete group"), tr("Are you sure you want to delete the group %1?").arg(groupName), + QMessageBox::Yes | QMessageBox::No); + if (reply == QMessageBox::Yes) { APPLICATION->instances()->deleteGroup(groupName); } } @@ -1234,6 +1140,12 @@ void MainWindow::on_actionViewInstanceFolder_triggered() DesktopServices::openDirectory(str); } +void MainWindow::on_actionViewLauncherRootFolder_triggered() +{ + const QString dataPath = QDir::currentPath(); + DesktopServices::openDirectory(dataPath); +} + void MainWindow::refreshInstances() { APPLICATION->instances()->loadList(); @@ -1246,12 +1158,9 @@ void MainWindow::on_actionViewCentralModsFolder_triggered() void MainWindow::checkForUpdates() { - if(BuildConfig.UPDATER_ENABLED) - { + if (BuildConfig.UPDATER_ENABLED) { APPLICATION->triggerUpdateCheck(); - } - else - { + } else { qWarning() << "Updater not set up. Cannot check for updates."; } } @@ -1279,7 +1188,17 @@ void MainWindow::globalSettingsClosed() void MainWindow::on_actionEditInstance_triggered() { - APPLICATION->showInstanceWindow(m_selectedInstance); + if (!m_selectedInstance) + return; + + if (m_selectedInstance->canEdit()) { + APPLICATION->showInstanceWindow(m_selectedInstance); + } else { + CustomMessageBox::selectable(this, tr("Instance not editable"), + tr("This instance is not editable. It may be broken, invalid, or too old. Check logs for details."), + QMessageBox::Critical) + ->show(); + } } void MainWindow::on_actionManageAccounts_triggered() @@ -1341,7 +1260,8 @@ void MainWindow::newsButtonClicked() news_dialog.exec(); } -void MainWindow::onCatChanged(int) { +void MainWindow::onCatChanged(int) +{ setCatBackground(APPLICATION->settings()->get("TheCat").toBool()); } @@ -1372,14 +1292,15 @@ void MainWindow::on_actionDeleteInstance_triggered() auto linkedInstances = APPLICATION->instances()->getLinkedInstancesById(id); if (!linkedInstances.empty()) { - response = CustomMessageBox::selectable( - this, tr("There are linked instances"), - tr("The following instance(s) might reference files in this instance:\n\n" - "%1\n\n" - "Deleting it could break the other instance(s), \n\n" - "Do you wish to proceed?", nullptr, linkedInstances.count()).arg(linkedInstances.join("\n")), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No - )->exec(); + response = CustomMessageBox::selectable(this, tr("There are linked instances"), + tr("The following instance(s) might reference files in this instance:\n\n" + "%1\n\n" + "Deleting it could break the other instance(s), \n\n" + "Do you wish to proceed?", + nullptr, linkedInstances.count()) + .arg(linkedInstances.join("\n")), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); if (response != QMessageBox::Yes) return; } @@ -1394,8 +1315,7 @@ void MainWindow::on_actionDeleteInstance_triggered() void MainWindow::on_actionExportInstanceZip_triggered() { - if (m_selectedInstance) - { + if (m_selectedInstance) { ExportInstanceDialog dlg(m_selectedInstance, this); dlg.exec(); } @@ -1403,31 +1323,60 @@ void MainWindow::on_actionExportInstanceZip_triggered() void MainWindow::on_actionExportInstanceMrPack_triggered() { - if (m_selectedInstance) - { - ExportMrPackDialog dlg(m_selectedInstance, this); + if (m_selectedInstance) { + ExportPackDialog dlg(m_selectedInstance, this); dlg.exec(); } } +void MainWindow::on_actionExportInstanceToModList_triggered() +{ + if (m_selectedInstance) { + ExportToModListDialog dlg(m_selectedInstance, this); + dlg.exec(); + } +} + +void MainWindow::on_actionExportInstanceFlamePack_triggered() +{ + if (m_selectedInstance) { + auto instance = dynamic_cast(m_selectedInstance.get()); + if (instance) { + QString errorMsg; + if (instance->getPackProfile()->getComponent("org.quiltmc.quilt-loader")) { + errorMsg = tr("Quilt is currently not supported by CurseForge modpacks."); + } else if (auto cmp = instance->getPackProfile()->getComponent("net.minecraft"); + cmp && cmp->getVersionFile() && cmp->getVersionFile()->type == "snapshot") { + errorMsg = tr("Snapshots are currently not supported by CurseForge modpacks."); + } + if (!errorMsg.isEmpty()) { + QMessageBox msgBox; + msgBox.setText(errorMsg); + msgBox.exec(); + return; + } + ExportPackDialog dlg(m_selectedInstance, this, ModPlatform::ResourceProvider::FLAME); + dlg.exec(); + } + } +} + void MainWindow::on_actionRenameInstance_triggered() { - if (m_selectedInstance) - { + if (m_selectedInstance) { view->edit(view->currentIndex()); } } void MainWindow::on_actionViewSelectedInstFolder_triggered() { - if (m_selectedInstance) - { + if (m_selectedInstance) { QString str = m_selectedInstance->instanceRoot(); DesktopServices::openDirectory(QDir(str).absolutePath()); } } -void MainWindow::closeEvent(QCloseEvent *event) +void MainWindow::closeEvent(QCloseEvent* event) { // Save the window state and geometry. APPLICATION->settings()->set("MainWindowState", saveState().toBase64()); @@ -1439,8 +1388,7 @@ void MainWindow::closeEvent(QCloseEvent *event) void MainWindow::changeEvent(QEvent* event) { - if (event->type() == QEvent::LanguageChange) - { + if (event->type() == QEvent::LanguageChange) { retranslateUi(); } QMainWindow::changeEvent(event); @@ -1460,8 +1408,7 @@ void MainWindow::instanceActivated(QModelIndex index) void MainWindow::on_actionLaunchInstance_triggered() { - if(m_selectedInstance && !m_selectedInstance->isRunning()) - { + if (m_selectedInstance && !m_selectedInstance->isRunning()) { APPLICATION->launch(m_selectedInstance); } } @@ -1473,187 +1420,185 @@ void MainWindow::activateInstance(InstancePtr instance) void MainWindow::on_actionLaunchInstanceOffline_triggered() { - if (m_selectedInstance) - { + if (m_selectedInstance) { APPLICATION->launch(m_selectedInstance, false); } } void MainWindow::on_actionLaunchInstanceDemo_triggered() { - if (m_selectedInstance) - { + if (m_selectedInstance) { APPLICATION->launch(m_selectedInstance, false, true); } } void MainWindow::on_actionKillInstance_triggered() { - if(m_selectedInstance && m_selectedInstance->isRunning()) - { + if (m_selectedInstance && m_selectedInstance->isRunning()) { APPLICATION->kill(m_selectedInstance); } } void MainWindow::on_actionCreateInstanceShortcut_triggered() { - if (m_selectedInstance) - { - auto desktopPath = FS::getDesktopDir(); - if (desktopPath.isEmpty()) { - // TODO come up with an alternative solution (open "save file" dialog) - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Couldn't find desktop?!")); - return; - } + if (!m_selectedInstance) + return; + auto desktopPath = FS::getDesktopDir(); + if (desktopPath.isEmpty()) { + // TODO come up with an alternative solution (open "save file" dialog) + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Couldn't find desktop?!")); + return; + } + QString desktopFilePath; + QString appPath = QApplication::applicationFilePath(); + QString iconPath; + QStringList args; #if defined(Q_OS_MACOS) - QString appPath = QApplication::applicationFilePath(); - if (appPath.startsWith("/private/var/")) { - QMessageBox::critical(this, tr("Create instance shortcut"), tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); - return; - } + appPath = QApplication::applicationFilePath(); + if (appPath.startsWith("/private/var/")) { + QMessageBox::critical(this, tr("Create instance shortcut"), + tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); + return; + } - if (FS::createShortcut(FS::PathCombine(desktopPath, m_selectedInstance->name()), - appPath, { "--launch", m_selectedInstance->id() }, - m_selectedInstance->name(), "")) { - QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); - } - else - { - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); - } + auto pIcon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); + if (pIcon == nullptr) { + pIcon = APPLICATION->icons()->icon("grass"); + } + + iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "Icon.icns"); + + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) { + QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application.")); + return; + } + + QIcon icon = pIcon->icon(); + + bool success = icon.pixmap(1024, 1024).save(iconPath, "ICNS"); + iconFile.close(); + + if (!success) { + iconFile.remove(); + QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application.")); + return; + } #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) - QString appPath = QApplication::applicationFilePath(); - if (appPath.startsWith("/tmp/.mount_")) { - // AppImage! - appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); - if (appPath.isEmpty()) - { - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); - } - else if (appPath.endsWith("/")) - { - appPath.chop(1); - } + if (appPath.startsWith("/tmp/.mount_")) { + // AppImage! + appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); + if (appPath.isEmpty()) { + QMessageBox::critical(this, tr("Create instance shortcut"), + tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); + } else if (appPath.endsWith("/")) { + appPath.chop(1); } + } - auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); - if (icon == nullptr) - { - icon = APPLICATION->icons()->icon("grass"); - } + auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); + if (icon == nullptr) { + icon = APPLICATION->icons()->icon("grass"); + } - QString iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.png"); + iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.png"); - QFile iconFile(iconPath); - if (!iconFile.open(QFile::WriteOnly)) - { - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } - bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); - iconFile.close(); + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } + bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); + iconFile.close(); - if (!success) - { - iconFile.remove(); - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } + if (!success) { + iconFile.remove(); + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } + + if (DesktopServices::isFlatpak()) { + desktopFilePath = FS::PathCombine(desktopPath, FS::RemoveInvalidFilenameChars(m_selectedInstance->name()) + ".desktop"); + QFileDialog fileDialog; + // workaround to make sure the portal file dialog opens in the desktop directory + fileDialog.setDirectoryUrl(desktopPath); + desktopFilePath = fileDialog.getSaveFileName(this, tr("Create Shortcut"), desktopFilePath, tr("Desktop Entries (*.desktop)")); + if (desktopFilePath.isEmpty()) + return; // file dialog canceled by user + appPath = "flatpak"; + QString flatpakAppId = BuildConfig.LAUNCHER_DESKTOPFILENAME; + flatpakAppId.remove(".desktop"); + args.append({ "run", flatpakAppId }); + } - QString desktopFilePath = FS::PathCombine(desktopPath, m_selectedInstance->name() + ".desktop"); - QStringList args; - if (DesktopServices::isFlatpak()) { - QFileDialog fileDialog; - // workaround to make sure the portal file dialog opens in the desktop directory - fileDialog.setDirectoryUrl(desktopPath); - desktopFilePath = fileDialog.getSaveFileName( - this, tr("Create Shortcut"), desktopFilePath, - tr("Desktop Entries (*.desktop)")); - if (desktopFilePath.isEmpty()) - return; // file dialog canceled by user - appPath = "flatpak"; - QString flatpakAppId = BuildConfig.LAUNCHER_DESKTOPFILENAME; - flatpakAppId.remove(".desktop"); - args.append({ "run", flatpakAppId }); - } - args.append({ "--launch", m_selectedInstance->id() }); - if (FS::createShortcut(desktopFilePath, appPath, args, m_selectedInstance->name(), iconPath)) { - QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); - } - else - { - iconFile.remove(); - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); - } #elif defined(Q_OS_WIN) - auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); - if (icon == nullptr) - { - icon = APPLICATION->icons()->icon("grass"); - } + auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); + if (icon == nullptr) { + icon = APPLICATION->icons()->icon("grass"); + } - QString iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.ico"); + iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.ico"); - // part of fix for weird bug involving the window icon being replaced - // dunno why it happens, but this 2-line fix seems to be enough, so w/e - auto appIcon = APPLICATION->getThemedIcon("logo"); + // part of fix for weird bug involving the window icon being replaced + // dunno why it happens, but this 2-line fix seems to be enough, so w/e + auto appIcon = APPLICATION->getThemedIcon("logo"); - QFile iconFile(iconPath); - if (!iconFile.open(QFile::WriteOnly)) - { - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } - bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); - iconFile.close(); + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } + bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); + iconFile.close(); - // restore original window icon - QGuiApplication::setWindowIcon(appIcon); + // restore original window icon + QGuiApplication::setWindowIcon(appIcon); - if (!success) - { - iconFile.remove(); - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } + if (!success) { + iconFile.remove(); + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } - if (FS::createShortcut(FS::PathCombine(desktopPath, m_selectedInstance->name()), - QApplication::applicationFilePath(), { "--launch", m_selectedInstance->id() }, - m_selectedInstance->name(), iconPath)) { - QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); - } - else - { - iconFile.remove(); - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); - } #else - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Not supported on your platform!")); + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Not supported on your platform!")); + return; #endif + args.append({ "--launch", m_selectedInstance->id() }); + if (FS::createShortcut(desktopFilePath, appPath, args, m_selectedInstance->name(), iconPath)) { +#if not defined(Q_OS_MACOS) + QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); +#else + QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance!")); +#endif + } else { +#if not defined(Q_OS_MACOS) + iconFile.remove(); +#endif + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); } } void MainWindow::taskEnd() { - QObject *sender = QObject::sender(); + QObject* sender = QObject::sender(); if (sender == m_versionLoadTask) m_versionLoadTask = NULL; sender->deleteLater(); } -void MainWindow::startTask(Task *task) +void MainWindow::startTask(Task* task) { connect(task, SIGNAL(succeeded()), SLOT(taskEnd())); connect(task, SIGNAL(failed(QString)), SLOT(taskEnd())); task->start(); } -void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex &previous) +void MainWindow::instanceChanged(const QModelIndex& current, const QModelIndex& previous) { - if (!current.isValid()) - { + if (!current.isValid()) { APPLICATION->settings()->set("SelectedInstance", QString()); selectionBad(); return; @@ -1663,8 +1608,7 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex & } QString id = current.data(InstanceList::InstanceIDRole).toString(); m_selectedInstance = APPLICATION->instances()->getInstanceById(id); - if (m_selectedInstance) - { + if (m_selectedInstance) { ui->instanceToolBar->setEnabled(true); setInstanceActionsEnabled(true); ui->actionLaunchInstance->setEnabled(m_selectedInstance->canLaunch()); @@ -1674,7 +1618,7 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex & // Disable demo-mode if not available. auto instance = dynamic_cast(m_selectedInstance.get()); if (instance) { - ui->actionLaunchInstanceDemo->setEnabled(instance->supportsDemo()); + ui->actionLaunchInstanceDemo->setEnabled(instance->supportsDemo()); } ui->actionKillInstance->setEnabled(m_selectedInstance->isRunning()); @@ -1689,9 +1633,7 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex & APPLICATION->settings()->set("SelectedInstance", m_selectedInstance->id()); connect(m_selectedInstance.get(), &BaseInstance::runningStatusChanged, this, &MainWindow::refreshCurrentInstance); - } - else - { + } else { ui->instanceToolBar->setEnabled(false); setInstanceActionsEnabled(false); ui->actionLaunchInstance->setEnabled(false); @@ -1709,12 +1651,11 @@ void MainWindow::instanceSelectRequest(QString id) setSelectedInstanceById(id); } -void MainWindow::instanceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +void MainWindow::instanceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { auto current = view->selectionModel()->currentIndex(); QItemSelection test(topLeft, bottomRight); - if (test.contains(current)) - { + if (test.contains(current)) { instanceChanged(current, current); } } @@ -1738,34 +1679,28 @@ void MainWindow::selectionBad() void MainWindow::checkInstancePathForProblems() { QString instanceFolder = APPLICATION->settings()->get("InstanceDir").toString(); - if (FS::checkProblemticPathJava(QDir(instanceFolder))) - { + if (FS::checkProblemticPathJava(QDir(instanceFolder))) { QMessageBox warning(this); warning.setText(tr("Your instance folder contains \'!\' and this is known to cause Java problems!")); - warning.setInformativeText( - tr( - "You have now two options:
    " - " - change the instance folder in the settings
    " - " - move this installation of %1 to a different folder" - ).arg(BuildConfig.LAUNCHER_DISPLAYNAME) - ); + warning.setInformativeText(tr("You have now two options:
    " + " - change the instance folder in the settings
    " + " - move this installation of %1 to a different folder") + .arg(BuildConfig.LAUNCHER_DISPLAYNAME)); warning.setDefaultButton(QMessageBox::Ok); warning.exec(); } - auto tempFolderText = tr("This is a problem:
    " - " - The launcher will likely be deleted without warning by the operating system
    " - " - close the launcher now and extract it to a real location, not a temporary folder"); + auto tempFolderText = + tr("This is a problem:
    " + " - The launcher will likely be deleted without warning by the operating system
    " + " - close the launcher now and extract it to a real location, not a temporary folder"); QString pathfoldername = QDir(instanceFolder).absolutePath(); - if (pathfoldername.contains("Rar$", Qt::CaseInsensitive)) - { + if (pathfoldername.contains("Rar$", Qt::CaseInsensitive)) { QMessageBox warning(this); warning.setText(tr("Your instance folder contains \'Rar$\' - that means you haven't extracted the launcher archive!")); warning.setInformativeText(tempFolderText); warning.setDefaultButton(QMessageBox::Ok); warning.exec(); - } - else if (pathfoldername.startsWith(QDir::tempPath()) || pathfoldername.contains("/TempState/")) - { + } else if (pathfoldername.startsWith(QDir::tempPath()) || pathfoldername.contains("/TempState/")) { QMessageBox warning(this); warning.setText(tr("Your instance folder is in a temporary folder: \'%1\'!").arg(QDir::tempPath())); warning.setInformativeText(tempFolderText); diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index a0f912df4..27c2756f6 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -113,6 +113,8 @@ private slots: void on_actionViewInstanceFolder_triggered(); + void on_actionViewLauncherRootFolder_triggered(); + void on_actionViewSelectedInstFolder_triggered(); void refreshInstances(); @@ -155,6 +157,8 @@ private slots: inline void on_actionExportInstance_triggered() { on_actionExportInstanceZip_triggered(); } void on_actionExportInstanceZip_triggered(); void on_actionExportInstanceMrPack_triggered(); + void on_actionExportInstanceFlamePack_triggered(); + void on_actionExportInstanceToModList_triggered(); void on_actionRenameInstance_triggered(); diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index 9e639ab05..e4421d400 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -187,6 +187,7 @@ true + @@ -478,6 +479,22 @@ Modrinth (mrpack) + + + + + + CurseForge (zip) + + + + + + + + Mod List + + @@ -541,6 +558,18 @@ Open the instance folder in a file browser. + + + + .. + + + &View Launcher Root Folder + + + Open the launcher's root folder in a file browser. + + @@ -564,7 +593,7 @@ .. - Report a &Bug... + Report a Bug or Suggest a Feature Open the bug tracker to report a bug with %1. diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp index 76e3d8ed0..88739463f 100644 --- a/launcher/ui/dialogs/AboutDialog.cpp +++ b/launcher/ui/dialogs/AboutDialog.cpp @@ -71,13 +71,18 @@ QString getCreditsHtml() //: %1 is the name of the launcher, determined at build time, e.g. "Prism Launcher Developers" stream << "

    " << QObject::tr("%1 Developers", "About Credits").arg(BuildConfig.LAUNCHER_DISPLAYNAME) << "

    \n"; stream << QString("

    Sefa Eyeoglu (Scrumplex) %1

    \n") .arg(getWebsite("https://scrumplex.net")); - stream << QString("

    dada513 %1

    \n") .arg(getGitHub("dada513")); + stream << QString("

    d-513 %1

    \n") .arg(getGitHub("d-513")); stream << QString("

    txtsd %1

    \n") .arg(getWebsite("https://ihavea.quest")); stream << QString("

    timoreo %1

    \n") .arg(getGitHub("timoreo22")); stream << QString("

    Ezekiel Smith (ZekeSmith) %1

    \n") .arg(getGitHub("ZekeSmith")); stream << QString("

    cozyGalvinism %1

    \n") .arg(getGitHub("cozyGalvinism")); - stream << QString("

    DioEgizio %1

    \n") .arg(getGitHub("DioEgizio")); - stream << QString("

    flowln %1

    \n") .arg(getGitHub("flowln")); + stream << QString("

    DioEgizio %1

    \n") .arg(getGitHub("DioEgizio")); + stream << QString("

    flowln %1

    \n") .arg(getGitHub("flowln")); + stream << QString("

    ViRb3 %1

    \n") .arg(getGitHub("ViRb3")); + stream << QString("

    Rachel Powers (Ryex) %1

    \n") .arg(getGitHub("Ryex")); + stream << QString("

    TayouVR %1

    \n") .arg(getGitHub("TayouVR")); + stream << QString("

    TheKodeToad %1

    \n") .arg(getGitHub("TheKodeToad")); + stream << QString("

    getchoo %1

    \n") .arg(getGitHub("getchoo")); stream << "
    \n"; // TODO: possibly retrieve from git history at build time? diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp index 8ecd91a90..d1a69b932 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.cpp +++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -3,6 +3,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad + * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -35,24 +36,29 @@ */ #include "ExportInstanceDialog.h" -#include "ui_ExportInstanceDialog.h" #include #include #include -#include #include +#include +#include "FileIgnoreProxy.h" +#include "QObjectPtr.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" +#include "ui_ExportInstanceDialog.h" -#include -#include -#include -#include -#include -#include "SeparatorPrefixTree.h" -#include "Application.h" -#include #include +#include +#include +#include +#include +#include +#include +#include +#include "Application.h" +#include "SeparatorPrefixTree.h" -ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget *parent) +ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget* parent) : QDialog(parent), ui(new Ui::ExportInstanceDialog), m_instance(instance) { ui->setupUi(this); @@ -60,13 +66,17 @@ ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget *parent model->setIconProvider(&icons); auto root = instance->instanceRoot(); proxyModel = new FileIgnoreProxy(root, this); - loadPackIgnore(); proxyModel->setSourceModel(model); + auto prefix = QDir(instance->instanceRoot()).relativeFilePath(instance->gameRoot()); + proxyModel->ignoreFilesWithPath().insert({ FS::PathCombine(prefix, "logs"), FS::PathCombine(prefix, "crash-reports") }); + proxyModel->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" }); + loadPackIgnore(); + ui->treeView->setModel(proxyModel); ui->treeView->setRootIndex(proxyModel->mapFromSource(model->index(root))); ui->treeView->sortByColumn(0, Qt::AscendingOrder); - connect(proxyModel, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(rowsInserted(QModelIndex,int,int))); + connect(proxyModel, SIGNAL(rowsInserted(QModelIndex, int, int)), SLOT(rowsInserted(QModelIndex, int, int))); model->setFilter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden); model->setRootPath(root); @@ -86,32 +96,26 @@ void SaveIcon(InstancePtr m_instance) auto iconKey = m_instance->iconKey(); auto iconList = APPLICATION->icons(); auto mmcIcon = iconList->icon(iconKey); - if(!mmcIcon || mmcIcon->isBuiltIn()) { + if (!mmcIcon || mmcIcon->isBuiltIn()) { return; } auto path = mmcIcon->getFilePath(); - if(!path.isNull()) { - QFileInfo inInfo (path); - FS::copy(path, FS::PathCombine(m_instance->instanceRoot(), inInfo.fileName())) (); + if (!path.isNull()) { + QFileInfo inInfo(path); + FS::copy(path, FS::PathCombine(m_instance->instanceRoot(), inInfo.fileName()))(); return; } - auto & image = mmcIcon->m_images[mmcIcon->type()]; - auto & icon = image.icon; + auto& image = mmcIcon->m_images[mmcIcon->type()]; + auto& icon = image.icon; auto sizes = icon.availableSizes(); - if(sizes.size() == 0) - { + if (sizes.size() == 0) { return; } - auto areaOf = [](QSize size) - { - return size.width() * size.height(); - }; + auto areaOf = [](QSize size) { return size.width() * size.height(); }; QSize largest = sizes[0]; // find variant with largest area - for(auto size: sizes) - { - if(areaOf(largest) < areaOf(size)) - { + for (auto size : sizes) { + if (areaOf(largest) < areaOf(size)) { largest = size; } } @@ -119,66 +123,57 @@ void SaveIcon(InstancePtr m_instance) pixmap.save(FS::PathCombine(m_instance->instanceRoot(), iconKey + ".png")); } -bool ExportInstanceDialog::doExport() +void ExportInstanceDialog::doExport() { auto name = FS::RemoveInvalidFilenameChars(m_instance->name()); - const QString output = QFileDialog::getSaveFileName( - this, tr("Export %1").arg(m_instance->name()), - FS::PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)", nullptr); - if (output.isEmpty()) - { - return false; + const QString output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(m_instance->name()), + FS::PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)", nullptr); + if (output.isEmpty()) { + QDialog::done(QDialog::Rejected); + return; } SaveIcon(m_instance); - auto & blocked = proxyModel->blockedPaths(); - using std::placeholders::_1; auto files = QFileInfoList(); if (!MMCZip::collectFileListRecursively(m_instance->instanceRoot(), nullptr, &files, - std::bind(&SeparatorPrefixTree<'/'>::covers, blocked, _1))) { + std::bind(&FileIgnoreProxy::filterFile, proxyModel, std::placeholders::_1))) { QMessageBox::warning(this, tr("Error"), tr("Unable to export instance")); - return false; + QDialog::done(QDialog::Rejected); + return; } - if (!MMCZip::compressDirFiles(output, m_instance->instanceRoot(), files, true)) - { - QMessageBox::warning(this, tr("Error"), tr("Unable to export instance")); - return false; - } - return true; + auto task = makeShared(output, m_instance->instanceRoot(), files, "", true); + + connect(task.get(), &Task::failed, this, + [this, output](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); + connect(task.get(), &Task::finished, this, [task] { task->deleteLater(); }); + + ProgressDialog progress(this); + progress.setSkipButton(true, tr("Abort")); + auto result = progress.execWithTask(task.get()); + QDialog::done(result); } void ExportInstanceDialog::done(int result) { savePackIgnore(); - if (result == QDialog::Accepted) - { - if (doExport()) - { - QDialog::done(QDialog::Accepted); - return; - } - else - { - return; - } + if (result == QDialog::Accepted) { + doExport(); + return; } QDialog::done(result); } void ExportInstanceDialog::rowsInserted(QModelIndex parent, int top, int bottom) { - //WARNING: possible off-by-one? - for(int i = top; i < bottom; i++) - { + // WARNING: possible off-by-one? + for (int i = top; i < bottom; i++) { auto node = proxyModel->index(i, 0, parent); - if(proxyModel->shouldExpand(node)) - { + if (proxyModel->shouldExpand(node)) { auto expNode = node.parent(); - if(!expNode.isValid()) - { + if (!expNode.isValid()) { continue; } ui->treeView->expand(node); @@ -195,8 +190,7 @@ void ExportInstanceDialog::loadPackIgnore() { auto filename = ignoreFileName(); QFile ignoreFile(filename); - if(!ignoreFile.open(QIODevice::ReadOnly)) - { + if (!ignoreFile.open(QIODevice::ReadOnly)) { return; } auto data = ignoreFile.readAll(); @@ -212,12 +206,9 @@ void ExportInstanceDialog::savePackIgnore() { auto data = proxyModel->blockedPaths().toStringList().join('\n').toUtf8(); auto filename = ignoreFileName(); - try - { + try { FS::write(filename, data); - } - catch (const Exception &e) - { + } catch (const Exception& e) { qWarning() << e.cause(); } } diff --git a/launcher/ui/dialogs/ExportInstanceDialog.h b/launcher/ui/dialogs/ExportInstanceDialog.h index 5e8018751..02f38f63d 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.h +++ b/launcher/ui/dialogs/ExportInstanceDialog.h @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad + * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -38,39 +39,37 @@ #include #include #include -#include "FileIgnoreProxy.h" #include "FastFileIconProvider.h" +#include "FileIgnoreProxy.h" class BaseInstance; typedef std::shared_ptr InstancePtr; -namespace Ui -{ +namespace Ui { class ExportInstanceDialog; } -class ExportInstanceDialog : public QDialog -{ +class ExportInstanceDialog : public QDialog { Q_OBJECT -public: - explicit ExportInstanceDialog(InstancePtr instance, QWidget *parent = 0); + public: + explicit ExportInstanceDialog(InstancePtr instance, QWidget* parent = 0); ~ExportInstanceDialog(); virtual void done(int result); -private: - bool doExport(); + private: + void doExport(); void loadPackIgnore(); void savePackIgnore(); QString ignoreFileName(); -private: - Ui::ExportInstanceDialog *ui; + private: + Ui::ExportInstanceDialog* ui; InstancePtr m_instance; - FileIgnoreProxy * proxyModel; + FileIgnoreProxy* proxyModel; FastFileIconProvider icons; -private slots: + private slots: void rowsInserted(QModelIndex parent, int top, int bottom); }; diff --git a/launcher/ui/dialogs/ExportMrPackDialog.cpp b/launcher/ui/dialogs/ExportPackDialog.cpp similarity index 55% rename from launcher/ui/dialogs/ExportMrPackDialog.cpp rename to launcher/ui/dialogs/ExportPackDialog.cpp index 239873f6d..2abe2805b 100644 --- a/launcher/ui/dialogs/ExportMrPackDialog.cpp +++ b/launcher/ui/dialogs/ExportPackDialog.cpp @@ -16,11 +16,13 @@ * along with this program. If not, see . */ -#include "ExportMrPackDialog.h" +#include "ExportPackDialog.h" #include "minecraft/mod/ModFolderModel.h" +#include "modplatform/ModIndex.h" +#include "modplatform/flame/FlamePackExportTask.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" -#include "ui_ExportMrPackDialog.h" +#include "ui_ExportPackDialog.h" #include #include @@ -32,17 +34,24 @@ #include "MMCZip.h" #include "modplatform/modrinth/ModrinthPackExportTask.h" -ExportMrPackDialog::ExportMrPackDialog(InstancePtr instance, QWidget* parent) - : QDialog(parent), instance(instance), ui(new Ui::ExportMrPackDialog) +ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPlatform::ResourceProvider provider) + : QDialog(parent), instance(instance), ui(new Ui::ExportPackDialog), m_provider(provider) { ui->setupUi(this); ui->name->setText(instance->name()); - ui->summary->setText(instance->notes().split(QRegularExpression("\\r?\\n"))[0]); + if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { + ui->summary->setText(instance->notes().split(QRegularExpression("\\r?\\n"))[0]); + setWindowTitle("Export Modrinth Pack"); + } else { + setWindowTitle("Export CurseForge Pack"); + ui->version->setText(""); + ui->summaryLabel->setText("Author"); + } // ensure a valid pack is generated // the name and version fields mustn't be empty - connect(ui->name, &QLineEdit::textEdited, this, &ExportMrPackDialog::validate); - connect(ui->version, &QLineEdit::textEdited, this, &ExportMrPackDialog::validate); + connect(ui->name, &QLineEdit::textEdited, this, &ExportPackDialog::validate); + connect(ui->version, &QLineEdit::textEdited, this, &ExportPackDialog::validate); // the instance name can technically be empty validate(); @@ -52,6 +61,8 @@ ExportMrPackDialog::ExportMrPackDialog(InstancePtr instance, QWidget* parent) // use the game root - everything outside cannot be exported const QDir root(instance->gameRoot()); proxy = new FileIgnoreProxy(instance->gameRoot(), this); + proxy->ignoreFilesWithPath().insert({ "logs", "crash-reports" }); + proxy->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" }); proxy->setSourceModel(model); const QDir::Filters filter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden); @@ -64,6 +75,7 @@ ExportMrPackDialog::ExportMrPackDialog(InstancePtr instance, QWidget* parent) MinecraftInstance* mcInstance = dynamic_cast(instance.get()); if (mcInstance) { + mcInstance->loaderModList()->update(); const QDir index = mcInstance->loaderModList()->indexDir(); if (index.exists()) proxy->blockedPaths().insert(root.relativeFilePath(index.absolutePath())); @@ -81,43 +93,54 @@ ExportMrPackDialog::ExportMrPackDialog(InstancePtr instance, QWidget* parent) headerView->setSectionResizeMode(0, QHeaderView::Stretch); } -ExportMrPackDialog::~ExportMrPackDialog() +ExportPackDialog::~ExportPackDialog() { delete ui; } -void ExportMrPackDialog::done(int result) +void ExportPackDialog::done(int result) { if (result == Accepted) { const QString filename = FS::RemoveInvalidFilenameChars(ui->name->text()); - const QString output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(ui->name->text()), - FS::PathCombine(QDir::homePath(), filename + ".mrpack"), - "Modrinth pack (*.mrpack *.zip)", nullptr); + QString output; + if (m_provider == ModPlatform::ResourceProvider::MODRINTH) + output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(ui->name->text()), + FS::PathCombine(QDir::homePath(), filename + ".mrpack"), "Modrinth pack (*.mrpack *.zip)", + nullptr); + else + output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(ui->name->text()), + FS::PathCombine(QDir::homePath(), filename + ".zip"), "CurseForge pack (*.zip)", nullptr); if (output.isEmpty()) return; + Task* task; + if (m_provider == ModPlatform::ResourceProvider::MODRINTH) + task = new ModrinthPackExportTask(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output, + std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1)); + else + task = new FlamePackExportTask(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output, + std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1)); - ModrinthPackExportTask task(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output, - [this](const QString& path) { return proxy->blockedPaths().covers(path); }); - - connect(&task, &Task::failed, + connect(task, &Task::failed, [this](const QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); - connect(&task, &Task::aborted, [this] { + connect(task, &Task::aborted, [this] { CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information) ->show(); }); + connect(task, &Task::finished, [task] { task->deleteLater(); }); ProgressDialog progress(this); progress.setSkipButton(true, tr("Abort")); - if (progress.execWithTask(&task) != QDialog::Accepted) + if (progress.execWithTask(task) != QDialog::Accepted) return; } QDialog::done(result); } -void ExportMrPackDialog::validate() +void ExportPackDialog::validate() { - const bool invalid = ui->name->text().isEmpty() || ui->version->text().isEmpty(); + const bool invalid = + ui->name->text().isEmpty() || ((m_provider == ModPlatform::ResourceProvider::MODRINTH) && ui->version->text().isEmpty()); ui->buttonBox->button(QDialogButtonBox::Ok)->setDisabled(invalid); } diff --git a/launcher/ui/dialogs/ExportMrPackDialog.h b/launcher/ui/dialogs/ExportPackDialog.h similarity index 71% rename from launcher/ui/dialogs/ExportMrPackDialog.h rename to launcher/ui/dialogs/ExportPackDialog.h index 1c70c4ae1..830c24d25 100644 --- a/launcher/ui/dialogs/ExportMrPackDialog.h +++ b/launcher/ui/dialogs/ExportPackDialog.h @@ -22,24 +22,28 @@ #include "BaseInstance.h" #include "FastFileIconProvider.h" #include "FileIgnoreProxy.h" +#include "modplatform/ModIndex.h" namespace Ui { -class ExportMrPackDialog; +class ExportPackDialog; } -class ExportMrPackDialog : public QDialog { +class ExportPackDialog : public QDialog { Q_OBJECT public: - explicit ExportMrPackDialog(InstancePtr instance, QWidget* parent = nullptr); - ~ExportMrPackDialog(); + explicit ExportPackDialog(InstancePtr instance, + QWidget* parent = nullptr, + ModPlatform::ResourceProvider provider = ModPlatform::ResourceProvider::MODRINTH); + ~ExportPackDialog(); void done(int result) override; void validate(); private: const InstancePtr instance; - Ui::ExportMrPackDialog* ui; + Ui::ExportPackDialog* ui; FileIgnoreProxy* proxy; FastFileIconProvider icons; + const ModPlatform::ResourceProvider m_provider; }; diff --git a/launcher/ui/dialogs/ExportMrPackDialog.ui b/launcher/ui/dialogs/ExportPackDialog.ui similarity index 93% rename from launcher/ui/dialogs/ExportMrPackDialog.ui rename to launcher/ui/dialogs/ExportPackDialog.ui index 9a7897378..3976e28f8 100644 --- a/launcher/ui/dialogs/ExportMrPackDialog.ui +++ b/launcher/ui/dialogs/ExportPackDialog.ui @@ -1,7 +1,7 @@ - ExportMrPackDialog - + ExportPackDialog + 0 @@ -11,7 +11,7 @@ - Export Modrinth Pack + Export Pack true @@ -24,7 +24,7 @@ - + Summary @@ -41,7 +41,7 @@ - + Version @@ -57,6 +57,7 @@
    + @@ -103,7 +104,7 @@ buttonBox accepted() - ExportMrPackDialog + ExportPackDialog accept() @@ -119,7 +120,7 @@ buttonBox rejected() - ExportMrPackDialog + ExportPackDialog reject() diff --git a/launcher/ui/dialogs/ExportToModListDialog.cpp b/launcher/ui/dialogs/ExportToModListDialog.cpp new file mode 100644 index 000000000..c811bfe6a --- /dev/null +++ b/launcher/ui/dialogs/ExportToModListDialog.cpp @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ExportToModListDialog.h" +#include +#include +#include +#include "FileSystem.h" +#include "Markdown.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/mod/ModFolderModel.h" +#include "modplatform/helpers/ExportToModList.h" +#include "ui_ExportToModListDialog.h" + +#include +#include +#include +#include +#include + +const QHash ExportToModListDialog::exampleLines = { + { ExportToModList::HTML, "
  • {name} [{version}] by {authors}
  • " }, + { ExportToModList::MARKDOWN, "[{name}]({url}) [{version}] by {authors}" }, + { ExportToModList::PLAINTXT, "{name} ({url}) [{version}] by {authors}" }, + { ExportToModList::JSON, "{\"name\":\"{name}\",\"url\":\"{url}\",\"version\":\"{version}\",\"authors\":\"{authors}\"}," }, + { ExportToModList::CSV, "{name},{url},{version},\"{authors}\"" }, +}; + +ExportToModListDialog::ExportToModListDialog(InstancePtr instance, QWidget* parent) + : QDialog(parent), m_template_changed(false), name(instance->name()), ui(new Ui::ExportToModListDialog) +{ + ui->setupUi(this); + enableCustom(false); + + MinecraftInstance* mcInstance = dynamic_cast(instance.get()); + if (mcInstance) { + mcInstance->loaderModList()->update(); + connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, [this, mcInstance]() { + m_allMods = mcInstance->loaderModList()->allMods(); + triggerImp(); + }); + } + + connect(ui->formatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ExportToModListDialog::formatChanged); + connect(ui->authorsCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); + connect(ui->versionCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); + connect(ui->urlCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); + connect(ui->authorsButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Authors); }); + connect(ui->versionButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Version); }); + connect(ui->urlButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Url); }); + connect(ui->templateText, &QTextEdit::textChanged, this, [this] { + if (ui->templateText->toPlainText() != exampleLines[format]) + ui->formatComboBox->setCurrentIndex(5); + else + triggerImp(); + }); + connect(ui->copyButton, &QPushButton::clicked, this, [this](bool) { + this->ui->finalText->selectAll(); + this->ui->finalText->copy(); + }); +} + +ExportToModListDialog::~ExportToModListDialog() +{ + delete ui; +} + +void ExportToModListDialog::formatChanged(int index) +{ + switch (index) { + case 0: { + enableCustom(false); + ui->resultText->show(); + format = ExportToModList::HTML; + break; + } + case 1: { + enableCustom(false); + ui->resultText->show(); + format = ExportToModList::MARKDOWN; + break; + } + case 2: { + enableCustom(false); + ui->resultText->hide(); + format = ExportToModList::PLAINTXT; + break; + } + case 3: { + enableCustom(false); + ui->resultText->hide(); + format = ExportToModList::JSON; + break; + } + case 4: { + enableCustom(false); + ui->resultText->hide(); + format = ExportToModList::CSV; + break; + } + case 5: { + m_template_changed = true; + enableCustom(true); + ui->resultText->hide(); + format = ExportToModList::CUSTOM; + break; + } + } + triggerImp(); +} + +void ExportToModListDialog::triggerImp() +{ + if (format == ExportToModList::CUSTOM) { + ui->finalText->setPlainText(ExportToModList::exportToModList(m_allMods, ui->templateText->toPlainText())); + return; + } + auto opt = 0; + if (ui->authorsCheckBox->isChecked()) + opt |= ExportToModList::Authors; + if (ui->versionCheckBox->isChecked()) + opt |= ExportToModList::Version; + if (ui->urlCheckBox->isChecked()) + opt |= ExportToModList::Url; + auto txt = ExportToModList::exportToModList(m_allMods, format, static_cast(opt)); + ui->finalText->setPlainText(txt); + switch (format) { + case ExportToModList::CUSTOM: + return; + case ExportToModList::HTML: + ui->resultText->setHtml(txt); + break; + case ExportToModList::MARKDOWN: + ui->resultText->setHtml(markdownToHTML(txt)); + break; + case ExportToModList::PLAINTXT: + break; + case ExportToModList::JSON: + break; + case ExportToModList::CSV: + break; + } + auto exampleLine = exampleLines[format]; + if (!m_template_changed && ui->templateText->toPlainText() != exampleLine) + ui->templateText->setPlainText(exampleLine); +} + +void ExportToModListDialog::done(int result) +{ + if (result == Accepted) { + const QString filename = FS::RemoveInvalidFilenameChars(name); + const QString output = + QFileDialog::getSaveFileName(this, tr("Export %1").arg(name), FS::PathCombine(QDir::homePath(), filename + extension()), + "File (*.txt *.html *.md *.json *.csv)", nullptr); + + if (output.isEmpty()) + return; + FS::write(output, ui->finalText->toPlainText().toUtf8()); + } + + QDialog::done(result); +} + +QString ExportToModListDialog::extension() +{ + switch (format) { + case ExportToModList::HTML: + return ".html"; + case ExportToModList::MARKDOWN: + return ".md"; + case ExportToModList::PLAINTXT: + return ".txt"; + case ExportToModList::CUSTOM: + return ".txt"; + case ExportToModList::JSON: + return ".json"; + case ExportToModList::CSV: + return ".csv"; + } + return ".txt"; +} + +void ExportToModListDialog::addExtra(ExportToModList::OptionalData option) +{ + if (format != ExportToModList::CUSTOM) + return; + switch (option) { + case ExportToModList::Authors: + ui->templateText->insertPlainText("{authors}"); + break; + case ExportToModList::Url: + ui->templateText->insertPlainText("{url}"); + break; + case ExportToModList::Version: + ui->templateText->insertPlainText("{version}"); + break; + } +} +void ExportToModListDialog::enableCustom(bool enabled) +{ + ui->authorsCheckBox->setHidden(enabled); + ui->versionCheckBox->setHidden(enabled); + ui->urlCheckBox->setHidden(enabled); + + ui->authorsButton->setHidden(!enabled); + ui->versionButton->setHidden(!enabled); + ui->urlButton->setHidden(!enabled); +} diff --git a/launcher/ui/dialogs/ExportToModListDialog.h b/launcher/ui/dialogs/ExportToModListDialog.h new file mode 100644 index 000000000..9886ae5a0 --- /dev/null +++ b/launcher/ui/dialogs/ExportToModListDialog.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include "BaseInstance.h" +#include "minecraft/mod/Mod.h" +#include "modplatform/helpers/ExportToModList.h" + +namespace Ui { +class ExportToModListDialog; +} + +class ExportToModListDialog : public QDialog { + Q_OBJECT + + public: + explicit ExportToModListDialog(InstancePtr instance, QWidget* parent = nullptr); + ~ExportToModListDialog(); + + void done(int result) override; + + protected slots: + void formatChanged(int index); + void triggerImp(); + void trigger(int) { triggerImp(); }; + void addExtra(ExportToModList::OptionalData option); + + private: + QString extension(); + void enableCustom(bool enabled); + QList m_allMods; + bool m_template_changed; + QString name; + ExportToModList::Formats format = ExportToModList::Formats::HTML; + Ui::ExportToModListDialog* ui; + static const QHash exampleLines; +}; diff --git a/launcher/ui/dialogs/ExportToModListDialog.ui b/launcher/ui/dialogs/ExportToModListDialog.ui new file mode 100644 index 000000000..25eb43429 --- /dev/null +++ b/launcher/ui/dialogs/ExportToModListDialog.ui @@ -0,0 +1,240 @@ + + + ExportToModListDialog + + + + 0 + 0 + 650 + 446 + + + + Export Pack to ModList + + + true + + + + + + + + Settings + + + + + + + HTML + + + + + Markdown + + + + + Plaintext + + + + + JSON + + + + + CSV + + + + + Custom + + + + + + + + Template + + + + + + + + + + + + Optional Info + + + + + + Version + + + + + + + Authors + + + + + + + URL + + + + + + + Version + + + + + + + Authors + + + + + + + URL + + + + + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + 1 + + + Format + + + + + + + + + + Result + + + + + + + 0 + 143 + + + + true + + + + + + + true + + + + + + + + + + This depends on the mods' metadata. To ensure it is available, run an update on the instance. Installing the updates isn't necessary. + + + true + + + + + + + + + + + Copy + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Save + + + + + + + + + + + buttonBox + accepted() + ExportToModListDialog + accept() + + + 334 + 435 + + + 324 + 206 + + + + + buttonBox + rejected() + ExportToModListDialog + reject() + + + 324 + 390 + + + 324 + 206 + + + + + diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp index aafaf2202..7b9bb944c 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.cpp +++ b/launcher/ui/dialogs/NewInstanceDialog.cpp @@ -54,7 +54,7 @@ #include #include "ui/widgets/PageContainer.h" -#include "ui/pages/modplatform/VanillaPage.h" +#include "ui/pages/modplatform/CustomPage.h" #include "ui/pages/modplatform/atlauncher/AtlPage.h" #include "ui/pages/modplatform/legacy_ftb/Page.h" #include "ui/pages/modplatform/flame/FlamePage.h" @@ -162,7 +162,7 @@ QList NewInstanceDialog::getPages() importPage = new ImportPage(this); - pages.append(new VanillaPage(this)); + pages.append(new CustomPage(this)); pages.append(importPage); pages.append(new AtlPage(this)); if (APPLICATION->capabilities() & Application::SupportsFlame) diff --git a/launcher/ui/dialogs/NewsDialog.cpp b/launcher/ui/dialogs/NewsDialog.cpp index e1b5dd740..b646e3918 100644 --- a/launcher/ui/dialogs/NewsDialog.cpp +++ b/launcher/ui/dialogs/NewsDialog.cpp @@ -32,7 +32,7 @@ NewsDialog::~NewsDialog() void NewsDialog::selectedArticleChanged(const QString& new_title) { - auto const& article_entry = m_entries.constFind(new_title).value(); + auto article_entry = m_entries.constFind(new_title).value(); ui->articleTitleLabel->setText(QString("%2").arg(article_entry->link, new_title)); diff --git a/launcher/ui/dialogs/ProgressDialog.cpp b/launcher/ui/dialogs/ProgressDialog.cpp index 246a0fd49..4243e2917 100644 --- a/launcher/ui/dialogs/ProgressDialog.cpp +++ b/launcher/ui/dialogs/ProgressDialog.cpp @@ -34,6 +34,7 @@ */ #include "ProgressDialog.h" +#include #include "ui_ProgressDialog.h" #include @@ -66,8 +67,9 @@ ProgressDialog::ProgressDialog(QWidget* parent) : QDialog(parent), ui(new Ui::Pr ui->taskProgressScrollArea->setHidden(true); this->setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint); setAttribute(Qt::WidgetAttribute::WA_QuitOnClose, true); - setSkipButton(false); changeProgress(0, 100); + updateSize(true); + setSkipButton(false); } void ProgressDialog::setSkipButton(bool present, QString label) @@ -93,24 +95,38 @@ ProgressDialog::~ProgressDialog() delete ui; } -void ProgressDialog::updateSize() +void ProgressDialog::updateSize(bool recenterParent) { QSize lastSize = this->size(); - QSize qSize = QSize(480, minimumSizeHint().height()); + QPoint lastPos = this->pos(); + int minHeight = ui->globalStatusDetailsLabel->minimumSize().height() + (ui->verticalLayout->spacing() * 2); + minHeight += ui->globalProgressBar->minimumSize().height() + ui->verticalLayout->spacing(); + if (!ui->taskProgressScrollArea->isHidden()) + minHeight += ui->taskProgressScrollArea->minimumSizeHint().height() + ui->verticalLayout->spacing(); + if (ui->skipButton->isVisible()) + minHeight += ui->skipButton->height() + ui->verticalLayout->spacing(); + minHeight = std::max(minHeight, 60); + QSize minSize = QSize(480, minHeight); - // if the current window is too small - if ((lastSize != qSize) && (lastSize.height() < qSize.height())) - { - resize(qSize); - - // keep the dialog in the center after a resize - this->move( - this->parentWidget()->x() + (this->parentWidget()->width() - this->width()) / 2, - this->parentWidget()->y() + (this->parentWidget()->height() - this->height()) / 2 - ); + setMinimumSize(minSize); + adjustSize(); + + QSize newSize = this->size(); + // if the current window is a different size + auto parent = this->parentWidget(); + if (recenterParent && parent) { + auto newX = std::max(0, parent->x() + ((parent->width() - newSize.width()) / 2)); + auto newY = std::max(0, parent->y() + ((parent->height() - newSize.height()) / 2)); + this->move(newX, newY); + } + else if (lastSize != newSize) + { + // center on old position after resize + QSize sizeDiff = lastSize - newSize; // last size was smaller, the results should be negative + auto newX = std::max(0, lastPos.x() + (sizeDiff.width() / 2)); + auto newY = std::max(0, lastPos.y() + (sizeDiff.height() / 2)); + this->move(newX, newY); } - - setMinimumSize(qSize); } @@ -201,7 +217,9 @@ void ProgressDialog::onTaskSucceeded() void ProgressDialog::changeStatus(const QString& status) { ui->globalStatusLabel->setText(task->getStatus()); + ui->globalStatusLabel->adjustSize(); ui->globalStatusDetailsLabel->setText(task->getDetails()); + ui->globalStatusDetailsLabel->adjustSize(); updateSize(); } diff --git a/launcher/ui/dialogs/ProgressDialog.h b/launcher/ui/dialogs/ProgressDialog.h index fc9a0fbc3..f062be084 100644 --- a/launcher/ui/dialogs/ProgressDialog.h +++ b/launcher/ui/dialogs/ProgressDialog.h @@ -62,7 +62,7 @@ public: explicit ProgressDialog(QWidget *parent = 0); ~ProgressDialog(); - void updateSize(); + void updateSize(bool recenterParent = false); int execWithTask(Task* task); int execWithTask(std::unique_ptr &&task); diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index 6d90480ff..b17eced35 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -18,6 +18,8 @@ */ #include "ResourceDownloadDialog.h" +#include +#include #include #include @@ -30,6 +32,10 @@ #include "minecraft/mod/ShaderPackFolderModel.h" #include "minecraft/mod/TexturePackFolderModel.h" +#include "minecraft/mod/tasks/GetModDependenciesTask.h" +#include "modplatform/ModIndex.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ReviewMessageBox.h" #include "ui/pages/modplatform/ResourcePage.h" @@ -37,6 +43,8 @@ #include "ui/pages/modplatform/flame/FlameResourcePages.h" #include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h" +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/modrinth/ModrinthAPI.h" #include "ui/widgets/PageContainer.h" namespace ResourceDownload { @@ -117,18 +125,71 @@ void ResourceDownloadDialog::connectButtons() connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help); } +static ModPlatform::ProviderCapabilities ProviderCaps; + +QStringList getRequiredBy(QList tasks, ResourceDownloadDialog::DownloadTaskPtr pack) +{ + auto addonId = pack->getPack()->addonId; + auto provider = pack->getPack()->provider; + auto version = pack->getVersionID(); + auto req = QStringList(); + for (auto& task : tasks) { + if (provider != task->getPack()->provider) + continue; + auto deps = task->getVersion().dependencies; + if (auto dep = std::find_if(deps.begin(), deps.end(), + [addonId, provider, version](const ModPlatform::Dependency& d) { + return d.type == ModPlatform::DependencyType::REQUIRED && + (provider == ModPlatform::ResourceProvider::MODRINTH && d.addonId.toString().isEmpty() + ? version == d.version + : d.addonId == addonId); + }); + dep != deps.end()) { + req.append(task->getName()); + } + } + return req; +} + void ResourceDownloadDialog::confirm() { + auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm %1 to download").arg(resourcesString())); + confirm_dialog->retranslateUi(resourcesString()); + + if (auto task = getModDependenciesTask(); task) { + connect(task.get(), &Task::failed, this, + [&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + + connect(task.get(), &Task::succeeded, this, [&]() { + QStringList warnings = task->warnings(); + if (warnings.count()) { + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec(); + } + }); + + // Check for updates + ProgressDialog progress_dialog(this); + progress_dialog.setSkipButton(true, tr("Abort")); + progress_dialog.setWindowTitle(tr("Checking for dependencies...")); + auto ret = progress_dialog.execWithTask(task.get()); + + // If the dialog was skipped / some download error happened + if (ret == QDialog::DialogCode::Rejected) { + QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); + return; + } else { + for (auto dep : task->getDependecies()) + addResource(dep->pack, dep->version); + } + } + auto selected = getTasks(); std::sort(selected.begin(), selected.end(), [](const DownloadTaskPtr& a, const DownloadTaskPtr& b) { return QString::compare(a->getName(), b->getName(), Qt::CaseInsensitive) < 0; }); - - auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm %1 to download").arg(resourcesString())); - confirm_dialog->retranslateUi(resourcesString()); - for (auto& task : selected) { - confirm_dialog->appendResource({ task->getName(), task->getFilename(), task->getCustomPath() }); + confirm_dialog->appendResource({ task->getName(), task->getFilename(), task->getCustomPath(), + ProviderCaps.name(task->getProvider()), getRequiredBy(selected, task) }); } if (confirm_dialog->exec()) { @@ -222,8 +283,11 @@ QList ModDownloadDialog::getPages() { QList pages; - pages.append(ModrinthModPage::create(this, *m_instance)); - if (APPLICATION->capabilities() & Application::SupportsFlame) + auto loaders = static_cast(m_instance)->getPackProfile()->getModLoaders().value(); + + if (ModrinthAPI::validateModLoaders(loaders)) + pages.append(ModrinthModPage::create(this, *m_instance)); + if (APPLICATION->capabilities() & Application::SupportsFlame && FlameAPI::validateModLoaders(loaders)) pages.append(FlameModPage::create(this, *m_instance)); m_selectedPage = dynamic_cast(pages[0]); @@ -231,6 +295,19 @@ QList ModDownloadDialog::getPages() return pages; } +GetModDependenciesTask::Ptr ModDownloadDialog::getModDependenciesTask() +{ + if (auto model = dynamic_cast(getBaseModel().get()); model) { + QList> selectedVers; + for (auto& selected : getTasks()) { + selectedVers.append(std::make_shared(selected->getPack(), selected->getVersion())); + } + + return makeShared(this, m_instance, model, selectedVers); + } + return nullptr; +}; + ResourcePackDownloadDialog::ResourcePackDownloadDialog(QWidget* parent, const std::shared_ptr& resource_packs, BaseInstance* instance) diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h index 5b5b48c63..f65daaa3f 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.h +++ b/launcher/ui/dialogs/ResourceDownloadDialog.h @@ -25,6 +25,7 @@ #include #include "QObjectPtr.h" +#include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "modplatform/ModIndex.h" #include "ui/pages/BasePageProvider.h" @@ -81,6 +82,8 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider { [[nodiscard]] virtual QString geometrySaveKey() const { return ""; } void setButtonStatus(); + [[nodiscard]] virtual GetModDependenciesTask::Ptr getModDependenciesTask() { return nullptr; } + protected: const std::shared_ptr m_base_model; @@ -103,6 +106,7 @@ class ModDownloadDialog final : public ResourceDownloadDialog { [[nodiscard]] QString geometrySaveKey() const override { return "ModDownloadGeometry"; } QList getPages() override; + GetModDependenciesTask::Ptr getModDependenciesTask() override; private: BaseInstance* m_instance; diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp index 7b2df2780..7b33765fd 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.cpp +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -40,7 +40,8 @@ void ReviewMessageBox::appendResource(ResourceInformation&& info) auto filenameItem = new QTreeWidgetItem(itemTop); filenameItem->setText(0, tr("Filename: %1").arg(info.filename)); - itemTop->insertChildren(0, { filenameItem }); + auto childIndx = 0; + itemTop->insertChildren(childIndx++, { filenameItem }); if (!info.custom_file_path.isEmpty()) { auto customPathItem = new QTreeWidgetItem(itemTop); @@ -49,7 +50,31 @@ void ReviewMessageBox::appendResource(ResourceInformation&& info) itemTop->insertChildren(1, { customPathItem }); itemTop->setIcon(1, QIcon(APPLICATION->getThemedIcon("status-yellow"))); - itemTop->setToolTip(1, tr("This file will be downloaded to a folder location different from the default, possibly due to its loader requiring it.")); + itemTop->setToolTip( + childIndx++, + tr("This file will be downloaded to a folder location different from the default, possibly due to its loader requiring it.")); + } + + auto providerItem = new QTreeWidgetItem(itemTop); + providerItem->setText(0, tr("Provider: %1").arg(info.provider)); + + itemTop->insertChildren(childIndx++, { providerItem }); + + if (!info.required_by.isEmpty()) { + auto requiredByItem = new QTreeWidgetItem(itemTop); + if (info.required_by.length() == 1) { + requiredByItem->setText(0, tr("Required by: %1").arg(info.required_by.back())); + } else { + requiredByItem->setText(0, tr("Required by:")); + auto i = 0; + for (auto req : info.required_by) { + auto reqItem = new QTreeWidgetItem(requiredByItem); + reqItem->setText(0, req); + reqItem->insertChildren(i++, { reqItem }); + } + } + + itemTop->insertChildren(childIndx++, { requiredByItem }); } ui->modTreeWidget->addTopLevelItem(itemTop); diff --git a/launcher/ui/dialogs/ReviewMessageBox.h b/launcher/ui/dialogs/ReviewMessageBox.h index 5ec2bc231..a520cc2a6 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.h +++ b/launcher/ui/dialogs/ReviewMessageBox.h @@ -13,9 +13,11 @@ class ReviewMessageBox : public QDialog { static auto create(QWidget* parent, QString&& title, QString&& icon = "") -> ReviewMessageBox*; using ResourceInformation = struct res_info { - QString name; - QString filename; - QString custom_file_path {}; + QString name; + QString filename; + QString custom_file_path{}; + QString provider; + QStringList required_by; }; void appendResource(ResourceInformation&& info); diff --git a/launcher/ui/dialogs/VersionSelectDialog.cpp b/launcher/ui/dialogs/VersionSelectDialog.cpp index d7880334f..5feb70d20 100644 --- a/launcher/ui/dialogs/VersionSelectDialog.cpp +++ b/launcher/ui/dialogs/VersionSelectDialog.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "VersionSelectDialog.h" @@ -22,15 +42,10 @@ #include #include -#include "ui/dialogs/ProgressDialog.h" #include "ui/widgets/VersionSelectWidget.h" -#include "ui/dialogs/CustomMessageBox.h" #include "BaseVersion.h" #include "BaseVersionList.h" -#include "tasks/Task.h" -#include "Application.h" -#include "VersionProxyModel.h" VersionSelectDialog::VersionSelectDialog(BaseVersionList *vlist, QString title, QWidget *parent, bool cancelable) : QDialog(parent) @@ -40,7 +55,7 @@ VersionSelectDialog::VersionSelectDialog(BaseVersionList *vlist, QString title, m_verticalLayout = new QVBoxLayout(this); m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); - m_versionWidget = new VersionSelectWidget(parent); + m_versionWidget = new VersionSelectWidget(true, parent); m_verticalLayout->addWidget(m_versionWidget); m_horizontalLayout = new QHBoxLayout(); diff --git a/launcher/ui/instanceview/AccessibleInstanceView.cpp b/launcher/ui/instanceview/AccessibleInstanceView.cpp index 7de3ac726..2e7b83000 100644 --- a/launcher/ui/instanceview/AccessibleInstanceView.cpp +++ b/launcher/ui/instanceview/AccessibleInstanceView.cpp @@ -248,8 +248,8 @@ bool AccessibleInstanceView::selectColumn(int column) if (view()->selectionBehavior() != QAbstractItemView::SelectColumns && rowCount() > 1) { return false; } - // fallthrough intentional } + /* fallthrough */ case QAbstractItemView::ContiguousSelection: { if ((!column || !view()->selectionModel()->isColumnSelected(column - 1, view()->rootIndex())) && !view()->selectionModel()->isColumnSelected(column + 1, view()->rootIndex())) { view()->clearSelection(); diff --git a/launcher/ui/instanceview/InstanceView.cpp b/launcher/ui/instanceview/InstanceView.cpp index fbeffe350..1911dd59a 100644 --- a/launcher/ui/instanceview/InstanceView.cpp +++ b/launcher/ui/instanceview/InstanceView.cpp @@ -48,6 +48,7 @@ #include #include "VisualGroup.h" +#include "ui/themes/ThemeManager.h" #include #include @@ -73,6 +74,7 @@ InstanceView::InstanceView(QWidget *parent) setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); setAcceptDrops(true); setAutoScroll(true); + setPaintCat(APPLICATION->settings()->get("TheCat").toBool()); } InstanceView::~InstanceView() @@ -498,12 +500,34 @@ void InstanceView::mouseDoubleClickEvent(QMouseEvent *event) } } -void InstanceView::paintEvent(QPaintEvent *event) +void InstanceView::setPaintCat(bool visible) +{ + m_catVisible = visible; + if (visible) + m_catPixmap.load(QString(":/backgrounds/%1").arg(ThemeManager::getCatImage())); + else + m_catPixmap = QPixmap(); +} + +void InstanceView::paintEvent(QPaintEvent* event) { executeDelayedItemsLayout(); QPainter painter(this->viewport()); + if (m_catVisible) { + int widWidth = this->viewport()->width(); + int widHeight = this->viewport()->height(); + if (m_catPixmap.width() < widWidth) + widWidth = m_catPixmap.width(); + if (m_catPixmap.height() < widHeight) + widHeight = m_catPixmap.height(); + auto pixmap = m_catPixmap.scaled(widWidth, widHeight, Qt::KeepAspectRatio); + QRect rectOfPixmap = pixmap.rect(); + rectOfPixmap.moveBottomRight(this->viewport()->rect().bottomRight()); + painter.drawPixmap(rectOfPixmap.topLeft(), pixmap); + } + #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem option; initViewItemOption(&option); diff --git a/launcher/ui/instanceview/InstanceView.h b/launcher/ui/instanceview/InstanceView.h index ac3382742..364056751 100644 --- a/launcher/ui/instanceview/InstanceView.h +++ b/launcher/ui/instanceview/InstanceView.h @@ -85,10 +85,8 @@ public: virtual QRegion visualRegionForSelection(const QItemSelection &selection) const override; - int spacing() const - { - return m_spacing; - }; + int spacing() const { return m_spacing; }; + void setPaintCat(bool visible); public slots: virtual void updateGeometries() override; @@ -139,6 +137,8 @@ private: int m_currentItemsPerRow = -1; int m_currentCursorColumn= -1; mutable QCache geometryCache; + bool m_catVisible = false; + QPixmap m_catPixmap; // point where the currently active mouse action started in geometry coordinates QPoint m_pressedPosition; diff --git a/launcher/ui/instanceview/VisualGroup.cpp b/launcher/ui/instanceview/VisualGroup.cpp index e6bca17d6..aaf31941d 100644 --- a/launcher/ui/instanceview/VisualGroup.cpp +++ b/launcher/ui/instanceview/VisualGroup.cpp @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -35,22 +36,18 @@ #include "VisualGroup.h" +#include +#include #include #include #include -#include -#include +#include #include "InstanceView.h" -VisualGroup::VisualGroup(const QString &text, InstanceView *view) : view(view), text(text), collapsed(false) -{ -} +VisualGroup::VisualGroup(QString text, InstanceView* view) : view(view), text(std::move(text)), collapsed(false) {} -VisualGroup::VisualGroup(const VisualGroup *other) - : view(other->view), text(other->text), collapsed(other->collapsed) -{ -} +VisualGroup::VisualGroup(const VisualGroup* other) : view(other->view), text(other->text), collapsed(other->collapsed) {} void VisualGroup::update() { @@ -64,13 +61,11 @@ void VisualGroup::update() int positionInRow = 0; int currentRow = 0; int offsetFromTop = 0; - for (auto item: temp_items) - { - if(positionInRow == itemsPerRow) - { + for (auto item : temp_items) { + if (positionInRow == itemsPerRow) { rows[currentRow].height = maxRowHeight; rows[currentRow].top = offsetFromTop; - currentRow ++; + currentRow++; offsetFromTop += maxRowHeight + 5; positionInRow = 0; maxRowHeight = 0; @@ -83,8 +78,7 @@ void VisualGroup::update() #endif auto itemHeight = view->itemDelegate()->sizeHint(viewItemOption, item).height(); - if(itemHeight > maxRowHeight) - { + if (itemHeight > maxRowHeight) { maxRowHeight = itemHeight; } rows[currentRow].items.append(item); @@ -94,16 +88,13 @@ void VisualGroup::update() rows[currentRow].top = offsetFromTop; } -QPair VisualGroup::positionOf(const QModelIndex &index) const +QPair VisualGroup::positionOf(const QModelIndex& index) const { int y = 0; - for (auto & row: rows) - { - for(auto x = 0; x < row.items.size(); x++) - { - if(row.items[x] == index) - { - return qMakePair(x,y); + for (auto& row : rows) { + for (auto x = 0; x < row.items.size(); x++) { + if (row.items[x] == index) { + return qMakePair(x, y); } } y++; @@ -112,193 +103,109 @@ QPair VisualGroup::positionOf(const QModelIndex &index) const return qMakePair(0, 0); } -int VisualGroup::rowTopOf(const QModelIndex &index) const +int VisualGroup::rowTopOf(const QModelIndex& index) const { auto position = positionOf(index); return rows[position.second].top; } -int VisualGroup::rowHeightOf(const QModelIndex &index) const +int VisualGroup::rowHeightOf(const QModelIndex& index) const { auto position = positionOf(index); return rows[position.second].height; } -VisualGroup::HitResults VisualGroup::hitScan(const QPoint &pos) const +VisualGroup::HitResults VisualGroup::hitScan(const QPoint& pos) const { VisualGroup::HitResults results = VisualGroup::NoHit; int y_start = verticalPosition(); int body_start = y_start + headerHeight(); - int body_end = body_start + contentHeight() + 5; // FIXME: wtf is this 5? + int body_end = body_start + contentHeight(); int y = pos.y(); // int x = pos.x(); - if (y < y_start) - { + if (y < y_start) { results = VisualGroup::NoHit; - } - else if (y < body_start) - { + } else if (y < body_start) { results = VisualGroup::HeaderHit; int collapseSize = headerHeight() - 4; // the icon - QRect iconRect = QRect(view->m_leftMargin + 2, 2 + y_start, collapseSize, collapseSize); - if (iconRect.contains(pos)) - { + QRect iconRect = QRect(view->m_leftMargin + 2, 2 + y_start, view->width() - 4, collapseSize); + if (iconRect.contains(pos)) { results |= VisualGroup::CheckboxHit; } - } - else if (y < body_end) - { + } else if (y < body_end) { results |= VisualGroup::BodyHit; } return results; } -void VisualGroup::drawHeader(QPainter *painter, const QStyleOptionViewItem &option) +void VisualGroup::drawHeader(QPainter* painter, const QStyleOptionViewItem& option) const { - painter->setRenderHint(QPainter::Antialiasing); - - const QRect optRect = option.rect; + QRect optRect = option.rect; + optRect.setTop(optRect.top() + 7); QFont font(QApplication::font()); font.setBold(true); const QFontMetrics fontMetrics = QFontMetrics(font); + painter->setFont(font); - QColor outlineColor = option.palette.text().color(); - outlineColor.setAlphaF(0.35); + QPen pen; + pen.setWidth(2); + QColor penColor = option.palette.text().color(); + penColor.setAlphaF(0.6); + pen.setColor(penColor); + painter->setPen(pen); + painter->setRenderHint(QPainter::Antialiasing); - //BEGIN: top left corner + // sizes and offsets, to keep things consistent below + int arrowOffsetLeft = fontMetrics.height() / 2 + 7; + int textOffsetLeft = arrowOffsetLeft * 2; + int arrowSize = 6; + int centerHeight = optRect.top() + fontMetrics.height() / 2; + + // BEGIN: arrow { - painter->save(); - painter->setPen(outlineColor); - const QPointF topLeft(optRect.topLeft()); - QRectF arc(topLeft, QSizeF(4, 4)); - arc.translate(0.5, 0.5); - painter->drawArc(arc, 1440, 1440); - painter->restore(); - } - //END: top left corner - - //BEGIN: left vertical line - { - QPoint start(optRect.topLeft()); - start.ry() += 3; - QPoint verticalGradBottom(optRect.topLeft()); - verticalGradBottom.ry() += fontMetrics.height() + 5; - QLinearGradient gradient(start, verticalGradBottom); - gradient.setColorAt(0, outlineColor); - gradient.setColorAt(1, Qt::transparent); - painter->fillRect(QRect(start, QSize(1, fontMetrics.height() + 5)), gradient); - } - //END: left vertical line - - //BEGIN: horizontal line - { - QPoint start(optRect.topLeft()); - start.rx() += 3; - QPoint horizontalGradTop(optRect.topLeft()); - horizontalGradTop.rx() += optRect.width() - 6; - painter->fillRect(QRect(start, QSize(optRect.width() - 6, 1)), outlineColor); - } - //END: horizontal line - - //BEGIN: top right corner - { - painter->save(); - painter->setPen(outlineColor); - QPointF topRight(optRect.topRight()); - topRight.rx() -= 4; - QRectF arc(topRight, QSizeF(4, 4)); - arc.translate(0.5, 0.5); - painter->drawArc(arc, 0, 1440); - painter->restore(); - } - //END: top right corner - - //BEGIN: right vertical line - { - QPoint start(optRect.topRight()); - start.ry() += 3; - QPoint verticalGradBottom(optRect.topRight()); - verticalGradBottom.ry() += fontMetrics.height() + 5; - QLinearGradient gradient(start, verticalGradBottom); - gradient.setColorAt(0, outlineColor); - gradient.setColorAt(1, Qt::transparent); - painter->fillRect(QRect(start, QSize(1, fontMetrics.height() + 5)), gradient); - } - //END: right vertical line - - //BEGIN: checkboxy thing - { - painter->save(); - painter->setRenderHint(QPainter::Antialiasing, false); - painter->setFont(font); - QColor penColor(option.palette.text().color()); - penColor.setAlphaF(0.6); - painter->setPen(penColor); - QRect iconSubRect(option.rect); - iconSubRect.setTop(iconSubRect.top() + 7); - iconSubRect.setLeft(iconSubRect.left() + 7); - - int sizing = fontMetrics.height(); - int even = ( (sizing - 1) % 2 ); - - iconSubRect.setHeight(sizing - even); - iconSubRect.setWidth(sizing - even); - painter->drawRect(iconSubRect); - - - /* - if(collapsed) - painter->drawText(iconSubRect, Qt::AlignHCenter | Qt::AlignVCenter, "+"); - else - painter->drawText(iconSubRect, Qt::AlignHCenter | Qt::AlignVCenter, "-"); - */ - painter->setBrush(option.palette.text()); - painter->fillRect(iconSubRect.x(), iconSubRect.y() + iconSubRect.height() / 2, - iconSubRect.width(), 2, penColor); - if (collapsed) - { - painter->fillRect(iconSubRect.x() + iconSubRect.width() / 2, iconSubRect.y(), 2, - iconSubRect.height(), penColor); + QPolygon arrowPolygon; + if (collapsed) { + arrowPolygon << QPoint(arrowOffsetLeft - arrowSize / 2, centerHeight - arrowSize) + << QPoint(arrowOffsetLeft + arrowSize / 2, centerHeight) + << QPoint(arrowOffsetLeft - arrowSize / 2, centerHeight + arrowSize); + painter->drawPolyline(arrowPolygon); + } else { + arrowPolygon << QPoint(arrowOffsetLeft - arrowSize, centerHeight - arrowSize / 2) + << QPoint(arrowOffsetLeft, centerHeight + arrowSize / 2) + << QPoint(arrowOffsetLeft + arrowSize, centerHeight - arrowSize / 2); + painter->drawPolyline(arrowPolygon); } - - painter->restore(); } - //END: checkboxy thing + // END: arrow - //BEGIN: text + // BEGIN: text { - QRect textRect(option.rect); - textRect.setTop(textRect.top() + 7); - textRect.setLeft(textRect.left() + 7 + fontMetrics.height() + 7); + QRect textRect(optRect); + textRect.setTop(textRect.top()); + textRect.setLeft(textOffsetLeft); textRect.setHeight(fontMetrics.height()); textRect.setRight(textRect.right() - 7); - painter->save(); - painter->setFont(font); - QColor penColor(option.palette.text().color()); - penColor.setAlphaF(0.6); - painter->setPen(penColor); - painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, text); - painter->restore(); + painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, !text.isEmpty() ? text : QObject::tr("Ungrouped")); } - //END: text + // END: text } int VisualGroup::totalHeight() const { - return headerHeight() + 5 + contentHeight(); // FIXME: wtf is that '5'? + return headerHeight() + contentHeight(); } -int VisualGroup::headerHeight() const +int VisualGroup::headerHeight() { QFont font(QApplication::font()); font.setBold(true); QFontMetrics fontMetrics(font); const int height = fontMetrics.height() + 1 /* 1 pixel-width gradient */ - + 11 /* top and bottom separation */; + + 11 /* top and bottom separation */; return height; /* int raw = view->viewport()->fontMetrics().height() + 4; @@ -311,8 +218,7 @@ int VisualGroup::headerHeight() const int VisualGroup::contentHeight() const { - if (collapsed) - { + if (collapsed) { return 0; } auto last = rows[numRows() - 1]; @@ -321,7 +227,7 @@ int VisualGroup::contentHeight() const int VisualGroup::numRows() const { - return rows.size(); + return (int)rows.size(); } int VisualGroup::verticalPosition() const @@ -332,11 +238,9 @@ int VisualGroup::verticalPosition() const QList VisualGroup::items() const { QList indices; - for (int i = 0; i < view->model()->rowCount(); ++i) - { + for (int i = 0; i < view->model()->rowCount(); ++i) { const QModelIndex index = view->model()->index(i, 0); - if (index.data(InstanceViewRoles::GroupRole).toString() == text) - { + if (index.data(InstanceViewRoles::GroupRole).toString() == text) { indices.append(index); } } diff --git a/launcher/ui/instanceview/VisualGroup.h b/launcher/ui/instanceview/VisualGroup.h index 5a743aa18..697298c2d 100644 --- a/launcher/ui/instanceview/VisualGroup.h +++ b/launcher/ui/instanceview/VisualGroup.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Tayou * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once @@ -42,8 +62,8 @@ struct VisualRow struct VisualGroup { /* constructors */ - VisualGroup(const QString &text, InstanceView *view); - VisualGroup(const VisualGroup *other); + VisualGroup(QString text, InstanceView *view); + explicit VisualGroup(const VisualGroup *other); /* data */ InstanceView *view = nullptr; @@ -58,13 +78,13 @@ struct VisualGroup void update(); /// draw the header at y-position. - void drawHeader(QPainter *painter, const QStyleOptionViewItem &option); + void drawHeader(QPainter *painter, const QStyleOptionViewItem &option) const; /// height of the group, in total. includes a small bit of padding. int totalHeight() const; /// height of the group header, in pixels - int headerHeight() const; + static int headerHeight() ; /// height of the group content, in pixels int contentHeight() const; diff --git a/launcher/ui/pages/BasePage.h b/launcher/ui/pages/BasePage.h index ceb240401..dc2bde992 100644 --- a/launcher/ui/pages/BasePage.h +++ b/launcher/ui/pages/BasePage.h @@ -35,15 +35,16 @@ #pragma once -#include #include +#include +#include #include #include "BasePageContainer.h" -class BasePage -{ -public: +class BasePage { + public: + using updateExtraInfoFunc = std::function; virtual ~BasePage() {} virtual QString id() const = 0; virtual QString displayName() const = 0; @@ -63,17 +64,16 @@ public: } virtual void openedImpl() {} virtual void closedImpl() {} - virtual void setParentContainer(BasePageContainer * container) - { - m_container = container; - }; - virtual void retranslate() { } + virtual void setParentContainer(BasePageContainer* container) { m_container = container; }; + virtual void retranslate() {} -public: + public: int stackIndex = -1; int listIndex = -1; -protected: - BasePageContainer * m_container = nullptr; + updateExtraInfoFunc updateExtraInfo; + + protected: + BasePageContainer* m_container = nullptr; bool isOpened = false; }; diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index dca1b3a63..668aa0078 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -81,6 +81,8 @@ APIPage::APIPage(QWidget *parent) : connect(ui->pasteTypeComboBox, currentIndexChangedSignal, this, &APIPage::updateBaseURLPlaceholder); // This function needs to be called even when the ComboBox's index is still in its default state. updateBaseURLPlaceholder(ui->pasteTypeComboBox->currentIndex()); + // NOTE: this allows http://, but we replace that with https later anyway + ui->metaURL->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->metaURL)); ui->baseURLEntry->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->baseURLEntry)); ui->msaClientID->setValidator(new QRegularExpressionValidator(validMSAClientID, ui->msaClientID)); ui->flameKey->setValidator(new QRegularExpressionValidator(validFlameKey, ui->flameKey)); @@ -163,7 +165,7 @@ void APIPage::applySettings() QString msaClientID = ui->msaClientID->text(); s->set("MSAClientIDOverride", msaClientID); - QUrl metaURL = ui->metaURL->text(); + QUrl metaURL(ui->metaURL->text()); // Add required trailing slash if (!metaURL.isEmpty() && !metaURL.path().endsWith('/')) { diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index 278f45c49..fced5ff4a 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -159,19 +159,6 @@ void AccountListPage::on_actionAddMojang_triggered() void AccountListPage::on_actionAddMicrosoft_triggered() { - if(BuildConfig.BUILD_PLATFORM == "osx64") { - CustomMessageBox::selectable( - this, - tr("Microsoft Accounts not available"), - //: %1 refers to the launcher itself - tr( - "Microsoft accounts are only usable on macOS 10.13 or newer, with fully updated %1.\n\n" - "Please update both your operating system and %1." - ).arg(BuildConfig.LAUNCHER_DISPLAYNAME), - QMessageBox::Warning - )->exec(); - return; - } MinecraftAccountPtr account = MSALoginDialog::newAccount( this, tr("Please enter your Mojang account email and password to add your account.") diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 816dde723..2080b56f0 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -3,7 +3,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (c) 2022 dada513 - * Copyright (C) 2022 Tayou + * Copyright (C) 2022 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 55bd3eea7..26408f44f 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -169,10 +169,10 @@ - Disable using metadata provided by mod providers (like Modrinth or Curseforge) for mods. + Disable using metadata provided by mod providers (like Modrinth or CurseForge) for mods. - Disable using metadata for mods? + Disable using metadata for mods @@ -307,21 +307,21 @@ - Show console while the game is &running? + Show console while the game is &running - &Automatically close console when the game quits? + &Automatically close console when the game quits - Show console when the game &crashes? + Show console when the game &crashes diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp index eca3e8657..954823564 100644 --- a/launcher/ui/pages/global/MinecraftPage.cpp +++ b/launcher/ui/pages/global/MinecraftPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2023 seth * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -99,6 +100,9 @@ void MinecraftPage::applySettings() // Miscellaneous s->set("CloseAfterLaunch", ui->closeAfterLaunchCheck->isChecked()); s->set("QuitAfterGameStop", ui->quitAfterGameStopCheck->isChecked()); + + // Mod loader settings + s->set("DisableQuiltBeacon", ui->disableQuiltBeaconCheckBox->isChecked()); } void MinecraftPage::loadSettings() @@ -137,6 +141,8 @@ void MinecraftPage::loadSettings() ui->closeAfterLaunchCheck->setChecked(s->get("CloseAfterLaunch").toBool()); ui->quitAfterGameStopCheck->setChecked(s->get("QuitAfterGameStop").toBool()); + + ui->disableQuiltBeaconCheckBox->setChecked(s->get("DisableQuiltBeacon").toBool()); } void MinecraftPage::retranslate() diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui index 103881b57..a3188dccb 100644 --- a/launcher/ui/pages/global/MinecraftPage.ui +++ b/launcher/ui/pages/global/MinecraftPage.ui @@ -51,7 +51,7 @@ - Start Minecraft &maximized? + Start Minecraft &maximized @@ -190,6 +190,25 @@ Tweaks + + + + Mod loader settings + + + + + + Disable Quilt Loader Beacon + + + Disable Quilt loader's beacon for counting monthly active users + + + + + + diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index 1115ddc3b..12038f88f 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "ExternalResourcesPage.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui_ExternalResourcesPage.h" @@ -9,6 +44,7 @@ #include #include +#include ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared_ptr model, QWidget* parent) : QMainWindow(parent), m_instance(instance), ui(new Ui::ExternalResourcesPage), m_model(model) @@ -24,6 +60,8 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared m_filterModel->setSourceModel(m_model.get()); m_filterModel->setFilterKeyColumn(-1); ui->treeView->setModel(m_filterModel); + // must come after setModel + ui->treeView->setResizeModes(m_model->columnResizeModes()); ui->treeView->installEventFilter(this); ui->treeView->sortByColumn(1, Qt::AscendingOrder); @@ -43,7 +81,21 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared auto selection_model = ui->treeView->selectionModel(); connect(selection_model, &QItemSelectionModel::currentChanged, this, &ExternalResourcesPage::current); + auto updateExtra = [this]() { + if (updateExtraInfo) + updateExtraInfo(id(), extraHeaderInfoString()); + }; + connect(selection_model, &QItemSelectionModel::selectionChanged, this, updateExtra); + connect(model.get(), &ResourceFolderModel::updateFinished, this, updateExtra); + connect(ui->filterEdit, &QLineEdit::textChanged, this, &ExternalResourcesPage::filterTextChanged); + + auto viewHeader = ui->treeView->header(); + viewHeader->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(viewHeader, &QHeaderView::customContextMenuRequested, this, &ExternalResourcesPage::ShowHeaderContextMenu); + + m_model->loadHiddenColumns(ui->treeView); } ExternalResourcesPage::~ExternalResourcesPage() @@ -65,6 +117,13 @@ void ExternalResourcesPage::ShowContextMenu(const QPoint& pos) delete menu; } +void ExternalResourcesPage::ShowHeaderContextMenu(const QPoint& pos) +{ + auto menu = m_model->createHeaderContextMenu(ui->treeView); + menu->exec(ui->treeView->mapToGlobal(pos)); + menu->deleteLater(); +} + void ExternalResourcesPage::openedImpl() { m_model->startWatching(); @@ -92,11 +151,7 @@ void ExternalResourcesPage::retranslate() void ExternalResourcesPage::itemActivated(const QModelIndex&) { - if (!m_controlsEnabled) - return; - auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); - m_model->setResourceEnabled(selection.indexes(), EnableAction::TOGGLE); } void ExternalResourcesPage::filterTextChanged(const QString& newContents) @@ -139,9 +194,6 @@ bool ExternalResourcesPage::eventFilter(QObject* obj, QEvent* ev) void ExternalResourcesPage::addItem() { - if (!m_controlsEnabled) - return; - auto list = GuiUtil::BrowseForFiles( helpPage(), tr("Select %1", "Select whatever type of files the page contains. Example: 'Loader Mods'").arg(displayName()), m_fileSelectionFilter.arg(displayName()), APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); @@ -155,9 +207,6 @@ void ExternalResourcesPage::addItem() void ExternalResourcesPage::removeItem() { - if (!m_controlsEnabled) - return; - auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); int count = 0; @@ -201,23 +250,37 @@ void ExternalResourcesPage::removeItem() void ExternalResourcesPage::removeItems(const QItemSelection& selection) { + if (m_instance != nullptr && m_instance->isRunning()) { + auto response = CustomMessageBox::selectable(this, "Confirm Delete", + "If you remove this resource while the game is running it may crash your game.\n" + "Are you sure you want to do this?", + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } m_model->deleteResources(selection.indexes()); } void ExternalResourcesPage::enableItem() { - if (!m_controlsEnabled) - return; - auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); m_model->setResourceEnabled(selection.indexes(), EnableAction::ENABLE); } void ExternalResourcesPage::disableItem() { - if (!m_controlsEnabled) - return; + if (m_instance != nullptr && m_instance->isRunning()) { + auto response = CustomMessageBox::selectable(this, "Confirm disable", + "If you disable this resource while the game is running it may crash your game.\n" + "Are you sure you want to do this?", + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + if (response != QMessageBox::Yes) + return; + } auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); m_model->setResourceEnabled(selection.indexes(), EnableAction::DISABLE); } @@ -248,6 +311,15 @@ bool ExternalResourcesPage::onSelectionChanged(const QModelIndex& current, const int row = sourceCurrent.row(); Resource const& resource = m_model->at(row); ui->frame->updateWithResource(resource); - return true; } + +QString ExternalResourcesPage::extraHeaderInfoString() +{ + if (ui && ui->treeView && ui->treeView->selectionModel()) { + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + if (auto count = std::count_if(selection.cbegin(), selection.cend(), [](auto v) { return v.column() == 0; }); count != 0) + return tr(" (%1 installed, %2 selected)").arg(m_model->size()).arg(count); + } + return tr(" (%1 installed)").arg(m_model->size()); +} diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.h b/launcher/ui/pages/instance/ExternalResourcesPage.h index d17fbb7f1..97d922d88 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.h +++ b/launcher/ui/pages/instance/ExternalResourcesPage.h @@ -29,6 +29,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage { virtual QString helpPage() const override = 0; virtual bool shouldDisplay() const override = 0; + QString extraHeaderInfoString(); void openedImpl() override; void closedImpl() override; @@ -60,6 +61,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage { virtual void viewConfigs(); void ShowContextMenu(const QPoint& pos); + void ShowHeaderContextMenu(const QPoint& pos); protected: BaseInstance* m_instance = nullptr; @@ -71,7 +73,5 @@ class ExternalResourcesPage : public QMainWindow, public BasePage { QString m_fileSelectionFilter; QString m_viewFilter; - bool m_controlsEnabled = true; - std::shared_ptr m_wide_bar_setting = nullptr; }; diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui index 33a033366..3c8366917 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.ui +++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui @@ -62,6 +62,9 @@ QAbstractItemView::DropOnly + + true + @@ -154,6 +157,17 @@ Try to check or update all selected resources (all resources if none are selected) + + + false + + + Visit mod's page + + + Go to mods home page + + diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index 63b79b858..4d3cd16dd 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -4,6 +4,7 @@ * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad + * Copyright (C) 2023 seth * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -51,9 +52,9 @@ #include "Application.h" #include "minecraft/auth/AccountList.h" +#include "FileSystem.h" #include "java/JavaInstallList.h" #include "java/JavaUtils.h" -#include "FileSystem.h" InstanceSettingsPage::InstanceSettingsPage(BaseInstance *inst, QWidget *parent) : QWidget(parent), ui(new Ui::InstanceSettingsPage), m_instance(inst) @@ -64,18 +65,13 @@ InstanceSettingsPage::InstanceSettingsPage(BaseInstance *inst, QWidget *parent) connect(ui->openGlobalJavaSettingsButton, &QCommandLinkButton::clicked, this, &InstanceSettingsPage::globalSettingsButtonClicked); connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::applySettings); connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings); - connect(ui->instanceAccountSelector, QOverload::of(&QComboBox::currentIndexChanged), this, &InstanceSettingsPage::changeInstanceAccount); + connect(ui->instanceAccountSelector, QOverload::of(&QComboBox::currentIndexChanged), this, + &InstanceSettingsPage::changeInstanceAccount); loadSettings(); - updateThresholds(); } -bool InstanceSettingsPage::shouldDisplay() const -{ - return !m_instance->isRunning(); -} - InstanceSettingsPage::~InstanceSettingsPage() { delete ui; @@ -87,12 +83,12 @@ void InstanceSettingsPage::globalSettingsButtonClicked(bool) case 0: APPLICATION->ShowGlobalSettings(this, "java-settings"); return; - case 1: - APPLICATION->ShowGlobalSettings(this, "minecraft-settings"); - return; case 2: APPLICATION->ShowGlobalSettings(this, "custom-commands"); return; + default: + APPLICATION->ShowGlobalSettings(this, "minecraft-settings"); + return; } } @@ -286,6 +282,14 @@ void InstanceSettingsPage::applySettings() m_settings->reset("InstanceAccountId"); } + bool overrideModLoaderSettings = ui->modLoaderSettingsGroupBox->isChecked(); + m_settings->set("OverrideModLoaderSettings", overrideModLoaderSettings); + if (overrideModLoaderSettings) { + m_settings->set("DisableQuiltBeacon", ui->disableQuiltBeaconCheckBox->isChecked()); + } else { + m_settings->reset("DisableQuiltBeacon"); + } + bool onlineFixes = ui->onlineFixes->isChecked(); m_settings->set("OnlineFixes", onlineFixes); @@ -390,6 +394,10 @@ void InstanceSettingsPage::loadSettings() ui->instanceAccountGroupBox->setChecked(m_settings->get("UseAccountForInstance").toBool()); updateAccountsMenu(); + // Mod loader specific settings + ui->modLoaderSettingsGroupBox->setChecked(m_settings->get("OverrideModLoaderSettings").toBool()); + ui->disableQuiltBeaconCheckBox->setChecked(m_settings->get("DisableQuiltBeacon").toBool()); + ui->onlineFixes->setChecked(m_settings->get("OnlineFixes").toBool()); ui->onlineFixes->setVisible(m_instance->traits().contains("legacyServices")); } diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h index 043c3e25b..036b41818 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.h +++ b/launcher/ui/pages/instance/InstanceSettingsPage.h @@ -75,12 +75,11 @@ public: { return "Instance-settings"; } - virtual bool shouldDisplay() const override; void retranslate() override; void updateThresholds(); -private slots: + private slots: void on_javaDetectBtn_clicked(); void on_javaTestBtn_clicked(); void on_javaBrowseBtn_clicked(); diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui index b3b45b431..9018659aa 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.ui +++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui @@ -269,7 +269,7 @@ - Start Minecraft maximized? + Start Minecraft maximized @@ -341,21 +341,21 @@ - Show console while the game is running? + Show console while the game is running - Automatically close console when the game quits? + Automatically close console when the game quits - Show console when the game crashes? + Show console when the game crashes @@ -541,6 +541,31 @@ Miscellaneous + + + + true + + + false + + + Mod loader settings + + + + + + Disable Quilt Loader Beacon + + + Disable Quilt loader's beacon for counting monthly active users + + + + + + diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp index d0701a7ad..0fc0c9867 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.cpp +++ b/launcher/ui/pages/instance/ManagedPackPage.cpp @@ -69,7 +69,6 @@ class NoBigComboBoxStyle : public QProxyStyle { private: NoBigComboBoxStyle(QStyle* style) : QProxyStyle(style) {} - }; ManagedPackPage* ManagedPackPage::createPage(BaseInstance* inst, QString type, QWidget* parent) @@ -91,13 +90,13 @@ ManagedPackPage::ManagedPackPage(BaseInstance* inst, InstanceWindow* instance_wi // NOTE: GTK2 themes crash with the proxy style. // This seems like an upstream bug, so there's not much else that can be done. - if (!QStyleFactory::keys().contains("gtk2")){ + if (!QStyleFactory::keys().contains("gtk2")) { auto comboStyle = NoBigComboBoxStyle::getInstance(ui->versionsComboBox->style()); ui->versionsComboBox->setStyle(comboStyle); } ui->reloadButton->setVisible(false); - connect(ui->reloadButton, &QPushButton::clicked, this, [this](bool){ + connect(ui->reloadButton, &QPushButton::clicked, this, [this](bool) { ui->reloadButton->setVisible(false); m_loaded = false; @@ -205,7 +204,7 @@ ModrinthManagedPackPage::ModrinthManagedPackPage(BaseInstance* inst, InstanceWin { Q_ASSERT(inst->isManagedPack()); connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion())); - connect(ui->updateButton, &QPushButton::pressed, this, &ModrinthManagedPackPage::update); + connect(ui->updateButton, &QPushButton::clicked, this, &ModrinthManagedPackPage::update); } // MODRINTH @@ -226,7 +225,8 @@ void ModrinthManagedPackPage::parseManagedPack() QString id = m_inst->getManagedPackID(); - m_fetch_job->addNetAction(Net::Download::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response.get())); + m_fetch_job->addNetAction( + Net::Download::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] { QJsonParseError parse_error{}; @@ -267,7 +267,6 @@ void ModrinthManagedPackPage::parseManagedPack() if (version.version == m_inst->getManagedPackVersionName()) name = tr("%1 (Current)").arg(name); - ui->versionsComboBox->addItem(name, QVariant(version.id)); } @@ -291,6 +290,10 @@ QString ModrinthManagedPackPage::url() const void ModrinthManagedPackPage::suggestVersion() { auto index = ui->versionsComboBox->currentIndex(); + if (m_pack.versions.length() == 0) { + setFailState(); + return; + } auto version = m_pack.versions.at(index); ui->changelogTextBrowser->setHtml(markdownToHTML(version.changelog.toUtf8())); @@ -301,6 +304,10 @@ void ModrinthManagedPackPage::suggestVersion() void ModrinthManagedPackPage::update() { auto index = ui->versionsComboBox->currentIndex(); + if (m_pack.versions.length() == 0) { + setFailState(); + return; + } auto version = m_pack.versions.at(index); QMap extra_info; @@ -332,7 +339,7 @@ FlameManagedPackPage::FlameManagedPackPage(BaseInstance* inst, InstanceWindow* i { Q_ASSERT(inst->isManagedPack()); connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion())); - connect(ui->updateButton, &QPushButton::pressed, this, &FlameManagedPackPage::update); + connect(ui->updateButton, &QPushButton::clicked, this, &FlameManagedPackPage::update); } void FlameManagedPackPage::parseManagedPack() @@ -369,7 +376,7 @@ void FlameManagedPackPage::parseManagedPack() QString id = m_inst->getManagedPackID(); - m_fetch_job->addNetAction(Net::Download::makeByteArray(QString("%1/mods/%2/files").arg(BuildConfig.FLAME_BASE_URL, id), response.get())); + m_fetch_job->addNetAction(Net::Download::makeByteArray(QString("%1/mods/%2/files").arg(BuildConfig.FLAME_BASE_URL, id), response)); QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] { QJsonParseError parse_error{}; @@ -429,6 +436,10 @@ QString FlameManagedPackPage::url() const void FlameManagedPackPage::suggestVersion() { auto index = ui->versionsComboBox->currentIndex(); + if (m_pack.versions.length() == 0) { + setFailState(); + return; + } auto version = m_pack.versions.at(index); ui->changelogTextBrowser->setHtml(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId)); @@ -439,6 +450,10 @@ void FlameManagedPackPage::suggestVersion() void FlameManagedPackPage::update() { auto index = ui->versionsComboBox->currentIndex(); + if (m_pack.versions.length() == 0) { + setFailState(); + return; + } auto version = m_pack.versions.at(index); QMap extra_info; diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 4548af59a..cef292bd9 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -4,6 +4,7 @@ * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad + * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -44,6 +45,7 @@ #include #include #include +#include #include "Application.h" @@ -59,6 +61,7 @@ #include "minecraft/mod/Mod.h" #include "minecraft/mod/ModFolderModel.h" +#include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" #include "Version.h" @@ -85,45 +88,40 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr ui->actionsToolbar->insertActionAfter(ui->actionAddItem, ui->actionUpdateItem); connect(ui->actionUpdateItem, &QAction::triggered, this, &ModFolderPage::updateMods); - auto check_allow_update = [this] { - return (!m_instance || !m_instance->isRunning()) && - (ui->treeView->selectionModel()->hasSelection() || !m_model->empty()); - }; + ui->actionVisitItemPage->setToolTip(tr("Go to mod's home page")); + ui->actionsToolbar->addAction(ui->actionVisitItemPage); + connect(ui->actionVisitItemPage, &QAction::triggered, this, &ModFolderPage::visitModPages); + + auto check_allow_update = [this] { return ui->treeView->selectionModel()->hasSelection() || !m_model->empty(); }; connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); + + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + auto mods_list = m_model->selectedMods(selection); + auto selected = std::count_if(mods_list.cbegin(), mods_list.cend(), + [](Mod* v) { return v->metadata() != nullptr || v->homeurl().size() != 0; }); + if (selected <= 1) { + ui->actionVisitItemPage->setText(tr("Visit mod's page")); + ui->actionVisitItemPage->setToolTip(tr("Go to mod's home page")); + } else { + ui->actionVisitItemPage->setText(tr("Visit mods' pages")); + ui->actionVisitItemPage->setToolTip(tr("Go to the pages of the selected mods")); + } + ui->actionVisitItemPage->setEnabled(selected != 0); }); - connect(mods.get(), &ModFolderModel::rowsInserted, this, [this, check_allow_update] { - ui->actionUpdateItem->setEnabled(check_allow_update()); - }); + connect(mods.get(), &ModFolderModel::rowsInserted, this, + [this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); }); - connect(mods.get(), &ModFolderModel::rowsRemoved, this, [this, check_allow_update] { - ui->actionUpdateItem->setEnabled(check_allow_update()); - }); + connect(mods.get(), &ModFolderModel::rowsRemoved, this, + [this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); }); - connect(mods.get(), &ModFolderModel::updateFinished, this, [this, check_allow_update, mods] { - ui->actionUpdateItem->setEnabled(check_allow_update()); - - // Prevent a weird crash when trying to open the mods page twice in a session o.O - disconnect(mods.get(), &ModFolderModel::updateFinished, this, 0); - }); - - connect(m_instance, &BaseInstance::runningStatusChanged, this, &ModFolderPage::runningStateChanged); - ModFolderPage::runningStateChanged(m_instance && m_instance->isRunning()); + connect(mods.get(), &ModFolderModel::updateFinished, this, + [this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); }); } } -void ModFolderPage::runningStateChanged(bool running) -{ - ui->actionDownloadItem->setEnabled(!running); - ui->actionUpdateItem->setEnabled(!running); - ui->actionAddItem->setEnabled(!running); - ui->actionEnableItem->setEnabled(!running); - ui->actionDisableItem->setEnabled(!running); - ui->actionRemoveItem->setEnabled(!running); -} - bool ModFolderPage::shouldDisplay() const { return true; @@ -140,15 +138,23 @@ bool ModFolderPage::onSelectionChanged(const QModelIndex& current, const QModelI return true; } -void ModFolderPage::removeItems(const QItemSelection &selection) +void ModFolderPage::removeItems(const QItemSelection& selection) { + if (m_instance != nullptr && m_instance->isRunning()) { + auto response = CustomMessageBox::selectable(this, "Confirm Delete", + "If you remove mods while the game is running it may crash your game.\n" + "Are you sure you want to do this?", + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } m_model->deleteMods(selection.indexes()); } void ModFolderPage::installMods() { - if (!m_controlsEnabled) - return; if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance @@ -214,8 +220,7 @@ void ModFolderPage::updateMods() message = tr("All selected mods are up-to-date! :)"); } } - CustomMessageBox::selectable(this, tr("Update checker"), message) - ->exec(); + CustomMessageBox::selectable(this, tr("Update checker"), message)->exec(); return; } @@ -282,3 +287,13 @@ bool NilModFolderPage::shouldDisplay() const { return m_model->dir().exists(); } + +void ModFolderPage::visitModPages() +{ + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + for (auto mod : m_model->selectedMods(selection)) { + auto url = mod->metaurl(); + if (!url.isEmpty()) + DesktopServices::openUrl(url); + } +} diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index 2fc7b5748..a23dcae18 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -4,6 +4,7 @@ * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad + * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -59,11 +60,11 @@ class ModFolderPage : public ExternalResourcesPage { bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override; private slots: - void runningStateChanged(bool running); - void removeItems(const QItemSelection &selection) override; + void removeItems(const QItemSelection& selection) override; void installMods(); void updateMods(); + void visitModPages(); protected: std::shared_ptr m_model; diff --git a/launcher/ui/pages/instance/ResourcePackPage.cpp b/launcher/ui/pages/instance/ResourcePackPage.cpp index 24bfb38dc..12b371df4 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.cpp +++ b/launcher/ui/pages/instance/ResourcePackPage.cpp @@ -67,8 +67,6 @@ bool ResourcePackPage::onSelectionChanged(const QModelIndex& current, const QMod void ResourcePackPage::downloadRPs() { - if (!m_controlsEnabled) - return; if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index ca368d3b7..bcce5f572 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -36,6 +36,7 @@ */ #include "ScreenshotsPage.h" +#include "BuildConfig.h" #include "ui_ScreenshotsPage.h" #include @@ -96,37 +97,30 @@ public: return; if ((info.suffix().compare("png", Qt::CaseInsensitive) != 0)) return; - int tries = 5; - while (tries) - { - if (!m_cache->stale(m_path)) - return; - QImage image(m_path); - if (image.isNull()) - { - QThread::msleep(500); - tries--; - continue; - } - QImage small; - if (image.width() > image.height()) - small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation); - else - small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation); - QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2); - QImage square(QSize(256, 256), QImage::Format_ARGB32); - square.fill(Qt::transparent); - - QPainter painter(&square); - painter.drawImage(offset, small); - painter.end(); - - QIcon icon(QPixmap::fromImage(square)); - m_cache->add(m_path, icon); - m_resultEmitter.emitResultsReady(m_path); + if (!m_cache->stale(m_path)) + return; + QImage image(m_path); + if (image.isNull()) { + m_resultEmitter.emitResultsFailed(m_path); + qDebug() << "Error loading screenshot: " + m_path + ". Perhaps too large?"; return; } - m_resultEmitter.emitResultsFailed(m_path); + QImage small; + if (image.width() > image.height()) + small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation); + else + small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation); + QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2); + QImage square(QSize(256, 256), QImage::Format_ARGB32); + square.fill(Qt::transparent); + + QPainter painter(&square); + painter.drawImage(offset, small); + painter.end(); + + QIcon icon(QPixmap::fromImage(square)); + m_cache->add(m_path, icon); + m_resultEmitter.emitResultsReady(m_path); } QString m_path; SharedIconCachePtr m_cache; @@ -145,9 +139,12 @@ public: m_thumbnailCache = std::make_shared(); m_thumbnailCache->add("placeholder", APPLICATION->getThemedIcon("screenshot-placeholder")); connect(&watcher, SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString))); - // FIXME: the watched file set is not updated when files are removed } - virtual ~FilterModel() { m_thumbnailingPool.waitForDone(500); } + virtual ~FilterModel() { + m_thumbnailingPool.clear(); + if (!m_thumbnailingPool.waitForDone(500)) + qDebug() << "Thumbnail pool took longer than 500ms to finish"; + } virtual QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const { auto model = sourceModel(); @@ -214,10 +211,12 @@ private slots: void fileChanged(QString filepath) { m_thumbnailCache->setStale(filepath); - thumbnailImage(filepath); // reinsert the path... watcher.removePath(filepath); - watcher.addPath(filepath); + if (QFile::exists(filepath)) { + watcher.addPath(filepath); + thumbnailImage(filepath); + } } private: @@ -380,16 +379,18 @@ void ScreenshotsPage::on_actionUpload_triggered() if (selection.isEmpty()) return; - QString text; + QUrl baseUrl(BuildConfig.IMGUR_BASE_URL); if (selection.size() > 1) - text = tr("You are about to upload %1 screenshots.\n\n" + text = tr("You are about to upload %1 screenshots to %2.\n" + "You should double-check for personal information.\n\n" "Are you sure?") - .arg(selection.size()); + .arg(QString::number(selection.size()), baseUrl.host()); else - text = - tr("You are about to upload the selected screenshot.\n\n" - "Are you sure?"); + text = tr("You are about to upload the selected screenshot to %1.\n" + "You should double-check for personal information.\n\n" + "Are you sure?") + .arg(baseUrl.host()); auto response = CustomMessageBox::selectable(this, "Confirm Upload", text, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) diff --git a/launcher/ui/pages/instance/ShaderPackPage.cpp b/launcher/ui/pages/instance/ShaderPackPage.cpp index 2d0c10aaf..dc8b0a05b 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.cpp +++ b/launcher/ui/pages/instance/ShaderPackPage.cpp @@ -46,7 +46,6 @@ #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ResourceDownloadDialog.h" - ShaderPackPage::ShaderPackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent) : ExternalResourcesPage(instance, model, parent) { @@ -61,8 +60,6 @@ ShaderPackPage::ShaderPackPage(MinecraftInstance* instance, std::shared_ptrtypeName() != "Minecraft") return; // this is a null instance or a legacy instance diff --git a/launcher/ui/pages/instance/TexturePackPage.cpp b/launcher/ui/pages/instance/TexturePackPage.cpp index 427aba11a..e477ceda3 100644 --- a/launcher/ui/pages/instance/TexturePackPage.cpp +++ b/launcher/ui/pages/instance/TexturePackPage.cpp @@ -69,8 +69,6 @@ bool TexturePackPage::onSelectionChanged(const QModelIndex& current, const QMode void TexturePackPage::downloadTPs() { - if (!m_controlsEnabled) - return; if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 74b7ec7c2..a180c8041 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -40,14 +40,13 @@ #include "Application.h" -#include -#include +#include #include #include -#include -#include -#include +#include #include +#include +#include #include #include @@ -55,49 +54,42 @@ #include "ui_VersionPage.h" #include "ui/dialogs/CustomMessageBox.h" -#include "ui/dialogs/VersionSelectDialog.h" #include "ui/dialogs/NewComponentDialog.h" #include "ui/dialogs/ProgressDialog.h" +#include "ui/dialogs/VersionSelectDialog.h" #include "ui/GuiUtil.h" +#include "DesktopServices.h" +#include "Exception.h" +#include "Version.h" +#include "icons/IconList.h" #include "minecraft/PackProfile.h" #include "minecraft/auth/AccountList.h" #include "minecraft/mod/Mod.h" -#include "icons/IconList.h" -#include "Exception.h" -#include "Version.h" -#include "DesktopServices.h" #include "meta/Index.h" #include "meta/VersionList.h" -class IconProxy : public QIdentityProxyModel -{ +class IconProxy : public QIdentityProxyModel { Q_OBJECT -public: - - IconProxy(QWidget *parentWidget) : QIdentityProxyModel(parentWidget) + public: + IconProxy(QWidget* parentWidget) : QIdentityProxyModel(parentWidget) { connect(parentWidget, &QObject::destroyed, this, &IconProxy::widgetGone); m_parentWidget = parentWidget; } - virtual QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const override + virtual QVariant data(const QModelIndex& proxyIndex, int role = Qt::DisplayRole) const override { QVariant var = QIdentityProxyModel::data(proxyIndex, role); int column = proxyIndex.column(); - if(column == 0 && role == Qt::DecorationRole && m_parentWidget) - { - if(!var.isNull()) - { + if (column == 0 && role == Qt::DecorationRole && m_parentWidget) { + if (!var.isNull()) { auto string = var.toString(); - if(string == "warning") - { + if (string == "warning") { return APPLICATION->getThemedIcon("status-yellow"); - } - else if(string == "error") - { + } else if (string == "error") { return APPLICATION->getThemedIcon("status-bad"); } } @@ -105,14 +97,11 @@ public: } return var; } -private slots: - void widgetGone() - { - m_parentWidget = nullptr; - } + private slots: + void widgetGone() { m_parentWidget = nullptr; } -private: - QWidget *m_parentWidget = nullptr; + private: + QWidget* m_parentWidget = nullptr; }; QIcon VersionPage::icon() const @@ -144,15 +133,14 @@ void VersionPage::closedImpl() m_wide_bar_setting->set(ui->toolBar->getVisibilityState()); } -QMenu * VersionPage::createPopupMenu() +QMenu* VersionPage::createPopupMenu() { QMenu* filteredMenu = QMainWindow::createPopupMenu(); - filteredMenu->removeAction( ui->toolBar->toggleViewAction() ); + filteredMenu->removeAction(ui->toolBar->toggleViewAction()); return filteredMenu; } -VersionPage::VersionPage(MinecraftInstance *inst, QWidget *parent) - : QMainWindow(parent), ui(new Ui::VersionPage), m_inst(inst) +VersionPage::VersionPage(MinecraftInstance* inst, QWidget* parent) : QMainWindow(parent), ui(new Ui::VersionPage), m_inst(inst) { ui->setupUi(this); @@ -182,10 +170,8 @@ VersionPage::VersionPage(MinecraftInstance *inst, QWidget *parent) connect(smodel, &QItemSelectionModel::currentChanged, this, &VersionPage::packageCurrent); connect(m_profile.get(), &PackProfile::minecraftChanged, this, &VersionPage::updateVersionControls); - controlsEnabled = !m_inst->isRunning(); updateVersionControls(); preselect(0); - connect(m_inst, &BaseInstance::runningStatusChanged, this, &VersionPage::updateRunningStatus); connect(ui->packageView, &ModListView::customContextMenuRequested, this, &VersionPage::showContextMenu); connect(ui->filterEdit, &QLineEdit::textChanged, this, &VersionPage::onFilterTextChanged); } @@ -202,18 +188,16 @@ void VersionPage::showContextMenu(const QPoint& pos) delete menu; } -void VersionPage::packageCurrent(const QModelIndex ¤t, const QModelIndex &previous) +void VersionPage::packageCurrent(const QModelIndex& current, const QModelIndex& previous) { - if (!current.isValid()) - { + if (!current.isValid()) { ui->frame->clear(); return; } int row = current.row(); auto patch = m_profile->getComponent(row); auto severity = patch->getProblemSeverity(); - switch(severity) - { + switch (severity) { case ProblemSeverity::Warning: ui->frame->setName(tr("%1 possibly has issues.").arg(patch->getName())); break; @@ -226,16 +210,12 @@ void VersionPage::packageCurrent(const QModelIndex ¤t, const QModelIndex & return; } - auto &problems = patch->getProblems(); + auto& problems = patch->getProblems(); QString problemOut; - for (auto &problem: problems) - { - if(problem.m_severity == ProblemSeverity::Error) - { + for (auto& problem : problems) { + if (problem.m_severity == ProblemSeverity::Error) { problemOut += tr("Error: "); - } - else if(problem.m_severity == ProblemSeverity::Warning) - { + } else if (problem.m_severity == ProblemSeverity::Warning) { problemOut += tr("Warning: "); } problemOut += problem.m_description; @@ -244,72 +224,47 @@ void VersionPage::packageCurrent(const QModelIndex ¤t, const QModelIndex & ui->frame->setDescription(problemOut); } -void VersionPage::updateRunningStatus(bool running) -{ - if(controlsEnabled == running) { - controlsEnabled = !running; - updateVersionControls(); - } -} - void VersionPage::updateVersionControls() { // FIXME: this is a dirty hack auto minecraftVersion = Version(m_profile->getComponentVersion("net.minecraft")); - ui->actionInstall_Forge->setEnabled(controlsEnabled); - bool supportsFabric = minecraftVersion >= Version("1.14"); - ui->actionInstall_Fabric->setEnabled(controlsEnabled && supportsFabric); + ui->actionInstall_Fabric->setEnabled(supportsFabric); bool supportsQuilt = minecraftVersion >= Version("1.14"); - ui->actionInstall_Quilt->setEnabled(controlsEnabled && supportsQuilt); + ui->actionInstall_Quilt->setEnabled(supportsQuilt); bool supportsLiteLoader = minecraftVersion <= Version("1.12.2"); - ui->actionInstall_LiteLoader->setEnabled(controlsEnabled && supportsLiteLoader); + ui->actionInstall_LiteLoader->setEnabled(supportsLiteLoader); updateButtons(); } void VersionPage::updateButtons(int row) { - if(row == -1) + if (row == -1) row = currentRow(); auto patch = m_profile->getComponent(row); - ui->actionRemove->setEnabled(controlsEnabled && patch && patch->isRemovable()); - ui->actionMove_down->setEnabled(controlsEnabled && patch && patch->isMoveable()); - ui->actionMove_up->setEnabled(controlsEnabled && patch && patch->isMoveable()); - ui->actionChange_version->setEnabled(controlsEnabled && patch && patch->isVersionChangeable()); - ui->actionEdit->setEnabled(controlsEnabled && patch && patch->isCustom()); - ui->actionCustomize->setEnabled(controlsEnabled && patch && patch->isCustomizable()); - ui->actionRevert->setEnabled(controlsEnabled && patch && patch->isRevertible()); - ui->actionDownload_All->setEnabled(controlsEnabled); - ui->actionAdd_Empty->setEnabled(controlsEnabled); - ui->actionImport_Components->setEnabled(controlsEnabled); - ui->actionReload->setEnabled(controlsEnabled); - ui->actionInstall_mods->setEnabled(controlsEnabled); - ui->actionReplace_Minecraft_jar->setEnabled(controlsEnabled); - ui->actionAdd_to_Minecraft_jar->setEnabled(controlsEnabled); - ui->actionAdd_Agents->setEnabled(controlsEnabled); + ui->actionRemove->setEnabled(patch && patch->isRemovable()); + ui->actionMove_down->setEnabled(patch && patch->isMoveable()); + ui->actionMove_up->setEnabled(patch && patch->isMoveable()); + ui->actionChange_version->setEnabled(patch && patch->isVersionChangeable()); + ui->actionEdit->setEnabled(patch && patch->isCustom()); + ui->actionCustomize->setEnabled(patch && patch->isCustomizable()); + ui->actionRevert->setEnabled(patch && patch->isRevertible()); } bool VersionPage::reloadPackProfile() { - try - { + try { m_profile->reload(Net::Mode::Online); return true; - } - catch (const Exception &e) - { + } catch (const Exception& e) { QMessageBox::critical(this, tr("Error"), e.cause()); return false; - } - catch (...) - { - QMessageBox::critical( - this, tr("Error"), - tr("Couldn't load the instance profile.")); + } catch (...) { + QMessageBox::critical(this, tr("Error"), tr("Couldn't load the instance profile.")); return false; } } @@ -322,14 +277,12 @@ void VersionPage::on_actionReload_triggered() void VersionPage::on_actionRemove_triggered() { - if (!ui->packageView->currentIndex().isValid()) - { + if (!ui->packageView->currentIndex().isValid()) { return; } int index = ui->packageView->currentIndex().row(); auto component = m_profile->getComponent(index); - if (component->isCustom()) - { + if (component->isCustom()) { auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), tr("You are about to remove \"%1\".\n" "This is permanent and will completely remove the custom component.\n\n" @@ -342,8 +295,7 @@ void VersionPage::on_actionRemove_triggered() return; } // FIXME: use actual model, not reloading. - if (!m_profile->remove(index)) - { + if (!m_profile->remove(index)) { QMessageBox::critical(this, tr("Error"), tr("Couldn't remove file")); } updateButtons(); @@ -353,17 +305,16 @@ void VersionPage::on_actionRemove_triggered() void VersionPage::on_actionInstall_mods_triggered() { - if(m_container) - { + if (m_container) { m_container->selectPage("mods"); } } void VersionPage::on_actionAdd_to_Minecraft_jar_triggered() { - auto list = GuiUtil::BrowseForFiles("jarmod", tr("Select jar mods"), tr("Minecraft.jar mods (*.zip *.jar)"), APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); - if(!list.empty()) - { + auto list = GuiUtil::BrowseForFiles("jarmod", tr("Select jar mods"), tr("Minecraft.jar mods (*.zip *.jar)"), + APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); + if (!list.empty()) { m_profile->installJarMods(list); } updateButtons(); @@ -371,9 +322,9 @@ void VersionPage::on_actionAdd_to_Minecraft_jar_triggered() void VersionPage::on_actionReplace_Minecraft_jar_triggered() { - auto jarPath = GuiUtil::BrowseForFile("jar", tr("Select jar"), tr("Minecraft.jar replacement (*.jar)"), APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); - if(!jarPath.isEmpty()) - { + auto jarPath = GuiUtil::BrowseForFile("jar", tr("Select jar"), tr("Minecraft.jar replacement (*.jar)"), + APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); + if (!jarPath.isEmpty()) { m_profile->installCustomJar(jarPath); } updateButtons(); @@ -407,12 +358,9 @@ void VersionPage::on_actionAdd_Agents_triggered() void VersionPage::on_actionMove_up_triggered() { - try - { + try { m_profile->move(currentRow(), PackProfile::MoveUp); - } - catch (const Exception &e) - { + } catch (const Exception& e) { QMessageBox::critical(this, tr("Error"), e.cause()); } updateButtons(); @@ -420,12 +368,9 @@ void VersionPage::on_actionMove_up_triggered() void VersionPage::on_actionMove_down_triggered() { - try - { + try { m_profile->move(currentRow(), PackProfile::MoveDown); - } - catch (const Exception &e) - { + } catch (const Exception& e) { QMessageBox::critical(this, tr("Error"), e.cause()); } updateButtons(); @@ -434,39 +379,32 @@ void VersionPage::on_actionMove_down_triggered() void VersionPage::on_actionChange_version_triggered() { auto versionRow = currentRow(); - if(versionRow == -1) - { + if (versionRow == -1) { return; } auto patch = m_profile->getComponent(versionRow); auto name = patch->getName(); auto list = patch->getVersionList(); - if(!list) - { + if (!list) { return; } auto uid = list->uid(); // FIXME: this is a horrible HACK. Get version filtering information from the actual metadata... - if(uid == "net.minecraftforge") - { + if (uid == "net.minecraftforge") { on_actionInstall_Forge_triggered(); return; - } - else if (uid == "com.mumfrey.liteloader") - { + } else if (uid == "com.mumfrey.liteloader") { on_actionInstall_LiteLoader_triggered(); return; } VersionSelectDialog vselect(list.get(), tr("Change %1 version").arg(name), this); - if (uid == "net.fabricmc.intermediary" || uid == "org.quiltmc.hashed") - { + if (uid == "net.fabricmc.intermediary" || uid == "org.quiltmc.hashed") { vselect.setEmptyString(tr("No intermediary mappings versions are currently available.")); vselect.setEmptyErrorString(tr("Couldn't load or download the intermediary mappings version lists!")); vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft")); } auto currentVersion = patch->getVersion(); - if(!currentVersion.isEmpty()) - { + if (!currentVersion.isEmpty()) { vselect.setCurrentVersion(currentVersion); } if (!vselect.exec() || !vselect.selectedVersion()) @@ -474,8 +412,7 @@ void VersionPage::on_actionChange_version_triggered() qDebug() << "Change" << uid << "to" << vselect.selectedVersion()->descriptor(); bool important = false; - if(uid == "net.minecraft") - { + if (uid == "net.minecraft") { important = true; } m_profile->setComponentVersion(uid, vselect.selectedVersion()->descriptor(), important); @@ -485,19 +422,17 @@ void VersionPage::on_actionChange_version_triggered() void VersionPage::on_actionDownload_All_triggered() { - if (!APPLICATION->accounts()->anyAccountIsValid()) - { - CustomMessageBox::selectable( - this, tr("Error"), - tr("Cannot download Minecraft or update instances unless you have at least " - "one account added.\nPlease add your Mojang or Minecraft account."), - QMessageBox::Warning)->show(); + if (!APPLICATION->accounts()->anyAccountIsValid()) { + CustomMessageBox::selectable(this, tr("Error"), + tr("Cannot download Minecraft or update instances unless you have at least " + "one account added.\nPlease add your Mojang or Minecraft account."), + QMessageBox::Warning) + ->show(); return; } auto updateTask = m_inst->createUpdateTask(Net::Mode::Online); - if (!updateTask) - { + if (!updateTask) { return; } ProgressDialog tDialog(this); @@ -511,28 +446,26 @@ void VersionPage::on_actionDownload_All_triggered() void VersionPage::on_actionInstall_Forge_triggered() { auto vlist = APPLICATION->metadataIndex()->get("net.minecraftforge"); - if(!vlist) - { + if (!vlist) { return; } VersionSelectDialog vselect(vlist.get(), tr("Select Forge version"), this); vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft")); - vselect.setEmptyString(tr("No Forge versions are currently available for Minecraft ") + m_profile->getComponentVersion("net.minecraft")); + vselect.setEmptyString(tr("No Forge versions are currently available for Minecraft ") + + m_profile->getComponentVersion("net.minecraft")); vselect.setEmptyErrorString(tr("Couldn't load or download the Forge version lists!")); auto currentVersion = m_profile->getComponentVersion("net.minecraftforge"); - if(!currentVersion.isEmpty()) - { + if (!currentVersion.isEmpty()) { vselect.setCurrentVersion(currentVersion); } - if (vselect.exec() && vselect.selectedVersion()) - { + if (vselect.exec() && vselect.selectedVersion()) { auto vsn = vselect.selectedVersion(); m_profile->setComponentVersion("net.minecraftforge", vsn->descriptor()); m_profile->resolve(Net::Mode::Online); // m_profile->installVersion(); - preselect(m_profile->rowCount(QModelIndex())-1); + preselect(m_profile->rowCount(QModelIndex()) - 1); m_container->refreshContainer(); } } @@ -540,8 +473,7 @@ void VersionPage::on_actionInstall_Forge_triggered() void VersionPage::on_actionInstall_Fabric_triggered() { auto vlist = APPLICATION->metadataIndex()->get("net.fabricmc.fabric-loader"); - if(!vlist) - { + if (!vlist) { return; } VersionSelectDialog vselect(vlist.get(), tr("Select Fabric Loader version"), this); @@ -549,17 +481,15 @@ void VersionPage::on_actionInstall_Fabric_triggered() vselect.setEmptyErrorString(tr("Couldn't load or download the Fabric Loader version lists!")); auto currentVersion = m_profile->getComponentVersion("net.fabricmc.fabric-loader"); - if(!currentVersion.isEmpty()) - { + if (!currentVersion.isEmpty()) { vselect.setCurrentVersion(currentVersion); } - if (vselect.exec() && vselect.selectedVersion()) - { + if (vselect.exec() && vselect.selectedVersion()) { auto vsn = vselect.selectedVersion(); m_profile->setComponentVersion("net.fabricmc.fabric-loader", vsn->descriptor()); m_profile->resolve(Net::Mode::Online); - preselect(m_profile->rowCount(QModelIndex())-1); + preselect(m_profile->rowCount(QModelIndex()) - 1); m_container->refreshContainer(); } } @@ -567,8 +497,7 @@ void VersionPage::on_actionInstall_Fabric_triggered() void VersionPage::on_actionInstall_Quilt_triggered() { auto vlist = APPLICATION->metadataIndex()->get("org.quiltmc.quilt-loader"); - if(!vlist) - { + if (!vlist) { return; } VersionSelectDialog vselect(vlist.get(), tr("Select Quilt Loader version"), this); @@ -576,17 +505,15 @@ void VersionPage::on_actionInstall_Quilt_triggered() vselect.setEmptyErrorString(tr("Couldn't load or download the Quilt Loader version lists!")); auto currentVersion = m_profile->getComponentVersion("org.quiltmc.quilt-loader"); - if(!currentVersion.isEmpty()) - { + if (!currentVersion.isEmpty()) { vselect.setCurrentVersion(currentVersion); } - if (vselect.exec() && vselect.selectedVersion()) - { + if (vselect.exec() && vselect.selectedVersion()) { auto vsn = vselect.selectedVersion(); m_profile->setComponentVersion("org.quiltmc.quilt-loader", vsn->descriptor()); m_profile->resolve(Net::Mode::Online); - preselect(m_profile->rowCount(QModelIndex())-1); + preselect(m_profile->rowCount(QModelIndex()) - 1); m_container->refreshContainer(); } } @@ -595,14 +522,12 @@ void VersionPage::on_actionAdd_Empty_triggered() { NewComponentDialog compdialog(QString(), QString(), this); QStringList blacklist; - for(int i = 0; i < m_profile->rowCount(); i++) - { + for (int i = 0; i < m_profile->rowCount(); i++) { auto comp = m_profile->getComponent(i); blacklist.push_back(comp->getID()); } compdialog.setBlacklist(blacklist); - if (compdialog.exec()) - { + if (compdialog.exec()) { qDebug() << "name:" << compdialog.name(); qDebug() << "uid:" << compdialog.uid(); m_profile->installEmpty(compdialog.uid(), compdialog.name()); @@ -612,28 +537,26 @@ void VersionPage::on_actionAdd_Empty_triggered() void VersionPage::on_actionInstall_LiteLoader_triggered() { auto vlist = APPLICATION->metadataIndex()->get("com.mumfrey.liteloader"); - if(!vlist) - { + if (!vlist) { return; } VersionSelectDialog vselect(vlist.get(), tr("Select LiteLoader version"), this); vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft")); - vselect.setEmptyString(tr("No LiteLoader versions are currently available for Minecraft ") + m_profile->getComponentVersion("net.minecraft")); + vselect.setEmptyString(tr("No LiteLoader versions are currently available for Minecraft ") + + m_profile->getComponentVersion("net.minecraft")); vselect.setEmptyErrorString(tr("Couldn't load or download the LiteLoader version lists!")); auto currentVersion = m_profile->getComponentVersion("com.mumfrey.liteloader"); - if(!currentVersion.isEmpty()) - { + if (!currentVersion.isEmpty()) { vselect.setCurrentVersion(currentVersion); } - if (vselect.exec() && vselect.selectedVersion()) - { + if (vselect.exec() && vselect.selectedVersion()) { auto vsn = vselect.selectedVersion(); m_profile->setComponentVersion("com.mumfrey.liteloader", vsn->descriptor()); m_profile->resolve(Net::Mode::Online); // m_profile->installVersion(vselect.selectedVersion()); - preselect(m_profile->rowCount(QModelIndex())-1); + preselect(m_profile->rowCount(QModelIndex()) - 1); m_container->refreshContainer(); } } @@ -648,7 +571,7 @@ void VersionPage::on_actionMinecraftFolder_triggered() DesktopServices::openDirectory(m_inst->gameRoot(), true); } -void VersionPage::versionCurrent(const QModelIndex ¤t, const QModelIndex &previous) +void VersionPage::versionCurrent(const QModelIndex& current, const QModelIndex& previous) { currentIdx = current.row(); updateButtons(currentIdx); @@ -656,16 +579,13 @@ void VersionPage::versionCurrent(const QModelIndex ¤t, const QModelIndex & void VersionPage::preselect(int row) { - if(row < 0) - { + if (row < 0) { row = 0; } - if(row >= m_profile->rowCount(QModelIndex())) - { + if (row >= m_profile->rowCount(QModelIndex())) { row = m_profile->rowCount(QModelIndex()) - 1; } - if(row < 0) - { + if (row < 0) { return; } auto model_index = m_profile->index(row); @@ -681,8 +601,7 @@ void VersionPage::onGameUpdateError(QString error) ComponentPtr VersionPage::current() { auto row = currentRow(); - if(row < 0) - { + if (row < 0) { return nullptr; } return m_profile->getComponent(row); @@ -690,8 +609,7 @@ ComponentPtr VersionPage::current() int VersionPage::currentRow() { - if (ui->packageView->selectionModel()->selectedRows().isEmpty()) - { + if (ui->packageView->selectionModel()->selectedRows().isEmpty()) { return -1; } return ui->packageView->selectionModel()->selectedRows().first().row(); @@ -700,18 +618,15 @@ int VersionPage::currentRow() void VersionPage::on_actionCustomize_triggered() { auto version = currentRow(); - if(version == -1) - { + if (version == -1) { return; } auto patch = m_profile->getComponent(version); - if(!patch->getVersionFile()) - { + if (!patch->getVersionFile()) { // TODO: wait for the update task to finish here... return; } - if(!m_profile->customize(version)) - { + if (!m_profile->customize(version)) { // TODO: some error box here } updateButtons(); @@ -721,13 +636,11 @@ void VersionPage::on_actionCustomize_triggered() void VersionPage::on_actionEdit_triggered() { auto version = current(); - if(!version) - { + if (!version) { return; } auto filename = version->getFilename(); - if(!QFileInfo::exists(filename)) - { + if (!QFileInfo::exists(filename)) { qWarning() << "file" << filename << "can't be opened for editing, doesn't exist!"; return; } @@ -737,8 +650,7 @@ void VersionPage::on_actionEdit_triggered() void VersionPage::on_actionRevert_triggered() { auto version = currentRow(); - if(version == -1) - { + if (version == -1) { return; } auto component = m_profile->getComponent(version); @@ -754,8 +666,7 @@ void VersionPage::on_actionRevert_triggered() if (response != QMessageBox::Yes) return; - if(!m_profile->revertToBase(version)) - { + if (!m_profile->revertToBase(version)) { // TODO: some error box here } updateButtons(); @@ -763,7 +674,7 @@ void VersionPage::on_actionRevert_triggered() m_container->refreshContainer(); } -void VersionPage::onFilterTextChanged(const QString &newContents) +void VersionPage::onFilterTextChanged(const QString& newContents) { m_filterModel->setFilterFixedString(newContents); } diff --git a/launcher/ui/pages/instance/VersionPage.h b/launcher/ui/pages/instance/VersionPage.h index d00877142..45d383f41 100644 --- a/launcher/ui/pages/instance/VersionPage.h +++ b/launcher/ui/pages/instance/VersionPage.h @@ -46,38 +46,27 @@ #include "minecraft/PackProfile.h" #include "ui/pages/BasePage.h" -namespace Ui -{ +namespace Ui { class VersionPage; } -class VersionPage : public QMainWindow, public BasePage -{ +class VersionPage : public QMainWindow, public BasePage { Q_OBJECT -public: - explicit VersionPage(MinecraftInstance *inst, QWidget *parent = 0); + public: + explicit VersionPage(MinecraftInstance* inst, QWidget* parent = 0); virtual ~VersionPage(); - virtual QString displayName() const override - { - return tr("Version"); - } + virtual QString displayName() const override { return tr("Version"); } virtual QIcon icon() const override; - virtual QString id() const override - { - return "version"; - } - virtual QString helpPage() const override - { - return "Instance-Version"; - } + virtual QString id() const override { return "version"; } + virtual QString helpPage() const override { return "Instance-Version"; } virtual bool shouldDisplay() const override; void retranslate() override; void openedImpl() override; void closedImpl() override; -private slots: + private slots: void on_actionChange_version_triggered(); void on_actionInstall_Forge_triggered(); void on_actionInstall_Fabric_triggered(); @@ -103,36 +92,34 @@ private slots: void updateVersionControls(); -private: + private: ComponentPtr current(); int currentRow(); void updateButtons(int row = -1); void preselect(int row = 0); int doUpdate(); -protected: - QMenu * createPopupMenu() override; + protected: + QMenu* createPopupMenu() override; /// FIXME: this shouldn't be necessary! bool reloadPackProfile(); -private: - Ui::VersionPage *ui; - QSortFilterProxyModel *m_filterModel; + private: + Ui::VersionPage* ui; + QSortFilterProxyModel* m_filterModel; std::shared_ptr m_profile; - MinecraftInstance *m_inst; + MinecraftInstance* m_inst; int currentIdx = 0; - bool controlsEnabled = false; std::shared_ptr m_wide_bar_setting = nullptr; -public slots: - void versionCurrent(const QModelIndex ¤t, const QModelIndex &previous); + public slots: + void versionCurrent(const QModelIndex& current, const QModelIndex& previous); -private slots: - void updateRunningStatus(bool running); + private slots: void onGameUpdateError(QString error); - void packageCurrent(const QModelIndex ¤t, const QModelIndex &previous); - void showContextMenu(const QPoint &pos); - void onFilterTextChanged(const QString & newContents); + void packageCurrent(const QModelIndex& current, const QModelIndex& previous); + void showContextMenu(const QPoint& pos); + void onFilterTextChanged(const QString& newContents); }; diff --git a/launcher/ui/pages/instance/VersionPage.ui b/launcher/ui/pages/instance/VersionPage.ui index 4777eafe0..a73c42d6c 100644 --- a/launcher/ui/pages/instance/VersionPage.ui +++ b/launcher/ui/pages/instance/VersionPage.ui @@ -102,7 +102,6 @@ - @@ -112,7 +111,6 @@ - @@ -204,14 +202,6 @@ Install the LiteLoader package. - - - Install mods - - - Install normal mods. - - Add to Minecraft.jar diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index b6ad159e1..b2200b1a7 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -339,6 +339,7 @@ void WorldListPage::mceditState(LoggedProcess::State state) { failed = true; } + /* fallthrough */ case LoggedProcess::Running: case LoggedProcess::Finished: { diff --git a/launcher/ui/pages/modplatform/VanillaPage.cpp b/launcher/ui/pages/modplatform/CustomPage.cpp similarity index 79% rename from launcher/ui/pages/modplatform/VanillaPage.cpp rename to launcher/ui/pages/modplatform/CustomPage.cpp index 29fecb854..e164171a4 100644 --- a/launcher/ui/pages/modplatform/VanillaPage.cpp +++ b/launcher/ui/pages/modplatform/CustomPage.cpp @@ -33,8 +33,8 @@ * limitations under the License. */ -#include "VanillaPage.h" -#include "ui_VanillaPage.h" +#include "CustomPage.h" +#include "ui_CustomPage.h" #include @@ -46,32 +46,32 @@ #include "minecraft/VanillaInstanceCreationTask.h" #include "ui/dialogs/NewInstanceDialog.h" -VanillaPage::VanillaPage(NewInstanceDialog *dialog, QWidget *parent) - : QWidget(parent), dialog(dialog), ui(new Ui::VanillaPage) +CustomPage::CustomPage(NewInstanceDialog *dialog, QWidget *parent) + : QWidget(parent), dialog(dialog), ui(new Ui::CustomPage) { ui->setupUi(this); ui->tabWidget->tabBar()->hide(); - connect(ui->versionList, &VersionSelectWidget::selectedVersionChanged, this, &VanillaPage::setSelectedVersion); + connect(ui->versionList, &VersionSelectWidget::selectedVersionChanged, this, &CustomPage::setSelectedVersion); filterChanged(); - connect(ui->alphaFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); - connect(ui->betaFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); - connect(ui->snapshotFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); - connect(ui->oldSnapshotFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); - connect(ui->releaseFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); - connect(ui->experimentsFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); - connect(ui->refreshBtn, &QPushButton::clicked, this, &VanillaPage::refresh); + connect(ui->alphaFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); + connect(ui->betaFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); + connect(ui->snapshotFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); + connect(ui->oldSnapshotFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); + connect(ui->releaseFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); + connect(ui->experimentsFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); + connect(ui->refreshBtn, &QPushButton::clicked, this, &CustomPage::refresh); - connect(ui->loaderVersionList, &VersionSelectWidget::selectedVersionChanged, this, &VanillaPage::setSelectedLoaderVersion); - connect(ui->noneFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged); - connect(ui->forgeFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged); - connect(ui->fabricFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged); - connect(ui->quiltFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged); - connect(ui->liteLoaderFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged); - connect(ui->loaderRefreshBtn, &QPushButton::clicked, this, &VanillaPage::loaderRefresh); + connect(ui->loaderVersionList, &VersionSelectWidget::selectedVersionChanged, this, &CustomPage::setSelectedLoaderVersion); + connect(ui->noneFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); + connect(ui->forgeFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); + connect(ui->fabricFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); + connect(ui->quiltFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); + connect(ui->liteLoaderFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); + connect(ui->loaderRefreshBtn, &QPushButton::clicked, this, &CustomPage::loaderRefresh); } -void VanillaPage::openedImpl() +void CustomPage::openedImpl() { if(!initialized) { @@ -85,19 +85,19 @@ void VanillaPage::openedImpl() } } -void VanillaPage::refresh() +void CustomPage::refresh() { ui->versionList->loadList(); } -void VanillaPage::loaderRefresh() +void CustomPage::loaderRefresh() { if(ui->noneFilter->isChecked()) return; ui->loaderVersionList->loadList(); } -void VanillaPage::filterChanged() +void CustomPage::filterChanged() { QStringList out; if(ui->alphaFilter->isChecked()) @@ -116,7 +116,7 @@ void VanillaPage::filterChanged() ui->versionList->setFilter(BaseVersionList::TypeRole, new RegexpFilter(regexp, false)); } -void VanillaPage::loaderFilterChanged() +void CustomPage::loaderFilterChanged() { QString minecraftVersion; if (m_selectedVersion) @@ -172,37 +172,37 @@ void VanillaPage::loaderFilterChanged() ui->loaderVersionList->setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion)); } -VanillaPage::~VanillaPage() +CustomPage::~CustomPage() { delete ui; } -bool VanillaPage::shouldDisplay() const +bool CustomPage::shouldDisplay() const { return true; } -void VanillaPage::retranslate() +void CustomPage::retranslate() { ui->retranslateUi(this); } -BaseVersion::Ptr VanillaPage::selectedVersion() const +BaseVersion::Ptr CustomPage::selectedVersion() const { return m_selectedVersion; } -BaseVersion::Ptr VanillaPage::selectedLoaderVersion() const +BaseVersion::Ptr CustomPage::selectedLoaderVersion() const { return m_selectedLoaderVersion; } -QString VanillaPage::selectedLoader() const +QString CustomPage::selectedLoader() const { return m_selectedLoader; } -void VanillaPage::suggestCurrent() +void CustomPage::suggestCurrent() { if (!isOpened) { @@ -227,14 +227,14 @@ void VanillaPage::suggestCurrent() dialog->setSuggestedIcon("default"); } -void VanillaPage::setSelectedVersion(BaseVersion::Ptr version) +void CustomPage::setSelectedVersion(BaseVersion::Ptr version) { m_selectedVersion = version; suggestCurrent(); loaderFilterChanged(); } -void VanillaPage::setSelectedLoaderVersion(BaseVersion::Ptr version) +void CustomPage::setSelectedLoaderVersion(BaseVersion::Ptr version) { m_selectedLoaderVersion = version; suggestCurrent(); diff --git a/launcher/ui/pages/modplatform/VanillaPage.h b/launcher/ui/pages/modplatform/CustomPage.h similarity index 92% rename from launcher/ui/pages/modplatform/VanillaPage.h rename to launcher/ui/pages/modplatform/CustomPage.h index 39aba760b..8b5a5011c 100644 --- a/launcher/ui/pages/modplatform/VanillaPage.h +++ b/launcher/ui/pages/modplatform/CustomPage.h @@ -43,21 +43,21 @@ namespace Ui { -class VanillaPage; +class CustomPage; } class NewInstanceDialog; -class VanillaPage : public QWidget, public BasePage +class CustomPage : public QWidget, public BasePage { Q_OBJECT public: - explicit VanillaPage(NewInstanceDialog *dialog, QWidget *parent = 0); - virtual ~VanillaPage(); + explicit CustomPage(NewInstanceDialog *dialog, QWidget *parent = 0); + virtual ~CustomPage(); virtual QString displayName() const override { - return tr("Vanilla"); + return tr("Custom"); } virtual QIcon icon() const override { @@ -96,7 +96,7 @@ private: private: bool initialized = false; NewInstanceDialog *dialog = nullptr; - Ui::VanillaPage *ui = nullptr; + Ui::CustomPage *ui = nullptr; bool m_versionSetByUser = false; BaseVersion::Ptr m_selectedVersion; BaseVersion::Ptr m_selectedLoaderVersion; diff --git a/launcher/ui/pages/modplatform/VanillaPage.ui b/launcher/ui/pages/modplatform/CustomPage.ui similarity index 99% rename from launcher/ui/pages/modplatform/VanillaPage.ui rename to launcher/ui/pages/modplatform/CustomPage.ui index 431109273..0d89b5956 100644 --- a/launcher/ui/pages/modplatform/VanillaPage.ui +++ b/launcher/ui/pages/modplatform/CustomPage.ui @@ -1,7 +1,7 @@ - VanillaPage - + CustomPage + 0 diff --git a/launcher/ui/pages/modplatform/ImportPage.h b/launcher/ui/pages/modplatform/ImportPage.h index 8d13ac100..c2acb92da 100644 --- a/launcher/ui/pages/modplatform/ImportPage.h +++ b/launcher/ui/pages/modplatform/ImportPage.h @@ -57,7 +57,7 @@ public: virtual ~ImportPage(); virtual QString displayName() const override { - return tr("Import from zip"); + return tr("Import"); } virtual QIcon icon() const override { diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index afd8b2921..b75378905 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -6,12 +6,14 @@ #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" +#include "minecraft/mod/ModFolderModel.h" #include +#include namespace ResourceDownload { -ModModel::ModModel(BaseInstance const& base_inst, ResourceAPI* api) : ResourceModel(api), m_base_instance(base_inst) {} +ModModel::ModModel(BaseInstance& base_inst, ResourceAPI* api) : ResourceModel(api), m_base_instance(base_inst) {} /******** Make data requests ********/ @@ -24,7 +26,7 @@ ResourceAPI::SearchArgs ModModel::createSearchArguments() std::optional> versions{}; - { // Version filter + { // Version filter if (!m_filter->versions.empty()) versions = m_filter->versions; } @@ -67,4 +69,14 @@ void ModModel::searchWithTerm(const QString& term, unsigned int sort, bool filte refresh(); } +bool ModModel::isPackInstalled(ModPlatform::IndexedPack::Ptr pack) const +{ + auto allMods = static_cast(m_base_instance).loaderModList()->allMods(); + return std::any_of(allMods.cbegin(), allMods.cend(), [pack](Mod* mod) { + if (auto meta = mod->metadata(); meta) + return meta->provider == pack->provider && meta->project_id == pack->addonId; + return false; + }); +} + } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index 5d4a77859..dd187aa8d 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -24,7 +24,7 @@ class ModModel : public ResourceModel { Q_OBJECT public: - ModModel(const BaseInstance&, ResourceAPI* api); + ModModel(BaseInstance&, ResourceAPI* api); /* Ask the API for more information */ void searchWithTerm(const QString& term, unsigned int sort, bool filter_changed); @@ -32,6 +32,7 @@ class ModModel : public ResourceModel { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override = 0; void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override = 0; void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override = 0; + virtual ModPlatform::IndexedVersion loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) = 0; void setFilter(std::shared_ptr filter) { m_filter = filter; } @@ -42,9 +43,10 @@ class ModModel : public ResourceModel { protected: auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0; + virtual bool isPackInstalled(ModPlatform::IndexedPack::Ptr) const override; protected: - const BaseInstance& m_base_instance; + BaseInstance& m_base_instance; std::shared_ptr m_filter = nullptr; }; diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 95064d16a..60a43128a 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -89,17 +89,13 @@ void ModPage::filterMods() void ModPage::triggerSearch() { - auto changed = m_filter_widget->changed(); m_filter = m_filter_widget->getFilter(); + m_ui->packView->clearSelection(); + m_ui->packDescription->clear(); + m_ui->versionSelectionBox->clear(); + updateSelectionButton(); - if (changed) { - m_ui->packView->clearSelection(); - m_ui->packDescription->clear(); - m_ui->versionSelectionBox->clear(); - updateSelectionButton(); - } - - static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), changed); + static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), m_filter_widget->changed()); m_fetch_progress.watch(m_model->activeSearchJob().get()); } @@ -122,6 +118,8 @@ void ModPage::updateVersionList() QString mcVersion = packProfile->getComponentVersion("net.minecraft"); auto current_pack = getCurrentPack(); + if (!current_pack) + return; for (int i = 0; i < current_pack->versions.size(); i++) { auto version = current_pack->versions[i]; bool valid = false; diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index a5ea1ca9f..49405a02b 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -77,6 +77,8 @@ auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant return pack->description; case UserDataTypes::SELECTED: return pack->isAnyVersionSelected(); + case UserDataTypes::INSTALLED: + return this->isPackInstalled(pack); default: break; } @@ -95,6 +97,7 @@ QHash ResourceModel::roleNames() const roles[UserDataTypes::TITLE] = "title"; roles[UserDataTypes::DESCRIPTION] = "description"; roles[UserDataTypes::SELECTED] = "selected"; + roles[UserDataTypes::INSTALLED] = "installed"; return roles; } diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h index 69e234730..6533d9c6a 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.h +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -116,6 +116,8 @@ class ResourceModel : public QAbstractListModel { virtual void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&); virtual void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&); + virtual bool isPackInstalled(ModPlatform::IndexedPack::Ptr) const { return false; } + protected: /* Basic search parameters */ enum class SearchState { None, CanFetchMore, ResetRequested, Finished } m_search_state = SearchState::None; diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index 736034add..48afbd900 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -104,6 +104,7 @@ void ResourcePage::openedImpl() updateSelectionButton(); triggerSearch(); + m_ui->searchEdit->setFocus(); } auto ResourcePage::eventFilter(QObject* watched, QEvent* event) -> bool @@ -174,7 +175,11 @@ ModPlatform::IndexedPack::Ptr ResourcePage::getCurrentPack() const void ResourcePage::updateUi() { auto current_pack = getCurrentPack(); - + if (!current_pack) { + m_ui->packDescription->setHtml({}); + m_ui->packDescription->flush(); + return; + } QString text = ""; QString name = current_pack->name; @@ -240,10 +245,13 @@ void ResourcePage::updateSelectionButton() } m_ui->resourceSelectionButton->setEnabled(true); - if (!getCurrentPack()->isVersionSelected(m_selected_version_index)) { - m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString())); + if (auto current_pack = getCurrentPack(); current_pack) { + if (!current_pack->isVersionSelected(m_selected_version_index)) + m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString())); + else + m_ui->resourceSelectionButton->setText(tr("Deselect %1 for download").arg(resourceString())); } else { - m_ui->resourceSelectionButton->setText(tr("Deselect %1 for download").arg(resourceString())); + qWarning() << "Tried to update the selected button but there is not a pack selected"; } } @@ -255,13 +263,14 @@ void ResourcePage::updateVersionList() m_ui->versionSelectionBox->clear(); m_ui->versionSelectionBox->blockSignals(false); - for (int i = 0; i < current_pack->versions.size(); i++) { - auto& version = current_pack->versions[i]; - if (optedOut(version)) - continue; + if (current_pack) + for (int i = 0; i < current_pack->versions.size(); i++) { + auto& version = current_pack->versions[i]; + if (optedOut(version)) + continue; - m_ui->versionSelectionBox->addItem(current_pack->versions[i].version, QVariant(i)); - } + m_ui->versionSelectionBox->addItem(current_pack->versions[i].version, QVariant(i)); + } if (m_ui->versionSelectionBox->count() == 0) { m_ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1)); @@ -280,7 +289,7 @@ void ResourcePage::onSelectionChanged(QModelIndex curr, QModelIndex prev) auto current_pack = getCurrentPack(); bool request_load = false; - if (!current_pack->versionsLoaded) { + if (!current_pack || !current_pack->versionsLoaded) { m_ui->resourceSelectionButton->setText(tr("Loading versions...")); m_ui->resourceSelectionButton->setEnabled(false); @@ -289,7 +298,7 @@ void ResourcePage::onSelectionChanged(QModelIndex curr, QModelIndex prev) updateVersionList(); } - if (!current_pack->extraDataLoaded) + if (current_pack && !current_pack->extraDataLoaded) request_load = true; if (request_load) @@ -337,7 +346,7 @@ void ResourcePage::onResourceSelected() return; auto current_pack = getCurrentPack(); - if (!current_pack->versionsLoaded) + if (!current_pack || !current_pack->versionsLoaded) return; auto& version = current_pack->versions[m_selected_version_index]; @@ -383,7 +392,7 @@ void ResourcePage::openUrl(const QUrl& url) const QString slug = match.captured(1); // ensure the user isn't opening the same mod - if (slug != getCurrentPack()->slug) { + if (auto current_pack = getCurrentPack(); current_pack && slug != current_pack->slug) { m_parent_dialog->selectPage(page); auto newPage = m_parent_dialog->getSelectedPage(); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp index 9ad26f472..c6b087d67 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp @@ -16,62 +16,49 @@ #include "AtlListModel.h" -#include #include +#include #include namespace Atl { -ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) -{ -} +ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {} -ListModel::~ListModel() -{ -} +ListModel::~ListModel() {} -int ListModel::rowCount(const QModelIndex &parent) const +int ListModel::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : modpacks.size(); } -int ListModel::columnCount(const QModelIndex &parent) const +int ListModel::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : 1; } -QVariant ListModel::data(const QModelIndex &index, int role) const +QVariant ListModel::data(const QModelIndex& index, int role) const { int pos = index.row(); - if(pos >= modpacks.size() || pos < 0 || !index.isValid()) - { + if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } ATLauncher::IndexedPack pack = modpacks.at(pos); - if(role == Qt::DisplayRole) - { + if (role == Qt::DisplayRole) { return pack.name; - } - else if (role == Qt::ToolTipRole) - { + } else if (role == Qt::ToolTipRole) { return pack.name; - } - else if(role == Qt::DecorationRole) - { - if(m_logoMap.contains(pack.safeName)) - { + } else if (role == Qt::DecorationRole) { + if (m_logoMap.contains(pack.safeName)) { return (m_logoMap.value(pack.safeName)); } auto icon = APPLICATION->getThemedIcon("atlauncher-placeholder"); auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(pack.safeName.toLower()); - ((ListModel *)this)->requestLogo(pack.safeName, url); + ((ListModel*)this)->requestLogo(pack.safeName, url); return icon; - } - else if(role == Qt::UserRole) - { + } else if (role == Qt::UserRole) { QVariant v; v.setValue(pack); return v; @@ -88,7 +75,7 @@ void ListModel::request() auto netJob = makeShared("Atl::Request", APPLICATION->network()); auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/json/packsnew.json"); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response)); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), response)); jobPtr = netJob; jobPtr->start(); @@ -101,36 +88,38 @@ void ListModel::requestFinished() jobPtr.reset(); QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); - if(parse_error.error != QJsonParseError::NoError) { + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from ATL at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << response; + qWarning() << *response; return; } QList newList; auto packs = doc.array(); - for(auto packRaw : packs) { + for (auto packRaw : packs) { auto packObj = packRaw.toObject(); ATLauncher::IndexedPack pack; try { ATLauncher::loadIndexedPack(pack, packObj); - } - catch (const JSONValidationError &e) { - qDebug() << QString::fromUtf8(response); + } catch (const JSONValidationError& e) { + qDebug() << QString::fromUtf8(*response); qWarning() << "Error while reading pack manifest from ATLauncher: " << e.cause(); return; } // ignore packs without a published version - if(pack.versions.length() == 0) continue; + if (pack.versions.length() == 0) + continue; // only display public packs (for now) - if(pack.type != ATLauncher::PackType::Public) continue; + if (pack.type != ATLauncher::PackType::Public) + continue; // ignore "system" packs (Vanilla, Vanilla with Forge, etc) - if(pack.system) continue; + if (pack.system) + continue; newList.append(pack); } @@ -145,14 +134,12 @@ void ListModel::requestFailed(QString reason) jobPtr.reset(); } -void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) +void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback) { - if(m_logoMap.contains(logo)) - { - callback(APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); - } - else - { + if (m_logoMap.contains(logo)) { + callback( + APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); + } else { requestLogo(logo, logoUrl); } } @@ -168,36 +155,34 @@ void ListModel::logoLoaded(QString logo, QIcon out) m_loadingLogos.removeAll(logo); m_logoMap.insert(logo, out); - for(int i = 0; i < modpacks.size(); i++) { - if(modpacks[i].safeName == logo) { - emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); + for (int i = 0; i < modpacks.size(); i++) { + if (modpacks[i].safeName == logo) { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); } } } void ListModel::requestLogo(QString file, QString url) { - if(m_loadingLogos.contains(file) || m_failedLogos.contains(file)) - { + if (m_loadingLogos.contains(file) || m_failedLogos.contains(file)) { return; } MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(file.section(".", 0, 0))); - NetJob *job = new NetJob(QString("ATLauncher Icon Download %1").arg(file), APPLICATION->network()); + auto job = new NetJob(QString("ATLauncher Icon Download %1").arg(file), APPLICATION->network()); job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::succeeded, this, [this, file, fullPath] - { + QObject::connect(job, &NetJob::succeeded, this, [this, file, fullPath, job] { + job->deleteLater(); emit logoLoaded(file, QIcon(fullPath)); - if(waitingCallbacks.contains(file)) - { + if (waitingCallbacks.contains(file)) { waitingCallbacks.value(file)(fullPath); } }); - QObject::connect(job, &NetJob::failed, this, [this, file] - { + QObject::connect(job, &NetJob::failed, this, [this, file, job] { + job->deleteLater(); emit logoFailed(file); }); @@ -206,4 +191,4 @@ void ListModel::requestLogo(QString file, QString url) m_loadingLogos.append(file); } -} +} // namespace Atl diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h index 2574c48d6..ed1fdc9f9 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h @@ -18,42 +18,41 @@ #include -#include "net/NetJob.h" -#include #include +#include +#include "net/NetJob.h" namespace Atl { typedef QMap LogoMap; typedef std::function LogoCallback; -class ListModel : public QAbstractListModel -{ +class ListModel : public QAbstractListModel { Q_OBJECT -public: - ListModel(QObject *parent); + public: + ListModel(QObject* parent); virtual ~ListModel(); - int rowCount(const QModelIndex &parent) const override; - int columnCount(const QModelIndex &parent) const override; - QVariant data(const QModelIndex &index, int role) const override; + int rowCount(const QModelIndex& parent) const override; + int columnCount(const QModelIndex& parent) const override; + QVariant data(const QModelIndex& index, int role) const override; void request(); - void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); + void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); -private slots: + private slots: void requestFinished(); void requestFailed(QString reason); void logoFailed(QString logo); void logoLoaded(QString logo, QIcon out); -private: + private: void requestLogo(QString file, QString url); -private: + private: QList modpacks; QStringList m_failedLogos; @@ -62,7 +61,7 @@ private: QMap waitingCallbacks; NetJob::Ptr jobPtr; - QByteArray response; + std::shared_ptr response = std::make_shared(); }; -} +} // namespace Atl diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp index cdb4532c8..7b61daa71 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -152,7 +152,7 @@ Qt::ItemFlags AtlOptionalModListModel::flags(const QModelIndex &index) const { void AtlOptionalModListModel::useShareCode(const QString& code) { m_jobPtr.reset(new NetJob("Atl::Request", APPLICATION->network())); auto url = QString(BuildConfig.ATL_API_BASE_URL + "share-codes/" + code); - m_jobPtr->addNetAction(Net::Download::makeByteArray(QUrl(url), &m_response)); + m_jobPtr->addNetAction(Net::Download::makeByteArray(QUrl(url), m_response)); connect(m_jobPtr.get(), &NetJob::succeeded, this, &AtlOptionalModListModel::shareCodeSuccess); @@ -166,10 +166,10 @@ void AtlOptionalModListModel::shareCodeSuccess() { m_jobPtr.reset(); QJsonParseError parse_error {}; - auto doc = QJsonDocument::fromJson(m_response, &parse_error); + auto doc = QJsonDocument::fromJson(*m_response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from ATL at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << m_response; + qWarning() << *m_response; return; } auto obj = doc.object(); @@ -179,7 +179,7 @@ void AtlOptionalModListModel::shareCodeSuccess() { ATLauncher::loadShareCodeResponse(response, obj); } catch (const JSONValidationError& e) { - qDebug() << QString::fromUtf8(m_response); + qDebug() << QString::fromUtf8(*m_response); qWarning() << "Error while reading response from ATLauncher: " << e.cause(); return; } diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h index 8e02444e4..639f0d489 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h @@ -82,9 +82,9 @@ private: void toggleMod(ATLauncher::VersionMod mod, int index); void setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit = true); -private: + private: NetJob::Ptr m_jobPtr; - QByteArray m_response; + std::shared_ptr m_response = std::make_shared(); ATLauncher::PackVersion m_version; QVector m_mods; diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h index 37010b3ff..adeb53cbc 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h @@ -42,15 +42,15 @@ class AtlUserInteractionSupportImpl : public QObject, public ATLauncher::UserInteractionSupport { Q_OBJECT -public: + public: AtlUserInteractionSupportImpl(QWidget* parent); + virtual ~AtlUserInteractionSupportImpl() = default; -private: + private: QString chooseVersion(Meta::VersionList::Ptr vlist, QString minecraftVersion) override; std::optional> chooseOptionalMods(ATLauncher::PackVersion version, QVector mods) override; void displayMessage(QString message) override; -private: + private: QWidget* m_parent; - }; diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index 5961ea026..fa55aa686 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -60,6 +60,8 @@ QVariant ListModel::data(const QModelIndex& index, int role) const return pack.description; case UserDataTypes::SELECTED: return false; + case UserDataTypes::INSTALLED: + return false; default: break; } @@ -169,7 +171,7 @@ void ListModel::performPaginatedSearch() .arg(currentSearchTerm) .arg(currentSort + 1); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); jobPtr = netJob; jobPtr->start(); QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished); @@ -202,11 +204,11 @@ void Flame::ListModel::searchRequestFinished() jobPtr.reset(); QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << response; + qWarning() << *response; return; } diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.h b/launcher/ui/pages/modplatform/flame/FlameModel.h index cab666cc3..b3bc96b8c 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameModel.h @@ -3,46 +3,44 @@ #include #include -#include -#include #include -#include #include +#include +#include #include #include -#include +#include +#include -#include #include +#include #include namespace Flame { - typedef QMap LogoMap; typedef std::function LogoCallback; -class ListModel : public QAbstractListModel -{ +class ListModel : public QAbstractListModel { Q_OBJECT -public: - ListModel(QObject *parent); + public: + ListModel(QObject* parent); virtual ~ListModel(); - int rowCount(const QModelIndex &parent) const override; - int columnCount(const QModelIndex &parent) const override; - QVariant data(const QModelIndex &index, int role) const override; - bool setData(const QModelIndex &index, const QVariant &value, int role) override; - Qt::ItemFlags flags(const QModelIndex &index) const override; - bool canFetchMore(const QModelIndex & parent) const override; - void fetchMore(const QModelIndex & parent) override; + int rowCount(const QModelIndex& parent) const override; + int columnCount(const QModelIndex& parent) const override; + QVariant data(const QModelIndex& index, int role) const override; + bool setData(const QModelIndex& index, const QVariant& value, int role) override; + Qt::ItemFlags flags(const QModelIndex& index) const override; + bool canFetchMore(const QModelIndex& parent) const override; + void fetchMore(const QModelIndex& parent) override; - void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); - void searchWithTerm(const QString & term, const int sort); + void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); + void searchWithTerm(const QString& term, const int sort); -private slots: + private slots: void performPaginatedSearch(); void logoFailed(QString logo); @@ -51,10 +49,10 @@ private slots: void searchRequestFinished(); void searchRequestFailed(QString reason); -private: + private: void requestLogo(QString file, QString url); -private: + private: QList modpacks; QStringList m_failedLogos; QStringList m_loadingLogos; @@ -64,14 +62,9 @@ private: QString currentSearchTerm; int currentSort = 0; int nextSearchOffset = 0; - enum SearchState { - None, - CanPossiblyFetchMore, - ResetRequested, - Finished - } searchState = None; + enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; NetJob::Ptr jobPtr; - QByteArray response; + std::shared_ptr response = std::make_shared(); }; -} +} // namespace Flame diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index f9ac4a789..cef26bb6b 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -130,7 +130,7 @@ void FlamePage::onSelectionChanged(QModelIndex curr, QModelIndex prev) if (current.versionsLoaded == false) { qDebug() << "Loading flame modpack versions"; auto netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name), APPLICATION->network()); - auto response = new QByteArray(); + auto response = std::make_shared(); int addonId = current.addonId; netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.curseforge.com/v1/mods/%1/files").arg(addonId), response)); @@ -170,10 +170,7 @@ void FlamePage::onSelectionChanged(QModelIndex curr, QModelIndex prev) } suggestCurrent(); }); - QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { - netJob->deleteLater(); - delete response; - }); + QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); }); netJob->start(); } else { for (auto version : current.versions) { diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp index e3d0bc144..0fb67c507 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp @@ -11,7 +11,7 @@ namespace ResourceDownload { -FlameModModel::FlameModModel(BaseInstance const& base) : ModModel(base, new FlameAPI) {} +FlameModModel::FlameModModel(BaseInstance& base) : ModModel(base, new FlameAPI) {} void FlameModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) { @@ -29,6 +29,11 @@ void FlameModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonAr FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); } +auto FlameModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion +{ + return FlameMod::loadDependencyVersions(m, arr); +}; + auto FlameModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray { return Json::ensureArray(obj.object(), "data"); @@ -81,7 +86,7 @@ void FlameTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, auto const& mc_versions = version.mcVersion; if (std::any_of(mc_versions.constBegin(), mc_versions.constEnd(), - [this](auto const& mc_version){ return Version(mc_version) <= maximumTexturePackVersion(); })) + [this](auto const& mc_version) { return Version(mc_version) <= maximumTexturePackVersion(); })) filtered_versions.push_back(version); } diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h index 0252ac403..6cfd6a6f8 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h @@ -14,7 +14,7 @@ class FlameModModel : public ModModel { Q_OBJECT public: - FlameModModel(const BaseInstance&); + FlameModModel(BaseInstance&); ~FlameModModel() override = default; private: @@ -24,6 +24,7 @@ class FlameModModel : public ModModel { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; + auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion override; auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; }; diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index 2343b79f2..330dd4fb8 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -38,11 +38,11 @@ #include "net/HttpMetaCache.h" #include "net/NetJob.h" -#include "StringUtils.h" #include +#include "StringUtils.h" -#include #include +#include #include @@ -50,33 +50,33 @@ namespace LegacyFTB { -FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent) +FilterModel::FilterModel(QObject* parent) : QSortFilterProxyModel(parent) { currentSorting = Sorting::ByGameVersion; sortings.insert(tr("Sort by Name"), Sorting::ByName); sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion); } -bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value(); Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value(); - if(currentSorting == Sorting::ByGameVersion) { + if (currentSorting == Sorting::ByGameVersion) { Version lv(leftPack.mcVersion); Version rv(rightPack.mcVersion); return lv < rv; - } else if(currentSorting == Sorting::ByName) { + } else if (currentSorting == Sorting::ByName) { return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; } - //UHM, some inavlid value set?! + // UHM, some inavlid value set?! qWarning() << "Invalid sorting set!"; return true; } -bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { return true; } @@ -102,18 +102,13 @@ FilterModel::Sorting FilterModel::getCurrentSorting() return currentSorting; } -ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) -{ -} +ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {} -ListModel::~ListModel() -{ -} +ListModel::~ListModel() {} QString ListModel::translatePackType(PackType type) const { - switch(type) - { + switch (type) { case PackType::Public: return tr("Public Modpack"); case PackType::ThirdParty: @@ -125,67 +120,51 @@ QString ListModel::translatePackType(PackType type) const return QString(); } -int ListModel::rowCount(const QModelIndex &parent) const +int ListModel::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : modpacks.size(); } -int ListModel::columnCount(const QModelIndex &parent) const +int ListModel::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : 1; } -QVariant ListModel::data(const QModelIndex &index, int role) const +QVariant ListModel::data(const QModelIndex& index, int role) const { int pos = index.row(); - if(pos >= modpacks.size() || pos < 0 || !index.isValid()) - { + if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } Modpack pack = modpacks.at(pos); - if(role == Qt::DisplayRole) - { + if (role == Qt::DisplayRole) { return pack.name + "\n" + translatePackType(pack.type); - } - else if (role == Qt::ToolTipRole) - { - if(pack.description.length() > 100) - { - //some magic to prevent to long tooltips and replace html linebreaks + } else if (role == Qt::ToolTipRole) { + if (pack.description.length() > 100) { + // some magic to prevent to long tooltips and replace html linebreaks QString edit = pack.description.left(97); edit = edit.left(edit.lastIndexOf("
    ")).left(edit.lastIndexOf(" ")).append("..."); return edit; - } return pack.description; - } - else if(role == Qt::DecorationRole) - { - if(m_logoMap.contains(pack.logo)) - { + } else if (role == Qt::DecorationRole) { + if (m_logoMap.contains(pack.logo)) { return (m_logoMap.value(pack.logo)); } QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); - ((ListModel *)this)->requestLogo(pack.logo); + ((ListModel*)this)->requestLogo(pack.logo); return icon; - } - else if(role == Qt::ForegroundRole) - { - if(pack.broken) - { - //FIXME: Hardcoded color + } else if (role == Qt::ForegroundRole) { + if (pack.broken) { + // FIXME: Hardcoded color return QColor(255, 0, 50); - } - else if(pack.bugged) - { - //FIXME: Hardcoded color - //bugged pack, currently only indicates bugged xml + } else if (pack.bugged) { + // FIXME: Hardcoded color + // bugged pack, currently only indicates bugged xml return QColor(244, 229, 66); } - } - else if(role == Qt::UserRole) - { + } else if (role == Qt::UserRole) { QVariant v; v.setValue(pack); return v; @@ -222,8 +201,7 @@ Modpack ListModel::at(int row) void ListModel::remove(int row) { - if(row < 0 || row >= modpacks.size()) - { + if (row < 0 || row >= modpacks.size()) { qWarning() << "Attempt to remove FTB modpacks with invalid row" << row; return; } @@ -247,27 +225,25 @@ void ListModel::logoFailed(QString logo) void ListModel::requestLogo(QString file) { - if(m_loadingLogos.contains(file) || m_failedLogos.contains(file)) - { + if (m_loadingLogos.contains(file) || m_failedLogos.contains(file)) { return; } MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(file.section(".", 0, 0))); - NetJob *job = new NetJob(QString("FTB Icon Download for %1").arg(file), APPLICATION->network()); + NetJob* job = new NetJob(QString("FTB Icon Download for %1").arg(file), APPLICATION->network()); job->addNetAction(Net::Download::makeCached(QUrl(QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1").arg(file)), entry)); auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::finished, this, [this, file, fullPath] - { + QObject::connect(job, &NetJob::finished, this, [this, file, fullPath, job] { + job->deleteLater(); emit logoLoaded(file, QIcon(fullPath)); - if(waitingCallbacks.contains(file)) - { + if (waitingCallbacks.contains(file)) { waitingCallbacks.value(file)(fullPath); } }); - QObject::connect(job, &NetJob::failed, this, [this, file] - { + QObject::connect(job, &NetJob::failed, this, [this, file, job] { + job->deleteLater(); emit logoFailed(file); }); @@ -276,21 +252,18 @@ void ListModel::requestLogo(QString file) m_loadingLogos.append(file); } -void ListModel::getLogo(const QString &logo, LogoCallback callback) +void ListModel::getLogo(const QString& logo, LogoCallback callback) { - if(m_logoMap.contains(logo)) - { + if (m_logoMap.contains(logo)) { callback(APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); - } - else - { + } else { requestLogo(logo); } } -Qt::ItemFlags ListModel::flags(const QModelIndex &index) const +Qt::ItemFlags ListModel::flags(const QModelIndex& index) const { return QAbstractListModel::flags(index); } -} +} // namespace LegacyFTB diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp index 98ab87999..b3f6261fe 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp @@ -116,8 +116,8 @@ Page::Page(NewInstanceDialog* dialog, QWidget *parent) connect(ui->thirdPartyPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onThirdPartyPackSelectionChanged); connect(ui->privatePackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPrivatePackSelectionChanged); - connect(ui->addPackBtn, &QPushButton::pressed, this, &Page::onAddPackClicked); - connect(ui->removePackBtn, &QPushButton::pressed, this, &Page::onRemovePackClicked); + connect(ui->addPackBtn, &QPushButton::clicked, this, &Page::onAddPackClicked); + connect(ui->removePackBtn, &QPushButton::clicked, this, &Page::onRemovePackClicked); connect(ui->tabWidget, &QTabWidget::currentChanged, this, &Page::onTabChanged); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 346a00b0e..e0046d887 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -106,6 +106,8 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian return pack.description; case UserDataTypes::SELECTED: return false; + case UserDataTypes::INSTALLED: + return false; default: break; } @@ -129,27 +131,27 @@ void ModpackListModel::performPaginatedSearch() // TODO: Move to standalone API auto netJob = makeShared("Modrinth::SearchModpack", APPLICATION->network()); auto searchAllUrl = QString(BuildConfig.MODRINTH_PROD_URL + - "/search?" - "offset=%1&" - "limit=%2&" - "query=%3&" - "index=%4&" - "facets=[[\"project_type:modpack\"]]") + "/search?" + "offset=%1&" + "limit=%2&" + "query=%3&" + "index=%4&" + "facets=[[\"project_type:modpack\"]]") .arg(nextSearchOffset) .arg(m_modpacks_per_page) .arg(currentSearchTerm) .arg(currentSort); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchAllUrl), &m_all_response)); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchAllUrl), m_all_response)); QObject::connect(netJob.get(), &NetJob::succeeded, this, [this] { QJsonParseError parse_error_all{}; - QJsonDocument doc_all = QJsonDocument::fromJson(m_all_response, &parse_error_all); + QJsonDocument doc_all = QJsonDocument::fromJson(*m_all_response, &parse_error_all); if (parse_error_all.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from " << debugName() << " at " << parse_error_all.offset << " reason: " << parse_error_all.errorString(); - qWarning() << m_all_response; + qWarning() << *m_all_response; return; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 6e6be4b9e..b9e9c3da5 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -110,9 +110,9 @@ class ModpackListModel : public QAbstractListModel { NetJob::Ptr jobPtr; - QByteArray m_all_response; + std::shared_ptr m_all_response = std::make_shared(); QByteArray m_specific_response; int m_modpacks_per_page = 20; }; -} // namespace ModPlatform +} // namespace Modrinth diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 0bb11d834..c71dd9038 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -123,7 +123,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, QModelIndex prev) qDebug() << "Loading modrinth modpack information"; auto netJob = new NetJob(QString("Modrinth::PackInformation(%1)").arg(current.name), APPLICATION->network()); - auto response = new QByteArray(); + auto response = std::make_shared(); QString id = current.id; @@ -162,10 +162,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, QModelIndex prev) suggestCurrent(); }); - QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { - netJob->deleteLater(); - delete response; - }); + QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); }); netJob->start(); } else updateUI(); @@ -174,7 +171,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, QModelIndex prev) qDebug() << "Loading modrinth modpack versions"; auto netJob = new NetJob(QString("Modrinth::PackVersions(%1)").arg(current.name), APPLICATION->network()); - auto response = new QByteArray(); + auto response = std::make_shared(); QString id = current.id; @@ -217,10 +214,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, QModelIndex prev) suggestCurrent(); }); - QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { - netJob->deleteLater(); - delete response; - }); + QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); }); netJob->start(); } else { @@ -260,10 +254,8 @@ void ModrinthPage::updateUI() text += donates.join(", "); } - if (!current.extra.issuesUrl.isEmpty() - || !current.extra.sourceUrl.isEmpty() - || !current.extra.wikiUrl.isEmpty() - || !current.extra.discordUrl.isEmpty()) { + if (!current.extra.issuesUrl.isEmpty() || !current.extra.sourceUrl.isEmpty() || !current.extra.wikiUrl.isEmpty() || + !current.extra.discordUrl.isEmpty()) { text += "

    " + tr("External links:") + "
    "; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp index f5d1cc282..8aa649898 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp @@ -25,7 +25,7 @@ namespace ResourceDownload { -ModrinthModModel::ModrinthModModel(BaseInstance const& base) : ModModel(base, new ModrinthAPI) {} +ModrinthModModel::ModrinthModModel(BaseInstance& base) : ModModel(base, new ModrinthAPI) {} void ModrinthModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) { @@ -42,12 +42,17 @@ void ModrinthModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJso ::Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); } +auto ModrinthModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion +{ + return ::Modrinth::loadDependencyVersions(m, arr); +}; + auto ModrinthModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray { return obj.object().value("hits").toArray(); } -ModrinthResourcePackModel::ModrinthResourcePackModel(const BaseInstance& base) : ResourcePackResourceModel(base, new ModrinthAPI){} +ModrinthResourcePackModel::ModrinthResourcePackModel(const BaseInstance& base) : ResourcePackResourceModel(base, new ModrinthAPI) {} void ModrinthResourcePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) { @@ -69,7 +74,7 @@ auto ModrinthResourcePackModel::documentToArray(QJsonDocument& obj) const -> QJs return obj.object().value("hits").toArray(); } -ModrinthTexturePackModel::ModrinthTexturePackModel(const BaseInstance& base) : TexturePackResourceModel(base, new ModrinthAPI){} +ModrinthTexturePackModel::ModrinthTexturePackModel(const BaseInstance& base) : TexturePackResourceModel(base, new ModrinthAPI) {} void ModrinthTexturePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) { @@ -91,7 +96,7 @@ auto ModrinthTexturePackModel::documentToArray(QJsonDocument& obj) const -> QJso return obj.object().value("hits").toArray(); } -ModrinthShaderPackModel::ModrinthShaderPackModel(const BaseInstance& base) : ShaderPackResourceModel(base, new ModrinthAPI){} +ModrinthShaderPackModel::ModrinthShaderPackModel(const BaseInstance& base) : ShaderPackResourceModel(base, new ModrinthAPI) {} void ModrinthShaderPackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) { diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h index b351b19b9..d7c858f88 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h @@ -30,7 +30,7 @@ class ModrinthModModel : public ModModel { Q_OBJECT public: - ModrinthModModel(const BaseInstance&); + ModrinthModModel(BaseInstance&); ~ModrinthModModel() override = default; private: @@ -40,6 +40,7 @@ class ModrinthModModel : public ModModel { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; + auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion override; auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; }; diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index 50f0c72d1..f08eb2897 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -40,39 +40,28 @@ #include -Technic::ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) -{ -} +Technic::ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {} -Technic::ListModel::~ListModel() -{ -} +Technic::ListModel::~ListModel() {} QVariant Technic::ListModel::data(const QModelIndex& index, int role) const { int pos = index.row(); - if(pos >= modpacks.size() || pos < 0 || !index.isValid()) - { + if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } Modpack pack = modpacks.at(pos); - if(role == Qt::DisplayRole) - { + if (role == Qt::DisplayRole) { return pack.name; - } - else if(role == Qt::DecorationRole) - { - if(m_logoMap.contains(pack.logoName)) - { + } else if (role == Qt::DecorationRole) { + if (m_logoMap.contains(pack.logoName)) { return (m_logoMap.value(pack.logoName)); } QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); - ((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl); + ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); return icon; - } - else if(role == Qt::UserRole) - { + } else if (role == Qt::UserRole) { QVariant v; v.setValue(pack); return v; @@ -92,16 +81,15 @@ int Technic::ListModel::rowCount(const QModelIndex& parent) const void Technic::ListModel::searchWithTerm(const QString& term) { - if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull()) { + if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull()) { return; } currentSearchTerm = term; - if(jobPtr) { + if (jobPtr) { jobPtr->abort(); searchState = ResetRequested; return; - } - else { + } else { beginResetModel(); modpacks.clear(); endResetModel(); @@ -115,26 +103,20 @@ void Technic::ListModel::performSearch() auto netJob = makeShared("Technic::Search", APPLICATION->network()); QString searchUrl = ""; if (currentSearchTerm.isEmpty()) { - searchUrl = QString("%1trending?build=%2") - .arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD); + searchUrl = QString("%1trending?build=%2").arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD); searchMode = List; - } - else if (currentSearchTerm.startsWith("http://api.technicpack.net/modpack/")) { - searchUrl = QString("https://%1?build=%2") - .arg(currentSearchTerm.mid(7), BuildConfig.TECHNIC_API_BUILD); + } else if (currentSearchTerm.startsWith("http://api.technicpack.net/modpack/")) { + searchUrl = QString("https://%1?build=%2").arg(currentSearchTerm.mid(7), BuildConfig.TECHNIC_API_BUILD); searchMode = Single; - } - else if (currentSearchTerm.startsWith("https://api.technicpack.net/modpack/")) { + } else if (currentSearchTerm.startsWith("https://api.technicpack.net/modpack/")) { searchUrl = QString("%1?build=%2").arg(currentSearchTerm, BuildConfig.TECHNIC_API_BUILD); searchMode = Single; - } - else { - searchUrl = QString( - "%1search?build=%2&q=%3" - ).arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD, currentSearchTerm); + } else { + searchUrl = + QString("%1search?build=%2&q=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD, currentSearchTerm); searchMode = List; } - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); jobPtr = netJob; jobPtr->start(); QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished); @@ -146,11 +128,11 @@ void Technic::ListModel::searchRequestFinished() jobPtr.reset(); QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); - if(parse_error.error != QJsonParseError::NoError) - { - qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << response; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; return; } @@ -161,7 +143,7 @@ void Technic::ListModel::searchRequestFinished() switch (searchMode) { case List: { auto objs = Json::requireArray(root, "modpacks"); - for (auto technicPack: objs) { + for (auto technicPack : objs) { Modpack pack; auto technicPackObject = Json::requireObject(technicPack); pack.name = Json::requireString(technicPackObject, "name"); @@ -170,11 +152,10 @@ void Technic::ListModel::searchRequestFinished() continue; auto rawURL = Json::ensureString(technicPackObject, "iconUrl", "null"); - if(rawURL == "null") { + if (rawURL == "null") { pack.logoUrl = "null"; pack.logoName = "null"; - } - else { + } else { pack.logoUrl = rawURL; pack.logoName = rawURL.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); } @@ -199,8 +180,7 @@ void Technic::ListModel::searchRequestFinished() pack.logoUrl = iconUrl; pack.logoName = iconUrl.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); - } - else { + } else { pack.logoUrl = "null"; pack.logoName = "null"; } @@ -210,10 +190,8 @@ void Technic::ListModel::searchRequestFinished() break; } } - } - catch (const JSONValidationError &err) - { - qCritical() << "Couldn't parse technic search results:" << err.cause() ; + } catch (const JSONValidationError& err) { + qCritical() << "Couldn't parse technic search results:" << err.cause(); return; } searchState = Finished; @@ -229,12 +207,9 @@ void Technic::ListModel::searchRequestFinished() void Technic::ListModel::getLogo(const QString& logo, const QString& logoUrl, Technic::LogoCallback callback) { - if(m_logoMap.contains(logo)) - { + if (m_logoMap.contains(logo)) { callback(APPLICATION->metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo))->getFullPath()); - } - else - { + } else { requestLogo(logo, logoUrl); } } @@ -243,30 +218,24 @@ void Technic::ListModel::searchRequestFailed() { jobPtr.reset(); - if(searchState == ResetRequested) - { + if (searchState == ResetRequested) { beginResetModel(); modpacks.clear(); endResetModel(); performSearch(); - } - else - { + } else { searchState = Finished; } } - void Technic::ListModel::logoLoaded(QString logo, QString out) { m_loadingLogos.removeAll(logo); m_logoMap.insert(logo, QIcon(out)); - for(int i = 0; i < modpacks.size(); i++) - { - if(modpacks[i].logoName == logo) - { - emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); + for (int i = 0; i < modpacks.size(); i++) { + if (modpacks[i].logoName == logo) { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); } } } @@ -279,24 +248,23 @@ void Technic::ListModel::logoFailed(QString logo) void Technic::ListModel::requestLogo(QString logo, QString url) { - if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || logo == "null") - { + if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || logo == "null") { return; } MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo)); - NetJob *job = new NetJob(QString("Technic Icon Download %1").arg(logo), APPLICATION->network()); + auto job = new NetJob(QString("Technic Icon Download %1").arg(logo), APPLICATION->network()); job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath] - { + QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { + job->deleteLater(); logoLoaded(logo, fullPath); }); - QObject::connect(job, &NetJob::failed, this, [this, logo] - { + QObject::connect(job, &NetJob::failed, this, [this, logo, job] { + job->deleteLater(); logoFailed(logo); }); diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.h b/launcher/ui/pages/modplatform/technic/TechnicModel.h index 5eea124cc..0f1a814e2 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.h +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.h @@ -44,33 +44,32 @@ namespace Technic { typedef std::function LogoCallback; -class ListModel : public QAbstractListModel -{ +class ListModel : public QAbstractListModel { Q_OBJECT -public: - ListModel(QObject *parent); + public: + ListModel(QObject* parent); virtual ~ListModel(); virtual QVariant data(const QModelIndex& index, int role) const; virtual int columnCount(const QModelIndex& parent) const; virtual int rowCount(const QModelIndex& parent) const; - void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); - void searchWithTerm(const QString & term); + void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); + void searchWithTerm(const QString& term); -private slots: + private slots: void searchRequestFinished(); void searchRequestFailed(); void logoFailed(QString logo); void logoLoaded(QString logo, QString out); -private: + private: void performSearch(); void requestLogo(QString logo, QString url); -private: + private: QList modpacks; QStringList m_failedLogos; QStringList m_loadingLogos; @@ -78,17 +77,13 @@ private: QMap waitingCallbacks; QString currentSearchTerm; - enum SearchState { - None, - ResetRequested, - Finished - } searchState = None; + enum SearchState { None, ResetRequested, Finished } searchState = None; enum SearchMode { List, Single, } searchMode = List; NetJob::Ptr jobPtr; - QByteArray response; + std::shared_ptr response = std::make_shared(); }; -} +} // namespace Technic diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index 859da97e9..fc678fa20 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -143,7 +143,7 @@ void TechnicPage::suggestCurrent() auto netJob = makeShared(QString("Technic::PackMeta(%1)").arg(current.name), APPLICATION->network()); QString slug = current.slug; - netJob->addNetAction(Net::Download::makeByteArray(QString("%1modpack/%2?build=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, slug, BuildConfig.TECHNIC_API_BUILD), &response)); + netJob->addNetAction(Net::Download::makeByteArray(QString("%1modpack/%2?build=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, slug, BuildConfig.TECHNIC_API_BUILD), response)); QObject::connect(netJob.get(), &NetJob::succeeded, this, [this, slug] { jobPtr.reset(); @@ -154,7 +154,7 @@ void TechnicPage::suggestCurrent() } QJsonParseError parse_error {}; - QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); QJsonObject obj = doc.object(); if(parse_error.error != QJsonParseError::NoError) { @@ -249,7 +249,7 @@ void TechnicPage::metadataLoaded() auto netJob = makeShared(QString("Technic::SolderMeta(%1)").arg(current.name), APPLICATION->network()); auto url = QString("%1/modpack/%2").arg(current.url, current.slug); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response)); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), response)); QObject::connect(netJob.get(), &NetJob::succeeded, this, &TechnicPage::onSolderLoaded); @@ -291,11 +291,11 @@ void TechnicPage::onSolderLoaded() { current.versions.clear(); - QJsonParseError parse_error {}; - auto doc = QJsonDocument::fromJson(response, &parse_error); + QJsonParseError parse_error{}; + auto doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << response; + qWarning() << *response; fallback(); return; } @@ -304,8 +304,7 @@ void TechnicPage::onSolderLoaded() { TechnicSolder::Pack pack; try { TechnicSolder::loadPack(pack, obj); - } - catch (const JSONValidationError& err) { + } catch (const JSONValidationError& err) { qCritical() << "Couldn't parse Solder pack metadata:" << err.cause(); fallback(); return; diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.h b/launcher/ui/pages/modplatform/technic/TechnicPage.h index f4a3b61d1..753261b30 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.h +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.h @@ -104,5 +104,5 @@ private: QString selectedVersion; NetJob::Ptr jobPtr; - QByteArray response; + std::shared_ptr response = std::make_shared(); }; diff --git a/launcher/ui/setupwizard/JavaWizardPage.cpp b/launcher/ui/setupwizard/JavaWizardPage.cpp index 14683778a..2b70c47c3 100644 --- a/launcher/ui/setupwizard/JavaWizardPage.cpp +++ b/launcher/ui/setupwizard/JavaWizardPage.cpp @@ -69,6 +69,7 @@ bool JavaWizardPage::validatePage() case JavaSettingsWidget::ValidationStatus::AllOK: { settings->set("JavaPath", m_java_widget->javaPath()); + return true; } case JavaSettingsWidget::ValidationStatus::JavaBad: { diff --git a/launcher/ui/setupwizard/SetupWizard.cpp b/launcher/ui/setupwizard/SetupWizard.cpp index 3fd9bb233..0a47334fc 100644 --- a/launcher/ui/setupwizard/SetupWizard.cpp +++ b/launcher/ui/setupwizard/SetupWizard.cpp @@ -59,7 +59,7 @@ void SetupWizard::pageChanged(int id) { setButtonLayout({QWizard::CustomButton1, QWizard::Stretch, QWizard::BackButton, QWizard::NextButton, QWizard::FinishButton}); auto customButton = button(QWizard::CustomButton1); - connect(customButton, &QAbstractButton::pressed, [&](){ + connect(customButton, &QAbstractButton::clicked, [&](){ auto basePagePtr = getCurrentBasePage(); if(basePagePtr) { diff --git a/launcher/ui/setupwizard/ThemeWizardPage.cpp b/launcher/ui/setupwizard/ThemeWizardPage.cpp index 42826aba1..c3e0a5241 100644 --- a/launcher/ui/setupwizard/ThemeWizardPage.cpp +++ b/launcher/ui/setupwizard/ThemeWizardPage.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou + * Copyright (C) 2022 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/setupwizard/ThemeWizardPage.h b/launcher/ui/setupwizard/ThemeWizardPage.h index 61a3d0c01..f3d40b6d8 100644 --- a/launcher/ui/setupwizard/ThemeWizardPage.h +++ b/launcher/ui/setupwizard/ThemeWizardPage.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou + * Copyright (C) 2022 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/themes/CustomTheme.cpp b/launcher/ui/themes/CustomTheme.cpp index 198e76ba1..177edefad 100644 --- a/launcher/ui/themes/CustomTheme.cpp +++ b/launcher/ui/themes/CustomTheme.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou + * Copyright (C) 2022 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/themes/CustomTheme.h b/launcher/ui/themes/CustomTheme.h index f2b1b06ed..3ec4cafa2 100644 --- a/launcher/ui/themes/CustomTheme.h +++ b/launcher/ui/themes/CustomTheme.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou + * Copyright (C) 2022 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/themes/ITheme.cpp b/launcher/ui/themes/ITheme.cpp index 8f0757e1a..42d63b113 100644 --- a/launcher/ui/themes/ITheme.cpp +++ b/launcher/ui/themes/ITheme.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou + * Copyright (C) 2022 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/themes/ITheme.h b/launcher/ui/themes/ITheme.h index a0a638bd0..d85e7f983 100644 --- a/launcher/ui/themes/ITheme.h +++ b/launcher/ui/themes/ITheme.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou + * Copyright (C) 2022 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/themes/SystemTheme.cpp b/launcher/ui/themes/SystemTheme.cpp index 3a746d027..3b8cb24a4 100644 --- a/launcher/ui/themes/SystemTheme.cpp +++ b/launcher/ui/themes/SystemTheme.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou + * Copyright (C) 2022 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/themes/SystemTheme.h b/launcher/ui/themes/SystemTheme.h index 05f31233e..4f7d83e57 100644 --- a/launcher/ui/themes/SystemTheme.h +++ b/launcher/ui/themes/SystemTheme.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou + * Copyright (C) 2022 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/themes/ThemeManager.cpp b/launcher/ui/themes/ThemeManager.cpp index 94ac8a245..b50c6157f 100644 --- a/launcher/ui/themes/ThemeManager.cpp +++ b/launcher/ui/themes/ThemeManager.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou + * Copyright (C) 2022 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/themes/ThemeManager.h b/launcher/ui/themes/ThemeManager.h index 87f36d9c1..d2a6fb70a 100644 --- a/launcher/ui/themes/ThemeManager.h +++ b/launcher/ui/themes/ThemeManager.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou + * Copyright (C) 2022 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp index fdc581b41..a0fda952f 100644 --- a/launcher/ui/widgets/InfoFrame.cpp +++ b/launcher/ui/widgets/InfoFrame.cpp @@ -1,52 +1,70 @@ // SPDX-License-Identifier: GPL-3.0-only /* -* PolyMC - Minecraft Launcher -* Copyright (c) 2022 flowln -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, version 3. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see . -* -* This file incorporates work covered by the following copyright and -* permission notice: -* -* Copyright 2013-2021 MultiMC Contributors -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include #include +#include #include "InfoFrame.h" #include "ui_InfoFrame.h" #include "ui/dialogs/CustomMessageBox.h" -InfoFrame::InfoFrame(QWidget *parent) : - QFrame(parent), - ui(new Ui::InfoFrame) +void setupLinkToolTip(QLabel* label) +{ + QObject::connect(label, &QLabel::linkHovered, [label](const QString& link) { + if (auto url = QUrl(link); !url.isValid() || (url.scheme() != "http" && url.scheme() != "https")) + return; + label->setToolTip(link); + }); +} + +InfoFrame::InfoFrame(QWidget* parent) : QFrame(parent), ui(new Ui::InfoFrame) { ui->setupUi(this); ui->descriptionLabel->setHidden(true); ui->nameLabel->setHidden(true); + ui->licenseLabel->setHidden(true); + ui->issueTrackerLabel->setHidden(true); + + setupLinkToolTip(ui->iconLabel); + setupLinkToolTip(ui->descriptionLabel); + setupLinkToolTip(ui->nameLabel); + setupLinkToolTip(ui->licenseLabel); + setupLinkToolTip(ui->issueTrackerLabel); updateHiddenState(); } @@ -57,38 +75,70 @@ InfoFrame::~InfoFrame() void InfoFrame::updateWithMod(Mod const& m) { - if (m.type() == ResourceType::FOLDER) - { + if (m.type() == ResourceType::FOLDER) { clear(); return; } QString text = ""; QString name = ""; + QString link = m.metaurl(); if (m.name().isEmpty()) name = m.internal_id(); else name = m.name(); - if (m.homeurl().isEmpty()) + if (link.isEmpty()) text = name; - else - text = "" + name + ""; + else { + text = "" + name + ""; + } if (!m.authors().isEmpty()) text += " by " + m.authors().join(", "); setName(text); - if (m.description().isEmpty()) - { + if (m.description().isEmpty()) { setDescription(QString()); - } - else - { + } else { setDescription(m.description()); } - setImage(); + setImage(m.icon({ 64, 64 })); + + auto licenses = m.licenses(); + QString licenseText = ""; + if (!licenses.empty()) { + for (auto l : licenses) { + if (!licenseText.isEmpty()) { + licenseText += "\n"; // add newline between licenses + } + if (!l.name.isEmpty()) { + if (l.url.isEmpty()) { + licenseText += l.name; + } else { + licenseText += "" + l.name + ""; + } + } else if (!l.url.isEmpty()) { + licenseText += "" + l.url + ""; + } + if (!l.description.isEmpty() && l.description != l.name) { + licenseText += " " + l.description; + } + } + } + if (!licenseText.isEmpty()) { + setLicense(tr("License: %1").arg(licenseText)); + } else { + setLicense(); + } + + QString issueTracker = ""; + if (!m.issueTracker().isEmpty()) { + issueTracker += tr("Report issues to: "); + issueTracker += "" + m.issueTracker() + ""; + } + setIssueTracker(issueTracker); } void InfoFrame::updateWithResource(const Resource& resource) @@ -97,7 +147,8 @@ void InfoFrame::updateWithResource(const Resource& resource) setImage(); } -QString InfoFrame::renderColorCodes(QString input) { +QString InfoFrame::renderColorCodes(QString input) +{ // We have to manually set the colors for use. // // A color is set using §x, with x = a hex number from 0 to f. @@ -108,16 +159,12 @@ QString InfoFrame::renderColorCodes(QString input) { // TODO: Wrap links inside tags // https://minecraft.fandom.com/wiki/Formatting_codes#Color_codes - const QMap color_codes_map = { - {'0', "#000000"}, {'1', "#0000AA"}, {'2', "#00AA00"}, {'3', "#00AAAA"}, {'4', "#AA0000"}, - {'5', "#AA00AA"}, {'6', "#FFAA00"}, {'7', "#AAAAAA"}, {'8', "#555555"}, {'9', "#5555FF"}, - {'a', "#55FF55"}, {'b', "#55FFFF"}, {'c', "#FF5555"}, {'d', "#FF55FF"}, {'e', "#FFFF55"}, - {'f', "#FFFFFF"} - }; + const QMap color_codes_map = { { '0', "#000000" }, { '1', "#0000AA" }, { '2', "#00AA00" }, { '3', "#00AAAA" }, + { '4', "#AA0000" }, { '5', "#AA00AA" }, { '6', "#FFAA00" }, { '7', "#AAAAAA" }, + { '8', "#555555" }, { '9', "#5555FF" }, { 'a', "#55FF55" }, { 'b', "#55FFFF" }, + { 'c', "#FF5555" }, { 'd', "#FF55FF" }, { 'e', "#FFFF55" }, { 'f', "#FFFFFF" } }; // https://minecraft.fandom.com/wiki/Formatting_codes#Formatting_codes - const QMap formatting_codes_map = { - {'l', "b"}, {'m', "s"}, {'n', "u"}, {'o', "i"} - }; + const QMap formatting_codes_map = { { 'l', "b" }, { 'm', "s" }, { 'n', "u" }, { 'o', "i" } }; QString html(""); QList tags{}; @@ -162,14 +209,14 @@ void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack) { setName(renderColorCodes(resource_pack.name())); setDescription(renderColorCodes(resource_pack.description())); - setImage(resource_pack.image({64, 64})); + setImage(resource_pack.image({ 64, 64 })); } void InfoFrame::updateWithTexturePack(TexturePack& texture_pack) { setName(renderColorCodes(texture_pack.name())); setDescription(renderColorCodes(texture_pack.description())); - setImage(texture_pack.image({64, 64})); + setImage(texture_pack.image({ 64, 64 })); } void InfoFrame::clear() @@ -177,28 +224,25 @@ void InfoFrame::clear() setName(); setDescription(); setImage(); + setLicense(); + setIssueTracker(); } void InfoFrame::updateHiddenState() { - if(ui->descriptionLabel->isHidden() && ui->nameLabel->isHidden()) - { + if (ui->descriptionLabel->isHidden() && ui->nameLabel->isHidden() && ui->licenseLabel->isHidden() && + ui->issueTrackerLabel->isHidden()) { setHidden(true); - } - else - { + } else { setHidden(false); } } void InfoFrame::setName(QString text) { - if(text.isEmpty()) - { + if (text.isEmpty()) { ui->nameLabel->setHidden(true); - } - else - { + } else { ui->nameLabel->setText(text); ui->nameLabel->setHidden(false); } @@ -207,14 +251,11 @@ void InfoFrame::setName(QString text) void InfoFrame::setDescription(QString text) { - if(text.isEmpty()) - { + if (text.isEmpty()) { ui->descriptionLabel->setHidden(true); updateHiddenState(); return; - } - else - { + } else { ui->descriptionLabel->setHidden(false); updateHiddenState(); } @@ -224,9 +265,8 @@ void InfoFrame::setDescription(QString text) QChar rem('\n'); QString finaltext; finaltext.reserve(intermediatetext.size()); - foreach(const QChar& c, intermediatetext) - { - if(c == rem && prev){ + foreach (const QChar& c, intermediatetext) { + if (c == rem && prev) { continue; } prev = c == rem; @@ -234,23 +274,70 @@ void InfoFrame::setDescription(QString text) } QString labeltext; labeltext.reserve(300); - if(finaltext.length() > 290) - { + if (finaltext.length() > 290) { ui->descriptionLabel->setOpenExternalLinks(false); ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText); m_description = text; // This allows injecting HTML here. labeltext.append("" + finaltext.left(287) + "..."); QObject::connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler); - } - else - { + } else { ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText); labeltext.append(finaltext); } ui->descriptionLabel->setText(labeltext); } +void InfoFrame::setLicense(QString text) +{ + if (text.isEmpty()) { + ui->licenseLabel->setHidden(true); + updateHiddenState(); + return; + } else { + ui->licenseLabel->setHidden(false); + updateHiddenState(); + } + ui->licenseLabel->setToolTip(""); + QString intermediatetext = text.trimmed(); + bool prev(false); + QChar rem('\n'); + QString finaltext; + finaltext.reserve(intermediatetext.size()); + foreach (const QChar& c, intermediatetext) { + if (c == rem && prev) { + continue; + } + prev = c == rem; + finaltext += c; + } + QString labeltext; + labeltext.reserve(300); + if (finaltext.length() > 290) { + ui->licenseLabel->setOpenExternalLinks(false); + ui->licenseLabel->setTextFormat(Qt::TextFormat::RichText); + m_description = text; + // This allows injecting HTML here. + labeltext.append("" + finaltext.left(287) + "..."); + QObject::connect(ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler); + } else { + ui->licenseLabel->setTextFormat(Qt::TextFormat::AutoText); + labeltext.append(finaltext); + } + ui->licenseLabel->setText(labeltext); +} + +void InfoFrame::setIssueTracker(QString text) +{ + if (text.isEmpty()) { + ui->issueTrackerLabel->setHidden(true); + } else { + ui->issueTrackerLabel->setText(text); + ui->issueTrackerLabel->setHidden(false); + } + updateHiddenState(); +} + void InfoFrame::setImage(QPixmap img) { if (img.isNull()) { @@ -263,18 +350,26 @@ void InfoFrame::setImage(QPixmap img) void InfoFrame::descriptionEllipsisHandler(QString link) { - if(!m_current_box) - { + if (!m_current_box) { m_current_box = CustomMessageBox::selectable(this, "", m_description); connect(m_current_box, &QMessageBox::finished, this, &InfoFrame::boxClosed); m_current_box->show(); - } - else - { + } else { m_current_box->setText(m_description); } } +void InfoFrame::licenseEllipsisHandler(QString link) +{ + if (!m_current_box) { + m_current_box = CustomMessageBox::selectable(this, "", m_license); + connect(m_current_box, &QMessageBox::finished, this, &InfoFrame::boxClosed); + m_current_box->show(); + } else { + m_current_box->setText(m_license); + } +} + void InfoFrame::boxClosed(int result) { m_current_box = nullptr; diff --git a/launcher/ui/widgets/InfoFrame.h b/launcher/ui/widgets/InfoFrame.h index 84523e281..d6764baa2 100644 --- a/launcher/ui/widgets/InfoFrame.h +++ b/launcher/ui/widgets/InfoFrame.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once @@ -21,8 +41,7 @@ #include "minecraft/mod/ResourcePack.h" #include "minecraft/mod/TexturePack.h" -namespace Ui -{ +namespace Ui { class InfoFrame; } @@ -36,6 +55,8 @@ class InfoFrame : public QFrame { void setName(QString text = {}); void setDescription(QString text = {}); void setImage(QPixmap img = {}); + void setLicense(QString text = {}); + void setIssueTracker(QString text = {}); void clear(); @@ -48,6 +69,7 @@ class InfoFrame : public QFrame { public slots: void descriptionEllipsisHandler(QString link); + void licenseEllipsisHandler(QString link); void boxClosed(int result); private: @@ -56,5 +78,6 @@ class InfoFrame : public QFrame { private: Ui::InfoFrame* ui; QString m_description; + QString m_license; class QMessageBox* m_current_box = nullptr; }; diff --git a/launcher/ui/widgets/InfoFrame.ui b/launcher/ui/widgets/InfoFrame.ui index 9e407ce90..c4d8c83d3 100644 --- a/launcher/ui/widgets/InfoFrame.ui +++ b/launcher/ui/widgets/InfoFrame.ui @@ -35,25 +35,28 @@ 0 - - + + + + + 0 + 0 + + + + + 64 + 64 + + - - Qt::RichText + + false - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - true - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + 0 @@ -82,28 +85,69 @@
    - - - - - 0 - 0 - - - - - 64 - 64 - - + + - - false + + Qt::RichText - - 0 + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse diff --git a/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp index 159943198..c94fdd8d5 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.cpp +++ b/launcher/ui/widgets/JavaSettingsWidget.cpp @@ -46,7 +46,7 @@ void JavaSettingsWidget::setupUi() m_verticalLayout = new QVBoxLayout(this); m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); - m_versionWidget = new VersionSelectWidget(this); + m_versionWidget = new VersionSelectWidget(true, this); m_verticalLayout->addWidget(m_versionWidget); m_horizontalLayout = new QHBoxLayout(); diff --git a/launcher/ui/widgets/LanguageSelectionWidget.cpp b/launcher/ui/widgets/LanguageSelectionWidget.cpp index 256b09dad..37d053478 100644 --- a/launcher/ui/widgets/LanguageSelectionWidget.cpp +++ b/launcher/ui/widgets/LanguageSelectionWidget.cpp @@ -1,16 +1,16 @@ #include "LanguageSelectionWidget.h" -#include -#include +#include #include #include +#include +#include #include "Application.h" #include "BuildConfig.h" -#include "translations/TranslationsModel.h" #include "settings/Setting.h" +#include "translations/TranslationsModel.h" -LanguageSelectionWidget::LanguageSelectionWidget(QWidget *parent) : - QWidget(parent) +LanguageSelectionWidget::LanguageSelectionWidget(QWidget* parent) : QWidget(parent) { verticalLayout = new QVBoxLayout(this); verticalLayout->setObjectName(QStringLiteral("verticalLayout")); @@ -31,6 +31,13 @@ LanguageSelectionWidget::LanguageSelectionWidget(QWidget *parent) : helpUsLabel->setWordWrap(true); verticalLayout->addWidget(helpUsLabel); + formatCheckbox = new QCheckBox(this); + formatCheckbox->setObjectName(QStringLiteral("formatCheckbox")); + formatCheckbox->setCheckState(APPLICATION->settings()->get("UseSystemLocale").toBool() ? Qt::Checked : Qt::Unchecked); + connect(formatCheckbox, &QCheckBox::stateChanged, + [this]() { APPLICATION->translations()->setUseSystemLocale(formatCheckbox->isChecked()); }); + verticalLayout->addWidget(formatCheckbox); + auto translations = APPLICATION->translations(); auto index = translations->selectedIndex(); languageView->setModel(translations.get()); @@ -38,7 +45,7 @@ LanguageSelectionWidget::LanguageSelectionWidget(QWidget *parent) : languageView->header()->setSectionResizeMode(QHeaderView::ResizeToContents); languageView->header()->setSectionResizeMode(0, QHeaderView::Stretch); connect(languageView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &LanguageSelectionWidget::languageRowChanged); - verticalLayout->setContentsMargins(0,0,0,0); + verticalLayout->setContentsMargins(0, 0, 0, 0); auto language_setting = APPLICATION->settings()->getSetting("Language"); connect(language_setting.get(), &Setting::SettingChanged, this, &LanguageSelectionWidget::languageSettingChanged); @@ -53,15 +60,14 @@ QString LanguageSelectionWidget::getSelectedLanguageKey() const void LanguageSelectionWidget::retranslate() { QString text = tr("Don't see your language or the quality is poor?
    Help us with translations!") - .arg(BuildConfig.TRANSLATIONS_URL); + .arg(BuildConfig.TRANSLATIONS_URL); helpUsLabel->setText(text); - + formatCheckbox->setText(tr("Use system locales")); } void LanguageSelectionWidget::languageRowChanged(const QModelIndex& current, const QModelIndex& previous) { - if (current == previous) - { + if (current == previous) { return; } auto translations = APPLICATION->translations(); @@ -70,7 +76,7 @@ void LanguageSelectionWidget::languageRowChanged(const QModelIndex& current, con translations->updateLanguage(key); } -void LanguageSelectionWidget::languageSettingChanged(const Setting &, const QVariant) +void LanguageSelectionWidget::languageSettingChanged(const Setting&, const QVariant) { auto translations = APPLICATION->translations(); auto index = translations->selectedIndex(); diff --git a/launcher/ui/widgets/LanguageSelectionWidget.h b/launcher/ui/widgets/LanguageSelectionWidget.h index 4a88924c4..5e86a288f 100644 --- a/launcher/ui/widgets/LanguageSelectionWidget.h +++ b/launcher/ui/widgets/LanguageSelectionWidget.h @@ -21,23 +21,24 @@ class QVBoxLayout; class QTreeView; class QLabel; class Setting; +class QCheckBox; -class LanguageSelectionWidget: public QWidget -{ +class LanguageSelectionWidget : public QWidget { Q_OBJECT -public: - explicit LanguageSelectionWidget(QWidget *parent = 0); - virtual ~LanguageSelectionWidget() { }; + public: + explicit LanguageSelectionWidget(QWidget* parent = 0); + virtual ~LanguageSelectionWidget(){}; QString getSelectedLanguageKey() const; void retranslate(); -protected slots: - void languageRowChanged(const QModelIndex ¤t, const QModelIndex &previous); - void languageSettingChanged(const Setting &, const QVariant); + protected slots: + void languageRowChanged(const QModelIndex& current, const QModelIndex& previous); + void languageSettingChanged(const Setting&, const QVariant); -private: - QVBoxLayout *verticalLayout = nullptr; - QTreeView *languageView = nullptr; - QLabel *helpUsLabel = nullptr; + private: + QVBoxLayout* verticalLayout = nullptr; + QTreeView* languageView = nullptr; + QLabel* helpUsLabel = nullptr; + QCheckBox* formatCheckbox = nullptr; }; diff --git a/launcher/ui/widgets/ModListView.cpp b/launcher/ui/widgets/ModListView.cpp index 09b03a76a..80a918b64 100644 --- a/launcher/ui/widgets/ModListView.cpp +++ b/launcher/ui/widgets/ModListView.cpp @@ -14,9 +14,6 @@ */ #include "ModListView.h" - -#include "minecraft/mod/ModFolderModel.h" - #include #include #include @@ -65,17 +62,13 @@ void ModListView::setModel ( QAbstractItemModel* model ) for(int i = 1; i < head->count(); i++) head->setSectionResizeMode(i, QHeaderView::ResizeToContents); } +} - auto real_model = model; - if (auto proxy_model = dynamic_cast(model); proxy_model) - real_model = proxy_model->sourceModel(); - - if (auto mod_model = dynamic_cast(real_model); mod_model) { - connect(mod_model, &ModFolderModel::updateFinished, this, [this, mod_model]{ - auto mods = mod_model->allMods(); - // Hide the 'Provider' column if no mod has a defined provider! - setColumnHidden(ModFolderModel::Columns::ProviderColumn, - std::none_of(mods.constBegin(), mods.constEnd(), [](auto const mod){ return mod->provider().has_value(); })); - }); +void ModListView::setResizeModes(const QList &modes) +{ + auto head = header(); + for(int i = 0; i < modes.count(); i++) { + head->setSectionResizeMode(i, modes[i]); } } + diff --git a/launcher/ui/widgets/ModListView.h b/launcher/ui/widgets/ModListView.h index 881e092f7..3f0b3b0e1 100644 --- a/launcher/ui/widgets/ModListView.h +++ b/launcher/ui/widgets/ModListView.h @@ -14,6 +14,7 @@ */ #pragma once +#include #include class ModListView: public QTreeView @@ -22,4 +23,5 @@ class ModListView: public QTreeView public: explicit ModListView ( QWidget* parent = 0 ); virtual void setModel ( QAbstractItemModel* model ); + virtual void setResizeModes (const QList& modes); }; diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp index 38a228973..b98c9796f 100644 --- a/launcher/ui/widgets/PageContainer.cpp +++ b/launcher/ui/widgets/PageContainer.cpp @@ -93,6 +93,10 @@ PageContainer::PageContainer(BasePageProvider *pageProvider, QString defaultId, page->listIndex = counter; page->setParentContainer(this); counter++; + page->updateExtraInfo = [this](QString id, QString info) { + if (m_currentPage && id == m_currentPage->id()) + m_header->setText(m_currentPage->displayName() + info); + }; } m_model->setPages(pages); diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp index d1ff9dbc0..0085d6b2b 100644 --- a/launcher/ui/widgets/ProjectItem.cpp +++ b/launcher/ui/widgets/ProjectItem.cpp @@ -64,6 +64,17 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o font.setBold(true); font.setUnderline(true); } + if (index.data(UserDataTypes::INSTALLED).toBool()) { + auto hRect = opt.rect; + hRect.setX(hRect.x() + 1); + hRect.setY(hRect.y() + 1); + hRect.setHeight(hRect.height() - 2); + hRect.setWidth(hRect.width() - 2); + // Set nice font + font.setItalic(true); + font.setOverline(true); + painter->drawRect(hRect); + } font.setPointSize(font.pointSize() + 2); painter->setFont(font); diff --git a/launcher/ui/widgets/ProjectItem.h b/launcher/ui/widgets/ProjectItem.h index f668edf66..196055ea4 100644 --- a/launcher/ui/widgets/ProjectItem.h +++ b/launcher/ui/widgets/ProjectItem.h @@ -6,7 +6,8 @@ enum UserDataTypes { TITLE = 257, // QString DESCRIPTION = 258, // QString - SELECTED = 259 // bool + SELECTED = 259, // bool + INSTALLED = 260 // bool }; /** This is an item delegate composed of: diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.cpp b/launcher/ui/widgets/ThemeCustomizationWidget.cpp index dcf13303c..3bfcd8213 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.cpp +++ b/launcher/ui/widgets/ThemeCustomizationWidget.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou + * Copyright (C) 2022 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.h b/launcher/ui/widgets/ThemeCustomizationWidget.h index d955a2665..6c33c3c50 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.h +++ b/launcher/ui/widgets/ThemeCustomizationWidget.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou + * Copyright (C) 2022 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/widgets/VersionListView.cpp b/launcher/ui/widgets/VersionListView.cpp index 0e126c65b..06e58d221 100644 --- a/launcher/ui/widgets/VersionListView.cpp +++ b/launcher/ui/widgets/VersionListView.cpp @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -125,14 +126,9 @@ void VersionListView::paintEvent(QPaintEvent *event) QString VersionListView::currentEmptyString() const { - if(m_itemCount) { - return QString(); - } switch(m_emptyMode) { default: - case VersionListView::Empty: - return QString(); case VersionListView::String: return m_emptyString; case VersionListView::ErrorString: diff --git a/launcher/ui/widgets/VersionSelectWidget.cpp b/launcher/ui/widgets/VersionSelectWidget.cpp index 404860d90..a956ddb3c 100644 --- a/launcher/ui/widgets/VersionSelectWidget.cpp +++ b/launcher/ui/widgets/VersionSelectWidget.cpp @@ -1,15 +1,20 @@ #include "VersionSelectWidget.h" +#include +#include +#include +#include #include #include -#include #include "VersionProxyModel.h" #include "ui/dialogs/CustomMessageBox.h" -VersionSelectWidget::VersionSelectWidget(QWidget* parent) - : QWidget(parent) +VersionSelectWidget::VersionSelectWidget(QWidget* parent) : VersionSelectWidget(false, parent) {} + +VersionSelectWidget::VersionSelectWidget(bool focusSearch, QWidget* parent) + : QWidget(parent), focusSearch(focusSearch) { setObjectName(QStringLiteral("VersionSelectWidget")); verticalLayout = new QVBoxLayout(this); @@ -30,6 +35,21 @@ VersionSelectWidget::VersionSelectWidget(QWidget* parent) listView->setModel(m_proxyModel); verticalLayout->addWidget(listView); + search = new QLineEdit(this); + search->setPlaceholderText(tr("Search")); + search->setClearButtonEnabled(true); + verticalLayout->addWidget(search); + connect(search, &QLineEdit::textEdited, [this](const QString& value) { + m_proxyModel->setSearch(value); + if (!value.isEmpty() || !listView->selectionModel()->hasSelection()) { + const QModelIndex first = listView->model()->index(0, 0); + listView->selectionModel()->setCurrentIndex(first, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); + listView->scrollToTop(); + } else + listView->scrollTo(listView->selectionModel()->currentIndex(), QAbstractItemView::PositionAtCenter); + }); + search->installEventFilter(this); + sneakyProgressBar = new QProgressBar(this); sneakyProgressBar->setObjectName(QStringLiteral("sneakyProgressBar")); sneakyProgressBar->setFormat(QStringLiteral("%p%")); @@ -72,6 +92,23 @@ void VersionSelectWidget::setResizeOn(int column) listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::Stretch); } +bool VersionSelectWidget::eventFilter(QObject *watched, QEvent *event) { + if (watched == search && event->type() == QEvent::KeyPress) { + const QKeyEvent* keyEvent = (QKeyEvent*)event; + const bool up = keyEvent->key() == Qt::Key_Up; + const bool down = keyEvent->key() == Qt::Key_Down; + if (up || down) { + const QModelIndex index = listView->model()->index(listView->currentIndex().row() + (up ? -1 : 1), 0); + if (index.row() >= 0 && index.row() < listView->model()->rowCount()) { + listView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); + return true; + } + } + } + + return QObject::eventFilter(watched, event); +} + void VersionSelectWidget::initialize(BaseVersionList *vlist) { m_vlist = vlist; @@ -79,6 +116,9 @@ void VersionSelectWidget::initialize(BaseVersionList *vlist) listView->header()->setSectionResizeMode(QHeaderView::ResizeToContents); listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::Stretch); + if (focusSearch) + search->setFocus(); + if (!m_vlist->isLoaded()) { loadList(); diff --git a/launcher/ui/widgets/VersionSelectWidget.h b/launcher/ui/widgets/VersionSelectWidget.h index e75efc6f6..be4ba768d 100644 --- a/launcher/ui/widgets/VersionSelectWidget.h +++ b/launcher/ui/widgets/VersionSelectWidget.h @@ -1,22 +1,43 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once #include #include +#include #include "BaseVersionList.h" #include "VersionListView.h" @@ -30,7 +51,8 @@ class VersionSelectWidget: public QWidget { Q_OBJECT public: - explicit VersionSelectWidget(QWidget *parent = 0); + explicit VersionSelectWidget(QWidget *parent); + explicit VersionSelectWidget(bool focusSearch = false, QWidget *parent = 0); ~VersionSelectWidget(); //! loads the list if needed. @@ -52,6 +74,7 @@ public: void setEmptyErrorString(QString emptyErrorString); void setEmptyMode(VersionListView::EmptyMode mode); void setResizeOn(int column); + bool eventFilter(QObject* watched, QEvent* event) override; signals: void selectedVersionChanged(BaseVersion::Ptr version); @@ -75,9 +98,10 @@ private: int resizeOnColumn = 0; Task * loadTask; bool preselectedAlready = false; + bool focusSearch; -private: QVBoxLayout *verticalLayout = nullptr; VersionListView *listView = nullptr; + QLineEdit *search; QProgressBar *sneakyProgressBar = nullptr; }; diff --git a/launcher/ui/widgets/WideBar.cpp b/launcher/ui/widgets/WideBar.cpp index ac34e3aaa..a77c45fee 100644 --- a/launcher/ui/widgets/WideBar.cpp +++ b/launcher/ui/widgets/WideBar.cpp @@ -116,12 +116,21 @@ void WideBar::insertActionAfter(QAction* after, QAction* action) if (iter == m_entries.end()) return; + iter++; + // the action to insert after is present + // however, the element after it isn't valid + if (iter == m_entries.end()) { + // append the action instead of inserting it + addAction(action); + return; + } + BarEntry entry; - entry.bar_action = insertWidget((iter + 1)->bar_action, new ActionButton(action, this, m_use_default_action)); + entry.bar_action = insertWidget(iter->bar_action, new ActionButton(action, this, m_use_default_action)); entry.menu_action = action; entry.type = BarEntry::Type::Action; - m_entries.insert(iter + 1, entry); + m_entries.insert(iter, entry); m_menu_state = MenuState::Dirty; } diff --git a/libraries/murmur2/src/MurmurHash2.cpp b/libraries/murmur2/src/MurmurHash2.cpp index c13608f07..e73127953 100644 --- a/libraries/murmur2/src/MurmurHash2.cpp +++ b/libraries/murmur2/src/MurmurHash2.cpp @@ -89,8 +89,10 @@ void FourBytes_MurmurHash2(const unsigned char* data, IncrementalHashInfo& prev) switch (prev.len) { case 3: prev.h ^= data[2] << 16; + /* fall through */ case 2: prev.h ^= data[1] << 8; + /* fall through */ case 1: prev.h ^= data[0]; prev.h *= m; diff --git a/nix/dev.nix b/nix/dev.nix index a4ff2cc49..635c6bb41 100644 --- a/nix/dev.nix +++ b/nix/dev.nix @@ -37,7 +37,7 @@ nil ]; - inputsFrom = [self.packages.${system}.default]; + inputsFrom = [self.packages.${system}.prismlauncher-unwrapped]; buildInputs = with pkgs; [ccache ninja]; }; diff --git a/program_info/genicons.sh b/program_info/genicons.sh index 42592c4ee..fe8d2e35e 100755 --- a/program_info/genicons.sh +++ b/program_info/genicons.sh @@ -67,7 +67,4 @@ else fi # replace icon in themes -for dir in ../launcher/resources/*/scalable -do - cp -v org.prismlauncher.PrismLauncher.svg "$dir/launcher.svg" -done +cp -v org.prismlauncher.PrismLauncher.svg "../launcher/resources/multimc/scalable/launcher.svg" diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 36a3b0f8d..a26a49fec 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -9,9 +9,6 @@ ecm_add_test(GZip_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}:: ecm_add_test(GradleSpecifier_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME GradleSpecifier) -ecm_add_test(PackageManifest_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test - TEST_NAME PackageManifest) - ecm_add_test(MojangVersionFormat_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME MojangVersionFormat) diff --git a/tests/FileSystem_test.cpp b/tests/FileSystem_test.cpp index ec1f0bcff..a41345c2e 100644 --- a/tests/FileSystem_test.cpp +++ b/tests/FileSystem_test.cpp @@ -42,6 +42,10 @@ class LinkTask : public Task { m_lnk->debug(true); } + ~LinkTask() { + delete m_lnk; + } + void matcher(const IPathMatcher *filter) { m_lnk->matcher(filter); @@ -219,7 +223,8 @@ slots: qDebug() << tempDir.path(); qDebug() << target_dir.path(); FS::copy c(folder, target_dir.path()); - c.matcher(new RegexpMatcher("[.]?mcmeta")); + RegexpMatcher re("[.]?mcmeta"); + c.matcher(&re); c(); for(auto entry: target_dir.entryList()) @@ -253,7 +258,8 @@ slots: qDebug() << tempDir.path(); qDebug() << target_dir.path(); FS::copy c(folder, target_dir.path()); - c.matcher(new RegexpMatcher("[.]?mcmeta")); + RegexpMatcher re("[.]?mcmeta"); + c.matcher(&re); c.whitelist(true); c(); @@ -460,7 +466,8 @@ slots: qDebug() << target_dir.path(); LinkTask lnk_tsk(folder, target_dir.path()); - lnk_tsk.matcher(new RegexpMatcher("[.]?mcmeta")); + RegexpMatcher re("[.]?mcmeta"); + lnk_tsk.matcher(&re); lnk_tsk.linkRecursively(true); QObject::connect(&lnk_tsk, &Task::finished, [&]{ QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); @@ -511,7 +518,8 @@ slots: qDebug() << target_dir.path(); LinkTask lnk_tsk(folder, target_dir.path()); - lnk_tsk.matcher(new RegexpMatcher("[.]?mcmeta")); + RegexpMatcher re("[.]?mcmeta"); + lnk_tsk.matcher(&re); lnk_tsk.linkRecursively(true); lnk_tsk.whitelist(true); QObject::connect(&lnk_tsk, &Task::finished, [&]{ diff --git a/tests/INIFile_test.cpp b/tests/INIFile_test.cpp index 4be8133c0..95730e244 100644 --- a/tests/INIFile_test.cpp +++ b/tests/INIFile_test.cpp @@ -1,24 +1,19 @@ #include -#include -#include #include +#include +#include +#include +#include +#include "FileSystem.h" #include -class IniFileTest : public QObject -{ +class IniFileTest : public QObject { Q_OBJECT -private -slots: - void initTestCase() - { - - } - void cleanupTestCase() - { - - } + private slots: + void initTestCase() {} + void cleanupTestCase() {} void test_Escape_data() { @@ -47,17 +42,17 @@ slots: // load INIFile f2; f2.loadFile(filename); - QCOMPARE(f2.get("a","NOT SET").toString(), a); - QCOMPARE(f2.get("b","NOT SET").toString(), b); + QCOMPARE(f2.get("a", "NOT SET").toString(), a); + QCOMPARE(f2.get("b", "NOT SET").toString(), b); } void test_SaveLoadLists() { QString slist_strings = "(\"a\",\"b\",\"c\")"; - QStringList list_strings = {"a", "b", "c"}; + QStringList list_strings = { "a", "b", "c" }; QString slist_numbers = "(1,2,3,10)"; - QList list_numbers = {1, 2, 3, 10}; + QList list_numbers = { 1, 2, 3, 10 }; QString filename = "test_SaveLoadLists.ini"; @@ -72,13 +67,129 @@ slots: QStringList out_list_strings = f2.get("list_strings", QStringList()).toStringList(); qDebug() << "OutStringList" << out_list_strings; - + QList out_list_numbers = QVariantUtils::toList(f2.get("list_numbers", QVariantUtils::fromList(QList()))); qDebug() << "OutNumbersList" << out_list_numbers; QCOMPARE(out_list_strings, list_strings); QCOMPARE(out_list_numbers, list_numbers); } + + void test_SaveAlreadyExistingFile() + { + QString fileContent = R"(InstanceType=OneSix +iconKey=vanillia_icon +name=Minecraft Vanillia +OverrideCommands=true +PreLaunchCommand="$INST_JAVA" -jar packwiz-installer-bootstrap.jar link +Wrapperommand=)"; + fileContent += "\""; + fileContent += +R"(\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link =)"; + fileContent += "\"\n"; +#if defined(Q_OS_WIN) + QString fileName = "test_SaveAlreadyExistingFile.ini"; + QFile file(fileName); + QCOMPARE(file.open(QFile::WriteOnly | QFile::Text), true); +#else + QTemporaryFile file; + QCOMPARE(file.open(), true); + QCOMPARE(file.fileName().isEmpty(), false); + QString fileName = file.fileName(); +#endif + QTextStream stream(&file); + stream << fileContent; + file.close(); + + // load + INIFile f1; + f1.loadFile(fileName); + QCOMPARE(f1.get("PreLaunchCommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link"); + QCOMPARE(f1.get("Wrapperommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link ="); + f1.saveFile(fileName); + INIFile f2; + f2.loadFile(fileName); + QCOMPARE(f2.get("PreLaunchCommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link"); + QCOMPARE(f2.get("Wrapperommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link ="); + QCOMPARE(f2.get("ConfigVersion", "NOT SET").toString(), "1.2"); +#if defined(Q_OS_WIN) + FS::deletePath(fileName); +#endif + } + + void test_SaveAlreadyExistingFileWithSpecialChars() + { +#if defined(Q_OS_WIN) + QString fileName = "test_SaveAlreadyExistingFileWithSpecialChars.ini"; +#else + QTemporaryFile file; + QCOMPARE(file.open(), true); + QCOMPARE(file.fileName().isEmpty(), false); + QString fileName = file.fileName(); + file.close(); +#endif + QSettings settings{ fileName, QSettings::Format::IniFormat }; + settings.setFallbacksEnabled(false); + + settings.setValue("simple", "value1"); + settings.setValue("withQuotes", R"("value2" with quotes)"); + settings.setValue("withSpecialCharacters", "env mesa=true"); + settings.setValue("withSpecialCharacters2", "1,2,3,4"); + settings.setValue("withSpecialCharacters2", "1;2;3;4"); + settings.setValue("withAll", "val=\"$INST_JAVA\" -jar; ls "); + + settings.sync(); + + QCOMPARE(settings.status(), QSettings::Status::NoError); + + // load + INIFile f1; + f1.loadFile(fileName); + for (auto key : settings.allKeys()) + QCOMPARE(f1.get(key, "NOT SET").toString(), settings.value(key).toString()); + f1.saveFile(fileName); + INIFile f2; + f2.loadFile(fileName); + for (auto key : settings.allKeys()) + QCOMPARE(f2.get(key, "NOT SET").toString(), settings.value(key).toString()); + QCOMPARE(f2.get("ConfigVersion", "NOT SET").toString(), "1.2"); +#if defined(Q_OS_WIN) + FS::deletePath(fileName); +#endif + } + + void test_SaveAlreadyExistingFileWithSpecialCharsV1() + { + QString fileContent = R"(InstanceType=OneSix +ConfigVersion=1.1 +iconKey=vanillia_icon +name=Minecraft Vanillia +OverrideCommands=true +PreLaunchCommand=)"; + fileContent += "\"\\\"env mesa=true\\\"\"\n"; + +#if defined(Q_OS_WIN) + QString fileName = "test_SaveAlreadyExistingFileWithSpecialCharsV1.ini"; + QFile file(fileName); + QCOMPARE(file.open(QFile::WriteOnly | QFile::Text), true); +#else + QTemporaryFile file; + QCOMPARE(file.open(), true); + QCOMPARE(file.fileName().isEmpty(), false); + QString fileName = file.fileName(); +#endif + QTextStream stream(&file); + stream << fileContent; + file.close(); + + // load + INIFile f1; + f1.loadFile(fileName); + QCOMPARE(f1.get("PreLaunchCommand", "NOT SET").toString(), "env mesa=true"); + QCOMPARE(f1.get("ConfigVersion", "NOT SET").toString(), "1.2"); +#if defined(Q_OS_WIN) + FS::deletePath(fileName); +#endif + } }; QTEST_GUILESS_MAIN(IniFileTest) diff --git a/tests/PackageManifest_test.cpp b/tests/PackageManifest_test.cpp deleted file mode 100644 index e38abf808..000000000 --- a/tests/PackageManifest_test.cpp +++ /dev/null @@ -1,343 +0,0 @@ -#include -#include - -#include - -using namespace mojang_files; - -QDebug operator<<(QDebug debug, const Path &path) -{ - debug << path.toString(); - return debug; -} - -class PackageManifestTest : public QObject -{ - Q_OBJECT - -private slots: - void test_parse(); - void test_parse_file(); - void test_inspect(); -#ifndef Q_OS_WIN32 - void test_inspect_symlinks(); -#endif - void mkdir_deep(); - void rmdir_deep(); - - void identical_file(); - void changed_file(); - void added_file(); - void removed_file(); -}; - -namespace { -QByteArray basic_manifest = R"END( -{ - "files": { - "a/b.txt": { - "type": "file", - "downloads": { - "raw": { - "url": "http://dethware.org/b.txt", - "sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709", - "size": 0 - } - }, - "executable": true - }, - "a/b/c": { - "type": "directory" - }, - "a/b/c.txt": { - "type": "link", - "target": "../b.txt" - } - } -} -)END"; -} - -void PackageManifestTest::test_parse() -{ - auto manifest = Package::fromManifestContents(basic_manifest); - QVERIFY(manifest.valid == true); - QVERIFY(manifest.files.size() == 1); - QVERIFY(manifest.files.count(Path("a/b.txt"))); - auto &file = manifest.files[Path("a/b.txt")]; - QVERIFY(file.executable == true); - QVERIFY(file.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709"); - QVERIFY(file.size == 0); - QVERIFY(manifest.folders.size() == 4); - QVERIFY(manifest.folders.count(Path("."))); - QVERIFY(manifest.folders.count(Path("a"))); - QVERIFY(manifest.folders.count(Path("a/b"))); - QVERIFY(manifest.folders.count(Path("a/b/c"))); - QVERIFY(manifest.symlinks.size() == 1); - auto symlinkPath = Path("a/b/c.txt"); - QVERIFY(manifest.symlinks.count(symlinkPath)); - auto &symlink = manifest.symlinks[symlinkPath]; - QVERIFY(symlink == Path("../b.txt")); - QVERIFY(manifest.sources.size() == 1); -} - -void PackageManifestTest::test_parse_file() { - auto path = QFINDTESTDATA("testdata/PackageManifest/1.8.0_202-x64.json"); - auto manifest = Package::fromManifestFile(path); - QVERIFY(manifest.valid == true); -} - - -void PackageManifestTest::test_inspect() { - auto path = QFINDTESTDATA("testdata/PackageManifest/inspect_win/"); - auto manifest = Package::fromInspectedFolder(path); - QVERIFY(manifest.valid == true); - QVERIFY(manifest.files.size() == 2); - QVERIFY(manifest.files.count(Path("a/b.txt"))); - auto &file1 = manifest.files[Path("a/b.txt")]; - QVERIFY(file1.executable == false); - QVERIFY(file1.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709"); - QVERIFY(file1.size == 0); - QVERIFY(manifest.files.count(Path("a/b/b.txt"))); - auto &file2 = manifest.files[Path("a/b/b.txt")]; - QVERIFY(file2.executable == false); - QVERIFY(file2.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709"); - QVERIFY(file2.size == 0); - QVERIFY(manifest.folders.size() == 3); - QVERIFY(manifest.folders.count(Path("."))); - QVERIFY(manifest.folders.count(Path("a"))); - QVERIFY(manifest.folders.count(Path("a/b"))); - QVERIFY(manifest.symlinks.size() == 0); -} - -#ifndef Q_OS_WIN32 -void PackageManifestTest::test_inspect_symlinks() { - auto path = QFINDTESTDATA("testdata/PackageManifest/inspect/"); - auto manifest = Package::fromInspectedFolder(path); - QVERIFY(manifest.valid == true); - QVERIFY(manifest.files.size() == 1); - QVERIFY(manifest.files.count(Path("a/b.txt"))); - auto &file = manifest.files[Path("a/b.txt")]; - QVERIFY(file.executable == true); - QVERIFY(file.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709"); - QVERIFY(file.size == 0); - QVERIFY(manifest.folders.size() == 3); - QVERIFY(manifest.folders.count(Path("."))); - QVERIFY(manifest.folders.count(Path("a"))); - QVERIFY(manifest.folders.count(Path("a/b"))); - QVERIFY(manifest.symlinks.size() == 1); - QVERIFY(manifest.symlinks.count(Path("a/b/b.txt"))); - qDebug() << manifest.symlinks[Path("a/b/b.txt")]; - QVERIFY(manifest.symlinks[Path("a/b/b.txt")] == Path("../b.txt")); -} -#endif - -void PackageManifestTest::mkdir_deep() { - - Package from; - auto to = Package::fromManifestContents(R"END( -{ - "files": { - "a/b/c/d/e": { - "type": "directory" - } - } -} -)END"); - auto operations = UpdateOperations::resolve(from, to); - QVERIFY(operations.deletes.size() == 0); - QVERIFY(operations.rmdirs.size() == 0); - - QVERIFY(operations.mkdirs.size() == 6); - QVERIFY(operations.mkdirs[0] == Path(".")); - QVERIFY(operations.mkdirs[1] == Path("a")); - QVERIFY(operations.mkdirs[2] == Path("a/b")); - QVERIFY(operations.mkdirs[3] == Path("a/b/c")); - QVERIFY(operations.mkdirs[4] == Path("a/b/c/d")); - QVERIFY(operations.mkdirs[5] == Path("a/b/c/d/e")); - - QVERIFY(operations.downloads.size() == 0); - QVERIFY(operations.mklinks.size() == 0); - QVERIFY(operations.executable_fixes.size() == 0); -} - -void PackageManifestTest::rmdir_deep() { - - Package to; - auto from = Package::fromManifestContents(R"END( -{ - "files": { - "a/b/c/d/e": { - "type": "directory" - } - } -} -)END"); - auto operations = UpdateOperations::resolve(from, to); - QVERIFY(operations.deletes.size() == 0); - - QVERIFY(operations.rmdirs.size() == 6); - QVERIFY(operations.rmdirs[0] == Path("a/b/c/d/e")); - QVERIFY(operations.rmdirs[1] == Path("a/b/c/d")); - QVERIFY(operations.rmdirs[2] == Path("a/b/c")); - QVERIFY(operations.rmdirs[3] == Path("a/b")); - QVERIFY(operations.rmdirs[4] == Path("a")); - QVERIFY(operations.rmdirs[5] == Path(".")); - - QVERIFY(operations.mkdirs.size() == 0); - QVERIFY(operations.downloads.size() == 0); - QVERIFY(operations.mklinks.size() == 0); - QVERIFY(operations.executable_fixes.size() == 0); -} - -void PackageManifestTest::identical_file() { - QByteArray manifest = R"END( -{ - "files": { - "a/b/c/d/empty.txt": { - "type": "file", - "downloads": { - "raw": { - "url": "http://dethware.org/empty.txt", - "sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709", - "size": 0 - } - }, - "executable": false - } - } -} -)END"; - auto from = Package::fromManifestContents(manifest); - auto to = Package::fromManifestContents(manifest); - auto operations = UpdateOperations::resolve(from, to); - QVERIFY(operations.deletes.size() == 0); - QVERIFY(operations.rmdirs.size() == 0); - QVERIFY(operations.mkdirs.size() == 0); - QVERIFY(operations.downloads.size() == 0); - QVERIFY(operations.mklinks.size() == 0); - QVERIFY(operations.executable_fixes.size() == 0); -} - -void PackageManifestTest::changed_file() { - auto from = Package::fromManifestContents(R"END( -{ - "files": { - "a/b/c/d/file": { - "type": "file", - "downloads": { - "raw": { - "url": "http://dethware.org/empty.txt", - "sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709", - "size": 0 - } - }, - "executable": false - } - } -} -)END"); - auto to = Package::fromManifestContents(R"END( -{ - "files": { - "a/b/c/d/file": { - "type": "file", - "downloads": { - "raw": { - "url": "http://dethware.org/space.txt", - "sha1": "dd122581c8cd44d0227f9c305581ffcb4b6f1b46", - "size": 1 - } - }, - "executable": false - } - } -} -)END"); - auto operations = UpdateOperations::resolve(from, to); - QVERIFY(operations.deletes.size() == 1); - QCOMPARE(operations.deletes[0], Path("a/b/c/d/file")); - QVERIFY(operations.rmdirs.size() == 0); - QVERIFY(operations.mkdirs.size() == 0); - QVERIFY(operations.downloads.size() == 1); - QVERIFY(operations.mklinks.size() == 0); - QVERIFY(operations.executable_fixes.size() == 0); -} - -void PackageManifestTest::added_file() { - auto from = Package::fromManifestContents(R"END( -{ - "files": { - "a/b/c/d": { - "type": "directory" - } - } -} -)END"); - auto to = Package::fromManifestContents(R"END( -{ - "files": { - "a/b/c/d/file": { - "type": "file", - "downloads": { - "raw": { - "url": "http://dethware.org/space.txt", - "sha1": "dd122581c8cd44d0227f9c305581ffcb4b6f1b46", - "size": 1 - } - }, - "executable": false - } - } -} -)END"); - auto operations = UpdateOperations::resolve(from, to); - QVERIFY(operations.deletes.size() == 0); - QVERIFY(operations.rmdirs.size() == 0); - QVERIFY(operations.mkdirs.size() == 0); - QVERIFY(operations.downloads.size() == 1); - QVERIFY(operations.mklinks.size() == 0); - QVERIFY(operations.executable_fixes.size() == 0); -} - -void PackageManifestTest::removed_file() { - auto from = Package::fromManifestContents(R"END( -{ - "files": { - "a/b/c/d/file": { - "type": "file", - "downloads": { - "raw": { - "url": "http://dethware.org/space.txt", - "sha1": "dd122581c8cd44d0227f9c305581ffcb4b6f1b46", - "size": 1 - } - }, - "executable": false - } - } -} -)END"); - auto to = Package::fromManifestContents(R"END( -{ - "files": { - "a/b/c/d": { - "type": "directory" - } - } -} -)END"); - auto operations = UpdateOperations::resolve(from, to); - QVERIFY(operations.deletes.size() == 1); - QCOMPARE(operations.deletes[0], Path("a/b/c/d/file")); - QVERIFY(operations.rmdirs.size() == 0); - QVERIFY(operations.mkdirs.size() == 0); - QVERIFY(operations.downloads.size() == 0); - QVERIFY(operations.mklinks.size() == 0); - QVERIFY(operations.executable_fixes.size() == 0); -} - -QTEST_GUILESS_MAIN(PackageManifestTest) - -#include "PackageManifest_test.moc" - diff --git a/tests/ResourceModel_test.cpp b/tests/ResourceModel_test.cpp index c0d9cd95d..30353d3f9 100644 --- a/tests/ResourceModel_test.cpp +++ b/tests/ResourceModel_test.cpp @@ -38,6 +38,7 @@ class DummyResourceModel : public ResourceModel { public: DummyResourceModel() : ResourceModel(new DummyResourceAPI) {} + ~DummyResourceModel() {} [[nodiscard]] auto metaEntryBase() const -> QString override { return ""; }; @@ -58,7 +59,10 @@ class DummyResourceModel : public ResourceModel { class ResourceModelTest : public QObject { Q_OBJECT private slots: - void test_abstract_item_model() { [[maybe_unused]] auto tester = new QAbstractItemModelTester(new DummyResourceModel); } + void test_abstract_item_model() { + auto dummy = DummyResourceModel(); + auto tester = QAbstractItemModelTester(&dummy); + } void test_search() { @@ -78,6 +82,8 @@ class ResourceModelTest : public QObject { QVERIFY(processed_pack->addonId.toString() == Json::requireString(processed_response, "project_id")); QVERIFY(processed_pack->description == Json::requireString(processed_response, "description")); QVERIFY(processed_pack->authors.first().name == Json::requireString(processed_response, "author")); + + delete model; } }; diff --git a/tests/Task_test.cpp b/tests/Task_test.cpp index dabe5da26..c59d4bb73 100644 --- a/tests/Task_test.cpp +++ b/tests/Task_test.cpp @@ -1,6 +1,6 @@ #include -#include #include +#include #include #include @@ -19,10 +19,7 @@ class BasicTask : public Task { BasicTask(bool show_debug_log = true) : Task(nullptr, show_debug_log) {} private: - void executeTask() override - { - emitSucceeded(); - }; + void executeTask() override { emitSucceeded(); }; }; /* Does nothing. Only used for testing. */ @@ -34,7 +31,7 @@ class BasicTask_MultiStep : public Task { private: auto isMultiStep() const -> bool override { return true; } - void executeTask() override {}; + void executeTask() override{}; }; class BigConcurrentTask : public ConcurrentTask { @@ -44,7 +41,7 @@ class BigConcurrentTask : public ConcurrentTask { { // This is here only to help fill the stack a bit more quickly (if there's an issue, of course :^)) // Each tasks thus adds 1024 * 4 bytes to the stack, at the very least. - [[maybe_unused]] volatile std::array some_data_on_the_stack {}; + [[maybe_unused]] volatile std::array some_data_on_the_stack{}; ConcurrentTask::startNext(); } @@ -53,49 +50,42 @@ class BigConcurrentTask : public ConcurrentTask { class BigConcurrentTaskThread : public QThread { Q_OBJECT - BigConcurrentTask big_task; - + QTimer m_deadline; void run() override { - QTimer deadline; - deadline.setInterval(10000); - connect(&deadline, &QTimer::timeout, this, [this]{ passed_the_deadline = true; }); - deadline.start(); + BigConcurrentTask big_task; + m_deadline.setInterval(10000); // NOTE: Arbitrary value that manages to trigger a problem when there is one. // Considering each tasks, in a problematic state, adds 1024 * 4 bytes to the stack, // this number is enough to fill up 16 MiB of stack, more than enough to cause a problem. static const unsigned s_num_tasks = 1 << 12; - auto sub_tasks = new BasicTask::Ptr[s_num_tasks]; - for (unsigned i = 0; i < s_num_tasks; i++) { auto sub_task = makeShared(false); - sub_tasks[i] = sub_task; big_task.addTask(sub_task); } + connect(&big_task, &Task::finished, this, &QThread::quit); + connect(&m_deadline, &QTimer::timeout, this, [&] { passed_the_deadline = true; quit(); }); + + m_deadline.start(); big_task.run(); - while (!big_task.isFinished() && !passed_the_deadline) - QCoreApplication::processEvents(); - - emit finished(); + exec(); } public: bool passed_the_deadline = false; - - signals: - void finished(); }; class TaskTest : public QObject { Q_OBJECT private slots: - void test_SetStatus_NoMultiStep(){ + void test_SetStatus_NoMultiStep() + { BasicTask t; - QString status {"test status"}; + QString status{ "test status" }; t.setStatus(status); @@ -103,9 +93,10 @@ class TaskTest : public QObject { QCOMPARE(t.getStepProgress().isEmpty(), TaskStepProgressList{}.isEmpty()); } - void test_SetStatus_MultiStep(){ + void test_SetStatus_MultiStep() + { BasicTask_MultiStep t; - QString status {"test status"}; + QString status{ "test status" }; t.setStatus(status); @@ -115,7 +106,8 @@ class TaskTest : public QObject { QCOMPARE(t.getStepProgress().isEmpty(), TaskStepProgressList{}.isEmpty()); } - void test_SetProgress(){ + void test_SetProgress() + { BasicTask t; int current = 42; int total = 207; @@ -126,17 +118,18 @@ class TaskTest : public QObject { QCOMPARE(t.getTotalProgress(), total); } - void test_basicRun(){ + void test_basicRun() + { BasicTask t; - QObject::connect(&t, &Task::finished, [&]{ QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); }); + QObject::connect(&t, &Task::finished, + [&] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); }); t.start(); - QVERIFY2(QTest::qWaitFor([&]() { - return t.isFinished(); - }, 1000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } - void test_basicConcurrentRun(){ + void test_basicConcurrentRun() + { auto t1 = makeShared(); auto t2 = makeShared(); auto t3 = makeShared(); @@ -147,21 +140,20 @@ class TaskTest : public QObject { t.addTask(t2); t.addTask(t3); - QObject::connect(&t, &Task::finished, [&]{ - QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); - QVERIFY(t1->wasSuccessful()); - QVERIFY(t2->wasSuccessful()); - QVERIFY(t3->wasSuccessful()); + QObject::connect(&t, &Task::finished, [&t, &t1, &t2, &t3] { + QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); + QVERIFY(t1->wasSuccessful()); + QVERIFY(t2->wasSuccessful()); + QVERIFY(t3->wasSuccessful()); }); t.start(); - QVERIFY2(QTest::qWaitFor([&]() { - return t.isFinished(); - }, 1000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } // Tests if starting new tasks after the 6 initial ones is working - void test_moreConcurrentRun(){ + void test_moreConcurrentRun() + { auto t1 = makeShared(); auto t2 = makeShared(); auto t3 = makeShared(); @@ -184,26 +176,25 @@ class TaskTest : public QObject { t.addTask(t8); t.addTask(t9); - QObject::connect(&t, &Task::finished, [&]{ - QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); - QVERIFY(t1->wasSuccessful()); - QVERIFY(t2->wasSuccessful()); - QVERIFY(t3->wasSuccessful()); - QVERIFY(t4->wasSuccessful()); - QVERIFY(t5->wasSuccessful()); - QVERIFY(t6->wasSuccessful()); - QVERIFY(t7->wasSuccessful()); - QVERIFY(t8->wasSuccessful()); - QVERIFY(t9->wasSuccessful()); + QObject::connect(&t, &Task::finished, [&t, &t1, &t2, &t3, &t4, &t5, &t6, &t7, &t8, &t9] { + QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); + QVERIFY(t1->wasSuccessful()); + QVERIFY(t2->wasSuccessful()); + QVERIFY(t3->wasSuccessful()); + QVERIFY(t4->wasSuccessful()); + QVERIFY(t5->wasSuccessful()); + QVERIFY(t6->wasSuccessful()); + QVERIFY(t7->wasSuccessful()); + QVERIFY(t8->wasSuccessful()); + QVERIFY(t9->wasSuccessful()); }); t.start(); - QVERIFY2(QTest::qWaitFor([&]() { - return t.isFinished(); - }, 1000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } - void test_basicSequentialRun(){ + void test_basicSequentialRun() + { auto t1 = makeShared(); auto t2 = makeShared(); auto t3 = makeShared(); @@ -214,20 +205,19 @@ class TaskTest : public QObject { t.addTask(t2); t.addTask(t3); - QObject::connect(&t, &Task::finished, [&]{ - QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); - QVERIFY(t1->wasSuccessful()); - QVERIFY(t2->wasSuccessful()); - QVERIFY(t3->wasSuccessful()); + QObject::connect(&t, &Task::finished, [&t, &t1, &t2, &t3] { + QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); + QVERIFY(t1->wasSuccessful()); + QVERIFY(t2->wasSuccessful()); + QVERIFY(t3->wasSuccessful()); }); t.start(); - QVERIFY2(QTest::qWaitFor([&]() { - return t.isFinished(); - }, 1000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } - void test_basicMultipleOptionsRun(){ + void test_basicMultipleOptionsRun() + { auto t1 = makeShared(); auto t2 = makeShared(); auto t3 = makeShared(); @@ -238,33 +228,30 @@ class TaskTest : public QObject { t.addTask(t2); t.addTask(t3); - QObject::connect(&t, &Task::finished, [&]{ - QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); - QVERIFY(t1->wasSuccessful()); - QVERIFY(!t2->wasSuccessful()); - QVERIFY(!t3->wasSuccessful()); + QObject::connect(&t, &Task::finished, [&t, &t1, &t2, &t3] { + QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); + QVERIFY(t1->wasSuccessful()); + QVERIFY(!t2->wasSuccessful()); + QVERIFY(!t3->wasSuccessful()); }); t.start(); - QVERIFY2(QTest::qWaitFor([&]() { - return t.isFinished(); - }, 1000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } void test_stackOverflowInConcurrentTask() { QEventLoop loop; - auto thread = new BigConcurrentTaskThread; + BigConcurrentTaskThread thread; - connect(thread, &BigConcurrentTaskThread::finished, &loop, &QEventLoop::quit); + connect(&thread, &BigConcurrentTaskThread::finished, &loop, &QEventLoop::quit); - thread->start(); + thread.start(); loop.exec(); - QVERIFY(!thread->passed_the_deadline); - thread->deleteLater(); + QVERIFY(!thread.passed_the_deadline); } }; diff --git a/tests/Version_test.cpp b/tests/Version_test.cpp index afb4c6102..f5488cbce 100644 --- a/tests/Version_test.cpp +++ b/tests/Version_test.cpp @@ -20,6 +20,8 @@ class VersionTest : public QObject { Q_OBJECT + QStringList m_flex_test_names = {}; + void addDataColumns() { QTest::addColumn("first"); @@ -101,8 +103,9 @@ class VersionTest : public QObject { QString first{split_line.first().simplified()}; QString second{split_line.last().simplified()}; - auto new_test_name = test_name_template.arg(QString::number(test_number), "lessThan").toLatin1().data(); - QTest::newRow(new_test_name) << first << second << true << false; + auto new_test_name = test_name_template.arg(QString::number(test_number), "lessThan"); + m_flex_test_names.append(new_test_name); + QTest::newRow(m_flex_test_names.last().toLatin1().data()) << first << second << true << false; continue; } @@ -112,8 +115,9 @@ class VersionTest : public QObject { QString first{split_line.first().simplified()}; QString second{split_line.last().simplified()}; - auto new_test_name = test_name_template.arg(QString::number(test_number), "equals").toLatin1().data(); - QTest::newRow(new_test_name) << first << second << false << true; + auto new_test_name = test_name_template.arg(QString::number(test_number), "equals"); + m_flex_test_names.append(new_test_name); + QTest::newRow(m_flex_test_names.last().toLatin1().data()) << first << second << false << true; continue; } @@ -123,8 +127,9 @@ class VersionTest : public QObject { QString first{split_line.first().simplified()}; QString second{split_line.last().simplified()}; - auto new_test_name = test_name_template.arg(QString::number(test_number), "greaterThan").toLatin1().data(); - QTest::newRow(new_test_name) << first << second << false << false; + auto new_test_name = test_name_template.arg(QString::number(test_number), "greaterThan"); + m_flex_test_names.append(new_test_name); + QTest::newRow(m_flex_test_names.last().toLatin1().data()) << first << second << false << false; continue; }