diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 2163db45b..528b128b1 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -2,3 +2,6 @@ # tabs -> spaces bbb3b3e6f6e3c0f95873f22e6d0a4aaf350f49d9 + +# (nix) alejandra -> nixfmt +4c81d8c53d09196426568c4a31a4e752ed05397a diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1b230ddc6..b6abf0ef4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,6 +39,9 @@ on: APPLE_NOTARIZE_PASSWORD: description: Password used for notarizing macOS builds required: false + CACHIX_AUTH_TOKEN: + description: Private token for authenticating against Cachix cache + required: false GPG_PRIVATE_KEY: description: Private key for AppImage signing required: false @@ -56,14 +59,14 @@ jobs: qt_ver: 5 qt_host: linux qt_arch: "" - qt_version: "5.12.8" + qt_version: "5.15.2" qt_modules: "qtnetworkauth" - os: ubuntu-20.04 qt_ver: 6 qt_host: linux qt_arch: "" - qt_version: "6.2.4" + qt_version: "6.5.3" qt_modules: "qt5compat qtimageformats qtnetworkauth" - os: windows-2022 @@ -79,8 +82,10 @@ jobs: qt_ver: 6 qt_host: windows qt_arch: "" - qt_version: "6.7.2" + qt_version: "6.7.3" qt_modules: "qt5compat qtimageformats qtnetworkauth" + nscurl_tag: "v24.9.26.122" + nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0" - os: windows-2022 name: "Windows-MSVC-arm64" @@ -90,8 +95,10 @@ jobs: qt_ver: 6 qt_host: windows qt_arch: "win64_msvc2019_arm64" - qt_version: "6.7.2" + qt_version: "6.7.3" qt_modules: "qt5compat qtimageformats qtnetworkauth" + nscurl_tag: "v24.9.26.122" + nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0" - os: macos-14 name: macOS @@ -99,7 +106,7 @@ jobs: qt_ver: 6 qt_host: mac qt_arch: "" - qt_version: "6.7.2" + qt_version: "6.7.3" qt_modules: "qt5compat qtimageformats qtnetworkauth" - os: macos-14 @@ -166,7 +173,7 @@ jobs: - name: Retrieve ccache cache (Windows MinGW-w64) if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.2.0 with: path: '${{ github.workspace }}\.ccache' key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }} @@ -199,7 +206,7 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get -y update - sudo apt-get -y install ninja-build extra-cmake-modules scdoc appstream + sudo apt-get -y install ninja-build extra-cmake-modules scdoc appstream libxcb-cursor-dev - name: Install Dependencies (macOS) if: runner.os == 'macOS' @@ -272,23 +279,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 -DENABLE_JAVA_DOWNLOADER=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 + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=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 -DENABLE_JAVA_DOWNLOADER=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="" -DCMAKE_OSX_ARCHITECTURES="x86_64" -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=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="" -DCMAKE_OSX_ARCHITECTURES="x86_64" -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 -DENABLE_JAVA_DOWNLOADER=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 -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=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 -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} -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 -DENABLE_JAVA_DOWNLOADER=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 -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=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 -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} # https://github.com/ccache/ccache/wiki/MS-Visual-Studio (I coudn't figure out the compiler prefix) if ("${{ env.CCACHE_VAR }}") { @@ -303,7 +310,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 -DENABLE_JAVA_DOWNLOADER=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 }} -DLauncher_BUILD_ARTIFACT=Linux-Qt${{ 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_ENABLE_JAVA_DOWNLOADER=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 }} -DLauncher_BUILD_ARTIFACT=Linux-Qt${{ matrix.qt_ver }} -G Ninja ## # BUILD @@ -403,9 +410,8 @@ jobs: if: matrix.name == 'macOS' run: | if [ '${{ secrets.SPARKLE_ED25519_KEY }}' != '' ]; then - brew install openssl@3 echo '${{ secrets.SPARKLE_ED25519_KEY }}' > ed25519-priv.pem - signature=$(/usr/local/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.zip -inkey ed25519-priv.pem | openssl base64 | tr -d \\n) + signature=$(/opt/homebrew/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.zip -inkey ed25519-priv.pem | openssl base64 | tr -d \\n) rm ed25519-priv.pem cat >> $GITHUB_STEP_SUMMARY << EOF ### Artifact Information :information_source: @@ -471,6 +477,16 @@ jobs: - name: Package (Windows, installer) if: runner.os == 'Windows' run: | + if ('${{ matrix.nscurl_tag }}') { + New-Item -Name NSISPlugins -ItemType Directory + Invoke-Webrequest https://github.com/negrutiu/nsis-nscurl/releases/download/${{ matrix.nscurl_tag }}/NScurl.zip -OutFile NSISPlugins\NScurl.zip + $nscurl_hash = Get-FileHash NSISPlugins\NScurl.zip -Algorithm Sha256 | Select-Object -ExpandProperty Hash + if ( $nscurl_hash -ne "${{ matrix.nscurl_sha256 }}") { + echo "::error:: NSCurl.zip sha256 mismatch" + exit 1 + } + Expand-Archive -Path NSISPlugins\NScurl.zip -DestinationPath NSISPlugins\NScurl + } cd ${{ env.INSTALL_DIR }} makensis -NOCD "${{ github.workspace }}/${{ env.BUILD_DIR }}/program_info/win_install.nsi" @@ -617,17 +633,72 @@ jobs: flatpak: runs-on: ubuntu-latest container: - image: bilelmoussaoui/flatpak-github-actions:kde-6.7 + image: ghcr.io/flathub-infra/flatpak-github-actions:kde-6.8 options: --privileged steps: - name: Checkout uses: actions/checkout@v4 if: inputs.build_type == 'Debug' with: - submodules: "true" + submodules: true + + - name: Set short version + shell: bash + run: echo "VERSION=${GITHUB_SHA::7}" >> $GITHUB_ENV + - name: Build Flatpak (Linux) if: inputs.build_type == 'Debug' uses: flatpak/flatpak-github-actions/flatpak-builder@v6 with: - bundle: "Freesm Launcher.flatpak" + bundle: FreesmLauncher-${{ runner.os }}-${{ env.VERSION }}-Flatpak.flatpak manifest-path: flatpak/org.freesmTeam.freesmlauncher.yml + + nix: + name: Nix (${{ matrix.system }}) + + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-22.04 + system: x86_64-linux + + - os: macos-13 + system: x86_64-darwin + + - os: macos-14 + system: aarch64-darwin + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v30 + + # For PRs + - name: Setup Nix Magic Cache + uses: DeterminateSystems/magic-nix-cache-action@v8 + + # For in-tree builds + - name: Setup Cachix + uses: cachix/cachix-action@v15 + with: + name: prismlauncher + authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} + + - name: Run flake checks + run: | + nix flake check --print-build-logs --show-trace + + - name: Build debug package + if: ${{ inputs.build_type == 'Debug' }} + run: | + nix build --print-build-logs .#prismlauncher-debug + + - name: Build release package + if: ${{ inputs.build_type != 'Debug' }} + run: | + nix build --print-build-logs .#prismlauncher diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml index 750055fb6..d960fdea8 100644 --- a/.github/workflows/trigger_builds.yml +++ b/.github/workflows/trigger_builds.yml @@ -40,5 +40,6 @@ jobs: APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }} APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }} APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }} + CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }} diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index b987ab7af..392ad59e7 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -23,6 +23,7 @@ jobs: APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }} APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }} APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }} + CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }} diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index da0ca0e6f..a1ad72325 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -17,9 +17,9 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27 + - uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30 - - uses: DeterminateSystems/update-flake-lock@v23 + - uses: DeterminateSystems/update-flake-lock@v24 with: commit-msg: "chore(nix): update lockfile" pr-title: "chore(nix): update lockfile" diff --git a/CMakeLists.txt b/CMakeLists.txt index e501e5a1f..9c7297049 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,7 +99,7 @@ if ((CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebI 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 + # 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 /Oy-") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /Oy-") @@ -176,10 +176,12 @@ endif() set(Launcher_NEWS_RSS_URL "https://prismlauncher.org/feed/feed.xml" CACHE STRING "URL to fetch Prism Launcher's news RSS feed from.") set(Launcher_NEWS_OPEN_URL "https://prismlauncher.org/news" CACHE STRING "URL that gets opened when the user clicks 'More News'") 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(Launcher_LOGIN_CALLBACK_URL "https://prismlauncher.org/successful-login" CACHE STRING "URL that gets opened when the user successfully logins.") +set(Launcher_FMLLIBS_BASE_URL "https://files.prismlauncher.org/fmllibs/" CACHE STRING "URL for FML Libraries.") ######## Set version numbers ######## set(Launcher_VERSION_MAJOR 9) -set(Launcher_VERSION_MINOR 0) +set(Launcher_VERSION_MINOR 2) set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}") set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.0.0") @@ -204,33 +206,34 @@ set(Launcher_IMGUR_CLIENT_ID "5b97b0713fba4a3" CACHE STRING "Client ID you can g set(Launcher_BUG_TRACKER_URL "https://github.com/PrismLauncher/PrismLauncher/issues" CACHE STRING "URL for the bug tracker.") # Translations Platform URL -set(Launcher_TRANSLATIONS_URL "https://hosted.weblate.org/projects/launcher/launcher/" CACHE STRING "URL for the translations platform.") +set(Launcher_TRANSLATIONS_URL "https://hosted.weblate.org/projects/prismlauncher/launcher/" CACHE STRING "URL for the translations platform.") +set(Launcher_TRANSLATION_FILES_URL "https://i18n.prismlauncher.org/" CACHE STRING "URL for the translations files.") # Matrix Space -set(Launcher_MATRIX_URL "https://launcher.org/matrix" CACHE STRING "URL to the Matrix Space") +set(Launcher_MATRIX_URL "https://prismlauncher.org/matrix" CACHE STRING "URL to the Matrix Space") # Discord URL -set(Launcher_DISCORD_URL "https://launcher.org/discord" CACHE STRING "URL for the Discord guild.") +set(Launcher_DISCORD_URL "https://prismlauncher.org/discord" CACHE STRING "URL for the Discord guild.") # Subreddit URL -set(Launcher_SUBREDDIT_URL "https://launcher.org/reddit" CACHE STRING "URL for the subreddit.") +set(Launcher_SUBREDDIT_URL "https://prismlauncher.org/reddit" CACHE STRING "URL for the subreddit.") # Builds set(Launcher_FORCE_BUNDLED_LIBS OFF CACHE BOOL "Prevent using system libraries, if they are available as submodules") set(Launcher_QT_VERSION_MAJOR "6" CACHE STRING "Major Qt version to build against") # Java downloader -set(ENABLE_JAVA_DOWNLOADER_DEFAULT ON) +set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT ON) -# Although we recommend enabling this, we cannot guarantee binary compatibility on +# Although we recommend enabling this, we cannot guarantee binary compatibility on # differing Linux/BSD/etc distributions. Downstream packagers should be explicitly opt-ing into this # feature if they know it will work with their distribution. if(UNIX AND NOT APPLE) - set(ENABLE_JAVA_DOWNLOADER_DEFAULT OFF) + set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT OFF) endif() # Java downloader -option(ENABLE_JAVA_DOWNLOADER "Build the java downloader feature" ${ENABLE_JAVA_DOWNLOADER_DEFAULT}) +option(Launcher_ENABLE_JAVA_DOWNLOADER "Build the java downloader feature" ${Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT}) # Native libraries if(UNIX AND APPLE) @@ -258,7 +261,7 @@ set(Launcher_MSA_CLIENT_ID "c36a9fb6-4f2a-41ff-90bd-ae7cc92031eb" CACHE STRING " # By using this key in your builds you accept the terms and conditions laid down in # https://support.curseforge.com/en/support/solutions/articles/9000207405-curse-forge-3rd-party-api-terms-and-conditions # NOTE: CurseForge requires you to change this if you make any kind of derivative work. -# This key was issued specifically for Launcher +# This key was issued specifically for Prism Launcher set(Launcher_CURSEFORGE_API_KEY "$2a$10$wuAJuNZuted3NORVmpgUC.m8sI.pv1tOPKZyBgLFGjxFp/br0lZCC" CACHE STRING "API key for the CurseForge platform") set(Launcher_COMPILER_NAME ${CMAKE_CXX_COMPILER_ID}) @@ -394,8 +397,8 @@ if(UNIX AND APPLE) set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "v55ZWWD6QlPoXGV6VLzOTZxZUggWeE51X8cRQyQh6vA=" CACHE STRING "Public key for Sparkle update feed") set(MACOSX_SPARKLE_UPDATE_FEED_URL "https://prismlauncher.org/feed/appcast.xml" CACHE STRING "URL for Sparkle update feed") - set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.5.2/Sparkle-2.5.2.tar.xz" CACHE STRING "URL to Sparkle release archive") - set(MACOSX_SPARKLE_SHA256 "572dd67ae398a466f19f343a449e1890bac1ef74885b4739f68f979a8a89884b" CACHE STRING "SHA256 checksum for Sparkle release archive") + set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.6.4/Sparkle-2.6.4.tar.xz" CACHE STRING "URL to Sparkle release archive") + set(MACOSX_SPARKLE_SHA256 "50612a06038abc931f16011d7903b8326a362c1074dabccb718404ce8e585f0b" CACHE STRING "SHA256 checksum for Sparkle release archive") set(MACOSX_SPARKLE_DIR "${CMAKE_BINARY_DIR}/frameworks/Sparkle") # directories to look for dependencies @@ -435,10 +438,10 @@ elseif(UNIX) set(PLUGIN_DEST_DIR "plugins") set(BUNDLE_DEST_DIR ".") set(RESOURCES_DEST_DIR ".") - + # Apps to bundle set(APPS "\${CMAKE_INSTALL_PREFIX}/bin/${Launcher_APP_BINARY_NAME}") - + # directories to look for dependencies set(DIRS ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) endif() @@ -492,7 +495,7 @@ if(FORCE_BUNDLED_ZLIB) set(SKIP_INSTALL_ALL ON) add_subdirectory(libraries/zlib EXCLUDE_FROM_ALL) - # On OS where unistd.h exists, zlib's generated header defines `Z_HAVE_UNISTD_H`, while the included header does not. + # On OS where unistd.h exists, zlib's generated header defines `Z_HAVE_UNISTD_H`, while the included header does not. # We cannot safely undo the rename on those systems, and they generally have packages for zlib anyway. check_include_file(unistd.h NEED_GENERATED_ZCONF) if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h.included" AND NOT NEED_GENERATED_ZCONF) @@ -529,10 +532,12 @@ else() endif() if(NOT cmark_FOUND) message(STATUS "Using bundled cmark") + set(ORIGINAL_BUILD_TESTING ${BUILD_TESTING}) set(BUILD_TESTING 0) - set(BUILD_SHARED_LIBS 0) + set(BUILD_SHARED_LIBS 0) add_subdirectory(libraries/cmark EXCLUDE_FROM_ALL) # Markdown parser add_library(cmark::cmark ALIAS cmark) + set(BUILD_TESTING ${ORIGINAL_BUILD_TESTING}) else() message(STATUS "Using system cmark") endif() diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index a2b5c2187..b48232b43 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -81,8 +81,8 @@ Config::Config() UPDATER_ENABLED = true; } - #cmakedefine01 ENABLE_JAVA_DOWNLOADER - JAVA_DOWNLOADER_ENABLED = ENABLE_JAVA_DOWNLOADER; + #cmakedefine01 Launcher_ENABLE_JAVA_DOWNLOADER + JAVA_DOWNLOADER_ENABLED = Launcher_ENABLE_JAVA_DOWNLOADER; GIT_COMMIT = "@Launcher_GIT_COMMIT@"; GIT_TAG = "@Launcher_GIT_TAG@"; @@ -116,16 +116,19 @@ Config::Config() NEWS_RSS_URL = "@Launcher_NEWS_RSS_URL@"; NEWS_OPEN_URL = "@Launcher_NEWS_OPEN_URL@"; HELP_URL = "@Launcher_HELP_URL@"; + LOGIN_CALLBACK_URL = "@Launcher_LOGIN_CALLBACK_URL@"; IMGUR_CLIENT_ID = "@Launcher_IMGUR_CLIENT_ID@"; MSA_CLIENT_ID = "@Launcher_MSA_CLIENT_ID@"; FLAME_API_KEY = "@Launcher_CURSEFORGE_API_KEY@"; META_URL = "@Launcher_META_URL@"; + FMLLIBS_BASE_URL = "@Launcher_FMLLIBS_BASE_URL@"; GLFW_LIBRARY_NAME = "@Launcher_GLFW_LIBRARY_NAME@"; OPENAL_LIBRARY_NAME = "@Launcher_OPENAL_LIBRARY_NAME@"; BUG_TRACKER_URL = "@Launcher_BUG_TRACKER_URL@"; TRANSLATIONS_URL = "@Launcher_TRANSLATIONS_URL@"; + TRANSLATION_FILES_URL = "@Launcher_TRANSLATION_FILES_URL@"; MATRIX_URL = "@Launcher_MATRIX_URL@"; DISCORD_URL = "@Launcher_DISCORD_URL@"; SUBREDDIT_URL = "@Launcher_SUBREDDIT_URL@"; diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index bb633f297..ae705d098 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -133,6 +133,11 @@ class Config { */ QString HELP_URL; + /** + * URL that gets opened when the user succesfully logins. + */ + QString LOGIN_CALLBACK_URL; + /** * Client ID you can get from Imgur when you register an application */ @@ -165,8 +170,8 @@ class Config { QString RESOURCE_BASE = "https://resources.download.minecraft.net/"; QString LIBRARY_BASE = "https://libraries.minecraft.net/"; QString IMGUR_BASE_URL = "https://api.imgur.com/3/"; - QString FMLLIBS_BASE_URL = "https://files.prismlauncher.org/fmllibs/"; // FIXME: move into CMakeLists - QString TRANSLATIONS_BASE_URL = "https://i18n.prismlauncher.org/"; // FIXME: move into CMakeLists + QString FMLLIBS_BASE_URL; + QString TRANSLATION_FILES_URL; QString MODPACKSCH_API_BASE_URL = "https://api.modpacks.ch/"; diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in index f207faf77..f4d044042 100644 --- a/cmake/MacOSXBundleInfo.plist.in +++ b/cmake/MacOSXBundleInfo.plist.in @@ -8,6 +8,8 @@ A Minecraft mod wants to access your microphone. NSDownloadsFolderUsageDescription Freesm uses access to your Downloads folder to help you more quickly add mods that can't be automatically downloaded to your instance. You can change where Freesm scans for downloaded mods in Settings or the prompt that appears. + NSLocalNetworkUsageDescription + Minecraft uses the local network to find and connect to LAN servers. NSPrincipalClass NSApplication NSHighResolutionCapable diff --git a/flake.lock b/flake.lock index b64c6b472..f145022db 100644 --- a/flake.lock +++ b/flake.lock @@ -49,11 +49,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1721379653, - "narHash": "sha256-8MUgifkJ7lkZs3u99UDZMB4kbOxvMEXQZ31FO3SopZ0=", + "lastModified": 1729256560, + "narHash": "sha256-/uilDXvCIEs3C9l73JTACm4quuHUsIHcns1c+cHUJwA=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "1d9c2c9b3e71b9ee663d11c5d298727dace8d374", + "rev": "4c2fcb090b1f3e5b47eaa7bd33913b574a11e0a0", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index dedfabd30..dc2d95d09 100644 --- a/flake.nix +++ b/flake.nix @@ -2,8 +2,10 @@ description = "A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once (Fork of MultiMC)"; nixConfig = { - extra-substituters = [ "https://cache.garnix.io" ]; - extra-trusted-public-keys = [ "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" ]; + extra-substituters = [ "https://prismlauncher.cachix.org" ]; + extra-trusted-public-keys = [ + "prismlauncher.cachix.org-1:9/n/FGyABA2jLUVfY+DEp4hKds/rwO+SCOtbOkDzd+c=" + ]; }; inputs = { @@ -83,24 +85,18 @@ formatter = forAllSystems (system: nixpkgsFor.${system}.nixfmt-rfc-style); - overlays.default = - final: prev: - let - version = builtins.substring 0 8 self.lastModifiedDate or "dirty"; - in - { - prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix { - inherit - libnbtplusplus - nix-filter - self - version - ; - }; - - prismlauncher = final.callPackage ./nix/wrapper.nix { }; + overlays.default = final: prev: { + prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix { + inherit + libnbtplusplus + nix-filter + self + ; }; + prismlauncher = final.callPackage ./nix/wrapper.nix { }; + }; + packages = forAllSystems ( system: let @@ -118,5 +114,24 @@ # Only output them if they're available on the current system lib.filterAttrs (_: lib.meta.availableOn pkgs.stdenv.hostPlatform) packages ); + + # We put these under legacyPackages as they are meant for CI, not end user consumption + legacyPackages = forAllSystems ( + system: + let + prismPackages = self.packages.${system}; + legacyPackages = self.legacyPackages.${system}; + in + { + prismlauncher-debug = prismPackages.prismlauncher.override { + prismlauncher-unwrapped = legacyPackages.prismlauncher-unwrapped-debug; + }; + + prismlauncher-unwrapped-debug = prismPackages.prismlauncher-unwrapped.overrideAttrs { + cmakeBuildType = "Debug"; + dontStrip = true; + }; + } + ); }; } diff --git a/flatpak/flite.json b/flatpak/flite.json new file mode 100644 index 000000000..1bf280af1 --- /dev/null +++ b/flatpak/flite.json @@ -0,0 +1,20 @@ +{ + "name": "flite", + "config-opts": [ + "--enable-shared", + "--with-audio=pulseaudio" + ], + "no-parallel-make": true, + "sources": [ + { + "type": "git", + "url": "https://github.com/festvox/flite.git", + "tag": "v2.2", + "commit": "e9e2e37c329dbe98bfeb27a1828ef9a71fa84f88", + "x-checker-data": { + "type": "git", + "tag-pattern": "^v([\\d.]+)$" + } + } + ] +} diff --git a/flatpak/libdecor.json b/flatpak/libdecor.json index 589310a35..1652a2f04 100644 --- a/flatpak/libdecor.json +++ b/flatpak/libdecor.json @@ -1,22 +1,18 @@ { - "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" - ] + "name": "libdecor", + "buildsystem": "meson", + "config-opts": [ + "-Ddemo=false" + ], + "sources": [ + { + "type": "git", + "url": "https://gitlab.freedesktop.org/libdecor/libdecor.git", + "commit": "c2bd8ad6fa42c0cb17553ce77ad8a87d1f543b1f" + } + ], + "cleanup": [ + "/include", + "/lib/pkgconfig" + ] } diff --git a/flatpak/org.freesmTeam.freesmlauncher.yml b/flatpak/org.freesmTeam.freesmlauncher.yml index 622fee63d..b3a3fae42 100644 --- a/flatpak/org.freesmTeam.freesmlauncher.yml +++ b/flatpak/org.freesmTeam.freesmlauncher.yml @@ -1,11 +1,9 @@ id: org.freesmTeam.freesmlauncher runtime: org.kde.Platform -runtime-version: 6.7 +runtime-version: '6.8' sdk: org.kde.Sdk sdk-extensions: - - org.freedesktop.Sdk.Extension.openjdk21 - org.freedesktop.Sdk.Extension.openjdk17 - - org.freedesktop.Sdk.Extension.openjdk8 command: freesmlauncher finish-args: @@ -21,9 +19,12 @@ finish-args: - --filesystem=xdg-download:ro # FTBApp import - --filesystem=~/.ftba:ro - -cleanup: - - /lib/libGLU* + # Userspace visibility for manual hugepages configuration + # Required for -XX:+UseLargePages + - --filesystem=/sys/kernel/mm/hugepages:ro + # Userspace visibility for transparent hugepages configuration + # Required for -XX:+UseTransparentHugePages + - --filesystem=/sys/kernel/mm/transparent_hugepage:ro modules: # Might be needed by some Controller mods (see https://github.com/isXander/Controlify/issues/31) @@ -32,50 +33,39 @@ modules: # Needed for proper Wayland support - libdecor.json + # Text to Speech in the game + - flite.json + - name: freesmlauncher buildsystem: cmake-ninja builddir: true config-opts: - -DLauncher_BUILD_PLATFORM=flatpak + # This allows us to manage and update Java independently of this Flatpak + - -DLauncher_ENABLE_JAVA_DOWNLOADER=ON - -DCMAKE_BUILD_TYPE=RelWithDebInfo build-options: env: JAVA_HOME: /usr/lib/sdk/openjdk17/jvm/openjdk-17 JAVA_COMPILER: /usr/lib/sdk/openjdk17/jvm/openjdk-17/bin/javac + run-tests: true sources: - type: dir path: ../ - - name: openjdk - buildsystem: simple - build-commands: - - mkdir -p /app/jdk/ - - /usr/lib/sdk/openjdk21/install.sh - - mv /app/jre /app/jdk/21 - - /usr/lib/sdk/openjdk17/install.sh - - mv /app/jre /app/jdk/17 - - /usr/lib/sdk/openjdk8/install.sh - - mv /app/jre /app/jdk/8 - cleanup: - - /jre - - name: glfw buildsystem: cmake-ninja config-opts: - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DBUILD_SHARED_LIBS:BOOL=ON - - -DGLFW_USE_WAYLAND:BOOL=ON + - -DGLFW_BUILD_WAYLAND:BOOL=ON - -DGLFW_BUILD_DOCS:BOOL=OFF 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 + commit: 7b6aead9fb88b3623e3b3725ebb42670cbe4c579 # 3.4 - type: patch - path: patches/0007-Platform-Prefer-Wayland-over-X11.patch + path: patches/0009-Defer-setting-cursor-position-until-the-cursor-is-lo.patch cleanup: - /include - /lib/cmake @@ -85,8 +75,8 @@ modules: buildsystem: autotools sources: - type: archive - url: https://xorg.freedesktop.org/archive/individual/app/xrandr-1.5.2.tar.xz - sha256: c8bee4790d9058bacc4b6246456c58021db58a87ddda1a9d0139bf5f18f1f240 + url: https://xorg.freedesktop.org/archive/individual/app/xrandr-1.5.3.tar.xz + sha256: f8dd7566adb74147fab9964680b6bbadee87cf406a7fcff51718a5e6949b841c x-checker-data: type: anitya project-id: 14957 @@ -108,8 +98,8 @@ modules: sources: - type: archive dest-filename: gamemode.tar.gz - url: https://api.github.com/repos/FeralInteractive/gamemode/tarball/1.8.1 - sha256: 969cf85b5ca3944f3e315cd73a0ee9bea4f9c968cd7d485e9f4745bc1e679c4e + url: https://api.github.com/repos/FeralInteractive/gamemode/tarball/1.8.2 + sha256: 2886d4ce543c78bd2a364316d5e7fd59ef06b71de63f896b37c6d3dc97658f60 x-checker-data: type: json url: https://api.github.com/repos/FeralInteractive/gamemode/releases/latest 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 deleted file mode 100644 index 9130e856c..000000000 --- a/flatpak/patches/0003-Don-t-crash-on-calls-to-focus-or-icon.patch +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index b031d739f..000000000 --- a/flatpak/patches/0005-Add-warning-about-being-an-unofficial-patch.patch +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 4eeb81309..000000000 --- a/flatpak/patches/0007-Platform-Prefer-Wayland-over-X11.patch +++ /dev/null @@ -1,20 +0,0 @@ -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/0009-Defer-setting-cursor-position-until-the-cursor-is-lo.patch b/flatpak/patches/0009-Defer-setting-cursor-position-until-the-cursor-is-lo.patch new file mode 100644 index 000000000..70cec9981 --- /dev/null +++ b/flatpak/patches/0009-Defer-setting-cursor-position-until-the-cursor-is-lo.patch @@ -0,0 +1,59 @@ +From 9997ae55a47de469ea26f8437c30b51483abda5f Mon Sep 17 00:00:00 2001 +From: Dan Klishch +Date: Sat, 30 Sep 2023 23:38:05 -0400 +Subject: Defer setting cursor position until the cursor is locked + +--- + src/wl_platform.h | 3 +++ + src/wl_window.c | 14 ++++++++++++-- + 2 files changed, 15 insertions(+), 2 deletions(-) + +diff --git a/src/wl_platform.h b/src/wl_platform.h +index ca34f66e..cd1f227f 100644 +--- a/src/wl_platform.h ++++ b/src/wl_platform.h +@@ -403,6 +403,9 @@ typedef struct _GLFWwindowWayland + int scaleSize; + int compositorPreferredScale; + ++ double askedCursorPosX, askedCursorPosY; ++ GLFWbool didAskForSetCursorPos; ++ + struct zwp_relative_pointer_v1* relativePointer; + struct zwp_locked_pointer_v1* lockedPointer; + struct zwp_confined_pointer_v1* confinedPointer; +diff --git a/src/wl_window.c b/src/wl_window.c +index 1de26558..0df16747 100644 +--- a/src/wl_window.c ++++ b/src/wl_window.c +@@ -2586,8 +2586,9 @@ void _glfwGetCursorPosWayland(_GLFWwindow* window, double* xpos, double* ypos) + + void _glfwSetCursorPosWayland(_GLFWwindow* window, double x, double y) + { +- _glfwInputError(GLFW_FEATURE_UNAVAILABLE, +- "Wayland: The platform does not support setting the cursor position"); ++ window->wl.didAskForSetCursorPos = true; ++ window->wl.askedCursorPosX = x; ++ window->wl.askedCursorPosY = y; + } + + void _glfwSetCursorModeWayland(_GLFWwindow* window, int mode) +@@ -2819,6 +2820,15 @@ static const struct zwp_relative_pointer_v1_listener relativePointerListener = + static void lockedPointerHandleLocked(void* userData, + struct zwp_locked_pointer_v1* lockedPointer) + { ++ _GLFWwindow* window = userData; ++ ++ if (window->wl.didAskForSetCursorPos) ++ { ++ window->wl.didAskForSetCursorPos = false; ++ zwp_locked_pointer_v1_set_cursor_position_hint(window->wl.lockedPointer, ++ wl_fixed_from_double(window->wl.askedCursorPosX), ++ wl_fixed_from_double(window->wl.askedCursorPosY)); ++ } + } + + static void lockedPointerHandleUnlocked(void* userData, +-- +2.42.0 + diff --git a/flatpak/patches/weird_libdecor.patch b/flatpak/patches/weird_libdecor.patch deleted file mode 100644 index 3a400b820..000000000 --- a/flatpak/patches/weird_libdecor.patch +++ /dev/null @@ -1,40 +0,0 @@ -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/garnix.yaml b/garnix.yaml deleted file mode 100644 index a7c1b48a9..000000000 --- a/garnix.yaml +++ /dev/null @@ -1,10 +0,0 @@ -builds: - exclude: - # Currently broken on Garnix's end - - "*.x86_64-darwin.*" - include: - - "checks.x86_64-linux.*" - - "packages.x86_64-linux.*" - - "packages.aarch64-linux.*" - - "packages.x86_64-darwin.*" - - "packages.aarch64-darwin.*" diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 3cc368745..b78f801f4 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -48,6 +48,7 @@ #include "net/PasteUpload.h" #include "pathmatcher/MultiMatcher.h" #include "pathmatcher/SimplePrefixMatcher.h" +#include "tasks/Task.h" #include "tools/GenericProfiler.h" #include "ui/InstanceWindow.h" #include "ui/MainWindow.h" @@ -67,8 +68,10 @@ #include "ui/pages/global/MinecraftPage.h" #include "ui/pages/global/ProxyPage.h" +#include "ui/setupwizard/AutoJavaWizardPage.h" #include "ui/setupwizard/JavaWizardPage.h" #include "ui/setupwizard/LanguageWizardPage.h" +#include "ui/setupwizard/LoginWizardPage.h" #include "ui/setupwizard/PasteWizardPage.h" #include "ui/setupwizard/SetupWizard.h" #include "ui/setupwizard/ThemeWizardPage.h" @@ -559,6 +562,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("IconTheme", QString("fluent_dark")); m_settings->registerSetting("ApplicationTheme", QString("freesm")); m_settings->registerSetting("BackgroundCat", QString("typescript")); + m_settings->registerSetting("BackgroundCat", QString("miside-screenshot")); // Remembered state m_settings->registerSetting("LastUsedGroupForNewInstance", QString()); @@ -650,6 +654,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) auto defaultEnableAutoJava = m_settings->get("JavaPath").toString().isEmpty(); m_settings->registerSetting("AutomaticJavaSwitch", defaultEnableAutoJava); m_settings->registerSetting("AutomaticJavaDownload", defaultEnableAutoJava); + m_settings->registerSetting("UserAskedAboutAutomaticJavaDownload", false); // Legacy settings m_settings->registerSetting("OnlineFixes", false); @@ -777,6 +782,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // FTBApp instances m_settings->registerSetting("FTBAppInstancesPath", ""); + // Custom Technic Client ID + m_settings->registerSetting("TechnicClientID", ""); + // Init page provider { m_globalSettingsProvider = std::make_shared(tr("Settings")); @@ -1019,7 +1027,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } // notify user if /tmp is mounted with `noexec` (#1693) - { + QString jvmArgs = m_settings->get("JvmArgs").toString(); + if (jvmArgs.indexOf("java.io.tmpdir") == -1) { /* java.io.tmpdir is a valid workaround, so don't annoy */ bool is_tmp_noexec = false; #if defined(Q_OS_LINUX) @@ -1039,7 +1048,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) if (is_tmp_noexec) { auto infoMsg = tr("Your /tmp directory is currently mounted with the 'noexec' flag enabled.\n" - "Some versions of Minecraft may not launch.\n"); + "Some versions of Minecraft may not launch.\n" + "\n" + "You may solve this issue by remounting /tmp as 'exec' or setting " + "the java.io.tmpdir JVM argument to a writeable directory in a " + "filesystem where the 'exec' flag is set (e.g., /home/user/.local/tmp)\n"); auto msgBox = new QMessageBox(QMessageBox::Information, tr("Incompatible system configuration"), infoMsg, QMessageBox::Ok); msgBox->setDefaultButton(QMessageBox::Ok); msgBox->setAttribute(Qt::WA_DeleteOnClose); @@ -1060,6 +1073,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) bool Application::createSetupWizard() { bool javaRequired = [&]() { + if (BuildConfig.JAVA_DOWNLOADER_ENABLED && m_settings->get("AutomaticJavaDownload").toBool()) { + return false; + } bool ignoreJavaWizard = m_settings->get("IgnoreJavaWizard").toBool(); if (ignoreJavaWizard) { return false; @@ -1072,18 +1088,17 @@ bool Application::createSetupWizard() } QString currentJavaPath = settings()->get("JavaPath").toString(); QString actualPath = FS::ResolveExecutable(currentJavaPath); - if (actualPath.isNull()) { - return true; - } - return false; + return actualPath.isNull(); }(); + bool askjava = BuildConfig.JAVA_DOWNLOADER_ENABLED && !javaRequired && !m_settings->get("AutomaticJavaDownload").toBool() && + !m_settings->get("AutomaticJavaSwitch").toBool() && !m_settings->get("UserAskedAboutAutomaticJavaDownload").toBool(); bool languageRequired = settings()->get("Language").toString().isEmpty(); bool pasteInterventionRequired = settings()->get("PastebinURL") != ""; bool validWidgets = m_themeManager->isValidApplicationTheme(settings()->get("ApplicationTheme").toString()); bool validIcons = m_themeManager->isValidIconTheme(settings()->get("IconTheme").toString()); + bool login = !m_accounts->anyAccountIsValid() && capabilities() & Application::SupportsMSA; bool themeInterventionRequired = !validWidgets || !validIcons; - bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired || themeInterventionRequired; - + bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired || themeInterventionRequired || askjava || login; if (wizardRequired) { // set default theme after going into theme wizard if (!validIcons) @@ -1100,6 +1115,8 @@ bool Application::createSetupWizard() if (javaRequired) { m_setupWizard->addPage(new JavaWizardPage(m_setupWizard)); + } else if (askjava) { + m_setupWizard->addPage(new AutoJavaWizardPage(m_setupWizard)); } if (pasteInterventionRequired) { @@ -1110,11 +1127,14 @@ bool Application::createSetupWizard() m_setupWizard->addPage(new ThemeWizardPage(m_setupWizard)); } + if (login) { + m_setupWizard->addPage(new LoginWizardPage(m_setupWizard)); + } connect(m_setupWizard, &QDialog::finished, this, &Application::setupWizardFinished); m_setupWizard->show(); - return true; } - return false; + + return wizardRequired || login; } bool Application::updaterEnabled() @@ -1259,16 +1279,23 @@ Application::~Application() void Application::messageReceived(const QByteArray& message) { - if (status() != Initialized) { - qDebug() << "Received message" << message << "while still initializing. It will be ignored."; - return; - } - ApplicationMessage received; received.parse(message); auto& command = received.command; + if (status() != Initialized) { + bool isLoginAtempt = false; + if (command == "import") { + QString url = received.args["url"]; + isLoginAtempt = !url.isEmpty() && normalizeImportUrl(url).scheme() == BuildConfig.LAUNCHER_APP_BINARY_NAME; + } + if (!isLoginAtempt) { + qDebug() << "Received message" << message << "while still initializing. It will be ignored."; + return; + } + } + if (command == "activate") { showMainWindow(); } else if (command == "import") { @@ -1355,6 +1382,7 @@ bool Application::launch(InstancePtr instance, bool online, bool demo, Minecraft if (m_updateRunning) { qDebug() << "Cannot launch instances while an update is running. Please try again when updates are completed."; } else if (instance->canLaunch()) { + QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[instance->id()]; auto window = extras.window; if (window) { @@ -1379,7 +1407,7 @@ bool Application::launch(InstancePtr instance, bool online, bool demo, Minecraft connect(controller.get(), &LaunchController::failed, this, &Application::controllerFailed); connect(controller.get(), &LaunchController::aborted, this, [this] { controllerFailed(tr("Aborted")); }); addRunningInstance(); - controller->start(); + QMetaObject::invokeMethod(controller.get(), &Task::start, Qt::QueuedConnection); return true; } else if (instance->isRunning()) { showInstanceWindow(instance, "console"); @@ -1397,9 +1425,11 @@ bool Application::kill(InstancePtr instance) qWarning() << "Attempted to kill instance" << instance->id() << ", which isn't running."; return false; } + QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[instance->id()]; // NOTE: copy of the shared pointer keeps it alive auto controller = extras.controller; + locker.unlock(); if (controller) { return controller->abort(); } @@ -1453,12 +1483,14 @@ void Application::controllerSucceeded() if (!controller) return; auto id = controller->id(); + + QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[id]; // on success, do... if (controller->instance()->settings()->get("AutoCloseConsole").toBool()) { if (extras.window) { - extras.window->close(); + QMetaObject::invokeMethod(extras.window, &QWidget::close, Qt::QueuedConnection); } } extras.controller.reset(); @@ -1478,6 +1510,7 @@ void Application::controllerFailed(const QString& error) if (!controller) return; auto id = controller->id(); + QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[id]; // on failure, do... nothing @@ -1535,6 +1568,7 @@ InstanceWindow* Application::showInstanceWindow(InstancePtr instance, QString pa if (!instance) return nullptr; auto id = instance->id(); + QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[id]; auto& window = extras.window; @@ -1572,6 +1606,7 @@ void Application::on_windowClose() m_openWindows--; auto instWindow = qobject_cast(QObject::sender()); if (instWindow) { + QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[instWindow->instanceId()]; extras.window = nullptr; if (extras.controller) { @@ -1819,7 +1854,7 @@ bool Application::handleDataMigration(const QString& currentData, matcher->add(std::make_shared("themes/")); ProgressDialog diag; - DataMigrationTask task(nullptr, oldData, currentData, matcher); + DataMigrationTask task(oldData, currentData, matcher); if (diag.execWithTask(&task)) { qDebug() << "<> Migration succeeded"; setDoNotMigrate(); @@ -1853,7 +1888,36 @@ QUrl Application::normalizeImportUrl(QString const& url) return QUrl::fromUserInput(url); } } + const QString Application::javaPath() { return m_settings->get("JavaDir").toString(); } + +void Application::addQSavePath(QString path) +{ + QMutexLocker locker(&m_qsaveResourcesMutex); + m_qsaveResources[path] = m_qsaveResources.value(path, 0) + 1; +} + +void Application::removeQSavePath(QString path) +{ + QMutexLocker locker(&m_qsaveResourcesMutex); + auto count = m_qsaveResources.value(path, 0) - 1; + if (count <= 0) { + m_qsaveResources.remove(path); + } else { + m_qsaveResources[path] = count; + } +} + +bool Application::checkQSavePath(QString path) +{ + QMutexLocker locker(&m_qsaveResourcesMutex); + for (auto partialPath : m_qsaveResources.keys()) { + if (path.startsWith(partialPath) && m_qsaveResources.value(partialPath, 0) > 0) { + return true; + } + } + return false; +} \ No newline at end of file diff --git a/launcher/Application.h b/launcher/Application.h index 7432c9683..164ec3a4f 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -42,6 +42,7 @@ #include #include #include +#include #include #include @@ -81,6 +82,12 @@ class Index; #endif #define APPLICATION (static_cast(QCoreApplication::instance())) +// Used for checking if is a test +#if defined(APPLICATION_DYN) +#undef APPLICATION_DYN +#endif +#define APPLICATION_DYN (dynamic_cast(QCoreApplication::instance())) + class Application : public QApplication { // friends for the purpose of limiting access to deprecated stuff Q_OBJECT @@ -272,6 +279,7 @@ class Application : public QApplication { shared_qobject_ptr controller; }; std::map m_instanceExtras; + mutable QMutex m_instanceExtrasMutex; // main state variables size_t m_openWindows = 0; @@ -297,4 +305,13 @@ class Application : public QApplication { QList m_urlsToImport; QString m_instanceIdToShowWindowOf; std::unique_ptr logFile; + + public: + void addQSavePath(QString); + void removeQSavePath(QString); + bool checkQSavePath(QString); + + private: + QHash m_qsaveResources; + mutable QMutex m_qsaveResourcesMutex; }; diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index 8c80331bc..2be28d1ec 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -181,7 +181,7 @@ class BaseInstance : public QObject, public std::enable_shared_from_this createUpdateTask() = 0; /// returns a valid launcher (task container) virtual shared_qobject_ptr createLaunchTask(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) = 0; @@ -215,7 +215,7 @@ class BaseInstance : public QObject, public std::enable_shared_from_this -DataMigrationTask::DataMigrationTask(QObject* parent, - const QString& sourcePath, - const QString& targetPath, - const IPathMatcher::Ptr pathMatcher) - : Task(parent), m_sourcePath(sourcePath), m_targetPath(targetPath), m_pathMatcher(pathMatcher), m_copy(sourcePath, targetPath) +DataMigrationTask::DataMigrationTask(const QString& sourcePath, const QString& targetPath, const IPathMatcher::Ptr pathMatcher) + : Task(), m_sourcePath(sourcePath), m_targetPath(targetPath), m_pathMatcher(pathMatcher), m_copy(sourcePath, targetPath) { m_copy.matcher(m_pathMatcher.get()).whitelist(true); } diff --git a/launcher/DataMigrationTask.h b/launcher/DataMigrationTask.h index aba9f2399..fc613cd5e 100644 --- a/launcher/DataMigrationTask.h +++ b/launcher/DataMigrationTask.h @@ -18,7 +18,7 @@ class DataMigrationTask : public Task { Q_OBJECT public: - explicit DataMigrationTask(QObject* parent, const QString& sourcePath, const QString& targetPath, IPathMatcher::Ptr pathmatcher); + explicit DataMigrationTask(const QString& sourcePath, const QString& targetPath, IPathMatcher::Ptr pathmatcher); ~DataMigrationTask() override = default; protected: diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index b5c2c95cf..512de28c2 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -45,7 +45,6 @@ #include #include #include -#include #include #include #include @@ -54,6 +53,7 @@ #include #include "DesktopServices.h" +#include "PSaveFile.h" #include "StringUtils.h" #if defined Q_OS_WIN32 @@ -191,8 +191,8 @@ void ensureExists(const QDir& dir) void write(const QString& filename, const QByteArray& data) { ensureExists(QFileInfo(filename).dir()); - QSaveFile file(filename); - if (!file.open(QSaveFile::WriteOnly)) { + PSaveFile file(filename); + if (!file.open(PSaveFile::WriteOnly)) { throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString()); } if (data.size() != file.write(data)) { @@ -213,8 +213,8 @@ void appendSafe(const QString& filename, const QByteArray& data) buffer = QByteArray(); } buffer.append(data); - QSaveFile file(filename); - if (!file.open(QSaveFile::WriteOnly)) { + PSaveFile file(filename); + if (!file.open(PSaveFile::WriteOnly)) { throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString()); } if (buffer.size() != file.write(buffer)) { @@ -921,6 +921,10 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri if (destination.isEmpty()) { destination = PathCombine(getDesktopDir(), RemoveInvalidFilenameChars(name)); } + if (!ensureFilePathExists(destination)) { + qWarning() << "Destination path can't be created!"; + return false; + } #if defined(Q_OS_MACOS) // Create the Application QDir applicationDirectory = @@ -967,8 +971,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri if (!args.empty()) argstring = " \"" + args.join("\" \"") + "\""; - stream << "#!/bin/bash" - << "\n"; + stream << "#!/bin/bash" << "\n"; stream << "\"" << target << "\" " << argstring << "\n"; stream.flush(); @@ -1012,12 +1015,9 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri if (!args.empty()) argstring = " '" + args.join("' '") + "'"; - stream << "[Desktop Entry]" - << "\n"; - stream << "Type=Application" - << "\n"; - stream << "Categories=Game;ActionGame;AdventureGame;Simulation" - << "\n"; + stream << "[Desktop Entry]" << "\n"; + stream << "Type=Application" << "\n"; + stream << "Categories=Game;ActionGame;AdventureGame;Simulation" << "\n"; stream << "Exec=\"" << target.toLocal8Bit() << "\"" << argstring.toLocal8Bit() << "\n"; stream << "Name=" << name.toLocal8Bit() << "\n"; if (!icon.isEmpty()) { diff --git a/launcher/Filter.h b/launcher/Filter.h index a8c9c14d8..ae835e724 100644 --- a/launcher/Filter.h +++ b/launcher/Filter.h @@ -57,5 +57,5 @@ class ExactListFilter : public Filter { bool accepts(const QString& value) override; private: - const QStringList& m_pattern; + QStringList m_pattern; }; diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp index ff2d37723..0220a4144 100644 --- a/launcher/InstanceCopyTask.cpp +++ b/launcher/InstanceCopyTask.cpp @@ -173,7 +173,11 @@ void InstanceCopyTask::copyFinished() allowed_symlinks_file .filePath()); // we dont want to modify the original. also make sure the resulting file is not itself a link. - FS::write(allowed_symlinks_file.filePath(), allowed_symlinks); + try { + FS::write(allowed_symlinks_file.filePath(), allowed_symlinks); + } catch (const FS::FileSystemException& e) { + qCritical() << "Failed to write symlink :" << e.cause(); + } } emitSucceeded(); diff --git a/launcher/InstanceCreationTask.cpp b/launcher/InstanceCreationTask.cpp index 9c17dfc9f..bd3514798 100644 --- a/launcher/InstanceCreationTask.cpp +++ b/launcher/InstanceCreationTask.cpp @@ -38,22 +38,29 @@ void InstanceCreationTask::executeTask() // files scheduled to, and we'd better not let the user abort in the middle of it, since it'd // put the instance in an invalid state. if (shouldOverride()) { + bool deleteFailed = false; + setAbortable(false); setStatus(tr("Removing old conflicting files...")); qDebug() << "Removing old files"; - for (auto path : m_files_to_remove) { + for (const QString& path : m_files_to_remove) { if (!QFile::exists(path)) continue; + qDebug() << "Removing" << path; - if (!FS::deletePath(path)) { - qCritical() << "Couldn't remove the old conflicting files."; - emitFailed(tr("Failed to remove old conflicting files.")); - return; + + if (!QFile::remove(path)) { + qCritical() << "Could not remove" << path; + deleteFailed = true; } } - } - emitSucceeded(); - return; + if (deleteFailed) { + emitFailed(tr("Failed to remove old conflicting files.")); + return; + } + } + if (!m_abort) + emitSucceeded(); } diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 57cc77527..71630656d 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -69,9 +69,11 @@ bool InstanceImportTask::abort() if (!canAbort()) return false; - if (task) - task->abort(); - return Task::abort(); + bool wasAborted = false; + if (m_task) + wasAborted = m_task->abort(); + Task::abort(); + return wasAborted; } void InstanceImportTask::executeTask() @@ -104,7 +106,7 @@ void InstanceImportTask::downloadFromUrl() connect(filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propagateStepProgress); connect(filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::emitFailed); connect(filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::emitAborted); - task.reset(filesNetJob); + m_task.reset(filesNetJob); filesNetJob->start(); } @@ -193,7 +195,7 @@ void InstanceImportTask::processZipPack() stepProgress(*progressStep); }); - connect(zipTask.get(), &Task::succeeded, this, &InstanceImportTask::extractFinished); + connect(zipTask.get(), &Task::succeeded, this, &InstanceImportTask::extractFinished, Qt::QueuedConnection); connect(zipTask.get(), &Task::aborted, this, &InstanceImportTask::emitAborted); connect(zipTask.get(), &Task::failed, this, [this, progressStep](QString reason) { progressStep->state = TaskStepState::Failed; @@ -210,12 +212,13 @@ void InstanceImportTask::processZipPack() progressStep->status = status; stepProgress(*progressStep); }); - task.reset(zipTask); + m_task.reset(zipTask); zipTask->start(); } void InstanceImportTask::extractFinished() { + setAbortable(false); QDir extractDir(m_stagingPath); qDebug() << "Fixing permissions for extracted pack files..."; @@ -289,8 +292,11 @@ void InstanceImportTask::processFlame() inst_creation_task->setGroup(m_instGroup); inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); - connect(inst_creation_task.get(), &Task::succeeded, this, [this, inst_creation_task] { - setOverride(inst_creation_task->shouldOverride(), inst_creation_task->originalInstanceID()); + auto weak = inst_creation_task.toWeakRef(); + connect(inst_creation_task.get(), &Task::succeeded, this, [this, weak] { + if (auto sp = weak.lock()) { + setOverride(sp->shouldOverride(), sp->originalInstanceID()); + } emitSucceeded(); }); connect(inst_creation_task.get(), &Task::failed, this, &InstanceImportTask::emitFailed); @@ -299,11 +305,12 @@ void InstanceImportTask::processFlame() connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus); connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails); - connect(this, &Task::aborted, inst_creation_task.get(), &InstanceCreationTask::abort); connect(inst_creation_task.get(), &Task::aborted, this, &Task::abort); connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); - inst_creation_task->start(); + m_task.reset(inst_creation_task); + setAbortable(true); + m_task->start(); } void InstanceImportTask::processTechnic() @@ -350,7 +357,7 @@ void InstanceImportTask::processMultiMC() void InstanceImportTask::processModrinth() { - ModrinthCreationTask* inst_creation_task = nullptr; + shared_qobject_ptr inst_creation_task = nullptr; if (!m_extra_info.isEmpty()) { auto pack_id_it = m_extra_info.constFind("pack_id"); Q_ASSERT(pack_id_it != m_extra_info.constEnd()); @@ -367,7 +374,7 @@ void InstanceImportTask::processModrinth() original_instance_id = original_instance_id_it.value(); inst_creation_task = - new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id); + makeShared(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id); } else { QString pack_id; if (!m_sourceUrl.isEmpty()) { @@ -376,7 +383,7 @@ void InstanceImportTask::processModrinth() } // FIXME: Find a way to get the ID in directly imported ZIPs - inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id); + inst_creation_task = makeShared(m_stagingPath, m_globalSettings, m_parent, pack_id); } inst_creation_task->setName(*this); @@ -384,20 +391,23 @@ void InstanceImportTask::processModrinth() inst_creation_task->setGroup(m_instGroup); inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); - connect(inst_creation_task, &Task::succeeded, this, [this, inst_creation_task] { - setOverride(inst_creation_task->shouldOverride(), inst_creation_task->originalInstanceID()); + auto weak = inst_creation_task.toWeakRef(); + connect(inst_creation_task.get(), &Task::succeeded, this, [this, weak] { + if (auto sp = weak.lock()) { + setOverride(sp->shouldOverride(), sp->originalInstanceID()); + } emitSucceeded(); }); - connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed); - connect(inst_creation_task, &Task::progress, this, &InstanceImportTask::setProgress); - connect(inst_creation_task, &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress); - connect(inst_creation_task, &Task::status, this, &InstanceImportTask::setStatus); - connect(inst_creation_task, &Task::details, this, &InstanceImportTask::setDetails); - connect(inst_creation_task, &Task::finished, inst_creation_task, &InstanceCreationTask::deleteLater); - - connect(this, &Task::aborted, inst_creation_task, &InstanceCreationTask::abort); - connect(inst_creation_task, &Task::aborted, this, &Task::abort); - connect(inst_creation_task, &Task::abortStatusChanged, this, &Task::setAbortable); - - inst_creation_task->start(); + connect(inst_creation_task.get(), &Task::failed, this, &InstanceImportTask::emitFailed); + connect(inst_creation_task.get(), &Task::progress, this, &InstanceImportTask::setProgress); + connect(inst_creation_task.get(), &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress); + connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus); + connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails); + + connect(inst_creation_task.get(), &Task::aborted, this, &Task::abort); + connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); + + m_task.reset(inst_creation_task); + setAbortable(true); + m_task->start(); } diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index cf86af4ea..8884e0801 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -40,16 +40,13 @@ #include #include "InstanceTask.h" -#include -#include - class QuaZip; class InstanceImportTask : public InstanceTask { Q_OBJECT public: explicit InstanceImportTask(const QUrl& sourceUrl, QWidget* parent = nullptr, QMap&& extra_info = {}); - + virtual ~InstanceImportTask() = default; bool abort() override; protected: @@ -70,7 +67,7 @@ class InstanceImportTask : public InstanceTask { private: /* data */ QUrl m_sourceUrl; QString m_archivePath; - Task::Ptr task; + Task::Ptr m_task; enum class ModpackType { Unknown, MultiMC, diff --git a/launcher/JavaCommon.cpp b/launcher/JavaCommon.cpp index 3cbf9f9d5..188edb943 100644 --- a/launcher/JavaCommon.cpp +++ b/launcher/JavaCommon.cpp @@ -116,7 +116,7 @@ void JavaCommon::TestCheck::run() emit finished(); return; } - checker.reset(new JavaChecker(m_path, "", 0, 0, 0, 0, this)); + checker.reset(new JavaChecker(m_path, "", 0, 0, 0, 0)); connect(checker.get(), &JavaChecker::checkFinished, this, &JavaCommon::TestCheck::checkFinished); checker->start(); } @@ -128,7 +128,7 @@ void JavaCommon::TestCheck::checkFinished(const JavaChecker::Result& result) emit finished(); return; } - checker.reset(new JavaChecker(m_path, m_args, m_maxMem, m_maxMem, result.javaVersion.requiresPermGen() ? m_permGen : 0, 0, this)); + checker.reset(new JavaChecker(m_path, m_args, m_maxMem, m_maxMem, result.javaVersion.requiresPermGen() ? m_permGen : 0, 0)); connect(checker.get(), &JavaChecker::checkFinished, this, &JavaCommon::TestCheck::checkFinishedWithArgs); checker->start(); } diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 73800574f..aaf84eac4 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -53,6 +53,7 @@ #include #include #include +#include #include #include "BuildConfig.h" @@ -60,7 +61,7 @@ #include "launch/steps/TextPrint.h" #include "tasks/Task.h" -LaunchController::LaunchController(QObject* parent) : Task(parent) {} +LaunchController::LaunchController() : Task() {} void LaunchController::executeTask() { diff --git a/launcher/LaunchController.h b/launcher/LaunchController.h index 6e2a94258..af52ad450 100644 --- a/launcher/LaunchController.h +++ b/launcher/LaunchController.h @@ -47,7 +47,7 @@ class LaunchController : public Task { public: void executeTask() override; - LaunchController(QObject* parent = nullptr); + LaunchController(); virtual ~LaunchController() = default; void setInstance(InstancePtr instance) { m_instance = instance; } diff --git a/launcher/Launcher.in b/launcher/Launcher.in index 1a23f2555..706d7022b 100755 --- a/launcher/Launcher.in +++ b/launcher/Launcher.in @@ -39,8 +39,16 @@ if [ "x$DEPS_LIST" = "x" ]; then # Just to be sure... chmod +x "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" + ARGS=("${LAUNCHER_DIR}/${LAUNCHER_NAME}" "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}") + + if [ -f portable.txt ]; then + ARGS+=("-d" "${LAUNCHER_DIR}") + fi + + ARGS+=("$@") + # Run the launcher - exec -a "${LAUNCHER_DIR}/${LAUNCHER_NAME}" "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" -d "${LAUNCHER_DIR}" "$@" + exec -a "${ARGS[@]}" # Run the launcher in valgrind # valgrind --log-file="valgrind.log" --leak-check=full --track-origins=yes "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" -d "${LAUNCHER_DIR}" "$@" diff --git a/launcher/LoggedProcess.cpp b/launcher/LoggedProcess.cpp index fadd64e68..35ce4e0e5 100644 --- a/launcher/LoggedProcess.cpp +++ b/launcher/LoggedProcess.cpp @@ -39,7 +39,8 @@ #include #include "MessageLevel.h" -LoggedProcess::LoggedProcess(QObject* parent) : QProcess(parent) +LoggedProcess::LoggedProcess(const QTextCodec* output_codec, QObject* parent) + : QProcess(parent), m_err_decoder(output_codec), m_out_decoder(output_codec) { // QProcess has a strange interface... let's map a lot of those into a few. connect(this, &QProcess::readyReadStandardOutput, this, &LoggedProcess::on_stdOut); diff --git a/launcher/LoggedProcess.h b/launcher/LoggedProcess.h index 46bdaa830..75ba15dfd 100644 --- a/launcher/LoggedProcess.h +++ b/launcher/LoggedProcess.h @@ -49,7 +49,7 @@ class LoggedProcess : public QProcess { enum State { NotRunning, Starting, FailedToStart, Running, Finished, Crashed, Aborted }; public: - explicit LoggedProcess(QObject* parent = 0); + explicit LoggedProcess(const QTextCodec* output_codec = QTextCodec::codecForLocale(), QObject* parent = 0); virtual ~LoggedProcess(); State state() const; @@ -80,8 +80,8 @@ class LoggedProcess : public QProcess { QStringList reprocess(const QByteArray& data, QTextDecoder& decoder); private: - QTextDecoder m_err_decoder = QTextDecoder(QTextCodec::codecForLocale()); - QTextDecoder m_out_decoder = QTextDecoder(QTextCodec::codecForLocale()); + QTextDecoder m_err_decoder; + QTextDecoder m_out_decoder; QString m_leftover_line; bool m_killed = false; State m_state = NotRunning; diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index dcf3d566f..b38aca17a 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -378,7 +378,7 @@ std::optional extractDir(QString fileCompressed, QString dir) if (fileInfo.size() == 22) { return QStringList(); } - qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError(); + qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError(); ; return std::nullopt; } @@ -395,7 +395,7 @@ std::optional extractDir(QString fileCompressed, QString subdir, QS if (fileInfo.size() == 22) { return QStringList(); } - qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError(); + qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError(); ; return std::nullopt; } @@ -412,7 +412,7 @@ bool extractFile(QString fileCompressed, QString file, QString target) if (fileInfo.size() == 22) { return true; } - qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError(); + qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError(); return false; } return extractRelFile(&zip, file, target); @@ -577,7 +577,7 @@ auto ExtractZipTask::extractZip() -> ZipResult auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(m_subdirectory.size())); auto original_name = relative_file_name; - setStatus("Unziping: " + relative_file_name); + setStatus("Unpacking: " + relative_file_name); // Fix subdirs/files ending with a / getting transformed into absolute paths if (relative_file_name.startsWith('/')) diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h index 1635f8b32..d81df9d81 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -153,6 +153,7 @@ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, Q #if defined(LAUNCHER_APPLICATION) class ExportToZipTask : public Task { + Q_OBJECT public: ExportToZipTask(QString outputPath, QDir dir, @@ -207,6 +208,7 @@ class ExportToZipTask : public Task { }; class ExtractZipTask : public Task { + Q_OBJECT public: ExtractZipTask(QString input, QDir outputDir, QString subdirectory = "") : ExtractZipTask(std::make_shared(input), outputDir, subdirectory) diff --git a/launcher/MTPixmapCache.h b/launcher/MTPixmapCache.h index b6bd13045..0ba9c5ac8 100644 --- a/launcher/MTPixmapCache.h +++ b/launcher/MTPixmapCache.h @@ -101,7 +101,7 @@ class PixmapCache final : public QObject { */ bool _markCacheMissByEviciton() { - static constexpr uint maxInt = static_cast(std::numeric_limits::max()); + static constexpr uint maxCache = static_cast(std::numeric_limits::max()) / 4; static constexpr uint step = 10240; static constexpr int oneSecond = 1000; @@ -118,8 +118,8 @@ class PixmapCache final : public QObject { if (m_consecutive_fast_evicitons >= m_consecutive_fast_evicitons_threshold) { // increase the cache size uint newSize = _cacheLimit() + step; - if (newSize >= maxInt) { // increase it until you overflow :D - newSize = maxInt; + if (newSize >= maxCache) { // increase it until you overflow :D + newSize = maxCache; qDebug() << m_consecutive_fast_evicitons << tr("pixmap cache misses by eviction happened too fast, doing nothing as the cache size reached it's limit"); } else { diff --git a/launcher/NullInstance.h b/launcher/NullInstance.h index 3ee38e76c..3d01c9d33 100644 --- a/launcher/NullInstance.h +++ b/launcher/NullInstance.h @@ -53,7 +53,7 @@ class NullInstance : public BaseInstance { QSet traits() const override { return {}; }; QString instanceConfigFolder() const override { return instanceRoot(); }; shared_qobject_ptr createLaunchTask(AuthSessionPtr, MinecraftTarget::Ptr) override { return nullptr; } - shared_qobject_ptr createUpdateTask([[maybe_unused]] Net::Mode mode) override { return nullptr; } + QList createUpdateTask() override { return {}; } QProcessEnvironment createEnvironment() override { return QProcessEnvironment(); } QProcessEnvironment createLaunchEnvironment() override { return QProcessEnvironment(); } QMap getVariables() override { return QMap(); } diff --git a/launcher/PSaveFile.h b/launcher/PSaveFile.h new file mode 100644 index 000000000..ba6154ad8 --- /dev/null +++ b/launcher/PSaveFile.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023-2024 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 "Application.h" + +#if defined(LAUNCHER_APPLICATION) + +/* PSaveFile + * A class that mimics QSaveFile for Windows. + * + * When reading resources, we need to avoid accessing temporary files + * generated by QSaveFile. If we start reading such a file, we may + * inadvertently keep it open while QSaveFile is trying to remove it, + * or we might detect the file just before it is removed, leading to + * race conditions and errors. + * + * Unfortunately, QSaveFile doesn't provide a way to retrieve the + * temporary file name or to set a specific template for the temporary + * file name it uses. By default, QSaveFile appends a `.XXXXXX` suffix + * to the original file name, where the `XXXXXX` part is dynamically + * generated to ensure uniqueness. + * + * This class acts like a lock by adding and removing the target file + * name into/from a global string set, helping to manage access to + * files during critical operations. + * + * Note: Please do not use the `setFileName` function directly, as it + * is not virtual and cannot be overridden. + */ +class PSaveFile : public QSaveFile { + public: + PSaveFile(const QString& name) : QSaveFile(name) { addPath(name); } + PSaveFile(const QString& name, QObject* parent) : QSaveFile(name, parent) { addPath(name); } + virtual ~PSaveFile() + { + if (auto app = APPLICATION_DYN) { + app->removeQSavePath(m_absoluteFilePath); + } + } + + private: + void addPath(const QString& path) + { + m_absoluteFilePath = QFileInfo(path).absoluteFilePath() + "."; // add dot for tmp files only + if (auto app = APPLICATION_DYN) { + app->addQSavePath(m_absoluteFilePath); + } + } + QString m_absoluteFilePath; +}; +#else +#define PSaveFile QSaveFile +#endif \ No newline at end of file diff --git a/launcher/RuntimeContext.h b/launcher/RuntimeContext.h index c57140d28..85304a5bc 100644 --- a/launcher/RuntimeContext.h +++ b/launcher/RuntimeContext.h @@ -20,13 +20,13 @@ #include #include +#include "SysInfo.h" #include "settings/SettingsObject.h" struct RuntimeContext { QString javaArchitecture; QString javaRealArchitecture; - QString javaPath; - QString system; + QString system = SysInfo::currentSystem(); QString mappedJavaRealArchitecture() const { @@ -45,8 +45,6 @@ struct RuntimeContext { { javaArchitecture = instanceSettings->get("JavaArchitecture").toString(); javaRealArchitecture = instanceSettings->get("JavaRealArchitecture").toString(); - javaPath = instanceSettings->get("JavaPath").toString(); - system = currentSystem(); } QString getClassifier() const { return system + "-" + mappedJavaRealArchitecture(); } @@ -68,21 +66,4 @@ struct RuntimeContext { return x; } - - static QString currentSystem() - { -#if defined(Q_OS_LINUX) - return "linux"; -#elif defined(Q_OS_MACOS) - return "osx"; -#elif defined(Q_OS_WINDOWS) - return "windows"; -#elif defined(Q_OS_FREEBSD) - return "freebsd"; -#elif defined(Q_OS_OPENBSD) - return "openbsd"; -#else - return "unknown"; -#endif - } }; diff --git a/launcher/SysInfo.cpp b/launcher/SysInfo.cpp index 0dfa74de7..cfcf63805 100644 --- a/launcher/SysInfo.cpp +++ b/launcher/SysInfo.cpp @@ -81,9 +81,9 @@ QString getSupportedJavaArchitecture() if (arch == "arm64") return "mac-os-arm64"; if (arch.contains("64")) - return "mac-os-64"; + return "mac-os-x64"; if (arch.contains("86")) - return "mac-os-86"; + return "mac-os-x86"; // Unknown, maybe something new, appending arch return "mac-os-" + arch; } else if (sys == "linux") { diff --git a/launcher/Version.cpp b/launcher/Version.cpp index 511aa9c35..2edb17e72 100644 --- a/launcher/Version.cpp +++ b/launcher/Version.cpp @@ -123,8 +123,7 @@ QDebug operator<<(QDebug debug, const Version& v) first = false; } - debug.nospace() << " ]" - << " }"; + debug.nospace() << " ]" << " }"; return debug; } diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp index 552900d35..12a82f73d 100644 --- a/launcher/VersionProxyModel.cpp +++ b/launcher/VersionProxyModel.cpp @@ -140,9 +140,9 @@ QVariant VersionProxyModel::headerData(int section, Qt::Orientation orientation, case Path: return tr("Filesystem path to this version"); case JavaName: - return tr("The alternative name of the java version"); + return tr("The alternative name of the Java version"); case JavaMajor: - return tr("The java major version"); + return tr("The Java major version"); case Time: return tr("Release date of this version"); } diff --git a/launcher/icons/IconUtils.cpp b/launcher/icons/IconUtils.cpp index 6825dd6da..87e948729 100644 --- a/launcher/icons/IconUtils.cpp +++ b/launcher/icons/IconUtils.cpp @@ -39,7 +39,7 @@ #include "FileSystem.h" namespace { -static const QStringList validIconExtensions = { { "svg", "png", "ico", "gif", "jpg", "jpeg" } }; +static const QStringList validIconExtensions = { { "svg", "png", "ico", "gif", "jpg", "jpeg", "webp" } }; } namespace IconUtils { diff --git a/launcher/java/JavaChecker.cpp b/launcher/java/JavaChecker.cpp index c54a5b04b..772c90e42 100644 --- a/launcher/java/JavaChecker.cpp +++ b/launcher/java/JavaChecker.cpp @@ -44,8 +44,8 @@ #include "FileSystem.h" #include "java/JavaUtils.h" -JavaChecker::JavaChecker(QString path, QString args, int minMem, int maxMem, int permGen, int id, QObject* parent) - : Task(parent), m_path(path), m_args(args), m_minMem(minMem), m_maxMem(maxMem), m_permGen(permGen), m_id(id) +JavaChecker::JavaChecker(QString path, QString args, int minMem, int maxMem, int permGen, int id) + : Task(), m_path(path), m_args(args), m_minMem(minMem), m_maxMem(maxMem), m_permGen(permGen), m_id(id) {} void JavaChecker::executeTask() diff --git a/launcher/java/JavaChecker.h b/launcher/java/JavaChecker.h index 171a18b76..a04b68170 100644 --- a/launcher/java/JavaChecker.h +++ b/launcher/java/JavaChecker.h @@ -1,7 +1,6 @@ #pragma once #include #include -#include #include "JavaVersion.h" #include "QObjectPtr.h" @@ -26,7 +25,7 @@ class JavaChecker : public Task { enum class Validity { Errored, ReturnedInvalidData, Valid } validity = Validity::Errored; }; - explicit JavaChecker(QString path, QString args, int minMem = 0, int maxMem = 0, int permGen = 0, int id = 0, QObject* parent = 0); + explicit JavaChecker(QString path, QString args, int minMem = 0, int maxMem = 0, int permGen = 0, int id = 0); signals: void checkFinished(const Result& result); diff --git a/launcher/java/JavaInstallList.cpp b/launcher/java/JavaInstallList.cpp index 569fda306..aa7fab8a0 100644 --- a/launcher/java/JavaInstallList.cpp +++ b/launcher/java/JavaInstallList.cpp @@ -163,7 +163,7 @@ void JavaListLoadTask::executeTask() JavaUtils ju; QList candidate_paths = m_only_managed_versions ? getPrismJavaBundle() : ju.FindJavaPaths(); - ConcurrentTask::Ptr job(new ConcurrentTask(this, "Java detection", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); + ConcurrentTask::Ptr job(new ConcurrentTask("Java detection", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); m_job.reset(job); connect(m_job.get(), &Task::finished, this, &JavaListLoadTask::javaCheckerFinished); connect(m_job.get(), &Task::progress, this, &Task::setProgress); @@ -171,7 +171,7 @@ void JavaListLoadTask::executeTask() qDebug() << "Probing the following Java paths: "; int id = 0; for (QString candidate : candidate_paths) { - auto checker = new JavaChecker(candidate, "", 0, 0, 0, id, this); + auto checker = new JavaChecker(candidate, "", 0, 0, 0, id); connect(checker, &JavaChecker::checkFinished, [this](const JavaChecker::Result& result) { m_results << result; }); job->addTask(Task::Ptr(checker)); id++; diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp index bc8026348..f3200428e 100644 --- a/launcher/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -102,6 +102,8 @@ QProcessEnvironment CleanEnviroment() QString newValue = stripVariableEntries(key, value, rawenv.value("LAUNCHER_" + key)); qDebug() << "Env: stripped" << key << value << "to" << newValue; + + value = newValue; } #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) // Strip IBus diff --git a/launcher/java/JavaVersion.cpp b/launcher/java/JavaVersion.cpp index 5e9700012..bca50f2c9 100644 --- a/launcher/java/JavaVersion.cpp +++ b/launcher/java/JavaVersion.cpp @@ -48,6 +48,12 @@ bool JavaVersion::requiresPermGen() const return !m_parseable || m_major < 8; } +bool JavaVersion::defaultsToUtf8() const +{ + // starting from Java 18, UTF-8 is the default charset: https://openjdk.org/jeps/400 + return m_parseable && m_major >= 18; +} + bool JavaVersion::isModular() const { return m_parseable && m_major >= 9; diff --git a/launcher/java/JavaVersion.h b/launcher/java/JavaVersion.h index dfb4770da..c070bdeec 100644 --- a/launcher/java/JavaVersion.h +++ b/launcher/java/JavaVersion.h @@ -25,7 +25,7 @@ class JavaVersion { bool operator>(const JavaVersion& rhs); bool requiresPermGen() const; - + bool defaultsToUtf8() const; bool isModular() const; QString toString() const; diff --git a/launcher/java/download/ArchiveDownloadTask.cpp b/launcher/java/download/ArchiveDownloadTask.cpp index ba1c96faf..bb7cc568d 100644 --- a/launcher/java/download/ArchiveDownloadTask.cpp +++ b/launcher/java/download/ArchiveDownloadTask.cpp @@ -65,7 +65,7 @@ void ArchiveDownloadTask::executeTask() void ArchiveDownloadTask::extractJava(QString input) { - setStatus(tr("Extracting java")); + setStatus(tr("Extracting Java")); if (input.endsWith("tar")) { setStatus(tr("Extracting Java (Progress is not reported for tar archives)")); QFile in(input); @@ -95,7 +95,7 @@ void ArchiveDownloadTask::extractJava(QString input) } auto files = zip->getFileNameList(); if (files.isEmpty()) { - emitFailed(tr("No files were found in the supplied zip file,")); + emitFailed(tr("No files were found in the supplied zip file.")); return; } m_task = makeShared(zip, m_final_path, files[0]); diff --git a/launcher/java/download/ManifestDownloadTask.cpp b/launcher/java/download/ManifestDownloadTask.cpp index 836afeaac..20b39e751 100644 --- a/launcher/java/download/ManifestDownloadTask.cpp +++ b/launcher/java/download/ManifestDownloadTask.cpp @@ -86,11 +86,10 @@ void ManifestDownloadTask::downloadJava(const QJsonDocument& doc) if (type == "directory") { FS::ensureFolderPathExists(file); } else if (type == "link") { - // this is linux only ! + // this is *nix only ! auto path = Json::ensureString(meta, "target"); if (!path.isEmpty()) { - auto target = FS::PathCombine(file, "../" + path); - QFile(target).link(file); + QFile::link(path, file); } } else if (type == "file") { // TODO download compressed version if it exists ? diff --git a/launcher/java/download/SymlinkTask.cpp b/launcher/java/download/SymlinkTask.cpp new file mode 100644 index 000000000..843c7caa9 --- /dev/null +++ b/launcher/java/download/SymlinkTask.cpp @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023-2024 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 "java/download/SymlinkTask.h" +#include + +#include "FileSystem.h" + +namespace Java { +SymlinkTask::SymlinkTask(QString final_path) : m_path(final_path) {} + +QString findBinPath(QString root, QString pattern) +{ + auto path = FS::PathCombine(root, pattern); + if (QFileInfo::exists(path)) { + return path; + } + + auto entries = QDir(root).entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); + for (auto& entry : entries) { + path = FS::PathCombine(entry.absoluteFilePath(), pattern); + if (QFileInfo::exists(path)) { + return path; + } + } + + return {}; +} + +void SymlinkTask::executeTask() +{ + setStatus(tr("Checking for Java binary path")); + const auto binPath = FS::PathCombine("bin", "java"); + const auto wantedPath = FS::PathCombine(m_path, binPath); + if (QFileInfo::exists(wantedPath)) { + emitSucceeded(); + return; + } + + setStatus(tr("Searching for Java binary path")); + const auto contentsPartialPath = FS::PathCombine("Contents", "Home", binPath); + const auto relativePathToBin = findBinPath(m_path, contentsPartialPath); + if (relativePathToBin.isEmpty()) { + emitFailed(tr("Failed to find Java binary path")); + return; + } + const auto folderToLink = relativePathToBin.chopped(binPath.length()); + + setStatus(tr("Collecting folders to symlink")); + auto entries = QDir(folderToLink).entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries); + QList files; + setProgress(0, entries.length()); + for (auto& entry : entries) { + files.append({ entry.absoluteFilePath(), FS::PathCombine(m_path, entry.fileName()) }); + } + + setStatus(tr("Symlinking Java binary path")); + FS::create_link folderLink(files); + connect(&folderLink, &FS::create_link::fileLinked, [this](QString src, QString dst) { setProgress(m_progress + 1, m_progressTotal); }); + if (!folderLink()) { + emitFailed(folderLink.getOSError().message().c_str()); + } else { + emitSucceeded(); + } +} + +} // namespace Java \ No newline at end of file diff --git a/launcher/java/download/SymlinkTask.h b/launcher/java/download/SymlinkTask.h new file mode 100644 index 000000000..88cb20dd7 --- /dev/null +++ b/launcher/java/download/SymlinkTask.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023-2024 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 "tasks/Task.h" +namespace Java { + +class SymlinkTask : public Task { + Q_OBJECT + public: + SymlinkTask(QString final_path); + virtual ~SymlinkTask() = default; + + void executeTask() override; + + protected: + QString m_path; + Task::Ptr m_task; +}; +} // namespace Java \ No newline at end of file diff --git a/launcher/launch/LaunchStep.cpp b/launcher/launch/LaunchStep.cpp index ebc534617..0b352ea9f 100644 --- a/launcher/launch/LaunchStep.cpp +++ b/launcher/launch/LaunchStep.cpp @@ -16,9 +16,8 @@ #include "LaunchStep.h" #include "LaunchTask.h" -void LaunchStep::bind(LaunchTask* parent) +LaunchStep::LaunchStep(LaunchTask* parent) : Task(), m_parent(parent) { - m_parent = parent; connect(this, &LaunchStep::readyForLaunch, parent, &LaunchTask::onReadyForLaunch); connect(this, &LaunchStep::logLine, parent, &LaunchTask::onLogLine); connect(this, &LaunchStep::logLines, parent, &LaunchTask::onLogLines); diff --git a/launcher/launch/LaunchStep.h b/launcher/launch/LaunchStep.h index 6a28afb1f..d49d7545b 100644 --- a/launcher/launch/LaunchStep.h +++ b/launcher/launch/LaunchStep.h @@ -24,11 +24,8 @@ class LaunchTask; class LaunchStep : public Task { Q_OBJECT public: /* methods */ - explicit LaunchStep(LaunchTask* parent) : Task(nullptr), m_parent(parent) { bind(parent); }; - virtual ~LaunchStep() {}; - - private: /* methods */ - void bind(LaunchTask* parent); + explicit LaunchStep(LaunchTask* parent); + virtual ~LaunchStep() = default; signals: void logLines(QStringList lines, MessageLevel::Enum level); diff --git a/launcher/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp index 06a32bd28..0251b302d 100644 --- a/launcher/launch/LaunchTask.cpp +++ b/launcher/launch/LaunchTask.cpp @@ -44,7 +44,6 @@ #include #include #include "MessageLevel.h" -#include "java/JavaChecker.h" #include "tasks/Task.h" void LaunchTask::init() @@ -52,14 +51,14 @@ void LaunchTask::init() m_instance->setRunning(true); } -shared_qobject_ptr LaunchTask::create(InstancePtr inst) +shared_qobject_ptr LaunchTask::create(MinecraftInstancePtr inst) { shared_qobject_ptr proc(new LaunchTask(inst)); proc->init(); return proc; } -LaunchTask::LaunchTask(InstancePtr instance) : m_instance(instance) {} +LaunchTask::LaunchTask(MinecraftInstancePtr instance) : m_instance(instance) {} void LaunchTask::appendStep(shared_qobject_ptr step) { diff --git a/launcher/launch/LaunchTask.h b/launcher/launch/LaunchTask.h index ae24b9a26..56065af5b 100644 --- a/launcher/launch/LaunchTask.h +++ b/launcher/launch/LaunchTask.h @@ -37,31 +37,31 @@ #pragma once #include +#include #include #include "BaseInstance.h" #include "LaunchStep.h" #include "LogModel.h" -#include "LoggedProcess.h" #include "MessageLevel.h" class LaunchTask : public Task { Q_OBJECT protected: - explicit LaunchTask(InstancePtr instance); + explicit LaunchTask(MinecraftInstancePtr instance); void init(); public: enum State { NotStarted, Running, Waiting, Failed, Aborted, Finished }; public: /* methods */ - static shared_qobject_ptr create(InstancePtr inst); - virtual ~LaunchTask() {}; + static shared_qobject_ptr create(MinecraftInstancePtr inst); + virtual ~LaunchTask() = default; void appendStep(shared_qobject_ptr step); void prependStep(shared_qobject_ptr step); void setCensorFilter(QMap filter); - InstancePtr instance() { return m_instance; } + MinecraftInstancePtr instance() { return m_instance; } void setPid(qint64 pid) { m_pid = pid; } @@ -116,7 +116,7 @@ class LaunchTask : public Task { void finalizeSteps(bool successful, const QString& error); protected: /* data */ - InstancePtr m_instance; + MinecraftInstancePtr m_instance; shared_qobject_ptr m_logModel; QList> m_steps; QMap m_censorFilter; diff --git a/launcher/launch/TaskStepWrapper.cpp b/launcher/launch/TaskStepWrapper.cpp new file mode 100644 index 000000000..db9e8fad2 --- /dev/null +++ b/launcher/launch/TaskStepWrapper.cpp @@ -0,0 +1,67 @@ +/* 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 "TaskStepWrapper.h" +#include "tasks/Task.h" + +void TaskStepWrapper::executeTask() +{ + if (m_state == Task::State::AbortedByUser) { + emitFailed(tr("Task aborted.")); + return; + } + connect(m_task.get(), &Task::finished, this, &TaskStepWrapper::updateFinished); + connect(m_task.get(), &Task::progress, this, &TaskStepWrapper::setProgress); + connect(m_task.get(), &Task::stepProgress, this, &TaskStepWrapper::propagateStepProgress); + connect(m_task.get(), &Task::status, this, &TaskStepWrapper::setStatus); + connect(m_task.get(), &Task::details, this, &TaskStepWrapper::setDetails); + emit progressReportingRequest(); +} + +void TaskStepWrapper::proceed() +{ + m_task->start(); +} + +void TaskStepWrapper::updateFinished() +{ + if (m_task->wasSuccessful()) { + m_task.reset(); + emitSucceeded(); + } else { + QString reason = tr("Instance update failed because: %1\n\n").arg(m_task->failReason()); + m_task.reset(); + emit logLine(reason, MessageLevel::Fatal); + emitFailed(reason); + } +} + +bool TaskStepWrapper::canAbort() const +{ + if (m_task) { + return m_task->canAbort(); + } + return true; +} + +bool TaskStepWrapper::abort() +{ + if (m_task && m_task->canAbort()) { + auto status = m_task->abort(); + emitFailed("Aborted."); + return status; + } + return Task::abort(); +} diff --git a/launcher/launch/steps/Update.h b/launcher/launch/TaskStepWrapper.h similarity index 73% rename from launcher/launch/steps/Update.h rename to launcher/launch/TaskStepWrapper.h index 878a43e7e..aec1b7037 100644 --- a/launcher/launch/steps/Update.h +++ b/launcher/launch/TaskStepWrapper.h @@ -21,12 +21,11 @@ #include #include -// FIXME: stupid. should be defined by the instance type? or even completely abstracted away... -class Update : public LaunchStep { +class TaskStepWrapper : public LaunchStep { Q_OBJECT public: - explicit Update(LaunchTask* parent, Net::Mode mode) : LaunchStep(parent), m_mode(mode) {}; - virtual ~Update() {}; + explicit TaskStepWrapper(LaunchTask* parent, Task::Ptr task) : LaunchStep(parent), m_task(task) {}; + virtual ~TaskStepWrapper() = default; void executeTask() override; bool canAbort() const override; @@ -38,7 +37,5 @@ class Update : public LaunchStep { void updateFinished(); private: - Task::Ptr m_updateTask; - bool m_aborted = false; - Net::Mode m_mode = Net::Mode::Offline; + Task::Ptr m_task; }; diff --git a/launcher/launch/steps/CheckJava.cpp b/launcher/launch/steps/CheckJava.cpp index 99ff62b67..0f8d27e94 100644 --- a/launcher/launch/steps/CheckJava.cpp +++ b/launcher/launch/steps/CheckJava.cpp @@ -94,7 +94,7 @@ void CheckJava::executeTask() // if timestamps are not the same, or something is missing, check! if (m_javaSignature != storedSignature || storedVersion.size() == 0 || storedArchitecture.size() == 0 || storedRealArchitecture.size() == 0 || storedVendor.size() == 0) { - m_JavaChecker.reset(new JavaChecker(realJavaPath, "", 0, 0, 0, 0, this)); + m_JavaChecker.reset(new JavaChecker(realJavaPath, "", 0, 0, 0, 0)); emit logLine(QString("Checking Java version..."), MessageLevel::Launcher); connect(m_JavaChecker.get(), &JavaChecker::checkFinished, this, &CheckJava::checkJavaFinished); m_JavaChecker->start(); @@ -106,6 +106,7 @@ void CheckJava::executeTask() auto vendorString = instance->settings()->get("JavaVendor").toString(); printJavaInfo(verString, archString, realArchString, vendorString); } + m_parent->instance()->updateRuntimeContext(); emitSucceeded(); } @@ -124,6 +125,7 @@ void CheckJava::checkJavaFinished(const JavaChecker::Result& result) emit logLine(QString("Java checker returned some invalid data we don't understand:"), MessageLevel::Error); emit logLines(result.outLog.split('\n'), MessageLevel::Warning); emit logLine("\nMinecraft might not start properly.", MessageLevel::Launcher); + m_parent->instance()->updateRuntimeContext(); emitSucceeded(); return; } @@ -135,6 +137,7 @@ void CheckJava::checkJavaFinished(const JavaChecker::Result& result) instance->settings()->set("JavaRealArchitecture", result.realPlatform); instance->settings()->set("JavaVendor", result.javaVendor); instance->settings()->set("JavaSignature", m_javaSignature); + m_parent->instance()->updateRuntimeContext(); emitSucceeded(); return; } diff --git a/launcher/launch/steps/CheckJava.h b/launcher/launch/steps/CheckJava.h index 7bb1ceb7e..1c59b0053 100644 --- a/launcher/launch/steps/CheckJava.h +++ b/launcher/launch/steps/CheckJava.h @@ -23,7 +23,7 @@ class CheckJava : public LaunchStep { Q_OBJECT public: explicit CheckJava(LaunchTask* parent) : LaunchStep(parent) {}; - virtual ~CheckJava() {}; + virtual ~CheckJava() = default; virtual void executeTask(); virtual bool canAbort() const { return false; } diff --git a/launcher/launch/steps/QuitAfterGameStop.h b/launcher/launch/steps/QuitAfterGameStop.h index d4324cce6..19ca59632 100644 --- a/launcher/launch/steps/QuitAfterGameStop.h +++ b/launcher/launch/steps/QuitAfterGameStop.h @@ -24,7 +24,7 @@ class QuitAfterGameStop : public LaunchStep { Q_OBJECT public: explicit QuitAfterGameStop(LaunchTask* parent) : LaunchStep(parent) {}; - virtual ~QuitAfterGameStop() {}; + virtual ~QuitAfterGameStop() = default; virtual void executeTask(); virtual bool canAbort() const { return false; } diff --git a/launcher/launch/steps/Update.cpp b/launcher/launch/steps/Update.cpp deleted file mode 100644 index f23c0bb4b..000000000 --- a/launcher/launch/steps/Update.cpp +++ /dev/null @@ -1,73 +0,0 @@ -/* 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 "Update.h" -#include - -void Update::executeTask() -{ - if (m_aborted) { - emitFailed(tr("Task aborted.")); - return; - } - m_updateTask.reset(m_parent->instance()->createUpdateTask(m_mode)); - if (m_updateTask) { - connect(m_updateTask.get(), &Task::finished, this, &Update::updateFinished); - connect(m_updateTask.get(), &Task::progress, this, &Update::setProgress); - connect(m_updateTask.get(), &Task::stepProgress, this, &Update::propagateStepProgress); - connect(m_updateTask.get(), &Task::status, this, &Update::setStatus); - connect(m_updateTask.get(), &Task::details, this, &Update::setDetails); - emit progressReportingRequest(); - return; - } - emitSucceeded(); -} - -void Update::proceed() -{ - m_updateTask->start(); -} - -void Update::updateFinished() -{ - if (m_updateTask->wasSuccessful()) { - m_updateTask.reset(); - emitSucceeded(); - } else { - QString reason = tr("Instance update failed because: %1\n\n").arg(m_updateTask->failReason()); - m_updateTask.reset(); - emit logLine(reason, MessageLevel::Fatal); - emitFailed(reason); - } -} - -bool Update::canAbort() const -{ - if (m_updateTask) { - return m_updateTask->canAbort(); - } - return true; -} - -bool Update::abort() -{ - m_aborted = true; - if (m_updateTask) { - if (m_updateTask->canAbort()) { - return m_updateTask->abort(); - } - } - return true; -} diff --git a/launcher/meta/Index.cpp b/launcher/meta/Index.cpp index bd0745b6b..1707854be 100644 --- a/launcher/meta/Index.cpp +++ b/launcher/meta/Index.cpp @@ -140,8 +140,8 @@ Task::Ptr Index::loadVersion(const QString& uid, const QString& version, Net::Mo } auto versionList = get(uid); - auto loadTask = makeShared( - this, tr("Load meta for %1:%2", "This is for the task name that loads the meta index.").arg(uid, version)); + auto loadTask = + makeShared(tr("Load meta for %1:%2", "This is for the task name that loads the meta index.").arg(uid, version)); if (status() != BaseEntity::LoadStatus::Remote || force) { loadTask->addTask(this->loadTask(mode)); } diff --git a/launcher/meta/VersionList.cpp b/launcher/meta/VersionList.cpp index 8b98223d1..f96355658 100644 --- a/launcher/meta/VersionList.cpp +++ b/launcher/meta/VersionList.cpp @@ -16,6 +16,7 @@ #include "VersionList.h" #include +#include #include "Application.h" #include "Index.h" @@ -33,8 +34,7 @@ VersionList::VersionList(const QString& uid, QObject* parent) : BaseVersionList( Task::Ptr VersionList::getLoadTask() { - auto loadTask = - makeShared(this, tr("Load meta for %1", "This is for the task name that loads the meta index.").arg(m_uid)); + auto loadTask = makeShared(tr("Load meta for %1", "This is for the task name that loads the meta index.").arg(m_uid)); loadTask->addTask(APPLICATION->metadataIndex()->loadTask(Net::Mode::Online)); loadTask->addTask(this->loadTask(Net::Mode::Online)); return loadTask; @@ -99,7 +99,7 @@ QVariant VersionList::data(const QModelIndex& index, int role) const case VersionPtrRole: return QVariant::fromValue(version); case RecommendedRole: - return version->isRecommended(); + return version->isRecommended() || m_externalRecommendsVersions.contains(version->version()); case JavaMajorRole: { auto major = version->version(); if (major.startsWith("java")) { @@ -192,6 +192,16 @@ void VersionList::parse(const QJsonObject& obj) parseVersionList(obj, this); } +void VersionList::addExternalRecommends(const QStringList& recommends) +{ + m_externalRecommendsVersions.append(recommends); +} + +void VersionList::clearExternalRecommends() +{ + m_externalRecommendsVersions.clear(); +} + // FIXME: this is dumb, we have 'recommended' as part of the metadata already... static const Meta::Version::Ptr& getBetterVersion(const Meta::Version::Ptr& a, const Meta::Version::Ptr& b) { @@ -276,4 +286,35 @@ void VersionList::waitToLoad() task->start(); ev.exec(); } + +Version::Ptr VersionList::getRecommendedForParent(const QString& uid, const QString& version) +{ + auto foundExplicit = std::find_if(m_versions.begin(), m_versions.end(), [uid, version](Version::Ptr ver) -> bool { + auto& reqs = ver->requiredSet(); + auto parentReq = std::find_if(reqs.begin(), reqs.end(), [uid, version](const Require& req) -> bool { + return req.uid == uid && req.equalsVersion == version; + }); + return parentReq != reqs.end() && ver->isRecommended(); + }); + if (foundExplicit != m_versions.end()) { + return *foundExplicit; + } + return nullptr; +} + +Version::Ptr VersionList::getLatestForParent(const QString& uid, const QString& version) +{ + Version::Ptr latestCompat = nullptr; + for (auto ver : m_versions) { + auto& reqs = ver->requiredSet(); + auto parentReq = std::find_if(reqs.begin(), reqs.end(), [uid, version](const Require& req) -> bool { + return req.uid == uid && req.equalsVersion == version; + }); + if (parentReq != reqs.end()) { + latestCompat = getBetterVersion(latestCompat, ver); + } + } + return latestCompat; +} + } // namespace Meta diff --git a/launcher/meta/VersionList.h b/launcher/meta/VersionList.h index 94aaae22c..4215439db 100644 --- a/launcher/meta/VersionList.h +++ b/launcher/meta/VersionList.h @@ -43,6 +43,8 @@ class VersionList : public BaseVersionList, public BaseEntity { void sortVersions() override; BaseVersion::Ptr getRecommended() const override; + Version::Ptr getRecommendedForParent(const QString& uid, const QString& version); + Version::Ptr getLatestForParent(const QString& uid, const QString& version); QVariant data(const QModelIndex& index, int role) const override; RoleList providesRoles() const override; @@ -70,6 +72,8 @@ class VersionList : public BaseVersionList, public BaseEntity { void merge(const VersionList::Ptr& other); void mergeFromIndex(const VersionList::Ptr& other); void parse(const QJsonObject& obj) override; + void addExternalRecommends(const QStringList& recommends); + void clearExternalRecommends(); signals: void nameChanged(const QString& name); @@ -79,6 +83,7 @@ class VersionList : public BaseVersionList, public BaseEntity { private: QVector m_versions; + QStringList m_externalRecommendsVersions; QHash m_lookup; QString m_uid; QString m_name; diff --git a/launcher/minecraft/Component.cpp b/launcher/minecraft/Component.cpp index 32a1deb68..ad7ef545c 100644 --- a/launcher/minecraft/Component.cpp +++ b/launcher/minecraft/Component.cpp @@ -44,10 +44,19 @@ #include "OneSixVersionFormat.h" #include "VersionFile.h" #include "meta/Version.h" +#include "minecraft/Component.h" #include "minecraft/PackProfile.h" #include +const QMap Component::KNOWN_MODLOADERS = { + { "net.neoforged", { ModPlatform::NeoForge, { "net.minecraftforge", "net.fabricmc.fabric-loader", "org.quiltmc.quilt-loader" } } }, + { "net.minecraftforge", { ModPlatform::Forge, { "net.neoforged", "net.fabricmc.fabric-loader", "org.quiltmc.quilt-loader" } } }, + { "net.fabricmc.fabric-loader", { ModPlatform::Fabric, { "net.minecraftforge", "net.neoforged", "org.quiltmc.quilt-loader" } } }, + { "org.quiltmc.quilt-loader", { ModPlatform::Quilt, { "net.minecraftforge", "net.neoforged", "net.fabricmc.fabric-loader" } } }, + { "com.mumfrey.liteloader", { ModPlatform::LiteLoader, {} } } +}; + Component::Component(PackProfile* parent, const QString& uid) { assert(parent); @@ -213,16 +222,33 @@ bool Component::isMoveable() return true; } -bool Component::isVersionChangeable() +bool Component::isVersionChangeable(bool wait) { auto list = getVersionList(); if (list) { - list->waitToLoad(); + if (wait) + list->waitToLoad(); return list->count() != 0; } return false; } +bool Component::isKnownModloader() +{ + auto iter = KNOWN_MODLOADERS.find(m_uid); + return iter != KNOWN_MODLOADERS.cend(); +} + +QStringList Component::knownConflictingComponents() +{ + auto iter = KNOWN_MODLOADERS.find(m_uid); + if (iter != KNOWN_MODLOADERS.cend()) { + return (*iter).knownConflictingComponents; + } else { + return {}; + } +} + void Component::setImportant(bool state) { if (m_important != state) { @@ -235,7 +261,8 @@ ProblemSeverity Component::getProblemSeverity() const { auto file = getVersionFile(); if (file) { - return file->getProblemSeverity(); + auto severity = file->getProblemSeverity(); + return m_componentProblemSeverity > severity ? m_componentProblemSeverity : severity; } return ProblemSeverity::Error; } @@ -244,11 +271,31 @@ const QList Component::getProblems() const { auto file = getVersionFile(); if (file) { - return file->getProblems(); + auto problems = file->getProblems(); + problems.append(m_componentProblems); + return problems; } return { { ProblemSeverity::Error, QObject::tr("Patch is not loaded yet.") } }; } +void Component::addComponentProblem(ProblemSeverity severity, const QString& description) +{ + if (severity > m_componentProblemSeverity) { + m_componentProblemSeverity = severity; + } + m_componentProblems.append({ severity, description }); + + emit dataChanged(); +} + +void Component::resetComponentProblems() +{ + m_componentProblems.clear(); + m_componentProblemSeverity = ProblemSeverity::None; + + emit dataChanged(); +} + void Component::setVersion(const QString& version) { if (version == m_version) { @@ -402,3 +449,36 @@ void Component::updateCachedData() emit dataChanged(); } } + +void Component::waitLoadMeta() +{ + if (!m_loaded) { + if (!m_metaVersion || !m_metaVersion->isLoaded()) { + // wait for the loaded version from meta + m_metaVersion = APPLICATION->metadataIndex()->getLoadedVersion(m_uid, m_version); + } + m_loaded = true; + updateCachedData(); + } +} + +void Component::setUpdateAction(UpdateAction action) +{ + m_updateAction = action; +} + +UpdateAction Component::getUpdateAction() +{ + return m_updateAction; +} + +void Component::clearUpdateAction() +{ + m_updateAction = UpdateAction{ UpdateActionNone{} }; +} + +QDebug operator<<(QDebug d, const Component& comp) +{ + d << "Component(" << comp.m_uid << " : " << comp.m_cachedVersion << ")"; + return d; +} diff --git a/launcher/minecraft/Component.h b/launcher/minecraft/Component.h index 8aa6b4743..203cc2241 100644 --- a/launcher/minecraft/Component.h +++ b/launcher/minecraft/Component.h @@ -4,9 +4,12 @@ #include #include #include +#include +#include #include "ProblemProvider.h" #include "QObjectPtr.h" #include "meta/JsonFormat.h" +#include "modplatform/ModIndex.h" class PackProfile; class LaunchProfile; @@ -16,6 +19,36 @@ class VersionList; } // namespace Meta class VersionFile; +struct UpdateActionChangeVersion { + /// version to change to + QString targetVersion; +}; +struct UpdateActionLatestRecommendedCompatible { + /// Parent uid + QString parentUid; + QString parentName; + /// Parent version + QString version; + /// +}; +struct UpdateActionRemove {}; +struct UpdateActionImportantChanged { + QString oldVersion; +}; + +using UpdateActionNone = std::monostate; + +using UpdateAction = std::variant; + +struct ModloaderMapEntry { + ModPlatform::ModLoaderType type; + QStringList knownConflictingComponents; +}; + class Component : public QObject, public ProblemProvider { Q_OBJECT public: @@ -26,6 +59,8 @@ class Component : public QObject, public ProblemProvider { virtual ~Component() {} + static const QMap KNOWN_MODLOADERS; + void applyTo(LaunchProfile* profile); bool isEnabled(); @@ -37,7 +72,9 @@ class Component : public QObject, public ProblemProvider { bool isRevertible(); bool isRemovable(); bool isCustom(); - bool isVersionChangeable(); + bool isVersionChangeable(bool wait = true); + bool isKnownModloader(); + QStringList knownConflictingComponents(); // DEPRECATED: explicit numeric order values, used for loading old non-component config. TODO: refactor and move to migration code void setOrder(int order); @@ -58,6 +95,8 @@ class Component : public QObject, public ProblemProvider { const QList getProblems() const override; ProblemSeverity getProblemSeverity() const override; + void addComponentProblem(ProblemSeverity severity, const QString& description); + void resetComponentProblems(); void setVersion(const QString& version); bool customize(); @@ -65,6 +104,12 @@ class Component : public QObject, public ProblemProvider { void updateCachedData(); + void waitLoadMeta(); + + void setUpdateAction(UpdateAction action); + void clearUpdateAction(); + UpdateAction getUpdateAction(); + signals: void dataChanged(); @@ -102,6 +147,11 @@ class Component : public QObject, public ProblemProvider { std::shared_ptr m_metaVersion; std::shared_ptr m_file; bool m_loaded = false; + + private: + QList m_componentProblems; + ProblemSeverity m_componentProblemSeverity = ProblemSeverity::None; + UpdateAction m_updateAction = UpdateAction{ UpdateActionNone{} }; }; using ComponentPtr = shared_qobject_ptr; diff --git a/launcher/minecraft/ComponentUpdateTask.cpp b/launcher/minecraft/ComponentUpdateTask.cpp index 4d205af6c..36a07ee72 100644 --- a/launcher/minecraft/ComponentUpdateTask.cpp +++ b/launcher/minecraft/ComponentUpdateTask.cpp @@ -1,13 +1,16 @@ #include "ComponentUpdateTask.h" +#include #include "Component.h" #include "ComponentUpdateTask_p.h" #include "PackProfile.h" #include "PackProfile_p.h" +#include "ProblemProvider.h" #include "Version.h" #include "cassert" #include "meta/Index.h" #include "meta/Version.h" +#include "minecraft/MinecraftInstance.h" #include "minecraft/OneSixVersionFormat.h" #include "minecraft/ProfileUtils.h" #include "net/Mode.h" @@ -15,6 +18,8 @@ #include "Application.h" #include "tasks/Task.h" +#include "minecraft/Logging.h" + /* * This is responsible for loading the components of a component list AND resolving dependency issues between them */ @@ -33,10 +38,10 @@ * If the component list changes, start over. */ -ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list, QObject* parent) : Task(parent) +ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list) : Task() { d.reset(new ComponentUpdateTaskData); - d->m_list = list; + d->m_profile = list; d->mode = mode; d->netmode = netmode; } @@ -45,7 +50,7 @@ ComponentUpdateTask::~ComponentUpdateTask() {} void ComponentUpdateTask::executeTask() { - qDebug() << "Loading components"; + qCDebug(instanceProfileResolveC) << "Loading components"; loadComponents(); } @@ -63,7 +68,7 @@ LoadResult composeLoadResult(LoadResult a, LoadResult b) static LoadResult loadComponent(ComponentPtr component, Task::Ptr& loadTask, Net::Mode netmode) { if (component->m_loaded) { - qDebug() << component->getName() << "is already loaded"; + qCDebug(instanceProfileResolveC) << component->getName() << "is already loaded"; return LoadResult::LoadedLocal; } @@ -144,10 +149,11 @@ void ComponentUpdateTask::loadComponents() d->remoteLoadSuccessful = true; // load all the components OR their lists... - for (auto component : d->m_list->d->components) { + for (auto component : d->m_profile->d->components) { Task::Ptr loadTask; LoadResult singleResult; RemoteLoadStatus::Type loadType; + component->resetComponentProblems(); // FIXME: to do this right, we need to load the lists and decide on which versions to use during dependency resolution. For now, // ignore all that... #if 0 @@ -175,7 +181,8 @@ void ComponentUpdateTask::loadComponents() } result = composeLoadResult(result, singleResult); if (loadTask) { - qDebug() << "Remote loading is being run for" << component->getName(); + qCDebug(instanceProfileResolveC) << d->m_profile->d->m_instance->name() << "|" + << "Remote loading is being run for" << component->getName(); connect(loadTask.get(), &Task::succeeded, this, [this, taskIndex]() { remoteLoadSucceeded(taskIndex); }); connect(loadTask.get(), &Task::failed, this, [this, taskIndex](const QString& error) { remoteLoadFailed(taskIndex, error); }); connect(loadTask.get(), &Task::aborted, this, [this, taskIndex]() { remoteLoadFailed(taskIndex, tr("Aborted")); }); @@ -192,6 +199,7 @@ void ComponentUpdateTask::loadComponents() switch (result) { case LoadResult::LoadedLocal: { // Everything got loaded. Advance to dependency resolution. + performUpdateActions(); resolveDependencies(d->mode == Mode::Launch || d->netmode == Net::Mode::Offline); break; } @@ -270,8 +278,8 @@ static bool gatherRequirementsFromComponents(const ComponentContainer& input, Re output.erase(componenRequireEx); output.insert(result.outcome); } else { - qCritical() << "Conflicting requirements:" << componentRequire.uid << "versions:" << componentRequire.equalsVersion - << ";" << (*found).equalsVersion; + qCCritical(instanceProfileResolveC) << "Conflicting requirements:" << componentRequire.uid + << "versions:" << componentRequire.equalsVersion << ";" << (*found).equalsVersion; } succeeded &= result.ok; } else { @@ -353,22 +361,22 @@ static bool getTrivialComponentChanges(const ComponentIndex& index, const Requir } while (false); switch (decision) { case Decision::Undetermined: - qCritical() << "No decision for" << reqStr; + qCCritical(instanceProfileResolveC) << "No decision for" << reqStr; succeeded = false; break; case Decision::Met: - qDebug() << reqStr << "Is met."; + qCDebug(instanceProfileResolveC) << reqStr << "Is met."; break; case Decision::Missing: - qDebug() << reqStr << "Is missing and should be added at" << req.indexOfFirstDependee; + qCDebug(instanceProfileResolveC) << reqStr << "Is missing and should be added at" << req.indexOfFirstDependee; toAdd.insert(req); break; case Decision::VersionNotSame: - qDebug() << reqStr << "already has different version that can be changed."; + qCDebug(instanceProfileResolveC) << reqStr << "already has different version that can be changed."; toChange.insert(req); break; case Decision::LockedVersionNotSame: - qDebug() << reqStr << "already has different version that cannot be changed."; + qCDebug(instanceProfileResolveC) << reqStr << "already has different version that cannot be changed."; succeeded = false; break; } @@ -376,12 +384,48 @@ static bool getTrivialComponentChanges(const ComponentIndex& index, const Requir return succeeded; } +ComponentContainer ComponentUpdateTask::collectTreeLinked(const QString& uid) +{ + ComponentContainer linked; + + auto& components = d->m_profile->d->components; + auto& componentIndex = d->m_profile->d->componentIndex; + auto& instance = d->m_profile->d->m_instance; + for (auto comp : components) { + qCDebug(instanceProfileResolveC) << instance->name() << "|" + << "scanning" << comp->getID() << ":" << comp->getVersion() << "for tree link"; + auto dep = std::find_if(comp->m_cachedRequires.cbegin(), comp->m_cachedRequires.cend(), + [uid](const Meta::Require& req) -> bool { return req.uid == uid; }); + if (dep != comp->m_cachedRequires.cend()) { + qCDebug(instanceProfileResolveC) << instance->name() << "|" << comp->getID() << ":" << comp->getVersion() << "depends on" + << uid; + linked.append(comp); + } + } + auto iter = componentIndex.find(uid); + if (iter != componentIndex.end()) { + ComponentPtr comp = *iter; + comp->updateCachedData(); + qCDebug(instanceProfileResolveC) << instance->name() << "|" << comp->getID() << ":" << comp->getVersion() << "has" + << comp->m_cachedRequires.size() << "dependencies"; + for (auto dep : comp->m_cachedRequires) { + qCDebug(instanceProfileC) << instance->name() << "|" << uid << "depends on" << dep.uid; + auto found = componentIndex.find(dep.uid); + if (found != componentIndex.end()) { + qCDebug(instanceProfileC) << instance->name() << "|" << (*found)->getID() << "is present"; + linked.append(*found); + } + } + } + return linked; +} + // FIXME, TODO: decouple dependency resolution from loading // FIXME: This works directly with the PackProfile internals. It shouldn't! It needs richer data types than PackProfile uses. // FIXME: throw all this away and use a graph void ComponentUpdateTask::resolveDependencies(bool checkOnly) { - qDebug() << "Resolving dependencies"; + qCDebug(instanceProfileResolveC) << "Resolving dependencies"; /* * this is a naive dependency resolving algorithm. all it does is check for following conditions and react in simple ways: * 1. There are conflicting dependencies on the same uid with different exact version numbers @@ -393,8 +437,8 @@ void ComponentUpdateTask::resolveDependencies(bool checkOnly) * * NOTE: this is a placeholder and should eventually be replaced with something 'serious' */ - auto& components = d->m_list->d->components; - auto& componentIndex = d->m_list->d->componentIndex; + auto& components = d->m_profile->d->components; + auto& componentIndex = d->m_profile->d->componentIndex; RequireExSet allRequires; QStringList toRemove; @@ -402,15 +446,16 @@ void ComponentUpdateTask::resolveDependencies(bool checkOnly) allRequires.clear(); toRemove.clear(); if (!gatherRequirementsFromComponents(components, allRequires)) { + finalizeComponents(); emitFailed(tr("Conflicting requirements detected during dependency checking!")); return; } getTrivialRemovals(components, allRequires, toRemove); if (!toRemove.isEmpty()) { - qDebug() << "Removing obsolete components..."; + qCDebug(instanceProfileResolveC) << "Removing obsolete components..."; for (auto& remove : toRemove) { - qDebug() << "Removing" << remove; - d->m_list->remove(remove); + qCDebug(instanceProfileResolveC) << "Removing" << remove; + d->m_profile->remove(remove); } } } while (!toRemove.isEmpty()); @@ -418,10 +463,12 @@ void ComponentUpdateTask::resolveDependencies(bool checkOnly) RequireExSet toChange; bool succeeded = getTrivialComponentChanges(componentIndex, allRequires, toAdd, toChange); if (!succeeded) { + finalizeComponents(); emitFailed(tr("Instance has conflicting dependencies.")); return; } if (checkOnly) { + finalizeComponents(); if (toAdd.size() || toChange.size()) { emitFailed(tr("Instance has unresolved dependencies while loading/checking for launch.")); } else { @@ -434,14 +481,15 @@ void ComponentUpdateTask::resolveDependencies(bool checkOnly) if (toAdd.size()) { // add stuff... for (auto& add : toAdd) { - auto component = makeShared(d->m_list, add.uid); + auto component = makeShared(d->m_profile, add.uid); if (!add.equalsVersion.isEmpty()) { // exact version - qDebug() << "Adding" << add.uid << "version" << add.equalsVersion << "at position" << add.indexOfFirstDependee; + qCDebug(instanceProfileResolveC) + << "Adding" << add.uid << "version" << add.equalsVersion << "at position" << add.indexOfFirstDependee; component->m_version = add.equalsVersion; } else { // version needs to be decided - qDebug() << "Adding" << add.uid << "at position" << add.indexOfFirstDependee; + qCDebug(instanceProfileResolveC) << "Adding" << add.uid << "at position" << add.indexOfFirstDependee; // ############################################################################################################ // HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded. if (!add.suggests.isEmpty()) { @@ -464,7 +512,7 @@ void ComponentUpdateTask::resolveDependencies(bool checkOnly) } component->m_dependencyOnly = true; // FIXME: this should not work directly with the component list - d->m_list->insertComponent(add.indexOfFirstDependee, component); + d->m_profile->insertComponent(add.indexOfFirstDependee, component); componentIndex[add.uid] = component; } recursionNeeded = true; @@ -473,7 +521,7 @@ void ComponentUpdateTask::resolveDependencies(bool checkOnly) // change a version of something that exists for (auto& change : toChange) { // FIXME: this should not work directly with the component list - qDebug() << "Setting version of " << change.uid << "to" << change.equalsVersion; + qCDebug(instanceProfileResolveC) << "Setting version of " << change.uid << "to" << change.equalsVersion; auto component = componentIndex[change.uid]; component->setVersion(change.equalsVersion); } @@ -483,14 +531,182 @@ void ComponentUpdateTask::resolveDependencies(bool checkOnly) if (recursionNeeded) { loadComponents(); } else { + finalizeComponents(); emitSucceeded(); } } +// Variant visitation via lambda +template +struct overload : Ts... { + using Ts::operator()...; +}; +template +overload(Ts...) -> overload; + +void ComponentUpdateTask::performUpdateActions() +{ + auto& instance = d->m_profile->d->m_instance; + bool addedActions; + QStringList toRemove; + do { + addedActions = false; + toRemove.clear(); + auto& components = d->m_profile->d->components; + auto& componentIndex = d->m_profile->d->componentIndex; + for (auto component : components) { + if (!component) { + continue; + } + auto action = component->getUpdateAction(); + auto visitor = + overload{ [](const UpdateActionNone&) { + // noop + }, + [&component, &instance](const UpdateActionChangeVersion& cv) { + qCDebug(instanceProfileResolveC) << instance->name() << "|" + << "UpdateActionChangeVersion" << component->getID() << ":" + << component->getVersion() << "change to" << cv.targetVersion; + component->setVersion(cv.targetVersion); + component->waitLoadMeta(); + }, + [&component, &instance](const UpdateActionLatestRecommendedCompatible lrc) { + qCDebug(instanceProfileResolveC) + << instance->name() << "|" + << "UpdateActionLatestRecommendedCompatible" << component->getID() << ":" << component->getVersion() + << "updating to latest recommend or compatible with" << lrc.parentUid << lrc.version; + auto versionList = APPLICATION->metadataIndex()->get(component->getID()); + if (versionList) { + versionList->waitToLoad(); + auto recommended = versionList->getRecommendedForParent(lrc.parentUid, lrc.version); + if (!recommended) { + recommended = versionList->getLatestForParent(lrc.parentUid, lrc.version); + } + if (recommended) { + component->setVersion(recommended->version()); + component->waitLoadMeta(); + return; + } else { + component->addComponentProblem(ProblemSeverity::Error, + QObject::tr("No compatible version of %1 found for %2 %3") + .arg(component->getName(), lrc.parentName, lrc.version)); + } + } else { + component->addComponentProblem( + ProblemSeverity::Error, + QObject::tr("No version list in metadata index for %1").arg(component->getID())); + } + }, + [&component, &instance, &toRemove](const UpdateActionRemove&) { + qCDebug(instanceProfileResolveC) + << instance->name() << "|" + << "UpdateActionRemove" << component->getID() << ":" << component->getVersion() << "removing"; + toRemove.append(component->getID()); + }, + [this, &component, &instance, &addedActions, &componentIndex](const UpdateActionImportantChanged& ic) { + qCDebug(instanceProfileResolveC) + << instance->name() << "|" + << "UpdateImportantChanged" << component->getID() << ":" << component->getVersion() << "was changed from" + << ic.oldVersion << "updating linked components"; + auto oldVersion = APPLICATION->metadataIndex()->getLoadedVersion(component->getID(), ic.oldVersion); + for (auto oldReq : oldVersion->requiredSet()) { + auto currentlyRequired = component->m_cachedRequires.find(oldReq); + if (currentlyRequired == component->m_cachedRequires.cend()) { + auto oldReqComp = componentIndex.find(oldReq.uid); + if (oldReqComp != componentIndex.cend()) { + (*oldReqComp)->setUpdateAction(UpdateAction{ UpdateActionRemove{} }); + addedActions = true; + } + } + } + auto linked = collectTreeLinked(component->getID()); + for (auto comp : linked) { + if (comp->isCustom()) { + continue; + } + auto compUid = comp->getID(); + auto parentReq = std::find_if(component->m_cachedRequires.begin(), component->m_cachedRequires.end(), + [compUid](const Meta::Require& req) { return req.uid == compUid; }); + if (parentReq != component->m_cachedRequires.end()) { + auto newVersion = parentReq->equalsVersion.isEmpty() ? parentReq->suggests : parentReq->equalsVersion; + if (!newVersion.isEmpty()) { + comp->setUpdateAction(UpdateAction{ UpdateActionChangeVersion{ newVersion } }); + } else { + comp->setUpdateAction(UpdateAction{ UpdateActionLatestRecommendedCompatible{ + component->getID(), + component->getName(), + component->getVersion(), + } }); + } + } else { + comp->setUpdateAction(UpdateAction{ UpdateActionLatestRecommendedCompatible{ + component->getID(), + component->getName(), + component->getVersion(), + } }); + } + addedActions = true; + } + } }; + std::visit(visitor, action); + component->clearUpdateAction(); + for (auto uid : toRemove) { + d->m_profile->remove(uid); + } + } + } while (addedActions); +} + +void ComponentUpdateTask::finalizeComponents() +{ + auto& components = d->m_profile->d->components; + auto& componentIndex = d->m_profile->d->componentIndex; + for (auto component : components) { + for (auto req : component->m_cachedRequires) { + auto found = componentIndex.find(req.uid); + if (found == componentIndex.cend()) { + component->addComponentProblem( + ProblemSeverity::Error, + QObject::tr("%1 is missing requirement %2 %3") + .arg(component->getName(), req.uid, req.equalsVersion.isEmpty() ? req.suggests : req.equalsVersion)); + } else { + auto reqComp = *found; + if (!reqComp->getProblems().isEmpty()) { + component->addComponentProblem( + reqComp->getProblemSeverity(), + QObject::tr("%1, a dependency of this component, has reported issues").arg(reqComp->getName())); + } + if (!req.equalsVersion.isEmpty() && req.equalsVersion != reqComp->getVersion()) { + component->addComponentProblem(ProblemSeverity::Error, + QObject::tr("%1, a dependency of this component, is not the required version %2") + .arg(reqComp->getName(), req.equalsVersion)); + } else if (!req.suggests.isEmpty() && req.suggests != reqComp->getVersion()) { + component->addComponentProblem(ProblemSeverity::Warning, + QObject::tr("%1, a dependency of this component, is not the suggested version %2") + .arg(reqComp->getName(), req.suggests)); + } + } + } + for (auto conflict : component->knownConflictingComponents()) { + auto found = componentIndex.find(conflict); + if (found != componentIndex.cend()) { + auto foundComp = *found; + if (foundComp->isCustom()) { + continue; + } + component->addComponentProblem( + ProblemSeverity::Warning, + QObject::tr("%1 and %2 are known to not work together. It is recommended to remove one of them.") + .arg(component->getName(), foundComp->getName())); + } + } + } +} + void ComponentUpdateTask::remoteLoadSucceeded(size_t taskIndex) { if (static_cast(d->remoteLoadStatusList.size()) < taskIndex) { - qWarning() << "Got task index outside of results" << taskIndex; + qCWarning(instanceProfileResolveC) << "Got task index outside of results" << taskIndex; return; } auto& taskSlot = d->remoteLoadStatusList[taskIndex]; @@ -498,16 +714,16 @@ void ComponentUpdateTask::remoteLoadSucceeded(size_t taskIndex) disconnect(taskSlot.task.get(), &Task::failed, this, nullptr); disconnect(taskSlot.task.get(), &Task::aborted, this, nullptr); if (taskSlot.finished) { - qWarning() << "Got multiple results from remote load task" << taskIndex; + qCWarning(instanceProfileResolveC) << "Got multiple results from remote load task" << taskIndex; return; } - qDebug() << "Remote task" << taskIndex << "succeeded"; + qCDebug(instanceProfileResolveC) << "Remote task" << taskIndex << "succeeded"; taskSlot.succeeded = false; taskSlot.finished = true; d->remoteTasksInProgress--; // update the cached data of the component from the downloaded version file. if (taskSlot.type == RemoteLoadStatus::Type::Version) { - auto component = d->m_list->getComponent(taskSlot.PackProfileIndex); + auto component = d->m_profile->getComponent(taskSlot.PackProfileIndex); component->m_loaded = true; component->updateCachedData(); } @@ -517,7 +733,7 @@ void ComponentUpdateTask::remoteLoadSucceeded(size_t taskIndex) void ComponentUpdateTask::remoteLoadFailed(size_t taskIndex, const QString& msg) { if (static_cast(d->remoteLoadStatusList.size()) < taskIndex) { - qWarning() << "Got task index outside of results" << taskIndex; + qCWarning(instanceProfileResolveC) << "Got task index outside of results" << taskIndex; return; } auto& taskSlot = d->remoteLoadStatusList[taskIndex]; @@ -525,10 +741,10 @@ void ComponentUpdateTask::remoteLoadFailed(size_t taskIndex, const QString& msg) disconnect(taskSlot.task.get(), &Task::failed, this, nullptr); disconnect(taskSlot.task.get(), &Task::aborted, this, nullptr); if (taskSlot.finished) { - qWarning() << "Got multiple results from remote load task" << taskIndex; + qCWarning(instanceProfileResolveC) << "Got multiple results from remote load task" << taskIndex; return; } - qDebug() << "Remote task" << taskIndex << "failed: " << msg; + qCDebug(instanceProfileResolveC) << "Remote task" << taskIndex << "failed: " << msg; d->remoteLoadSuccessful = false; taskSlot.succeeded = false; taskSlot.finished = true; @@ -546,6 +762,7 @@ void ComponentUpdateTask::checkIfAllFinished() if (d->remoteLoadSuccessful) { // nothing bad happened... clear the temp load status and proceed with looking at dependencies d->remoteLoadStatusList.clear(); + performUpdateActions(); resolveDependencies(d->mode == Mode::Launch); } else { // remote load failed... report error and bail diff --git a/launcher/minecraft/ComponentUpdateTask.h b/launcher/minecraft/ComponentUpdateTask.h index 2f396a049..64c55877b 100644 --- a/launcher/minecraft/ComponentUpdateTask.h +++ b/launcher/minecraft/ComponentUpdateTask.h @@ -1,5 +1,6 @@ #pragma once +#include "minecraft/Component.h" #include "net/Mode.h" #include "tasks/Task.h" @@ -13,7 +14,7 @@ class ComponentUpdateTask : public Task { enum class Mode { Launch, Resolution }; public: - explicit ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list, QObject* parent = 0); + explicit ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list); virtual ~ComponentUpdateTask(); protected: @@ -21,7 +22,11 @@ class ComponentUpdateTask : public Task { private: void loadComponents(); + /// collects components that are dependent on or dependencies of the component + QList collectTreeLinked(const QString& uid); void resolveDependencies(bool checkOnly); + void performUpdateActions(); + void finalizeComponents(); void remoteLoadSucceeded(size_t index); void remoteLoadFailed(size_t index, const QString& msg); diff --git a/launcher/minecraft/ComponentUpdateTask_p.h b/launcher/minecraft/ComponentUpdateTask_p.h index b82553700..2fc0b6d9a 100644 --- a/launcher/minecraft/ComponentUpdateTask_p.h +++ b/launcher/minecraft/ComponentUpdateTask_p.h @@ -6,6 +6,8 @@ #include "net/Mode.h" #include "tasks/Task.h" +#include "minecraft/ComponentUpdateTask.h" + class PackProfile; struct RemoteLoadStatus { @@ -18,7 +20,7 @@ struct RemoteLoadStatus { }; struct ComponentUpdateTaskData { - PackProfile* m_list = nullptr; + PackProfile* m_profile = nullptr; QList remoteLoadStatusList; bool remoteLoadSuccessful = true; size_t remoteTasksInProgress = 0; diff --git a/launcher/minecraft/LaunchProfile.cpp b/launcher/minecraft/LaunchProfile.cpp index 468798850..c11a0f915 100644 --- a/launcher/minecraft/LaunchProfile.cpp +++ b/launcher/minecraft/LaunchProfile.cpp @@ -164,6 +164,7 @@ void LaunchProfile::applyCompatibleJavaMajors(QList& javaMajor) { m_compatibleJavaMajors.append(javaMajor); } + void LaunchProfile::applyCompatibleJavaName(QString javaName) { if (!javaName.isEmpty()) diff --git a/launcher/minecraft/Logging.cpp b/launcher/minecraft/Logging.cpp new file mode 100644 index 000000000..92596de3e --- /dev/null +++ b/launcher/minecraft/Logging.cpp @@ -0,0 +1,25 @@ + +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * 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 "minecraft/Logging.h" +#include + +Q_LOGGING_CATEGORY(instanceProfileC, "launcher.instance.profile") +Q_LOGGING_CATEGORY(instanceProfileResolveC, "launcher.instance.profile.resolve") diff --git a/launcher/minecraft/Logging.h b/launcher/minecraft/Logging.h new file mode 100644 index 000000000..00d43f419 --- /dev/null +++ b/launcher/minecraft/Logging.h @@ -0,0 +1,26 @@ + +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * 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 + +Q_DECLARE_LOGGING_CATEGORY(instanceProfileC) +Q_DECLARE_LOGGING_CATEGORY(instanceProfileResolveC) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index d861056bf..d6d45af6b 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -43,6 +43,9 @@ #include "minecraft/launch/CreateGameFolders.h" #include "minecraft/launch/ExtractNatives.h" #include "minecraft/launch/PrintInstanceInfo.h" +#include "minecraft/update/AssetUpdateTask.h" +#include "minecraft/update/FMLLibrariesTask.h" +#include "minecraft/update/LibrariesTask.h" #include "settings/Setting.h" #include "settings/SettingsObject.h" @@ -53,13 +56,13 @@ #include "pathmatcher/RegexpMatcher.h" #include "launch/LaunchTask.h" +#include "launch/TaskStepWrapper.h" #include "launch/steps/CheckJava.h" #include "launch/steps/LookupServerAddress.h" #include "launch/steps/PostLaunchCommand.h" #include "launch/steps/PreLaunchCommand.h" #include "launch/steps/QuitAfterGameStop.h" #include "launch/steps/TextPrint.h" -#include "launch/steps/Update.h" #include "minecraft/launch/ClaimAccount.h" #include "minecraft/launch/LauncherPartLaunch.h" @@ -70,9 +73,6 @@ #include "java/JavaUtils.h" -#include "meta/Index.h" -#include "meta/VersionList.h" - #include "icons/IconList.h" #include "mod/ModFolderModel.h" @@ -84,7 +84,6 @@ #include "AssetsUtils.h" #include "MinecraftLoadAndCheck.h" -#include "MinecraftUpdate.h" #include "PackProfile.h" #include "minecraft/gameoptions/GameOptions.h" #include "minecraft/update/FoldersTask.h" @@ -194,7 +193,7 @@ void MinecraftInstance::loadSpecificSettings() } // Join server on launch, this does not have a global override - m_settings->registerSetting({ "JoinServerOnLaunch", "JoinOnLaunch" }, false); + m_settings->registerSetting("JoinServerOnLaunch", false); m_settings->registerSetting("JoinServerOnLaunchAddress", ""); m_settings->registerSetting("JoinWorldOnLaunch", ""); @@ -218,6 +217,7 @@ void MinecraftInstance::loadSpecificSettings() void MinecraftInstance::updateRuntimeContext() { m_runtimeContext.updateFromInstanceSettings(m_settings); + m_components->invalidateLaunchProfile(); } QString MinecraftInstance::typeName() const @@ -1030,18 +1030,18 @@ QString MinecraftInstance::getStatusbarDescription() return description; } -Task::Ptr MinecraftInstance::createUpdateTask(Net::Mode mode) +QList MinecraftInstance::createUpdateTask() { - updateRuntimeContext(); - switch (mode) { - case Net::Mode::Offline: { - return Task::Ptr(new MinecraftLoadAndCheck(this)); - } - case Net::Mode::Online: { - return Task::Ptr(new MinecraftUpdate(this)); - } - } - return nullptr; + return { + // create folders + makeShared(this), + // libraries download + makeShared(this), + // FML libraries download and copy into the instance + makeShared(this), + // assets update + makeShared(this), + }; } shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) @@ -1063,7 +1063,7 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt process->appendStep(makeShared(pptr)); } - if (!targetToJoin && settings()->get("JoinOnLaunch").toBool()) { + if (!targetToJoin && settings()->get("JoinServerOnLaunch").toBool()) { QString fullAddress = settings()->get("JoinServerOnLaunchAddress").toString(); if (!fullAddress.isEmpty()) { targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(fullAddress, false))); @@ -1090,14 +1090,10 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt process->appendStep(step); } - // if we aren't in offline mode,. - if (session->status != AuthSession::PlayableOffline) { - if (!session->demo) { - process->appendStep(makeShared(pptr, session)); - } - process->appendStep(makeShared(pptr, Net::Mode::Online)); - } else { - process->appendStep(makeShared(pptr, Net::Mode::Offline)); + // load meta + { + auto mode = session->status != AuthSession::PlayableOffline ? Net::Mode::Online : Net::Mode::Offline; + process->appendStep(makeShared(pptr, makeShared(this, mode))); } // check java @@ -1106,6 +1102,16 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt process->appendStep(makeShared(pptr)); } + // if we aren't in offline mode,. + if (session->status != AuthSession::PlayableOffline) { + if (!session->demo) { + process->appendStep(makeShared(pptr, session)); + } + for (auto t : createUpdateTask()) { + process->appendStep(makeShared(pptr, t)); + } + } + // if there are any jar mods { process->appendStep(makeShared(pptr)); diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index ad2cda186..75e97ae45 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -104,7 +104,7 @@ class MinecraftInstance : public BaseInstance { /** Returns whether the instance, with its version, has support for demo mode. */ [[nodiscard]] bool supportsDemo() const; - void updateRuntimeContext(); + void updateRuntimeContext() override; ////// Profile management ////// std::shared_ptr getPackProfile() const; @@ -120,7 +120,7 @@ class MinecraftInstance : public BaseInstance { std::shared_ptr gameOptionsModel(); ////// Launch stuff ////// - Task::Ptr createUpdateTask(Net::Mode mode) override; + QList createUpdateTask() override; shared_qobject_ptr createLaunchTask(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) override; QStringList extraArguments() override; QStringList verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) override; diff --git a/launcher/minecraft/MinecraftLoadAndCheck.cpp b/launcher/minecraft/MinecraftLoadAndCheck.cpp index 818e90cfc..b9fb7eb0c 100644 --- a/launcher/minecraft/MinecraftLoadAndCheck.cpp +++ b/launcher/minecraft/MinecraftLoadAndCheck.cpp @@ -2,41 +2,42 @@ #include "MinecraftInstance.h" #include "PackProfile.h" -MinecraftLoadAndCheck::MinecraftLoadAndCheck(MinecraftInstance* inst, QObject* parent) : Task(parent), m_inst(inst) {} +MinecraftLoadAndCheck::MinecraftLoadAndCheck(MinecraftInstance* inst, Net::Mode netmode) : m_inst(inst), m_netmode(netmode) {} void MinecraftLoadAndCheck::executeTask() { // add offline metadata load task auto components = m_inst->getPackProfile(); - components->reload(Net::Mode::Offline); + components->reload(m_netmode); m_task = components->getCurrentTask(); if (!m_task) { emitSucceeded(); return; } - connect(m_task.get(), &Task::succeeded, this, &MinecraftLoadAndCheck::subtaskSucceeded); - connect(m_task.get(), &Task::failed, this, &MinecraftLoadAndCheck::subtaskFailed); - connect(m_task.get(), &Task::aborted, this, [this] { subtaskFailed(tr("Aborted")); }); - connect(m_task.get(), &Task::progress, this, &MinecraftLoadAndCheck::progress); + connect(m_task.get(), &Task::succeeded, this, &MinecraftLoadAndCheck::emitSucceeded); + connect(m_task.get(), &Task::failed, this, &MinecraftLoadAndCheck::emitFailed); + connect(m_task.get(), &Task::aborted, this, [this] { emitFailed(tr("Aborted")); }); + connect(m_task.get(), &Task::progress, this, &MinecraftLoadAndCheck::setProgress); connect(m_task.get(), &Task::stepProgress, this, &MinecraftLoadAndCheck::propagateStepProgress); connect(m_task.get(), &Task::status, this, &MinecraftLoadAndCheck::setStatus); + connect(m_task.get(), &Task::details, this, &MinecraftLoadAndCheck::setDetails); } -void MinecraftLoadAndCheck::subtaskSucceeded() +bool MinecraftLoadAndCheck::canAbort() const { - if (isFinished()) { - qCritical() << "MinecraftUpdate: Subtask" << sender() << "succeeded, but work was already done!"; - return; + if (m_task) { + return m_task->canAbort(); } - emitSucceeded(); + return true; } -void MinecraftLoadAndCheck::subtaskFailed(QString error) +bool MinecraftLoadAndCheck::abort() { - if (isFinished()) { - qCritical() << "MinecraftUpdate: Subtask" << sender() << "failed, but work was already done!"; - return; + if (m_task && m_task->canAbort()) { + auto status = m_task->abort(); + emitFailed("Aborted."); + return status; } - emitFailed(error); -} + return Task::abort(); +} \ No newline at end of file diff --git a/launcher/minecraft/MinecraftLoadAndCheck.h b/launcher/minecraft/MinecraftLoadAndCheck.h index 09edaf909..c05698bca 100644 --- a/launcher/minecraft/MinecraftLoadAndCheck.h +++ b/launcher/minecraft/MinecraftLoadAndCheck.h @@ -15,32 +15,24 @@ #pragma once -#include -#include -#include - -#include +#include "net/Mode.h" #include "tasks/Task.h" -#include "QObjectPtr.h" - -class MinecraftVersion; class MinecraftInstance; class MinecraftLoadAndCheck : public Task { Q_OBJECT public: - explicit MinecraftLoadAndCheck(MinecraftInstance* inst, QObject* parent = 0); - virtual ~MinecraftLoadAndCheck() {}; + explicit MinecraftLoadAndCheck(MinecraftInstance* inst, Net::Mode netmode); + virtual ~MinecraftLoadAndCheck() = default; void executeTask() override; - private slots: - void subtaskSucceeded(); - void subtaskFailed(QString error); + bool canAbort() const override; + public slots: + bool abort() override; private: MinecraftInstance* m_inst = nullptr; Task::Ptr m_task; - QString m_preFailure; - QString m_fail_reason; + Net::Mode m_netmode; }; diff --git a/launcher/minecraft/MinecraftUpdate.cpp b/launcher/minecraft/MinecraftUpdate.cpp deleted file mode 100644 index b63430aa8..000000000 --- a/launcher/minecraft/MinecraftUpdate.cpp +++ /dev/null @@ -1,63 +0,0 @@ -/* 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 "MinecraftUpdate.h" -#include "MinecraftInstance.h" - -#include "minecraft/PackProfile.h" - -#include "tasks/SequentialTask.h" -#include "update/AssetUpdateTask.h" -#include "update/FMLLibrariesTask.h" -#include "update/FoldersTask.h" -#include "update/LibrariesTask.h" - -MinecraftUpdate::MinecraftUpdate(MinecraftInstance* inst, QObject* parent) : SequentialTask(parent), m_inst(inst) {} - -void MinecraftUpdate::executeTask() -{ - m_queue.clear(); - // create folders - { - addTask(makeShared(m_inst)); - } - - // add metadata update task if necessary - { - auto components = m_inst->getPackProfile(); - components->reload(Net::Mode::Online); - auto task = components->getCurrentTask(); - if (task) { - addTask(task); - } - } - - // libraries download - { - addTask(makeShared(m_inst)); - } - - // FML libraries download and copy into the instance - { - addTask(makeShared(m_inst)); - } - - // assets update - { - addTask(makeShared(m_inst)); - } - - SequentialTask::executeTask(); -} diff --git a/launcher/minecraft/MinecraftUpdate.h b/launcher/minecraft/MinecraftUpdate.h deleted file mode 100644 index 456a13518..000000000 --- a/launcher/minecraft/MinecraftUpdate.h +++ /dev/null @@ -1,33 +0,0 @@ -/* 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 "tasks/SequentialTask.h" - -class MinecraftInstance; - -// this needs to be a task because components->reload does stuff that may block -class MinecraftUpdate : public SequentialTask { - Q_OBJECT - public: - explicit MinecraftUpdate(MinecraftInstance* inst, QObject* parent = 0); - virtual ~MinecraftUpdate() = default; - - void executeTask() override; - - private: - MinecraftInstance* m_inst = nullptr; -}; diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index a8860935c..f1d2473c2 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -38,6 +38,7 @@ */ #include +#include #include #include #include @@ -47,10 +48,16 @@ #include #include #include +#include +#include +#include "Application.h" #include "Exception.h" #include "FileSystem.h" #include "Json.h" +#include "meta/Index.h" +#include "meta/JsonFormat.h" +#include "minecraft/Component.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/OneSixVersionFormat.h" #include "minecraft/ProfileUtils.h" @@ -60,11 +67,9 @@ #include "PackProfile_p.h" #include "modplatform/ModIndex.h" -static const QMap modloaderMapping{ { "net.neoforged", ModPlatform::NeoForge }, - { "net.minecraftforge", ModPlatform::Forge }, - { "net.fabricmc.fabric-loader", ModPlatform::Fabric }, - { "org.quiltmc.quilt-loader", ModPlatform::Quilt }, - { "com.mumfrey.liteloader", ModPlatform::LiteLoader } }; +#include "minecraft/Logging.h" + +#include "ui/dialogs/CustomMessageBox.h" PackProfile::PackProfile(MinecraftInstance* instance) : QAbstractListModel() { @@ -153,16 +158,16 @@ static bool savePackProfile(const QString& filename, const ComponentContainer& c obj.insert("components", orderArray); QSaveFile outFile(filename); if (!outFile.open(QFile::WriteOnly)) { - qCritical() << "Couldn't open" << outFile.fileName() << "for writing:" << outFile.errorString(); + qCCritical(instanceProfileC) << "Couldn't open" << outFile.fileName() << "for writing:" << outFile.errorString(); return false; } auto data = QJsonDocument(obj).toJson(QJsonDocument::Indented); if (outFile.write(data) != data.size()) { - qCritical() << "Couldn't write all the data into" << outFile.fileName() << "because:" << outFile.errorString(); + qCCritical(instanceProfileC) << "Couldn't write all the data into" << outFile.fileName() << "because:" << outFile.errorString(); return false; } if (!outFile.commit()) { - qCritical() << "Couldn't save" << outFile.fileName() << "because:" << outFile.errorString(); + qCCritical(instanceProfileC) << "Couldn't save" << outFile.fileName() << "because:" << outFile.errorString(); } return true; } @@ -175,12 +180,12 @@ static bool loadPackProfile(PackProfile* parent, { QFile componentsFile(filename); if (!componentsFile.exists()) { - qWarning() << "Components file doesn't exist. This should never happen."; + qCWarning(instanceProfileC) << "Components file" << filename << "doesn't exist. This should never happen."; return false; } if (!componentsFile.open(QFile::ReadOnly)) { - qCritical() << "Couldn't open" << componentsFile.fileName() << " for reading:" << componentsFile.errorString(); - qWarning() << "Ignoring overridden order"; + qCCritical(instanceProfileC) << "Couldn't open" << componentsFile.fileName() << " for reading:" << componentsFile.errorString(); + qCWarning(instanceProfileC) << "Ignoring overridden order"; return false; } @@ -188,8 +193,8 @@ static bool loadPackProfile(PackProfile* parent, QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(componentsFile.readAll(), &error); if (error.error != QJsonParseError::NoError) { - qCritical() << "Couldn't parse" << componentsFile.fileName() << ":" << error.errorString(); - qWarning() << "Ignoring overridden order"; + qCCritical(instanceProfileC) << "Couldn't parse" << componentsFile.fileName() << ":" << error.errorString(); + qCWarning(instanceProfileC) << "Ignoring overridden order"; return false; } @@ -207,7 +212,7 @@ static bool loadPackProfile(PackProfile* parent, container.append(componentFromJsonV1(parent, componentJsonPattern, comp_obj)); } } catch ([[maybe_unused]] const JSONValidationError& err) { - qCritical() << "Couldn't parse" << componentsFile.fileName() << ": bad file format"; + qCCritical(instanceProfileC) << "Couldn't parse" << componentsFile.fileName() << ": bad file format"; container.clear(); return false; } @@ -240,12 +245,12 @@ void PackProfile::buildingFromScratch() void PackProfile::scheduleSave() { if (!d->loaded) { - qDebug() << "Component list should never save if it didn't successfully load, instance:" << d->m_instance->name(); + qDebug() << d->m_instance->name() << "|" << "Component list should never save if it didn't successfully load"; return; } if (!d->dirty) { d->dirty = true; - qDebug() << "Component list save is scheduled for" << d->m_instance->name(); + qDebug() << d->m_instance->name() << "|" << "Component list save is scheduled"; } d->m_saveTimer.start(); } @@ -272,7 +277,7 @@ QString PackProfile::patchFilePathForUid(const QString& uid) const void PackProfile::save_internal() { - qDebug() << "Component list save performed now for" << d->m_instance->name(); + qDebug() << d->m_instance->name() << "|" << "Component list save performed now"; auto filename = componentsFilePath(); savePackProfile(filename, d->components); d->dirty = false; @@ -285,7 +290,7 @@ bool PackProfile::load() // load the new component list and swap it with the current one... ComponentContainer newComponents; if (!loadPackProfile(this, filename, patchesPattern(), newComponents)) { - qCritical() << "Failed to load the component config for instance" << d->m_instance->name(); + qCritical() << d->m_instance->name() << "|" << "Failed to load the component config"; return false; } else { // FIXME: actually use fine-grained updates, not this... @@ -298,7 +303,7 @@ bool PackProfile::load() d->componentIndex.clear(); for (auto component : newComponents) { if (d->componentIndex.contains(component->m_uid)) { - qWarning() << "Ignoring duplicate component entry" << component->m_uid; + qWarning() << d->m_instance->name() << "|" << "Ignoring duplicate component entry" << component->m_uid; continue; } connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); @@ -346,14 +351,14 @@ void PackProfile::resolve(Net::Mode netmode) void PackProfile::updateSucceeded() { - qDebug() << "Component list update/resolve task succeeded for" << d->m_instance->name(); + qCDebug(instanceProfileC) << d->m_instance->name() << "|" << "Component list update/resolve task succeeded"; d->m_updateTask.reset(); invalidateLaunchProfile(); } void PackProfile::updateFailed(const QString& error) { - qDebug() << "Component list update/resolve task failed for" << d->m_instance->name() << "Reason:" << error; + qCDebug(instanceProfileC) << d->m_instance->name() << "|" << "Component list update/resolve task failed " << "Reason:" << error; d->m_updateTask.reset(); invalidateLaunchProfile(); } @@ -369,11 +374,11 @@ void PackProfile::insertComponent(size_t index, ComponentPtr component) { auto id = component->getID(); if (id.isEmpty()) { - qWarning() << "Attempt to add a component with empty ID!"; + qCWarning(instanceProfileC) << d->m_instance->name() << "|" << "Attempt to add a component with empty ID!"; return; } if (d->componentIndex.contains(id)) { - qWarning() << "Attempt to add a component that is already present!"; + qCWarning(instanceProfileC) << d->m_instance->name() << "|" << "Attempt to add a component that is already present!"; return; } beginInsertRows(QModelIndex(), static_cast(index), static_cast(index)); @@ -388,7 +393,7 @@ void PackProfile::componentDataChanged() { auto objPtr = qobject_cast(sender()); if (!objPtr) { - qWarning() << "PackProfile got dataChanged signal from a non-Component!"; + qCWarning(instanceProfileC) << d->m_instance->name() << "|" << "PackProfile got dataChanged signal from a non-Component!"; return; } if (objPtr->getID() == "net.minecraft") { @@ -404,19 +409,20 @@ void PackProfile::componentDataChanged() } index++; } - qWarning() << "PackProfile got dataChanged signal from a Component which does not belong to it!"; + qCWarning(instanceProfileC) << d->m_instance->name() << "|" + << "PackProfile got dataChanged signal from a Component which does not belong to it!"; } bool PackProfile::remove(const int index) { auto patch = getComponent(index); if (!patch->isRemovable()) { - qWarning() << "Patch" << patch->getID() << "is non-removable"; + qCWarning(instanceProfileC) << d->m_instance->name() << "|" << "Patch" << patch->getID() << "is non-removable"; return false; } if (!removeComponent_internal(patch)) { - qCritical() << "Patch" << patch->getID() << "could not be removed"; + qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "Patch" << patch->getID() << "could not be removed"; return false; } @@ -445,11 +451,11 @@ bool PackProfile::customize(int index) { auto patch = getComponent(index); if (!patch->isCustomizable()) { - qDebug() << "Patch" << patch->getID() << "is not customizable"; + qCDebug(instanceProfileC) << d->m_instance->name() << "|" << "Patch" << patch->getID() << "is not customizable"; return false; } if (!patch->customize()) { - qCritical() << "Patch" << patch->getID() << "could not be customized"; + qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "Patch" << patch->getID() << "could not be customized"; return false; } invalidateLaunchProfile(); @@ -461,11 +467,11 @@ bool PackProfile::revertToBase(int index) { auto patch = getComponent(index); if (!patch->isRevertible()) { - qDebug() << "Patch" << patch->getID() << "is not revertible"; + qCDebug(instanceProfileC) << d->m_instance->name() << "|" << "Patch" << patch->getID() << "is not revertible"; return false; } if (!patch->revert()) { - qCritical() << "Patch" << patch->getID() << "could not be reverted"; + qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "Patch" << patch->getID() << "could not be reverted"; return false; } invalidateLaunchProfile(); @@ -678,7 +684,8 @@ bool PackProfile::installComponents(QStringList selectedFiles) const QString target = FS::PathCombine(patchDir, versionFile->uid + ".json"); if (!QFile::copy(source, target)) { - qWarning() << "Component" << source << "could not be copied to target" << target; + qCWarning(instanceProfileC) << d->m_instance->name() << "|" << "Component" << source << "could not be copied to target" + << target; result = false; continue; } @@ -711,7 +718,8 @@ bool PackProfile::installEmpty(const QString& uid, const QString& name) QString patchFileName = FS::PathCombine(patchDir, uid + ".json"); QFile file(patchFileName); if (!file.open(QFile::WriteOnly)) { - qCritical() << "Error opening" << file.fileName() << "for reading:" << file.errorString(); + qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "Error opening" << file.fileName() + << "for reading:" << file.errorString(); return false; } file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); @@ -731,7 +739,8 @@ bool PackProfile::removeComponent_internal(ComponentPtr patch) if (fileName.size()) { QFile patchFile(fileName); if (patchFile.exists() && !patchFile.remove()) { - qCritical() << "File" << fileName << "could not be removed because:" << patchFile.errorString(); + qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "File" << fileName + << "could not be removed because:" << patchFile.errorString(); return false; } } @@ -747,7 +756,8 @@ bool PackProfile::removeComponent_internal(ComponentPtr patch) if (finfo.exists()) { QFile jarModFile(jar[0]); if (!jarModFile.remove()) { - qCritical() << "File" << jar[0] << "could not be removed because:" << jarModFile.errorString(); + qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "File" << jar[0] + << "could not be removed because:" << jarModFile.errorString(); return false; } return true; @@ -804,7 +814,8 @@ bool PackProfile::installJarMods_internal(QStringList filepaths) QFile file(patchFileName); if (!file.open(QFile::WriteOnly)) { - qCritical() << "Error opening" << file.fileName() << "for reading:" << file.errorString(); + qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "Error opening" << file.fileName() + << "for reading:" << file.errorString(); return false; } file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); @@ -858,7 +869,8 @@ bool PackProfile::installCustomJar_internal(QString filepath) QFile file(patchFileName); if (!file.open(QFile::WriteOnly)) { - qCritical() << "Error opening" << file.fileName() << "for reading:" << file.errorString(); + qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "Error opening" << file.fileName() + << "for reading:" << file.errorString(); return false; } file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); @@ -913,7 +925,8 @@ bool PackProfile::installAgents_internal(QStringList filepaths) QFile patchFile(FS::PathCombine(patchDir, targetId + ".json")); if (!patchFile.open(QFile::WriteOnly)) { - qCritical() << "Error opening" << patchFile.fileName() << "for reading:" << patchFile.errorString(); + qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "Error opening" << patchFile.fileName() + << "for reading:" << patchFile.errorString(); return false; } @@ -935,12 +948,13 @@ std::shared_ptr PackProfile::getProfile() const try { auto profile = std::make_shared(); for (auto file : d->components) { - qDebug() << "Applying" << file->getID() << (file->getProblemSeverity() == ProblemSeverity::Error ? "ERROR" : "GOOD"); + qCDebug(instanceProfileC) << d->m_instance->name() << "|" << "Applying" << file->getID() + << (file->getProblemSeverity() == ProblemSeverity::Error ? "ERROR" : "GOOD"); file->applyTo(profile.get()); } d->m_profile = profile; } catch (const Exception& error) { - qWarning() << "Couldn't apply profile patches because: " << error.cause(); + qCWarning(instanceProfileC) << d->m_instance->name() << "|" << "Couldn't apply profile patches because: " << error.cause(); } } return d->m_profile; @@ -953,8 +967,16 @@ bool PackProfile::setComponentVersion(const QString& uid, const QString& version ComponentPtr component = *iter; // set existing if (component->revert()) { + // set new version + auto oldVersion = component->getVersion(); component->setVersion(version); component->setImportant(important); + + if (important) { + component->setUpdateAction(UpdateAction{ UpdateActionImportantChanged{ oldVersion } }); + resolve(Net::Mode::Online); + } + return true; } return false; @@ -993,12 +1015,12 @@ std::optional PackProfile::getModLoaders() ModPlatform::ModLoaderTypes result; bool has_any_loader = false; - QMapIterator i(modloaderMapping); + QMapIterator i(Component::KNOWN_MODLOADERS); while (i.hasNext()) { i.next(); if (auto c = getComponent(i.key()); c != nullptr && c->isEnabled()) { - result |= i.value(); + result |= i.value().type; has_any_loader = true; } } @@ -1026,8 +1048,8 @@ QList PackProfile::getModLoadersList() { QList result; for (auto c : d->components) { - if (c->isEnabled() && modloaderMapping.contains(c->getID())) { - result.append(modloaderMapping[c->getID()]); + if (c->isEnabled() && Component::KNOWN_MODLOADERS.contains(c->getID())) { + result.append(Component::KNOWN_MODLOADERS[c->getID()].type); } } diff --git a/launcher/minecraft/PackProfile.h b/launcher/minecraft/PackProfile.h index 9b6710cc3..b2de26ea0 100644 --- a/launcher/minecraft/PackProfile.h +++ b/launcher/minecraft/PackProfile.h @@ -148,13 +148,13 @@ class PackProfile : public QAbstractListModel { std::optional getSupportedModLoaders(); QList getModLoadersList(); + /// apply the component patches. Catches all the errors and returns true/false for success/failure + void invalidateLaunchProfile(); + private: void scheduleSave(); bool saveIsScheduled() const; - /// apply the component patches. Catches all the errors and returns true/false for success/failure - void invalidateLaunchProfile(); - /// insert component so that its index is ideally the specified one (returns real index) void insertComponent(size_t index, ComponentPtr component); diff --git a/launcher/minecraft/PackProfile_p.h b/launcher/minecraft/PackProfile_p.h index 0cd4fb839..4fb3621f0 100644 --- a/launcher/minecraft/PackProfile_p.h +++ b/launcher/minecraft/PackProfile_p.h @@ -3,8 +3,8 @@ #include #include #include -#include #include "Component.h" +#include "tasks/Task.h" class MinecraftInstance; using ComponentContainer = QList; diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp index 1eba148a5..bd28f9e9a 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -38,7 +38,6 @@ #include #include #include -#include #include #include @@ -57,6 +56,7 @@ #include #include "FileSystem.h" +#include "PSaveFile.h" using std::nullopt; using std::optional; @@ -183,7 +183,7 @@ bool putLevelDatDataToFS(const QFileInfo& file, QByteArray& data) if (fullFilePath.isNull()) { return false; } - QSaveFile f(fullFilePath); + PSaveFile f(fullFilePath); if (!f.open(QIODevice::WriteOnly)) { return false; } diff --git a/launcher/minecraft/auth/AuthFlow.cpp b/launcher/minecraft/auth/AuthFlow.cpp index 45926206c..19fbe15dd 100644 --- a/launcher/minecraft/auth/AuthFlow.cpp +++ b/launcher/minecraft/auth/AuthFlow.cpp @@ -19,7 +19,7 @@ #include -AuthFlow::AuthFlow(AccountData* data, Action action, QObject* parent) : Task(parent), m_data(data) +AuthFlow::AuthFlow(AccountData* data, Action action) : Task(), m_data(data) { if (data->type == AccountType::MSA) { if (action == Action::DeviceCode) { diff --git a/launcher/minecraft/auth/AuthFlow.h b/launcher/minecraft/auth/AuthFlow.h index 4d18ac845..bff4c04e4 100644 --- a/launcher/minecraft/auth/AuthFlow.h +++ b/launcher/minecraft/auth/AuthFlow.h @@ -17,7 +17,7 @@ class AuthFlow : public Task { public: enum class Action { Refresh, Login, DeviceCode }; - explicit AuthFlow(AccountData* data, Action action = Action::Refresh, QObject* parent = 0); + explicit AuthFlow(AccountData* data, Action action = Action::Refresh); virtual ~AuthFlow() = default; void executeTask() override; diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index 5b063604c..1ed39b5ca 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -121,7 +121,7 @@ shared_qobject_ptr MinecraftAccount::login(bool useDeviceCode) { Q_ASSERT(m_currentTask.get() == nullptr); - m_currentTask.reset(new AuthFlow(&data, useDeviceCode ? AuthFlow::Action::DeviceCode : AuthFlow::Action::Login, this)); + m_currentTask.reset(new AuthFlow(&data, useDeviceCode ? AuthFlow::Action::DeviceCode : AuthFlow::Action::Login)); connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed); connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); }); @@ -135,7 +135,7 @@ shared_qobject_ptr MinecraftAccount::refresh() return m_currentTask; } - m_currentTask.reset(new AuthFlow(&data, AuthFlow::Action::Refresh, this)); + m_currentTask.reset(new AuthFlow(&data, AuthFlow::Action::Refresh)); connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed); diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index 3aa458ace..f9d89baa2 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -180,6 +180,7 @@ bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output) if (!getString(skinObj.value("url"), skinOut.url)) { continue; } + skinOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); if (!getString(skinObj.value("variant"), skinOut.variant)) { continue; } @@ -221,9 +222,9 @@ namespace { // these skin URLs are for the MHF_Steve and MHF_Alex accounts (made by a Mojang employee) // they are needed because the session server doesn't return skin urls for default skins static const QString SKIN_URL_STEVE = - "http://textures.minecraft.net/texture/1a4af718455d4aab528e7a61f86fa25e6a369d1768dcb13f7df319a713eb810b"; + "https://textures.minecraft.net/texture/1a4af718455d4aab528e7a61f86fa25e6a369d1768dcb13f7df319a713eb810b"; static const QString SKIN_URL_ALEX = - "http://textures.minecraft.net/texture/83cee5ca6afcdb171285aa00e8049c297b2dbeba0efb8ff970a5677a1b644032"; + "https://textures.minecraft.net/texture/83cee5ca6afcdb171285aa00e8049c297b2dbeba0efb8ff970a5677a1b644032"; bool isDefaultModelSteve(QString uuid) { diff --git a/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp b/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp index c283b153e..38ff90a47 100644 --- a/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp +++ b/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp @@ -74,12 +74,12 @@ void MSADeviceCodeStep::perform() m_task->setAskRetry(false); m_task->addNetAction(m_request); - connect(m_task.get(), &Task::finished, this, &MSADeviceCodeStep::deviceAutorizationFinished); + connect(m_task.get(), &Task::finished, this, &MSADeviceCodeStep::deviceAuthorizationFinished); m_task->start(); } -struct DeviceAutorizationResponse { +struct DeviceAuthorizationResponse { QString device_code; QString user_code; QString verification_uri; @@ -90,17 +90,17 @@ struct DeviceAutorizationResponse { QString error_description; }; -DeviceAutorizationResponse parseDeviceAutorizationResponse(const QByteArray& data) +DeviceAuthorizationResponse parseDeviceAuthorizationResponse(const QByteArray& data) { QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(data, &err); if (err.error != QJsonParseError::NoError) { - qWarning() << "Failed to parse device autorization response due to err:" << err.errorString(); + qWarning() << "Failed to parse device authorization response due to err:" << err.errorString(); return {}; } if (!doc.isObject()) { - qWarning() << "Device autorization response is not an object"; + qWarning() << "Device authorization response is not an object"; return {}; } auto obj = doc.object(); @@ -111,9 +111,9 @@ DeviceAutorizationResponse parseDeviceAutorizationResponse(const QByteArray& dat }; } -void MSADeviceCodeStep::deviceAutorizationFinished() +void MSADeviceCodeStep::deviceAuthorizationFinished() { - auto rsp = parseDeviceAutorizationResponse(*m_response); + auto rsp = parseDeviceAuthorizationResponse(*m_response); if (!rsp.error.isEmpty() || !rsp.error_description.isEmpty()) { qWarning() << "Device authorization failed:" << rsp.error; emit finished(AccountTaskState::STATE_FAILED_HARD, @@ -208,12 +208,12 @@ AuthenticationResponse parseAuthenticationResponse(const QByteArray& data) QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(data, &err); if (err.error != QJsonParseError::NoError) { - qWarning() << "Failed to parse device autorization response due to err:" << err.errorString(); + qWarning() << "Failed to parse device authorization response due to err:" << err.errorString(); return {}; } if (!doc.isObject()) { - qWarning() << "Device autorization response is not an object"; + qWarning() << "Device authorization response is not an object"; return {}; } auto obj = doc.object(); @@ -274,4 +274,4 @@ void MSADeviceCodeStep::authenticationFinished() m_data->msaToken.refresh_token = rsp.refresh_token; m_data->msaToken.token = rsp.access_token; emit finished(AccountTaskState::STATE_WORKING, tr("Got")); -} \ No newline at end of file +} diff --git a/launcher/minecraft/auth/steps/MSADeviceCodeStep.h b/launcher/minecraft/auth/steps/MSADeviceCodeStep.h index 616008def..7f755563f 100644 --- a/launcher/minecraft/auth/steps/MSADeviceCodeStep.h +++ b/launcher/minecraft/auth/steps/MSADeviceCodeStep.h @@ -58,7 +58,7 @@ class MSADeviceCodeStep : public AuthStep { void authorizeWithBrowser(QString url, QString code, int expiresIn); private slots: - void deviceAutorizationFinished(); + void deviceAuthorizationFinished(); void startPoolTimer(); void authenticateUser(); void authenticationFinished(); diff --git a/launcher/minecraft/auth/steps/MSAStep.cpp b/launcher/minecraft/auth/steps/MSAStep.cpp index 3db04bf2f..151ee4e68 100644 --- a/launcher/minecraft/auth/steps/MSAStep.cpp +++ b/launcher/minecraft/auth/steps/MSAStep.cpp @@ -85,20 +85,20 @@ class CustomOAuthOobReplyHandler : public QOAuthOobReplyHandler { MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(silent) { m_clientId = APPLICATION->getMSAClientID(); - if (QCoreApplication::applicationFilePath().startsWith("/tmp/.mount_") || - QFile::exists(FS::PathCombine(APPLICATION->root(), "portable.txt")) || !isSchemeHandlerRegistered()) + if (QCoreApplication::applicationFilePath().startsWith("/tmp/.mount_") || APPLICATION->isPortable() || !isSchemeHandlerRegistered()) { auto replyHandler = new QOAuthHttpServerReplyHandler(this); - replyHandler->setCallbackText(R"XXX( + replyHandler->setCallbackText(QString(R"XXX( Login Successful, redirecting... - )XXX"); + )XXX") + .arg(BuildConfig.LOGIN_CALLBACK_URL)); oauth2.setReplyHandler(replyHandler); } else { oauth2.setReplyHandler(new CustomOAuthOobReplyHandler(this)); diff --git a/launcher/minecraft/launch/AutoInstallJava.cpp b/launcher/minecraft/launch/AutoInstallJava.cpp index 3f58a28a6..854590dd2 100644 --- a/launcher/minecraft/launch/AutoInstallJava.cpp +++ b/launcher/minecraft/launch/AutoInstallJava.cpp @@ -41,6 +41,7 @@ #include "Application.h" #include "FileSystem.h" #include "MessageLevel.h" +#include "QObjectPtr.h" #include "SysInfo.h" #include "java/JavaInstall.h" #include "java/JavaInstallList.h" @@ -48,15 +49,15 @@ #include "java/JavaVersion.h" #include "java/download/ArchiveDownloadTask.h" #include "java/download/ManifestDownloadTask.h" +#include "java/download/SymlinkTask.h" #include "meta/Index.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "net/Mode.h" +#include "tasks/SequentialTask.h" AutoInstallJava::AutoInstallJava(LaunchTask* parent) - : LaunchStep(parent) - , m_instance(std::dynamic_pointer_cast(m_parent->instance())) - , m_supported_arch(SysInfo::getSupportedJavaArchitecture()) {}; + : LaunchStep(parent), m_instance(m_parent->instance()), m_supported_arch(SysInfo::getSupportedJavaArchitecture()) {}; void AutoInstallJava::executeTask() { @@ -75,7 +76,7 @@ void AutoInstallJava::executeTask() auto java = std::dynamic_pointer_cast(javas->at(i)); if (java && packProfile->getProfile()->getCompatibleJavaMajors().contains(java->id.major())) { if (!java->is_64bit) { - emit logLine(tr("The automatic Java mechanism detected a 32-bit installation of Java."), MessageLevel::Info); + emit logLine(tr("The automatic Java mechanism detected a 32-bit installation of Java."), MessageLevel::Launcher); } setJavaPath(java->path); return; @@ -133,7 +134,7 @@ void AutoInstallJava::setJavaPath(QString path) settings->set("OverrideJavaLocation", true); settings->set("JavaPath", path); settings->set("AutomaticJava", true); - emit logLine(tr("Compatible Java found at: %1.").arg(path), MessageLevel::Info); + emit logLine(tr("Compatible Java found at: %1.").arg(path), MessageLevel::Launcher); emitSucceeded(); } @@ -175,15 +176,18 @@ void AutoInstallJava::downloadJava(Meta::Version::Ptr version, QString javaName) emitFailed(tr("Could not determine Java download type!")); return; } +#if defined(Q_OS_MACOS) + auto seq = makeShared(tr("Install Java")); + seq->addTask(m_current_task); + seq->addTask(makeShared(final_path)); + m_current_task = seq; +#endif auto deletePath = [final_path] { FS::deletePath(final_path); }; connect(m_current_task.get(), &Task::failed, this, [this, deletePath](QString reason) { deletePath(); emitFailed(reason); }); - connect(this, &Task::aborted, this, [this, deletePath] { - m_current_task->abort(); - deletePath(); - }); + connect(m_current_task.get(), &Task::aborted, this, [deletePath] { deletePath(); }); connect(m_current_task.get(), &Task::succeeded, this, &AutoInstallJava::setJavaPathFromPartial); connect(m_current_task.get(), &Task::failed, this, &AutoInstallJava::tryNextMajorJava); connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); @@ -236,7 +240,10 @@ void AutoInstallJava::tryNextMajorJava() } bool AutoInstallJava::abort() { - if (m_current_task && m_current_task->canAbort()) - return m_current_task->abort(); - return true; + if (m_current_task && m_current_task->canAbort()) { + auto status = m_current_task->abort(); + emitFailed("Aborted."); + return status; + } + return Task::abort(); } diff --git a/launcher/minecraft/launch/AutoInstallJava.h b/launcher/minecraft/launch/AutoInstallJava.h index 7e4efc50c..cbfcf5ee7 100644 --- a/launcher/minecraft/launch/AutoInstallJava.h +++ b/launcher/minecraft/launch/AutoInstallJava.h @@ -37,7 +37,6 @@ #include #include -#include "java/JavaMetadata.h" #include "meta/Version.h" #include "minecraft/MinecraftInstance.h" #include "tasks/Task.h" diff --git a/launcher/minecraft/launch/ClaimAccount.h b/launcher/minecraft/launch/ClaimAccount.h index 357a4d4c5..561f0e848 100644 --- a/launcher/minecraft/launch/ClaimAccount.h +++ b/launcher/minecraft/launch/ClaimAccount.h @@ -22,7 +22,7 @@ class ClaimAccount : public LaunchStep { Q_OBJECT public: explicit ClaimAccount(LaunchTask* parent, AuthSessionPtr session); - virtual ~ClaimAccount() {}; + virtual ~ClaimAccount() = default; void executeTask() override; void finalize() override; diff --git a/launcher/minecraft/launch/CreateGameFolders.cpp b/launcher/minecraft/launch/CreateGameFolders.cpp index 36f5e6407..07bdbb600 100644 --- a/launcher/minecraft/launch/CreateGameFolders.cpp +++ b/launcher/minecraft/launch/CreateGameFolders.cpp @@ -8,16 +8,15 @@ CreateGameFolders::CreateGameFolders(LaunchTask* parent) : LaunchStep(parent) {} void CreateGameFolders::executeTask() { auto instance = m_parent->instance(); - std::shared_ptr minecraftInstance = std::dynamic_pointer_cast(instance); - if (!FS::ensureFolderPathExists(minecraftInstance->gameRoot())) { + if (!FS::ensureFolderPathExists(instance->gameRoot())) { emit logLine("Couldn't create the main game folder", MessageLevel::Error); emitFailed(tr("Couldn't create the main game folder")); return; } // HACK: this is a workaround for MCL-3732 - 'server-resource-packs' folder is created. - if (!FS::ensureFolderPathExists(FS::PathCombine(minecraftInstance->gameRoot(), "server-resource-packs"))) { + if (!FS::ensureFolderPathExists(FS::PathCombine(instance->gameRoot(), "server-resource-packs"))) { emit logLine("Couldn't create the 'server-resource-packs' folder", MessageLevel::Error); } emitSucceeded(); diff --git a/launcher/minecraft/launch/ExtractNatives.cpp b/launcher/minecraft/launch/ExtractNatives.cpp index 405008f40..afe091758 100644 --- a/launcher/minecraft/launch/ExtractNatives.cpp +++ b/launcher/minecraft/launch/ExtractNatives.cpp @@ -70,17 +70,16 @@ static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibH void ExtractNatives::executeTask() { auto instance = m_parent->instance(); - std::shared_ptr minecraftInstance = std::dynamic_pointer_cast(instance); - auto toExtract = minecraftInstance->getNativeJars(); + auto toExtract = instance->getNativeJars(); if (toExtract.isEmpty()) { emitSucceeded(); return; } - auto settings = minecraftInstance->settings(); + auto settings = instance->settings(); - auto outputPath = minecraftInstance->getNativePath(); + auto outputPath = instance->getNativePath(); FS::ensureFolderPathExists(outputPath); - auto javaVersion = minecraftInstance->getJavaVersion(); + auto javaVersion = instance->getJavaVersion(); bool jniHackEnabled = javaVersion.major() >= 8; for (const auto& source : toExtract) { if (!unzipNatives(source, outputPath, jniHackEnabled)) { diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index 2b932ae47..d9a2b9b6b 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -48,18 +48,20 @@ #include "gamemode_client.h" #endif -LauncherPartLaunch::LauncherPartLaunch(LaunchTask* parent) : LaunchStep(parent) +LauncherPartLaunch::LauncherPartLaunch(LaunchTask* parent) + : LaunchStep(parent) + , m_process(parent->instance()->getJavaVersion().defaultsToUtf8() ? QTextCodec::codecForName("UTF-8") : QTextCodec::codecForLocale()) { - auto instance = parent->instance(); - if (instance->settings()->get("CloseAfterLaunch").toBool()) { + if (parent->instance()->settings()->get("CloseAfterLaunch").toBool()) { std::shared_ptr connection{ new QMetaObject::Connection }; - *connection = connect(&m_process, &LoggedProcess::log, this, [=](QStringList lines, [[maybe_unused]] MessageLevel::Enum level) { - qDebug() << lines; - if (lines.filter(QRegularExpression(".*Setting user.+", QRegularExpression::CaseInsensitiveOption)).length() != 0) { - APPLICATION->closeAllWindows(); - disconnect(*connection); - } - }); + *connection = + connect(&m_process, &LoggedProcess::log, this, [=](const QStringList& lines, [[maybe_unused]] MessageLevel::Enum level) { + qDebug() << lines; + if (lines.filter(QRegularExpression(".*Setting user.+", QRegularExpression::CaseInsensitiveOption)).length() != 0) { + APPLICATION->closeAllWindows(); + disconnect(*connection); + } + }); } connect(&m_process, &LoggedProcess::log, this, &LauncherPartLaunch::logLines); @@ -77,10 +79,9 @@ void LauncherPartLaunch::executeTask() } auto instance = m_parent->instance(); - std::shared_ptr minecraftInstance = std::dynamic_pointer_cast(instance); QString legacyJarPath; - if (minecraftInstance->getLauncher() == "legacy" || minecraftInstance->shouldApplyOnlineFixes()) { + if (instance->getLauncher() == "legacy" || instance->shouldApplyOnlineFixes()) { legacyJarPath = APPLICATION->getJarPath("NewLaunchLegacy.jar"); if (legacyJarPath.isEmpty()) { const char* reason = QT_TR_NOOP("Legacy launcher library could not be found. Please check your installation."); @@ -90,8 +91,8 @@ void LauncherPartLaunch::executeTask() } } - m_launchScript = minecraftInstance->createLaunchScript(m_session, m_targetToJoin); - QStringList args = minecraftInstance->javaArguments(); + m_launchScript = instance->createLaunchScript(m_session, m_targetToJoin); + QStringList args = instance->javaArguments(); QString allArgs = args.join(", "); emit logLine("Java Arguments:\n[" + m_parent->censorPrivateInfo(allArgs) + "]\n\n", MessageLevel::Launcher); @@ -102,13 +103,13 @@ void LauncherPartLaunch::executeTask() // make detachable - this will keep the process running even if the object is destroyed m_process.setDetachable(true); - auto classPath = minecraftInstance->getClassPath(); + auto classPath = instance->getClassPath(); classPath.prepend(jarPath); if (!legacyJarPath.isEmpty()) classPath.prepend(legacyJarPath); - auto natPath = minecraftInstance->getNativePath(); + auto natPath = instance->getNativePath(); #ifdef Q_OS_WIN natPath = FS::getPathNameInLocal8bit(natPath); #endif diff --git a/launcher/minecraft/launch/ModMinecraftJar.cpp b/launcher/minecraft/launch/ModMinecraftJar.cpp index 6e73333b1..e06080ba7 100644 --- a/launcher/minecraft/launch/ModMinecraftJar.cpp +++ b/launcher/minecraft/launch/ModMinecraftJar.cpp @@ -42,7 +42,7 @@ void ModMinecraftJar::executeTask() { - auto m_inst = std::dynamic_pointer_cast(m_parent->instance()); + auto m_inst = m_parent->instance(); if (!m_inst->getJarMods().size()) { emitSucceeded(); @@ -82,7 +82,7 @@ void ModMinecraftJar::finalize() bool ModMinecraftJar::removeJar() { - auto m_inst = std::dynamic_pointer_cast(m_parent->instance()); + auto m_inst = m_parent->instance(); auto finalJarPath = QDir(m_inst->binRoot()).absoluteFilePath("minecraft.jar"); QFile finalJar(finalJarPath); if (finalJar.exists()) { diff --git a/launcher/minecraft/launch/ReconstructAssets.cpp b/launcher/minecraft/launch/ReconstructAssets.cpp index 843ccc554..21ae395f0 100644 --- a/launcher/minecraft/launch/ReconstructAssets.cpp +++ b/launcher/minecraft/launch/ReconstructAssets.cpp @@ -22,12 +22,11 @@ void ReconstructAssets::executeTask() { auto instance = m_parent->instance(); - std::shared_ptr minecraftInstance = std::dynamic_pointer_cast(instance); - auto components = minecraftInstance->getPackProfile(); + auto components = instance->getPackProfile(); auto profile = components->getProfile(); auto assets = profile->getMinecraftAssets(); - if (!AssetsUtils::reconstructAssets(assets->id, minecraftInstance->resourcesDir())) { + if (!AssetsUtils::reconstructAssets(assets->id, instance->resourcesDir())) { emit logLine("Failed to reconstruct Minecraft assets.", MessageLevel::Error); } diff --git a/launcher/minecraft/launch/ScanModFolders.cpp b/launcher/minecraft/launch/ScanModFolders.cpp index 7e08a4e36..1a2ddf194 100644 --- a/launcher/minecraft/launch/ScanModFolders.cpp +++ b/launcher/minecraft/launch/ScanModFolders.cpp @@ -42,7 +42,7 @@ void ScanModFolders::executeTask() { - auto m_inst = std::dynamic_pointer_cast(m_parent->instance()); + auto m_inst = m_parent->instance(); auto loaders = m_inst->loaderModList(); connect(loaders.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::modsDone); diff --git a/launcher/minecraft/launch/VerifyJavaInstall.cpp b/launcher/minecraft/launch/VerifyJavaInstall.cpp index 1e7448089..bc950d673 100644 --- a/launcher/minecraft/launch/VerifyJavaInstall.cpp +++ b/launcher/minecraft/launch/VerifyJavaInstall.cpp @@ -46,7 +46,7 @@ void VerifyJavaInstall::executeTask() { - auto instance = std::dynamic_pointer_cast(m_parent->instance()); + auto instance = m_parent->instance(); auto packProfile = instance->getPackProfile(); auto settings = instance->settings(); auto storedVersion = settings->get("JavaVersion").toString(); diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 5442df7fe..9663026cd 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -36,6 +36,7 @@ */ #include "Mod.h" +#include #include #include @@ -241,7 +242,7 @@ void Mod::finishResolvingWithDetails(ModDetails&& details) if (metadata) setMetadata(std::move(metadata)); if (!iconPath().isEmpty()) { - m_pack_image_cache_key.was_read_attempt = false; + m_packImageCacheKey.wasReadAttempt = false; } } @@ -290,45 +291,53 @@ auto Mod::issueTracker() const -> QString return details().issue_tracker; } -void Mod::setIcon(QImage new_image) const +QPixmap 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); + if (m_packImageCacheKey.key.isValid()) + PixmapCache::remove(m_packImageCacheKey.key); // scale the image to avoid flooding the pixmapcache auto pixmap = QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); - 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; + m_packImageCacheKey.key = PixmapCache::insert(pixmap); + m_packImageCacheKey.wasEverUsed = true; + m_packImageCacheKey.wasReadAttempt = true; + return pixmap; } QPixmap Mod::icon(QSize size, Qt::AspectRatioMode mode) const { - QPixmap cached_image; - if (PixmapCache::find(m_pack_image_cache_key.key, &cached_image)) { + auto pixmap_transform = [&size, &mode](QPixmap pixmap) { if (size.isNull()) - return cached_image; - return cached_image.scaled(size, mode, Qt::SmoothTransformation); + return pixmap; + return pixmap.scaled(size, mode, Qt::SmoothTransformation); + }; + + QPixmap cached_image; + if (PixmapCache::find(m_packImageCacheKey.key, &cached_image)) { + return pixmap_transform(cached_image); } // 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()) + if ((!m_packImageCacheKey.wasEverUsed && m_packImageCacheKey.wasReadAttempt) || iconPath().isEmpty()) return {}; - if (m_pack_image_cache_key.was_ever_used) { + if (m_packImageCacheKey.wasEverUsed) { qDebug() << "Mod" << name() << "Had it's icon evicted from 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); + m_packImageCacheKey.wasReadAttempt = true; + if (ModUtils::loadIconFile(*this, &cached_image)) { + return pixmap_transform(cached_image); + } + // Image failed to load + return {}; } bool Mod::valid() const diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 9bd76c2fd..a0d9797ed 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -82,7 +82,7 @@ class Mod : public Resource { /** 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; + QPixmap setIcon(QImage new_image) const; auto metadata() -> std::shared_ptr; auto metadata() const -> const std::shared_ptr; @@ -111,7 +111,7 @@ class Mod : public Resource { struct { QPixmapCache::Key key; - bool was_ever_used = false; - bool was_read_attempt = false; - } mutable m_pack_image_cache_key; + bool wasEverUsed = false; + bool wasReadAttempt = false; + } mutable m_packImageCacheKey; }; diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index 941e7ce58..b577c7272 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -35,10 +35,9 @@ ResourceFolderModel::ResourceFolderModel(QDir dir, BaseInstance* instance, QObje connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ResourceFolderModel::directoryChanged); connect(&m_helper_thread_task, &ConcurrentTask::finished, this, [this] { m_helper_thread_task.clear(); }); -#ifndef LAUNCHER_TEST - // in tests the application macro doesn't work - m_helper_thread_task.setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); -#endif + if (APPLICATION_DYN) { // in tests the application macro doesn't work + m_helper_thread_task.setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); + } } ResourceFolderModel::~ResourceFolderModel() @@ -262,7 +261,7 @@ bool ResourceFolderModel::update() return true; } -void ResourceFolderModel::resolveResource(Resource* res) +void ResourceFolderModel::resolveResource(Resource::Ptr res) { if (!res->shouldResolve()) { return; @@ -278,11 +277,14 @@ void ResourceFolderModel::resolveResource(Resource* res) m_active_parse_tasks.insert(ticket, task); connect( - task.get(), &Task::succeeded, this, [=] { onParseSucceeded(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); - connect(task.get(), &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); + task.get(), &Task::succeeded, this, [this, ticket, res] { onParseSucceeded(ticket, res->internal_id()); }, + Qt::ConnectionType::QueuedConnection); + connect( + task.get(), &Task::failed, this, [this, ticket, res] { onParseFailed(ticket, res->internal_id()); }, + Qt::ConnectionType::QueuedConnection); connect( task.get(), &Task::finished, this, - [=] { + [this, ticket] { m_active_parse_tasks.remove(ticket); emit parseFinished(); }, @@ -318,7 +320,7 @@ void ResourceFolderModel::onUpdateSucceeded() void ResourceFolderModel::onParseSucceeded(int ticket, QString resource_id) { auto iter = m_active_parse_tasks.constFind(ticket); - if (iter == m_active_parse_tasks.constEnd()) + if (iter == m_active_parse_tasks.constEnd() || !m_resources_index.contains(resource_id)) return; int row = m_resources_index[resource_id]; @@ -630,7 +632,7 @@ QString ResourceFolderModel::instDirPath() const void ResourceFolderModel::onParseFailed(int ticket, QString resource_id) { auto iter = m_active_parse_tasks.constFind(ticket); - if (iter == m_active_parse_tasks.constEnd()) + if (iter == m_active_parse_tasks.constEnd() || !m_resources_index.contains(resource_id)) return; auto removed_index = m_resources_index[resource_id]; diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index ca919d3e3..354eedc28 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -76,7 +76,7 @@ class ResourceFolderModel : public QAbstractListModel { virtual bool update(); /** Creates a new parse task, if needed, for 'res' and start it.*/ - virtual void resolveResource(Resource* res); + virtual void resolveResource(Resource::Ptr res); [[nodiscard]] qsizetype size() const { return m_resources.size(); } [[nodiscard]] bool empty() const { return size() == 0; } @@ -285,7 +285,7 @@ void ResourceFolderModel::applyUpdates(QSet& current_set, QSet } m_resources[row].reset(new_resource); - resolveResource(m_resources.at(row).get()); + resolveResource(m_resources.at(row)); emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); } } @@ -333,7 +333,7 @@ void ResourceFolderModel::applyUpdates(QSet& current_set, QSet for (auto& added : added_set) { auto res = new_resources[added]; m_resources.append(res); - resolveResource(m_resources.last().get()); + resolveResource(m_resources.last()); } endInsertRows(); diff --git a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h index 2bce2c137..168e4b6fa 100644 --- a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h +++ b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h @@ -7,6 +7,7 @@ #include +#include "Application.h" #include "FileSystem.h" #include "minecraft/mod/Resource.h" @@ -25,16 +26,12 @@ class BasicFolderLoadTask : public Task { [[nodiscard]] ResultPtr result() const { return m_result; } public: - BasicFolderLoadTask(QDir dir) : Task(nullptr, false), m_dir(dir), m_result(new Result), m_thread_to_spawn_into(thread()) + BasicFolderLoadTask(QDir dir) : Task(false), m_dir(dir), m_result(new Result), m_thread_to_spawn_into(thread()) { m_create_func = [](QFileInfo const& entry) -> Resource::Ptr { return makeShared(entry); }; } BasicFolderLoadTask(QDir dir, std::function create_function) - : Task(nullptr, false) - , m_dir(dir) - , m_result(new Result) - , m_create_func(std::move(create_function)) - , m_thread_to_spawn_into(thread()) + : Task(false), m_dir(dir), m_result(new Result), m_create_func(std::move(create_function)), m_thread_to_spawn_into(thread()) {} [[nodiscard]] bool canAbort() const override { return true; } @@ -52,6 +49,9 @@ class BasicFolderLoadTask : public Task { m_dir.refresh(); for (auto entry : m_dir.entryInfoList()) { auto filePath = entry.absoluteFilePath(); + if (auto app = APPLICATION_DYN; app && app->checkQSavePath(filePath)) { + continue; + } auto newFilePath = FS::getUniqueResourceName(filePath); if (newFilePath != filePath) { FS::move(filePath, newFilePath); diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp index b9288d2b3..b63d36361 100644 --- a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp @@ -52,11 +52,10 @@ static bool checkDependencies(std::shared_ptrversion.loaders || sel->version.loaders & loaders); } -GetModDependenciesTask::GetModDependenciesTask(QObject* parent, - BaseInstance* instance, +GetModDependenciesTask::GetModDependenciesTask(BaseInstance* instance, ModFolderModel* folder, QList> selected) - : SequentialTask(parent, tr("Get dependencies")) + : SequentialTask(tr("Get dependencies")) , m_selected(selected) , m_flame_provider{ ModPlatform::ResourceProvider::FLAME, std::make_shared(*instance), std::make_shared() } @@ -185,7 +184,7 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen 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())); + QString("DependencyInfo: %1").arg(dep.addonId.toString().isEmpty() ? dep.version : dep.addonId.toString())); if (!dep.addonId.toString().isEmpty()) { tasks->addTask(getProjectInfoTask(pDep)); diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.h b/launcher/minecraft/mod/tasks/GetModDependenciesTask.h index 7202b01e0..29c77f9fe 100644 --- a/launcher/minecraft/mod/tasks/GetModDependenciesTask.h +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.h @@ -61,10 +61,7 @@ class GetModDependenciesTask : public SequentialTask { std::shared_ptr api; }; - explicit GetModDependenciesTask(QObject* parent, - BaseInstance* instance, - ModFolderModel* folder, - QList> selected); + explicit GetModDependenciesTask(BaseInstance* instance, ModFolderModel* folder, QList> selected); auto getDependecies() const -> QList> { return m_pack_dependencies; } QHash getExtraInfo(); diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp index 82f6b9df9..19b15709d 100644 --- a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp @@ -157,7 +157,7 @@ bool validate(QFileInfo file) } // namespace DataPackUtils -LocalDataPackParseTask::LocalDataPackParseTask(int token, DataPack& dp) : Task(nullptr, false), m_token(token), m_data_pack(dp) {} +LocalDataPackParseTask::LocalDataPackParseTask(int token, DataPack& dp) : Task(false), m_token(token), m_data_pack(dp) {} bool LocalDataPackParseTask::abort() { diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index 60257ce0c..7417dffe6 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "FileSystem.h" @@ -15,6 +16,8 @@ #include "minecraft/mod/ModDetails.h" #include "settings/INIFile.h" +static QRegularExpression newlineRegex("\r\n|\n|\r"); + namespace ModUtils { // NEW format @@ -487,11 +490,11 @@ bool processZIP(Mod& mod, [[maybe_unused]] ProcessingLevel level) } // quick and dirty line-by-line parser - auto manifestLines = file.readAll().split('\n'); + auto manifestLines = QString(file.readAll()).split(newlineRegex); QString manifestVersion = ""; for (auto& line : manifestLines) { - if (QString(line).startsWith("Implementation-Version: ")) { - manifestVersion = QString(line).remove("Implementation-Version: "); + if (line.startsWith("Implementation-Version: ", Qt::CaseInsensitive)) { + manifestVersion = line.remove("Implementation-Version: ", Qt::CaseInsensitive); break; } } @@ -647,11 +650,11 @@ bool validate(QFileInfo file) return ModUtils::process(mod, ProcessingLevel::BasicInfoOnly) && mod.valid(); } -bool processIconPNG(const Mod& mod, QByteArray&& raw_data) +bool processIconPNG(const Mod& mod, QByteArray&& raw_data, QPixmap* pixmap) { auto img = QImage::fromData(raw_data); if (!img.isNull()) { - mod.setIcon(img); + *pixmap = mod.setIcon(img); } else { qWarning() << "Failed to parse mod logo:" << mod.iconPath() << "from" << mod.name(); return false; @@ -659,15 +662,15 @@ bool processIconPNG(const Mod& mod, QByteArray&& raw_data) return true; } -bool loadIconFile(const Mod& mod) +bool loadIconFile(const Mod& mod, QPixmap* pixmap) { 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"; + auto png_invalid = [&mod](const QString& reason) { + qWarning() << "Mod at" << mod.fileinfo().filePath() << "does not have a valid icon:" << reason; return false; }; @@ -676,24 +679,26 @@ bool loadIconFile(const Mod& mod) 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; + if (!icon.open(QIODevice::ReadOnly)) { + return png_invalid("failed to open file " + icon_info.filePath()); + } auto data = icon.readAll(); - bool icon_result = ModUtils::processIconPNG(mod, std::move(data)); + bool icon_result = ModUtils::processIconPNG(mod, std::move(data), pixmap); icon.close(); if (!icon_result) { - return png_invalid(); // icon invalid + return png_invalid("invalid png image"); // icon invalid } + return true; } - return false; + return png_invalid("file '" + icon_info.filePath() + "' does not exists or is not a file"); } case ResourceType::ZIPFILE: { QuaZip zip(mod.fileinfo().filePath()); if (!zip.open(QuaZip::mdUnzip)) - return false; + return png_invalid("failed to open '" + mod.fileinfo().filePath() + "' as a zip archive"); QuaZipFile file(&zip); @@ -701,35 +706,34 @@ bool loadIconFile(const Mod& mod) if (!file.open(QIODevice::ReadOnly)) { qCritical() << "Failed to open file in zip."; zip.close(); - return png_invalid(); + return png_invalid("Failed to open '" + mod.iconPath() + "' in zip archive"); } auto data = file.readAll(); - bool icon_result = ModUtils::processIconPNG(mod, std::move(data)); + bool icon_result = ModUtils::processIconPNG(mod, std::move(data), pixmap); file.close(); if (!icon_result) { - return png_invalid(); // icon png invalid + return png_invalid("invalid png image"); // icon png invalid } - } else { - return png_invalid(); // could not set icon as current file. + return true; } - return false; + return png_invalid("Failed to set '" + mod.iconPath() + + "' as current file in zip archive"); // could not set icon as current file. } case ResourceType::LITEMOD: { - return false; // can lightmods even have icons? + return png_invalid("litemods do not have icons"); // can lightmods even have icons? } default: - qWarning() << "Invalid type for mod, can not load icon."; - return false; + return png_invalid("Invalid type for mod, can not load icon."); } } } // namespace ModUtils LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile) - : Task(nullptr, false), m_token(token), m_type(type), m_modFile(modFile), m_result(new Result()) + : Task(false), m_token(token), m_type(type), m_modFile(modFile), m_result(new Result()) {} bool LocalModParseTask::abort() diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h index a03217093..91ee6f253 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.h @@ -26,8 +26,8 @@ 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); +bool processIconPNG(const Mod& mod, QByteArray&& raw_data, QPixmap* pixmap); +bool loadIconFile(const Mod& mod, QPixmap* pixmap); } // namespace ModUtils class LocalModParseTask : public Task { diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.h b/launcher/minecraft/mod/tasks/LocalModUpdateTask.h index 080999294..5447083ba 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.h +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.h @@ -42,6 +42,6 @@ class LocalModUpdateTask : public Task { private: QDir m_index_dir; - ModPlatform::IndexedPack& m_mod; - ModPlatform::IndexedVersion& m_mod_version; + ModPlatform::IndexedPack m_mod; + ModPlatform::IndexedVersion m_mod_version; }; diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp index 27fbf3c6d..0b80db82d 100644 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp @@ -358,9 +358,7 @@ bool validate(QFileInfo file) } // namespace ResourcePackUtils -LocalResourcePackParseTask::LocalResourcePackParseTask(int token, ResourcePack& rp) - : Task(nullptr, false), m_token(token), m_resource_pack(rp) -{} +LocalResourcePackParseTask::LocalResourcePackParseTask(int token, ResourcePack& rp) : Task(false), m_token(token), m_resource_pack(rp) {} bool LocalResourcePackParseTask::abort() { diff --git a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp index 4deebcd1d..a6ecc5353 100644 --- a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp @@ -93,7 +93,7 @@ bool validate(QFileInfo file) } // namespace ShaderPackUtils -LocalShaderPackParseTask::LocalShaderPackParseTask(int token, ShaderPack& sp) : Task(nullptr, false), m_token(token), m_shader_pack(sp) {} +LocalShaderPackParseTask::LocalShaderPackParseTask(int token, ShaderPack& sp) : Task(false), m_token(token), m_shader_pack(sp) {} bool LocalShaderPackParseTask::abort() { diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp index 00cc2def2..18020808a 100644 --- a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp @@ -230,8 +230,7 @@ bool validate(QFileInfo file) } // namespace TexturePackUtils -LocalTexturePackParseTask::LocalTexturePackParseTask(int token, TexturePack& rp) : Task(nullptr, false), m_token(token), m_texture_pack(rp) -{} +LocalTexturePackParseTask::LocalTexturePackParseTask(int token, TexturePack& rp) : Task(false), m_token(token), m_texture_pack(rp) {} bool LocalTexturePackParseTask::abort() { diff --git a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp index 9d564ddb3..74d8d8d60 100644 --- a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp @@ -170,7 +170,7 @@ bool validate(QFileInfo file) } // namespace WorldSaveUtils -LocalWorldSaveParseTask::LocalWorldSaveParseTask(int token, WorldSave& save) : Task(nullptr, false), m_token(token), m_save(save) {} +LocalWorldSaveParseTask::LocalWorldSaveParseTask(int token, WorldSave& save) : Task(false), m_token(token), m_save(save) {} bool LocalWorldSaveParseTask::abort() { diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index 501d5be13..2c3439ca4 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -36,13 +36,14 @@ #include "ModFolderLoadTask.h" +#include "Application.h" #include "FileSystem.h" #include "minecraft/mod/MetadataHandler.h" #include ModFolderLoadTask::ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan) - : Task(nullptr, false) + : Task(false) , m_mods_dir(mods_dir) , m_index_dir(index_dir) , m_is_indexed(is_indexed) @@ -65,6 +66,9 @@ void ModFolderLoadTask::executeTask() m_mods_dir.refresh(); for (auto entry : m_mods_dir.entryInfoList()) { auto filePath = entry.absoluteFilePath(); + if (auto app = APPLICATION_DYN; app && app->checkQSavePath(filePath)) { + continue; + } auto newFilePath = FS::getUniqueResourceName(filePath); if (newFilePath != filePath) { FS::move(filePath, newFilePath); diff --git a/launcher/minecraft/skins/SkinList.cpp b/launcher/minecraft/skins/SkinList.cpp index fd883ad52..017cb8dc2 100644 --- a/launcher/minecraft/skins/SkinList.cpp +++ b/launcher/minecraft/skins/SkinList.cpp @@ -336,7 +336,11 @@ void SkinList::save() arr << s.toJSON(); } doc["skins"] = arr; - Json::write(doc, m_dir.absoluteFilePath("index.json")); + try { + Json::write(doc, m_dir.absoluteFilePath("index.json")); + } catch (const FS::FileSystemException& e) { + qCritical() << "Failed to write skin index file :" << e.cause(); + } } int SkinList::getSelectedAccountSkin() diff --git a/launcher/minecraft/skins/SkinModel.cpp b/launcher/minecraft/skins/SkinModel.cpp index d53b9e762..937864e2c 100644 --- a/launcher/minecraft/skins/SkinModel.cpp +++ b/launcher/minecraft/skins/SkinModel.cpp @@ -41,7 +41,7 @@ SkinModel::SkinModel(QDir skinDir, QJsonObject obj) QString SkinModel::name() const { - return QFileInfo(m_path).baseName(); + return QFileInfo(m_path).completeBaseName(); } bool SkinModel::rename(QString newName) diff --git a/launcher/minecraft/update/AssetUpdateTask.cpp b/launcher/minecraft/update/AssetUpdateTask.cpp index 8add02d84..acdddc833 100644 --- a/launcher/minecraft/update/AssetUpdateTask.cpp +++ b/launcher/minecraft/update/AssetUpdateTask.cpp @@ -1,5 +1,6 @@ #include "AssetUpdateTask.h" +#include "launch/LaunchStep.h" #include "minecraft/AssetsUtils.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" @@ -14,8 +15,6 @@ AssetUpdateTask::AssetUpdateTask(MinecraftInstance* inst) m_inst = inst; } -AssetUpdateTask::~AssetUpdateTask() {} - void AssetUpdateTask::executeTask() { setStatus(tr("Updating assets index...")); diff --git a/launcher/minecraft/update/AssetUpdateTask.h b/launcher/minecraft/update/AssetUpdateTask.h index 6f053a54a..88fac0ac5 100644 --- a/launcher/minecraft/update/AssetUpdateTask.h +++ b/launcher/minecraft/update/AssetUpdateTask.h @@ -7,7 +7,7 @@ class AssetUpdateTask : public Task { Q_OBJECT public: AssetUpdateTask(MinecraftInstance* inst); - virtual ~AssetUpdateTask(); + virtual ~AssetUpdateTask() = default; void executeTask() override; diff --git a/launcher/minecraft/update/FMLLibrariesTask.h b/launcher/minecraft/update/FMLLibrariesTask.h index 32712d239..4fe2648e8 100644 --- a/launcher/minecraft/update/FMLLibrariesTask.h +++ b/launcher/minecraft/update/FMLLibrariesTask.h @@ -9,7 +9,7 @@ class FMLLibrariesTask : public Task { Q_OBJECT public: FMLLibrariesTask(MinecraftInstance* inst); - virtual ~FMLLibrariesTask() {}; + virtual ~FMLLibrariesTask() = default; void executeTask() override; diff --git a/launcher/minecraft/update/FoldersTask.cpp b/launcher/minecraft/update/FoldersTask.cpp index c74e8d2ef..7d6fc4394 100644 --- a/launcher/minecraft/update/FoldersTask.cpp +++ b/launcher/minecraft/update/FoldersTask.cpp @@ -37,7 +37,7 @@ #include #include "minecraft/MinecraftInstance.h" -FoldersTask::FoldersTask(MinecraftInstance* inst) : Task() +FoldersTask::FoldersTask(MinecraftInstance* inst) { m_inst = inst; } diff --git a/launcher/minecraft/update/FoldersTask.h b/launcher/minecraft/update/FoldersTask.h index be4e33eb7..7df7ef81d 100644 --- a/launcher/minecraft/update/FoldersTask.h +++ b/launcher/minecraft/update/FoldersTask.h @@ -7,7 +7,7 @@ class FoldersTask : public Task { Q_OBJECT public: FoldersTask(MinecraftInstance* inst); - virtual ~FoldersTask() {}; + virtual ~FoldersTask() = default; void executeTask() override; diff --git a/launcher/minecraft/update/LibrariesTask.h b/launcher/minecraft/update/LibrariesTask.h index 441191154..838f9d9b4 100644 --- a/launcher/minecraft/update/LibrariesTask.h +++ b/launcher/minecraft/update/LibrariesTask.h @@ -7,7 +7,7 @@ class LibrariesTask : public Task { Q_OBJECT public: LibrariesTask(MinecraftInstance* inst); - virtual ~LibrariesTask() {}; + virtual ~LibrariesTask() = default; void executeTask() override; diff --git a/launcher/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h index 24b82c28e..ff064c904 100644 --- a/launcher/modplatform/CheckUpdateTask.h +++ b/launcher/modplatform/CheckUpdateTask.h @@ -16,7 +16,7 @@ class CheckUpdateTask : public Task { std::list& mcVersions, QList loadersList, std::shared_ptr mods_folder) - : Task(nullptr), m_mods(mods), m_game_versions(mcVersions), m_loaders_list(loadersList), m_mods_folder(mods_folder) {}; + : Task(), m_mods(mods), m_game_versions(mcVersions), m_loaders_list(loadersList), m_mods_folder(mods_folder) {}; struct UpdatableMod { QString name; diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp index 43acea1a2..fb4a32918 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -19,29 +19,33 @@ static ModrinthAPI modrinth_api; static FlameAPI flame_api; EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::ResourceProvider prov) - : Task(nullptr), m_index_dir(dir), m_provider(prov), m_hashing_task(nullptr), m_current_task(nullptr) + : Task(), m_index_dir(dir), m_provider(prov), m_hashingTask(nullptr), m_current_task(nullptr) { - auto hash_task = createNewHash(mod); - if (!hash_task) + auto hashTask = createNewHash(mod); + if (!hashTask) return; - 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(); + connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) { m_mods.insert(hash, mod); }); + connect(hashTask.get(), &Task::failed, [this, mod] { emitFail(mod, "", RemoveFromList::No); }); + m_hashingTask = hashTask; } EnsureMetadataTask::EnsureMetadataTask(QList& mods, QDir dir, ModPlatform::ResourceProvider prov) - : Task(nullptr), m_index_dir(dir), m_provider(prov), m_current_task(nullptr) + : Task(), m_index_dir(dir), m_provider(prov), m_current_task(nullptr) { - m_hashing_task.reset(new ConcurrentTask(this, "MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); + auto hashTask = makeShared("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); + m_hashingTask = hashTask; for (auto* mod : mods) { auto hash_task = createNewHash(mod); if (!hash_task) continue; 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); + hashTask->addTask(hash_task); } } +EnsureMetadataTask::EnsureMetadataTask(QHash& mods, QDir dir, ModPlatform::ResourceProvider prov) + : Task(), m_mods(mods), m_index_dir(dir), m_provider(prov), m_current_task(nullptr) +{} Hashing::Hasher::Ptr EnsureMetadataTask::createNewHash(Mod* mod) { diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h index 2f276e5a0..14b75a1a7 100644 --- a/launcher/modplatform/EnsureMetadataTask.h +++ b/launcher/modplatform/EnsureMetadataTask.h @@ -1,14 +1,14 @@ #pragma once #include "ModIndex.h" -#include "net/NetJob.h" #include "modplatform/helpers/HashUtils.h" #include "tasks/ConcurrentTask.h" +#include + class Mod; -class QDir; class EnsureMetadataTask : public Task { Q_OBJECT @@ -16,10 +16,11 @@ class EnsureMetadataTask : public Task { public: EnsureMetadataTask(Mod*, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); EnsureMetadataTask(QList&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); + EnsureMetadataTask(QHash&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); ~EnsureMetadataTask() = default; - Task::Ptr getHashingTask() { return m_hashing_task; } + Task::Ptr getHashingTask() { return m_hashingTask; } public slots: bool abort() override; @@ -57,6 +58,6 @@ class EnsureMetadataTask : public Task { ModPlatform::ResourceProvider m_provider; QHash m_temp_versions; - ConcurrentTask::Ptr m_hashing_task; + Task::Ptr m_hashingTask; Task::Ptr m_current_task; }; diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index 8c85ae122..c3ecccf8e 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -31,6 +31,19 @@ static const QMap s_indexed_version_ty { "alpha", IndexedVersionType::VersionType::Alpha } }; +static const QList loaderList = { NeoForge, Forge, Cauldron, LiteLoader, Quilt, Fabric }; + +QList modLoaderTypesToList(ModLoaderTypes flags) +{ + QList flagList; + for (auto flag : loaderList) { + if (flags.testFlag(flag)) { + flagList.append(flag); + } + } + return flagList; +} + IndexedVersionType::IndexedVersionType(const QString& type) : IndexedVersionType(enumFromString(type)) {} IndexedVersionType::IndexedVersionType(const IndexedVersionType::VersionType& type) diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index e3fe69e0c..8fae1bf6c 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -32,10 +32,11 @@ namespace ModPlatform { enum ModLoaderType { NeoForge = 1 << 0, Forge = 1 << 1, Cauldron = 1 << 2, LiteLoader = 1 << 3, Fabric = 1 << 4, Quilt = 1 << 5 }; Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) +QList modLoaderTypesToList(ModLoaderTypes flags); enum class ResourceProvider { MODRINTH, FLAME }; -enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK }; +enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK, MODPACK }; enum class DependencyType { REQUIRED, OPTIONAL, INCOMPATIBLE, EMBEDDED, TOOL, INCLUDE, UNKNOWN }; diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index 39b64f1c3..d69bf12c0 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -1,87 +1,101 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 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 "FileResolvingTask.h" +#include #include "Json.h" +#include "QObjectPtr.h" #include "modplatform/ModIndex.h" -#include "net/ApiDownload.h" -#include "net/ApiUpload.h" -#include "net/Upload.h" +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/flame/FlameModIndex.h" +#include "modplatform/modrinth/ModrinthAPI.h" #include "modplatform/modrinth/ModrinthPackIndex.h" +#include "net/NetJob.h" +#include "tasks/Task.h" + +static const FlameAPI flameAPI; +static ModrinthAPI modrinthAPI; Flame::FileResolvingTask::FileResolvingTask(const shared_qobject_ptr& network, Flame::Manifest& toProcess) - : m_network(network), m_toProcess(toProcess) + : m_network(network), m_manifest(toProcess) {} bool Flame::FileResolvingTask::abort() { bool aborted = true; - if (m_dljob) - aborted &= m_dljob->abort(); - if (m_checkJob) - aborted &= m_checkJob->abort(); + if (m_task) { + aborted = m_task->abort(); + } return aborted ? Task::abort() : false; } void Flame::FileResolvingTask::executeTask() { - if (m_toProcess.files.isEmpty()) { // no file to resolve so leave it empty and emit success immediately + if (m_manifest.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 - 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; - })); - QByteArray data = Json::toText(object); - auto dl = Net::ApiUpload::makeByteArray(QUrl("https://api.curseforge.com/v1/mods/files"), result, data); - m_dljob->addNetAction(dl); + m_result.reset(new QByteArray()); + + QStringList fileIds; + for (auto file : m_manifest.files) { + fileIds.push_back(QString::number(file.fileId)); + } + m_task = flameAPI.getFiles(fileIds, m_result); auto step_progress = std::make_shared(); - connect(m_dljob.get(), &NetJob::finished, this, [this, step_progress]() { + connect(m_task.get(), &Task::finished, this, [this, step_progress]() { step_progress->state = TaskStepState::Succeeded; stepProgress(*step_progress); netJobFinished(); }); - connect(m_dljob.get(), &NetJob::failed, this, [this, step_progress](QString reason) { + connect(m_task.get(), &Task::failed, this, [this, step_progress](QString reason) { step_progress->state = TaskStepState::Failed; stepProgress(*step_progress); emitFailed(reason); }); - connect(m_dljob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress); - connect(m_dljob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) { + connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress); + connect(m_task.get(), &Task::progress, this, [this, step_progress](qint64 current, qint64 total) { qDebug() << "Resolve slug progress" << current << total; step_progress->update(current, total); stepProgress(*step_progress); }); - connect(m_dljob.get(), &NetJob::status, this, [this, step_progress](QString status) { + connect(m_task.get(), &Task::status, this, [this, step_progress](QString status) { step_progress->status = status; stepProgress(*step_progress); }); - m_dljob->start(); + m_task->start(); } void Flame::FileResolvingTask::netJobFinished() { setProgress(1, 3); // job to check modrinth for blocked projects - m_checkJob.reset(new NetJob("Modrinth check", m_network)); - m_checkJob->setAskRetry(false); - blockedProjects = QMap>(); - QJsonDocument doc; QJsonArray array; try { - doc = Json::requireDocument(*result); + doc = Json::requireDocument(*m_result); array = Json::requireArray(doc.object()["data"]); } catch (Json::JsonException& e) { qCritical() << "Non-JSON data returned from the CF API"; @@ -92,125 +106,157 @@ void Flame::FileResolvingTask::netJobFinished() return; } + QStringList hashes; for (QJsonValueRef file : array) { - auto fileid = Json::requireInteger(Json::requireObject(file)["id"]); - auto& out = m_toProcess.files[fileid]; try { - out.parseFromObject(Json::requireObject(file)); - } catch ([[maybe_unused]] const JSONValidationError& e) { - qDebug() << "Blocked mod on curseforge" << out.fileName; - auto hash = out.hash; - 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::ApiDownload::makeByteArray(QUrl(url), output); - QObject::connect(dl.get(), &Task::succeeded, [&out]() { out.resolved = true; }); - - m_checkJob->addNetAction(dl); - blockedProjects.insert(&out, output); + auto obj = Json::requireObject(file); + auto version = FlameMod::loadIndexedPackVersion(obj); + auto fileid = version.fileId.toInt(); + m_manifest.files[fileid].version = version; + auto url = QUrl(version.downloadUrl, QUrl::TolerantMode); + if (!url.isValid() && "sha1" == version.hash_type && !version.hash.isEmpty()) { + hashes.push_back(version.hash); } + } catch (Json::JsonException& e) { + qCritical() << "Non-JSON data returned from the CF API"; + qCritical() << e.cause(); + + emitFailed(tr("Invalid data returned from the API.")); + + return; } } + if (hashes.isEmpty()) { + getFlameProjects(); + return; + } + m_result.reset(new QByteArray()); + m_task = modrinthAPI.currentVersions(hashes, "sha1", m_result); + (dynamic_cast(m_task.get()))->setAskRetry(false); auto step_progress = std::make_shared(); - connect(m_checkJob.get(), &NetJob::finished, this, [this, step_progress]() { + connect(m_task.get(), &Task::finished, this, [this, step_progress]() { step_progress->state = TaskStepState::Succeeded; stepProgress(*step_progress); - modrinthCheckFinished(); + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*m_result, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *m_result; + + getFlameProjects(); + return; + } + + try { + auto entries = Json::requireObject(doc); + for (auto& out : m_manifest.files) { + auto url = QUrl(out.version.downloadUrl, QUrl::TolerantMode); + if (!url.isValid() && "sha1" == out.version.hash_type && !out.version.hash.isEmpty()) { + try { + auto entry = Json::requireObject(entries, out.version.hash); + + auto file = Modrinth::loadIndexedPackVersion(entry); + + // If there's more than one mod loader for this version, we can't know for sure + // which file is relative to each loader, so it's best to not use any one and + // let the user download it manually. + if (!file.loaders || hasSingleModLoaderSelected(file.loaders)) { + out.version.downloadUrl = file.downloadUrl; + qDebug() << "Found alternative on modrinth " << out.version.fileName; + } + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << entries; + } + } + } + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << doc; + } + getFlameProjects(); }); - connect(m_checkJob.get(), &NetJob::failed, this, [this, step_progress](QString reason) { + connect(m_task.get(), &Task::failed, this, [this, step_progress](QString reason) { step_progress->state = TaskStepState::Failed; stepProgress(*step_progress); }); - connect(m_checkJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress); - connect(m_checkJob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) { + connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress); + connect(m_task.get(), &Task::progress, this, [this, step_progress](qint64 current, qint64 total) { qDebug() << "Resolve slug progress" << current << total; step_progress->update(current, total); stepProgress(*step_progress); }); - connect(m_checkJob.get(), &NetJob::status, this, [this, step_progress](QString status) { + connect(m_task.get(), &Task::status, this, [this, step_progress](QString status) { step_progress->status = status; stepProgress(*step_progress); }); - m_checkJob->start(); + m_task->start(); } -void Flame::FileResolvingTask::modrinthCheckFinished() +void Flame::FileResolvingTask::getFlameProjects() { setProgress(2, 3); - qDebug() << "Finished with blocked mods : " << blockedProjects.size(); + m_result.reset(new QByteArray()); + QStringList addonIds; + for (auto file : m_manifest.files) { + addonIds.push_back(QString::number(file.projectId)); + } - for (auto it = blockedProjects.keyBegin(); it != blockedProjects.keyEnd(); it++) { - auto& out = *it; - auto bytes = blockedProjects[out]; - if (!out->resolved) { - continue; - } + m_task = flameAPI.getProjects(addonIds, m_result); - QJsonDocument doc = QJsonDocument::fromJson(*bytes); - auto obj = doc.object(); - auto file = Modrinth::loadIndexedPackVersion(obj); - - // If there's more than one mod loader for this version, we can't know for sure - // which file is relative to each loader, so it's best to not use any one and - // let the user download it manually. - if (!file.loaders || hasSingleModLoaderSelected(file.loaders)) { - out->url = file.downloadUrl; - qDebug() << "Found alternative on modrinth " << out->fileName; - } else { - out->resolved = false; + auto step_progress = std::make_shared(); + connect(m_task.get(), &Task::succeeded, this, [this, step_progress] { + QJsonParseError parse_error{}; + auto doc = QJsonDocument::fromJson(*m_result, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Modrinth projects task at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *m_result; + return; } - } - // 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 - if (!block->empty()) { - // 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::ApiDownload::makeByteArray(url, output); - qDebug() << "Fetching url slug for file:" << mod->fileName; - QObject::connect(dl.get(), &Task::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 link = QString("%1/download/%2").arg(base, QString::number(mod->fileId)); - mod->websiteUrl = link; - }); - m_slugJob->addNetAction(dl); - index++; + + try { + QJsonArray entries; + entries = Json::requireArray(Json::requireObject(doc), "data"); + + for (auto entry : entries) { + auto entry_obj = Json::requireObject(entry); + auto id = Json::requireInteger(entry_obj, "id"); + auto file = std::find_if(m_manifest.files.begin(), m_manifest.files.end(), + [id](const Flame::File& file) { return file.projectId == id; }); + if (file == m_manifest.files.end()) { + continue; + } + + setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(file->version.fileName)); + FlameMod::loadIndexedPack(file->pack, entry_obj); + } + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << doc; } - auto step_progress = std::make_shared(); - connect(m_slugJob.get(), &NetJob::succeeded, this, [this, step_progress]() { - step_progress->state = TaskStepState::Succeeded; - stepProgress(*step_progress); - emitSucceeded(); - }); - connect(m_slugJob.get(), &NetJob::failed, this, [this, step_progress](QString reason) { - step_progress->state = TaskStepState::Failed; - stepProgress(*step_progress); - emitFailed(reason); - }); - connect(m_slugJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress); - connect(m_slugJob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) { - qDebug() << "Resolve slug progress" << current << total; - step_progress->update(current, total); - stepProgress(*step_progress); - }); - connect(m_slugJob.get(), &NetJob::status, this, [this, step_progress](QString status) { - step_progress->status = status; - stepProgress(*step_progress); - }); - - m_slugJob->start(); - } else { + step_progress->state = TaskStepState::Succeeded; + stepProgress(*step_progress); emitSucceeded(); - } + }); + + connect(m_task.get(), &Task::failed, this, [this, step_progress](QString reason) { + step_progress->state = TaskStepState::Failed; + stepProgress(*step_progress); + emitFailed(reason); + }); + connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress); + connect(m_task.get(), &Task::progress, this, [this, step_progress](qint64 current, qint64 total) { + qDebug() << "Resolve slug progress" << current << total; + step_progress->update(current, total); + stepProgress(*step_progress); + }); + connect(m_task.get(), &Task::status, this, [this, step_progress](QString status) { + step_progress->status = status; + stepProgress(*step_progress); + }); + + m_task->start(); } diff --git a/launcher/modplatform/flame/FileResolvingTask.h b/launcher/modplatform/flame/FileResolvingTask.h index cfa53cb22..edd9fce9a 100644 --- a/launcher/modplatform/flame/FileResolvingTask.h +++ b/launcher/modplatform/flame/FileResolvingTask.h @@ -1,7 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 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 "PackManifest.h" -#include "net/NetJob.h" #include "tasks/Task.h" namespace Flame { @@ -9,12 +27,12 @@ class FileResolvingTask : public Task { Q_OBJECT public: explicit FileResolvingTask(const shared_qobject_ptr& network, Flame::Manifest& toProcess); - virtual ~FileResolvingTask() {}; + virtual ~FileResolvingTask() = default; bool canAbort() const override { return true; } bool abort() override; - const Flame::Manifest& getResults() const { return m_toProcess; } + const Flame::Manifest& getResults() const { return m_manifest; } protected: virtual void executeTask() override; @@ -22,16 +40,13 @@ class FileResolvingTask : public Task { protected slots: void netJobFinished(); + private: + void getFlameProjects(); + private: /* data */ shared_qobject_ptr m_network; - Flame::Manifest m_toProcess; - std::shared_ptr result; - NetJob::Ptr m_dljob; - NetJob::Ptr m_checkJob; - NetJob::Ptr m_slugJob; - - void modrinthCheckFinished(); - - QMap> blockedProjects; + Flame::Manifest m_manifest; + std::shared_ptr m_result; + Task::Ptr m_task; }; } // namespace Flame diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index 72437976d..53eadcf02 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -221,14 +221,20 @@ QList FlameAPI::getSortingMethods() const { 8, "GameVersion", QObject::tr("Sort by Game Version") } }; } -Task::Ptr FlameAPI::getModCategories(std::shared_ptr response) +Task::Ptr FlameAPI::getCategories(std::shared_ptr response, ModPlatform::ResourceType type) { auto netJob = makeShared(QString("Flame::GetCategories"), APPLICATION->network()); - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl("https://api.curseforge.com/v1/categories?gameId=432&classId=6"), response)); + netJob->addNetAction(Net::ApiDownload::makeByteArray( + QUrl(QString("https://api.curseforge.com/v1/categories?gameId=432&classId=%1").arg(getClassId(type))), response)); QObject::connect(netJob.get(), &Task::failed, [](QString msg) { qDebug() << "Flame failed to get categories:" << msg; }); return netJob; } +Task::Ptr FlameAPI::getModCategories(std::shared_ptr response) +{ + return getCategories(response, ModPlatform::ResourceType::MOD); +} + QList FlameAPI::loadModCategories(std::shared_ptr response) { QList categories; @@ -264,21 +270,35 @@ std::optional FlameAPI::getLatestVersion(QList instanceLoaders, ModPlatform::ModLoaderTypes modLoaders) { - // edge case: mod has installed for forge but the instance is fabric => fabric version will be prioritizated on update - auto bestVersion = [&versions](ModPlatform::ModLoaderTypes loader) { - std::optional ver; - for (auto file_tmp : versions) { - if (file_tmp.loaders & loader && (!ver.has_value() || file_tmp.date > ver->date)) { - ver = file_tmp; + QHash bestMatch; + auto checkVersion = [&bestMatch](const ModPlatform::IndexedVersion& version, const ModPlatform::ModLoaderType& loader) { + if (bestMatch.contains(loader)) { + auto best = bestMatch.value(loader); + if (version.date > best.date) { + bestMatch[loader] = version; } + } else { + bestMatch[loader] = version; } - return ver; }; - for (auto l : instanceLoaders) { - auto ver = bestVersion(l); - if (ver.has_value()) { - return ver; + for (auto file_tmp : versions) { + auto loaders = ModPlatform::modLoaderTypesToList(file_tmp.loaders); + if (loaders.isEmpty()) { + checkVersion(file_tmp, ModPlatform::ModLoaderType(0)); + } else { + for (auto loader : loaders) { + checkVersion(file_tmp, loader); + } + } + } + // edge case: mod has installed for forge but the instance is fabric => fabric version will be prioritizated on update + auto currentLoaders = instanceLoaders + ModPlatform::modLoaderTypesToList(modLoaders); + currentLoaders.append(ModPlatform::ModLoaderType(0)); // add a fallback in case the versions do not define a loader + + for (auto loader : currentLoaders) { + if (bestMatch.contains(loader)) { + return bestMatch.value(loader); } } - return bestVersion(modLoaders); + return {}; } diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 1160151c5..3ca0d5448 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -25,6 +25,7 @@ class FlameAPI : public NetworkResourceAPI { Task::Ptr getFiles(const QStringList& fileIds, std::shared_ptr response) const; Task::Ptr getFile(const QString& addonId, const QString& fileId, std::shared_ptr response) const; + static Task::Ptr getCategories(std::shared_ptr response, ModPlatform::ResourceType type); static Task::Ptr getModCategories(std::shared_ptr response); static QList loadModCategories(std::shared_ptr response); @@ -46,6 +47,8 @@ class FlameAPI : public NetworkResourceAPI { return 12; case ModPlatform::ResourceType::SHADER_PACK: return 6552; + case ModPlatform::ResourceType::MODPACK: + return 4471; } } @@ -82,12 +85,9 @@ class FlameAPI : public NetworkResourceAPI { static const QString getModLoaderFilters(ModPlatform::ModLoaderTypes types) { return "[" + getModLoaderStrings(types).join(',') + "]"; } - private: + public: [[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(); - QStringList get_arguments; get_arguments.append(QString("classId=%1").arg(getClassId(args.type))); get_arguments.append(QString("index=%1").arg(args.offset)); @@ -97,20 +97,22 @@ class FlameAPI : public NetworkResourceAPI { if (args.sorting.has_value()) get_arguments.append(QString("sortField=%1").arg(args.sorting.value().index)); get_arguments.append("sortOrder=desc"); - if (args.loaders.has_value()) + if (args.loaders.has_value() && args.loaders.value() != 0) get_arguments.append(QString("modLoaderTypes=%1").arg(getModLoaderFilters(args.loaders.value()))); if (args.categoryIds.has_value() && !args.categoryIds->empty()) get_arguments.append(QString("categoryIds=[%1]").arg(args.categoryIds->join(","))); - get_arguments.append(gameVersionStr); + if (args.versions.has_value() && !args.versions.value().empty()) + get_arguments.append(QString("gameVersion=%1").arg(args.versions.value().front().toString())); return "https://api.curseforge.com/v1/mods/search?gameId=432&" + get_arguments.join('&'); - }; + } + private: [[nodiscard]] std::optional getInfoURL(QString const& id) const override { return QString("https://api.curseforge.com/v1/mods/%1").arg(id); - }; + } [[nodiscard]] std::optional getVersionsURL(VersionSearchArgs const& args) const override { @@ -125,7 +127,7 @@ class FlameAPI : public NetworkResourceAPI { url += QString("&modLoaderType=%1").arg(mappedModLoader); } return url; - }; + } [[nodiscard]] std::optional getDependencyURL(DependencySearchArgs const& args) const override { @@ -137,5 +139,5 @@ class FlameAPI : public NetworkResourceAPI { url += QString("&modLoaderType=%1").arg(mappedModLoader); } return url; - }; + } }; diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index a629cc15b..d6eb45ef5 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -35,8 +35,11 @@ #include "FlameInstanceCreationTask.h" +#include "QObjectPtr.h" +#include "minecraft/mod/tasks/LocalModUpdateTask.h" #include "modplatform/flame/FileResolvingTask.h" #include "modplatform/flame/FlameAPI.h" +#include "modplatform/flame/FlameModIndex.h" #include "modplatform/flame/PackManifest.h" #include "Application.h" @@ -51,6 +54,7 @@ #include "settings/INISettingsObject.h" +#include "tasks/ConcurrentTask.h" #include "ui/dialogs/BlockedModsDialog.h" #include "ui/dialogs/CustomMessageBox.h" @@ -58,7 +62,6 @@ #include #include "meta/Index.h" -#include "meta/VersionList.h" #include "minecraft/World.h" #include "minecraft/mod/tasks/LocalResourceParse.h" #include "net/ApiDownload.h" @@ -208,8 +211,7 @@ bool FlameCreationTask::updateInstance() Flame::File file; // We don't care about blocked mods, we just need local data to delete the file - file.parseFromObject(entry_obj, false); - + file.version = FlameMod::loadIndexedPackVersion(entry_obj); auto id = Json::requireInteger(entry_obj, "id"); old_files.insert(id, file); } @@ -219,10 +221,10 @@ bool FlameCreationTask::updateInstance() // Delete the files for (auto& file : old_files) { - if (file.fileName.isEmpty() || file.targetFolder.isEmpty()) + if (file.version.fileName.isEmpty() || file.targetFolder.isEmpty()) continue; - QString relative_path(FS::PathCombine(file.targetFolder, file.fileName)); + QString relative_path(FS::PathCombine(file.targetFolder, file.version.fileName)); qDebug() << "Scheduling" << relative_path << "for removal"; m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path)); } @@ -442,6 +444,7 @@ bool FlameCreationTask::createInstance() setError(tr("Unable to resolve mod IDs:\n") + reason); loop.quit(); }); + connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::aborted, &loop, &QEventLoop::quit); connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress); connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus); connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::stepProgress, this, &FlameCreationTask::propagateStepProgress); @@ -471,15 +474,15 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) QList blocked_mods; auto anyBlocked = false; for (const auto& result : results.files.values()) { - if (result.fileName.endsWith(".zip")) { - m_ZIP_resources.append(std::make_pair(result.fileName, result.targetFolder)); + if (result.version.fileName.endsWith(".zip")) { + m_ZIP_resources.append(std::make_pair(result.version.fileName, result.targetFolder)); } - if (!result.resolved || result.url.isEmpty()) { + if (result.version.downloadUrl.isEmpty()) { BlockedMod blocked_mod; - blocked_mod.name = result.fileName; - blocked_mod.websiteUrl = result.websiteUrl; - blocked_mod.hash = result.hash; + blocked_mod.name = result.version.fileName; + blocked_mod.websiteUrl = QString("%1/download/%2").arg(result.pack.websiteUrl, QString::number(result.fileId)); + blocked_mod.hash = result.version.hash; blocked_mod.matched = false; blocked_mod.localPath = ""; blocked_mod.targetFolder = result.targetFolder; @@ -521,7 +524,7 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop) QStringList optionalFiles; for (auto& result : results) { if (!result.required) { - optionalFiles << FS::PathCombine(result.targetFolder, result.fileName); + optionalFiles << FS::PathCombine(result.targetFolder, result.version.fileName); } } @@ -537,7 +540,7 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop) selectedOptionalMods = optionalModDialog.getResult(); } for (const auto& result : results) { - auto fileName = result.fileName; + auto fileName = result.version.fileName; fileName = FS::RemoveInvalidPathChars(fileName); auto relpath = FS::PathCombine(result.targetFolder, fileName); @@ -548,36 +551,16 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop) relpath = FS::PathCombine("minecraft", relpath); auto path = FS::PathCombine(m_stagingPath, relpath); - switch (result.type) { - case Flame::File::Type::Folder: { - logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath)); - // 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()) { - qDebug() << "Will download" << result.url << "to" << path; - auto dl = Net::ApiDownload::makeFile(result.url, path); - m_files_job->addNetAction(dl); - } - break; - } - case Flame::File::Type::Modpack: - logWarning(tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg(relpath)); - break; - case Flame::File::Type::Cmod2: - case Flame::File::Type::Ctoc: - case Flame::File::Type::Unknown: - logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath)); - break; + if (!result.version.downloadUrl.isEmpty()) { + qDebug() << "Will download" << result.version.downloadUrl << "to" << path; + auto dl = Net::ApiDownload::makeFile(result.version.downloadUrl, path); + m_files_job->addNetAction(dl); } } - m_mod_id_resolver.reset(); - connect(m_files_job.get(), &NetJob::succeeded, this, [&]() { + connect(m_files_job.get(), &NetJob::finished, this, [this, &loop]() { m_files_job.reset(); - validateZIPResources(); + validateZIPResources(loop); }); connect(m_files_job.get(), &NetJob::failed, [&](QString reason) { m_files_job.reset(); @@ -588,7 +571,6 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop) setProgress(current, total); }); connect(m_files_job.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress); - connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit); setStatus(tr("Downloading mods...")); m_files_job->start(); @@ -626,9 +608,10 @@ void FlameCreationTask::copyBlockedMods(QList const& blocked_mods) setAbortable(true); } -void FlameCreationTask::validateZIPResources() +void FlameCreationTask::validateZIPResources(QEventLoop& loop) { qDebug() << "Validating whether resources stored as .zip are in the right place"; + QStringList zipMods; for (auto [fileName, targetFolder] : m_ZIP_resources) { qDebug() << "Checking" << fileName << "..."; auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName); @@ -668,6 +651,7 @@ void FlameCreationTask::validateZIPResources() switch (type) { case PackedResourceType::Mod: validatePath(fileName, targetFolder, "mods"); + zipMods.push_back(fileName); break; case PackedResourceType::ResourcePack: validatePath(fileName, targetFolder, "resourcepacks"); @@ -693,4 +677,16 @@ void FlameCreationTask::validateZIPResources() break; } } + auto task = makeShared("CreateModMetadata", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); + auto results = m_mod_id_resolver->getResults().files; + auto folder = FS::PathCombine(m_stagingPath, "minecraft", "mods", ".index"); + for (auto file : results) { + if (file.targetFolder != "mods" || (file.version.fileName.endsWith(".zip") && !zipMods.contains(file.version.fileName))) { + continue; + } + task->addTask(makeShared(folder, file.pack, file.version)); + } + connect(task.get(), &Task::finished, &loop, &QEventLoop::quit); + m_process_update_file_info_job = task; + task->start(); } diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.h b/launcher/modplatform/flame/FlameInstanceCreationTask.h index 02ad48f2e..28ab176c2 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.h +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.h @@ -74,7 +74,7 @@ class FlameCreationTask final : public InstanceCreationTask { void idResolverSucceeded(QEventLoop&); void setupDownloadJob(QEventLoop&); void copyBlockedMods(QList const& blocked_mods); - void validateZIPResources(); + void validateZIPResources(QEventLoop& loop); QString getVersionForLoader(QString uid, QString loaderType, QString version, QString mcVersion); private: diff --git a/launcher/modplatform/flame/FlamePackExportTask.cpp b/launcher/modplatform/flame/FlamePackExportTask.cpp index d661f1f05..3405b702f 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.cpp +++ b/launcher/modplatform/flame/FlamePackExportTask.cpp @@ -103,8 +103,7 @@ void FlamePackExportTask::collectHashes() setStatus(tr("Finding file hashes...")); setProgress(1, 5); auto allMods = mcInstance->loaderModList()->allMods(); - ConcurrentTask::Ptr hashingTask( - new ConcurrentTask(this, "MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); + ConcurrentTask::Ptr hashingTask(new ConcurrentTask("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); task.reset(hashingTask); for (const QFileInfo& file : files) { const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath()); diff --git a/launcher/modplatform/flame/FlamePackExportTask.h b/launcher/modplatform/flame/FlamePackExportTask.h index 78b46e91f..b11eb17fa 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.h +++ b/launcher/modplatform/flame/FlamePackExportTask.h @@ -26,6 +26,7 @@ #include "tasks/Task.h" class FlamePackExportTask : public Task { + Q_OBJECT public: FlamePackExportTask(const QString& name, const QString& version, diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp index ca8e0a853..8c25b0482 100644 --- a/launcher/modplatform/flame/FlamePackIndex.cpp +++ b/launcher/modplatform/flame/FlamePackIndex.cpp @@ -3,6 +3,7 @@ #include #include "Json.h" +#include "modplatform/ModIndex.h" void Flame::loadIndexedPack(Flame::IndexedPack& pack, QJsonObject& obj) { @@ -88,8 +89,27 @@ void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr) continue; } + for (auto mcVer : versionArray) { + auto str = mcVer.toString(); + + if (str.contains('.')) + file.mcVersion.append(str); + + if (auto loader = str.toLower(); loader == "neoforge") + file.loaders |= ModPlatform::NeoForge; + else if (loader == "forge") + file.loaders |= ModPlatform::Forge; + else if (loader == "cauldron") + file.loaders |= ModPlatform::Cauldron; + else if (loader == "liteloader") + file.loaders |= ModPlatform::LiteLoader; + else if (loader == "fabric") + file.loaders |= ModPlatform::Fabric; + else if (loader == "quilt") + file.loaders |= ModPlatform::Quilt; + } + // pick the latest version supported - file.mcVersion = versionArray[0].toString(); file.version = Json::requireString(version, "displayName"); ModPlatform::IndexedVersionType::VersionType ver_type; diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h index b2a12a67f..11633deee 100644 --- a/launcher/modplatform/flame/FlamePackIndex.h +++ b/launcher/modplatform/flame/FlamePackIndex.h @@ -18,6 +18,7 @@ struct IndexedVersion { int fileId; QString version; ModPlatform::IndexedVersionType version_type; + ModPlatform::ModLoaderTypes loaders = {}; QString mcVersion; QString downloadUrl; }; diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp index 40a523d31..e576a6a84 100644 --- a/launcher/modplatform/flame/PackManifest.cpp +++ b/launcher/modplatform/flame/PackManifest.cpp @@ -68,35 +68,3 @@ void Flame::loadManifest(Flame::Manifest& m, const QString& filepath) } loadManifestV1(m, obj); } - -bool Flame::File::parseFromObject(const QJsonObject& obj, bool throw_on_blocked) -{ - fileName = Json::requireString(obj, "fileName"); - // This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience - // It is also optional - type = File::Type::SingleFile; - - targetFolder = "mods"; - - // get the hash - hash = QString(); - auto hashes = Json::ensureArray(obj, "hashes"); - for (QJsonValueRef item : hashes) { - auto hobj = Json::requireObject(item); - auto algo = Json::requireInteger(hobj, "algo"); - auto value = Json::requireString(hobj, "value"); - if (algo == 1) { - hash = value; - } - } - - // may throw, if the project is blocked - QString rawUrl = Json::ensureString(obj, "downloadUrl"); - url = QUrl(rawUrl, QUrl::TolerantMode); - if (!url.isValid() && throw_on_blocked) { - throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); - } - - resolved = true; - return true; -} diff --git a/launcher/modplatform/flame/PackManifest.h b/launcher/modplatform/flame/PackManifest.h index 4417c2430..49a0b2d68 100644 --- a/launcher/modplatform/flame/PackManifest.h +++ b/launcher/modplatform/flame/PackManifest.h @@ -40,26 +40,20 @@ #include #include #include +#include "modplatform/ModIndex.h" namespace Flame { struct File { - // NOTE: throws JSONValidationError - bool parseFromObject(const QJsonObject& object, bool throw_on_blocked = true); - int projectId = 0; int fileId = 0; // NOTE: the opposite to 'optional' bool required = true; - QString hash; - // NOTE: only set on blocked files ! Empty otherwise. - QString websiteUrl; + + ModPlatform::IndexedPack pack; + ModPlatform::IndexedVersion version; // our - bool resolved = false; - QString fileName; - QUrl url; QString targetFolder = QStringLiteral("mods"); - enum class Type { Unknown, Folder, Ctoc, SingleFile, Cmod2, Modpack, Mod } type = Type::Mod; }; struct Modloader { diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.cpp b/launcher/modplatform/helpers/NetworkResourceAPI.cpp index 974e732a7..7a234a8f8 100644 --- a/launcher/modplatform/helpers/NetworkResourceAPI.cpp +++ b/launcher/modplatform/helpers/NetworkResourceAPI.cpp @@ -43,11 +43,16 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks& callbacks.on_succeed(doc); }); - QObject::connect(netJob.get(), &NetJob::failed, [netJob, callbacks](const QString& reason) { + // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. + // This prevents the lambda from extending the lifetime of the shared resource, + // as it only temporarily locks the resource when needed. + auto weak = netJob.toWeakRef(); + QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { int network_error_code = -1; - if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) - network_error_code = failed_action->replyStatusCode(); - + if (auto netJob = weak.lock()) { + if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) + network_error_code = failed_action->replyStatusCode(); + } callbacks.on_fail(reason, network_error_code); }); QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); }); @@ -102,11 +107,17 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi callbacks.on_succeed(doc, args.pack); }); - QObject::connect(netJob.get(), &NetJob::failed, [netJob, callbacks](const QString& reason) { - int network_error_code = -1; - if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) - network_error_code = failed_action->replyStatusCode(); + // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. + // This prevents the lambda from extending the lifetime of the shared resource, + // as it only temporarily locks the resource when needed. + auto weak = netJob.toWeakRef(); + QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { + int network_error_code = -1; + if (auto netJob = weak.lock()) { + if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) + network_error_code = failed_action->replyStatusCode(); + } callbacks.on_fail(reason, network_error_code); }); @@ -153,11 +164,17 @@ Task::Ptr NetworkResourceAPI::getDependencyVersion(DependencySearchArgs&& args, callbacks.on_succeed(doc, args.dependency); }); - QObject::connect(netJob.get(), &NetJob::failed, [netJob, callbacks](const QString& reason) { - int network_error_code = -1; - if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) - network_error_code = failed_action->replyStatusCode(); + // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. + // This prevents the lambda from extending the lifetime of the shared resource, + // as it only temporarily locks the resource when needed. + auto weak = netJob.toWeakRef(); + QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { + int network_error_code = -1; + if (auto netJob = weak.lock()) { + if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) + network_error_code = failed_action->replyStatusCode(); + } callbacks.on_fail(reason, network_error_code); }); return netJob; diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp index 8f1a6e2ff..a0beeddcc 100644 --- a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp @@ -74,6 +74,7 @@ void PackFetchTask::fetchPrivate(const QStringList& toFetch) auto data = std::make_shared(); NetJob* job = new NetJob("Fetching private pack", m_network); job->addNetAction(Net::ApiDownload::makeByteArray(privatePackBaseUrl.arg(packCode), data)); + job->setAskRetry(false); QObject::connect(job, &NetJob::succeeded, this, [this, job, data, packCode] { ModpackList packs; diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp index 7157f7f2d..d6252663f 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -138,7 +138,7 @@ void PackInstallTask::install() if (unzipMcDir.exists()) { // ok, found minecraft dir, move contents to instance dir if (!FS::move(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/minecraft")) { - emitFailed(tr("Failed to move unzipped Minecraft!")); + emitFailed(tr("Failed to move unpacked Minecraft!")); return; } } diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp index 4798ace84..a954f65a5 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.cpp +++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp @@ -34,7 +34,7 @@ Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_f auto body_raw = body.toJson(); netJob->addNetAction(Net::ApiUpload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files"), response, body_raw)); - + netJob->setAskRetry(false); return netJob; } @@ -129,7 +129,7 @@ Task::Ptr ModrinthAPI::getModCategories(std::shared_ptr response) return netJob; } -QList ModrinthAPI::loadModCategories(std::shared_ptr response) +QList ModrinthAPI::loadCategories(std::shared_ptr response, QString projectType) { QList categories; QJsonParseError parse_error{}; @@ -147,7 +147,7 @@ QList ModrinthAPI::loadModCategories(std::shared_ptr ModrinthAPI::loadModCategories(std::shared_ptr ModrinthAPI::loadModCategories(std::shared_ptr response) +{ + return loadCategories(response, "mod"); +}; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index d1f8f712a..070f59dad 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -31,6 +31,7 @@ class ModrinthAPI : public NetworkResourceAPI { Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const override; static Task::Ptr getModCategories(std::shared_ptr response); + static QList loadCategories(std::shared_ptr response, QString projectType); static QList loadModCategories(std::shared_ptr response); public: @@ -90,6 +91,8 @@ class ModrinthAPI : public NetworkResourceAPI { return "resourcepack"; case ModPlatform::ResourceType::SHADER_PACK: return "shader"; + case ModPlatform::ResourceType::MODPACK: + return "modpack"; default: qWarning() << "Invalid resource type for Modrinth API!"; break; @@ -102,9 +105,9 @@ class ModrinthAPI : public NetworkResourceAPI { { QStringList facets_list; - if (args.loaders.has_value()) + if (args.loaders.has_value() && args.loaders.value() != 0) facets_list.append(QString("[%1]").arg(getModLoaderFilters(args.loaders.value()))); - if (args.versions.has_value()) + if (args.versions.has_value() && !args.versions.value().empty()) facets_list.append(QString("[%1]").arg(getGameVersionsArray(args.versions.value()))); if (args.side.has_value()) { auto side = getSideFilters(args.side.value()); @@ -122,7 +125,7 @@ class ModrinthAPI : public NetworkResourceAPI { public: [[nodiscard]] inline auto getSearchURL(SearchArgs const& args) const -> std::optional override { - if (args.loaders.has_value()) { + if (args.loaders.has_value() && args.loaders.value() != 0) { if (!validateModLoaders(args.loaders.value())) { qWarning() << "Modrinth - or our interface - does not support any the provided mod loaders!"; return {}; @@ -163,7 +166,7 @@ class ModrinthAPI : public NetworkResourceAPI { .arg(BuildConfig.MODRINTH_PROD_URL, args.pack.addonId.toString(), get_arguments.isEmpty() ? "" : "?", get_arguments.join('&')); }; - auto getGameVersionsArray(std::list mcVersions) const -> QString + QString getGameVersionsArray(std::list mcVersions) const { QString s; for (auto& ver : mcVersions) { diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index 70bf138a8..fcabe24fc 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -8,6 +8,7 @@ #include "QObjectPtr.h" #include "ResourceDownloadTask.h" +#include "modplatform/ModIndex.h" #include "modplatform/helpers/HashUtils.h" #include "tasks/ConcurrentTask.h" @@ -40,7 +41,7 @@ void ModrinthCheckUpdate::executeTask() setProgress(0, 9); auto hashing_task = - makeShared(this, "MakeModrinthHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); + makeShared("MakeModrinthHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); for (auto* mod : m_mods) { auto hash = mod->metadata()->hash; @@ -91,20 +92,15 @@ void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr resp // it means this specific version is not available if (project_obj.isEmpty()) { qDebug() << "Mod " << m_mappings.find(hash).value()->name() << " got an empty response." << "Hash: " << hash; - continue; } // Sometimes a version may have multiple files, one with "forge" and one with "fabric", // so we may want to filter it QString loader_filter; - static auto flags = { ModPlatform::ModLoaderType::NeoForge, ModPlatform::ModLoaderType::Forge, - ModPlatform::ModLoaderType::Quilt, ModPlatform::ModLoaderType::Fabric }; - for (auto flag : flags) { - if (loader.testFlag(flag)) { - loader_filter = ModPlatform::getModLoaderAsString(flag); - break; - } + for (auto flag : ModPlatform::modLoaderTypesToList(loader)) { + loader_filter = ModPlatform::getModLoaderAsString(flag); + break; } // Currently, we rely on a couple heuristics to determine whether an update is actually available or not: diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index 517864790..3592d2ed7 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -5,8 +5,12 @@ #include "InstanceList.h" #include "Json.h" +#include "QObjectPtr.h" +#include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" +#include "minecraft/mod/Mod.h" +#include "modplatform/EnsureMetadataTask.h" #include "modplatform/helpers/OverrideUtils.h" #include "modplatform/modrinth/ModrinthPackManifest.h" @@ -21,6 +25,7 @@ #include #include +#include #include bool ModrinthCreationTask::abort() @@ -29,8 +34,8 @@ bool ModrinthCreationTask::abort() return false; m_abort = true; - if (m_files_job) - m_files_job->abort(); + if (m_task) + m_task->abort(); return Task::abort(); } @@ -131,7 +136,7 @@ bool ModrinthCreationTask::updateInstance() } auto old_client_overrides = Override::readOverrides("client-overrides", old_index_folder); - for (const auto& entry : old_overrides) { + for (const auto& entry : old_client_overrides) { if (entry.isEmpty()) continue; qDebug() << "Scheduling" << entry << "for removal"; @@ -234,11 +239,11 @@ bool ModrinthCreationTask::createInstance() instance.setName(name()); instance.saveNow(); - m_files_job.reset(new NetJob(tr("Mod Download Modrinth"), APPLICATION->network())); + auto downloadMods = makeShared(tr("Mod Download Modrinth"), APPLICATION->network()); auto root_modpack_path = FS::PathCombine(m_stagingPath, m_root_path); auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path); - + QHash mods; for (auto file : m_files) { auto fileName = file.path; fileName = FS::RemoveInvalidPathChars(fileName); @@ -249,20 +254,29 @@ bool ModrinthCreationTask::createInstance() .arg(fileName)); return false; } - + if (fileName.startsWith("mods/")) { + auto mod = new Mod(file_path); + ModDetails d; + d.mod_id = file_path; + mod->setDetails(d); + mods[file.hash.toHex()] = mod; + } + if (file.downloads.empty()) { + setError(tr("The file '%1' is missing a download link. This is invalid in the pack format.").arg(fileName)); + return false; + } qDebug() << "Will try to download" << file.downloads.front() << "to" << file_path; auto dl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path); dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); - m_files_job->addNetAction(dl); - + downloadMods->addNetAction(dl); if (!file.downloads.empty()) { // FIXME: This really needs to be put into a ConcurrentTask of // MultipleOptionsTask's , once those exist :) auto param = dl.toWeakRef(); - connect(dl.get(), &Task::failed, [this, &file, file_path, param] { + connect(dl.get(), &Task::failed, [&file, file_path, param, downloadMods] { auto ndl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path); ndl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); - m_files_job->addNetAction(ndl); + downloadMods->addNetAction(ndl); if (auto shared = param.lock()) shared->succeeded(); }); @@ -271,23 +285,51 @@ bool ModrinthCreationTask::createInstance() bool ended_well = false; - connect(m_files_job.get(), &NetJob::succeeded, this, [&]() { ended_well = true; }); - connect(m_files_job.get(), &NetJob::failed, [&](const QString& reason) { + connect(downloadMods.get(), &NetJob::succeeded, this, [&]() { ended_well = true; }); + connect(downloadMods.get(), &NetJob::failed, [&](const QString& reason) { ended_well = false; setError(reason); }); - connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit); - connect(m_files_job.get(), &NetJob::progress, [&](qint64 current, qint64 total) { + connect(downloadMods.get(), &NetJob::finished, &loop, &QEventLoop::quit); + connect(downloadMods.get(), &NetJob::progress, [&](qint64 current, qint64 total) { setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); setProgress(current, total); }); - connect(m_files_job.get(), &NetJob::stepProgress, this, &ModrinthCreationTask::propagateStepProgress); + connect(downloadMods.get(), &NetJob::stepProgress, this, &ModrinthCreationTask::propagateStepProgress); setStatus(tr("Downloading mods...")); - m_files_job->start(); + downloadMods->start(); + m_task = downloadMods; loop.exec(); + if (!ended_well) { + for (auto m : mods) { + delete m; + } + return ended_well; + } + + QEventLoop ensureMetaLoop; + QDir folder = FS::PathCombine(instance.modsRoot(), ".index"); + auto ensureMetadataTask = makeShared(mods, folder, ModPlatform::ResourceProvider::MODRINTH); + connect(ensureMetadataTask.get(), &Task::succeeded, this, [&]() { ended_well = true; }); + connect(ensureMetadataTask.get(), &Task::finished, &ensureMetaLoop, &QEventLoop::quit); + connect(ensureMetadataTask.get(), &Task::progress, [&](qint64 current, qint64 total) { + setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); + setProgress(current, total); + }); + connect(ensureMetadataTask.get(), &Task::stepProgress, this, &ModrinthCreationTask::propagateStepProgress); + + ensureMetadataTask->start(); + m_task = ensureMetadataTask; + + ensureMetaLoop.exec(); + for (auto m : mods) { + delete m; + } + mods.clear(); + // Update information of the already installed instance, if any. if (m_instance && ended_well) { setAbortable(false); diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h index f07734a58..ddfa7ae95 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h @@ -1,15 +1,11 @@ #pragma once -#include "InstanceCreationTask.h" - #include - -#include "minecraft/MinecraftInstance.h" +#include "BaseInstance.h" +#include "InstanceCreationTask.h" #include "modplatform/modrinth/ModrinthPackManifest.h" -#include "net/NetJob.h" - class ModrinthCreationTask final : public InstanceCreationTask { Q_OBJECT @@ -43,7 +39,7 @@ class ModrinthCreationTask final : public InstanceCreationTask { QString m_managed_id, m_managed_version_id, m_managed_name; std::vector m_files; - NetJob::Ptr m_files_job; + Task::Ptr m_task; std::optional m_instance; diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.h b/launcher/modplatform/modrinth/ModrinthPackExportTask.h index 81c2f25bf..ee740a456 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.h +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.h @@ -27,6 +27,7 @@ #include "tasks/Task.h" class ModrinthPackExportTask : public Task { + Q_OBJECT public: ModrinthPackExportTask(const QString& name, const QString& version, diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index f360df43a..c52a1743b 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -135,6 +135,21 @@ auto loadIndexedVersion(QJsonObject& obj) -> ModpackVersion if (!gameVersions.isEmpty()) { file.gameVersion = Json::ensureString(gameVersions[0]); } + auto loaders = Json::requireArray(obj, "loaders"); + for (auto loader : loaders) { + if (loader == "neoforge") + file.loaders |= ModPlatform::NeoForge; + else if (loader == "forge") + file.loaders |= ModPlatform::Forge; + else if (loader == "cauldron") + file.loaders |= ModPlatform::Cauldron; + else if (loader == "liteloader") + file.loaders |= ModPlatform::LiteLoader; + else if (loader == "fabric") + file.loaders |= ModPlatform::Fabric; + else if (loader == "quilt") + file.loaders |= ModPlatform::Quilt; + } file.version_type = ModPlatform::IndexedVersionType(Json::requireString(obj, "version_type")); file.changelog = Json::ensureString(obj, "changelog"); diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index 2bd61c5d9..2e5e2da84 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -87,6 +87,7 @@ struct ModpackVersion { QString gameVersion; ModPlatform::IndexedVersionType version_type; QString changelog; + ModPlatform::ModLoaderTypes loaders = {}; QString id; QString project_id; diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 77a0935f3..04c8a926e 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -72,7 +72,7 @@ auto stringEntry(toml::table table, QString entry_name) -> QString { auto node = table[StringUtils::toStdString(entry_name)]; if (!node) { - qCritical() << "Failed to read str property '" + entry_name + "' in mod metadata."; + qWarning() << "Failed to read str property '" + entry_name + "' in mod metadata."; return {}; } @@ -83,7 +83,7 @@ auto intEntry(toml::table table, QString entry_name) -> int { auto node = table[StringUtils::toStdString(entry_name)]; if (!node) { - qCritical() << "Failed to read int property '" + entry_name + "' in mod metadata."; + qWarning() << "Failed to read int property '" + entry_name + "' in mod metadata."; return {}; } @@ -186,11 +186,8 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) } toml::array loaders; - for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Cauldron, ModPlatform::LiteLoader, ModPlatform::Fabric, - ModPlatform::Quilt }) { - if (mod.loaders & loader) { - loaders.push_back(getModLoaderAsString(loader).toStdString()); - } + for (auto loader : ModPlatform::modLoaderTypesToList(mod.loaders)) { + loaders.push_back(getModLoaderAsString(loader).toStdString()); } toml::array mcVersions; for (auto version : mod.mcVersions) { @@ -208,9 +205,9 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) auto tbl = toml::table{ { "name", mod.name.toStdString() }, { "filename", mod.filename.toStdString() }, { "side", sideToString(mod.side).toStdString() }, - { "loaders", loaders }, - { "mcVersions", mcVersions }, - { "releaseType", mod.releaseType.toString().toStdString() }, + { "x-prismlauncher-loaders", loaders }, + { "x-prismlauncher-mc-versions", mcVersions }, + { "x-prismlauncher-release-type", mod.releaseType.toString().toStdString() }, { "download", toml::table{ { "mode", mod.mode.toStdString() }, @@ -295,15 +292,15 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod mod.name = stringEntry(table, "name"); mod.filename = stringEntry(table, "filename"); mod.side = stringToSide(stringEntry(table, "side")); - mod.releaseType = ModPlatform::IndexedVersionType(stringEntry(table, "releaseType")); - if (auto loaders = table["loaders"]; loaders && loaders.is_array()) { + mod.releaseType = ModPlatform::IndexedVersionType(stringEntry(table, "x-prismlauncher-release-type")); + if (auto loaders = table["x-prismlauncher-loaders"]; loaders && loaders.is_array()) { for (auto&& loader : *loaders.as_array()) { if (loader.is_string()) { mod.loaders |= ModPlatform::getModLoaderFromString(QString::fromStdString(loader.as_string()->value_or(""))); } } } - if (auto versions = table["mcVersions"]; versions && versions.is_array()) { + if (auto versions = table["x-prismlauncher-mc-versions"]; versions && versions.is_array()) { for (auto&& version : *versions.as_array()) { if (version.is_string()) { auto ver = QString::fromStdString(version.as_string()->value_or("")); diff --git a/launcher/net/FileSink.cpp b/launcher/net/FileSink.cpp index 1ecb21fdf..95c1a8f44 100644 --- a/launcher/net/FileSink.cpp +++ b/launcher/net/FileSink.cpp @@ -55,7 +55,7 @@ Task::State FileSink::init(QNetworkRequest& request) } wroteAnyData = false; - m_output_file.reset(new QSaveFile(m_filename)); + m_output_file.reset(new PSaveFile(m_filename)); if (!m_output_file->open(QIODevice::WriteOnly)) { qCCritical(taskNetLogC) << "Could not open " + m_filename + " for writing"; return Task::State::Failed; diff --git a/launcher/net/FileSink.h b/launcher/net/FileSink.h index 816254ff9..272f8ddc3 100644 --- a/launcher/net/FileSink.h +++ b/launcher/net/FileSink.h @@ -35,8 +35,7 @@ #pragma once -#include - +#include "PSaveFile.h" #include "Sink.h" namespace Net { @@ -60,6 +59,6 @@ class FileSink : public Sink { protected: QString m_filename; bool wroteAnyData = false; - std::unique_ptr m_output_file; + std::unique_ptr m_output_file; }; } // namespace Net diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp index e363c911d..335e360b2 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -45,10 +45,10 @@ #endif NetJob::NetJob(QString job_name, shared_qobject_ptr network, int max_concurrent) - : ConcurrentTask(nullptr, job_name), m_network(network) + : ConcurrentTask(job_name), m_network(network) { #if defined(LAUNCHER_APPLICATION) - if (max_concurrent < 0) + if (APPLICATION_DYN && max_concurrent < 0) max_concurrent = APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt(); #endif if (max_concurrent > 0) @@ -161,7 +161,8 @@ bool NetJob::isOnline() void NetJob::emitFailed(QString reason) { #if defined(LAUNCHER_APPLICATION) - if (m_ask_retry && m_manual_try < APPLICATION->settings()->get("NumberOfManualRetries").toInt() && isOnline()) { + + if (APPLICATION_DYN && m_ask_retry && m_manual_try < APPLICATION->settings()->get("NumberOfManualRetries").toInt() && isOnline()) { m_manual_try++; auto response = CustomMessageBox::selectable(nullptr, "Confirm retry", "The tasks failed.\n" diff --git a/launcher/resources/backgrounds/backgrounds.qrc b/launcher/resources/backgrounds/backgrounds.qrc index c74aa81eb..35116aa27 100644 --- a/launcher/resources/backgrounds/backgrounds.qrc +++ b/launcher/resources/backgrounds/backgrounds.qrc @@ -1,6 +1,7 @@ + miside-screenshot.png typescript.png kitteh.png kitteh-xmas.png diff --git a/launcher/resources/backgrounds/miside-screenshot.png b/launcher/resources/backgrounds/miside-screenshot.png new file mode 100644 index 000000000..604899c8b Binary files /dev/null and b/launcher/resources/backgrounds/miside-screenshot.png differ diff --git a/launcher/resources/multimc/index.theme b/launcher/resources/multimc/index.theme index 4da8072d9..497106d6f 100644 --- a/launcher/resources/multimc/index.theme +++ b/launcher/resources/multimc/index.theme @@ -1,7 +1,6 @@ [Icon Theme] Name=Legacy Comment=Default Icons -Inherits=default Directories=8x8,16x16,22x22,24x24,32x32,32x32/instances,48x48,50x50/instances,64x64,128x128/instances,256x256,scalable,scalable/instances [8x8] diff --git a/launcher/settings/INIFile.cpp b/launcher/settings/INIFile.cpp index e97741f20..2c7620e65 100644 --- a/launcher/settings/INIFile.cpp +++ b/launcher/settings/INIFile.cpp @@ -39,7 +39,6 @@ #include #include -#include #include #include #include diff --git a/launcher/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp index 6f4a94e7f..ad2a14c42 100644 --- a/launcher/tasks/ConcurrentTask.cpp +++ b/launcher/tasks/ConcurrentTask.cpp @@ -38,8 +38,7 @@ #include #include "tasks/Task.h" -ConcurrentTask::ConcurrentTask(QObject* parent, QString task_name, int max_concurrent) - : Task(parent), m_name(task_name), m_total_max_size(max_concurrent) +ConcurrentTask::ConcurrentTask(QString task_name, int max_concurrent) : Task(), m_total_max_size(max_concurrent) { setObjectName(task_name); } @@ -104,9 +103,9 @@ void ConcurrentTask::clear() m_done.clear(); m_failed.clear(); m_queue.clear(); + m_task_progress.clear(); m_progress = 0; - m_stepProgress = 0; } void ConcurrentTask::executeNextSubTask() @@ -139,7 +138,7 @@ void ConcurrentTask::startSubTask(Task::Ptr next) connect(next.get(), &Task::status, this, [this, next](QString msg) { subTaskStatus(next, msg); }); connect(next.get(), &Task::details, this, [this, next](QString msg) { subTaskDetails(next, msg); }); - connect(next.get(), &Task::stepProgress, this, [this, next](TaskStepProgress const& tp) { subTaskStepProgress(next, tp); }); + connect(next.get(), &Task::stepProgress, this, &ConcurrentTask::stepProgress); connect(next.get(), &Task::progress, this, [this, next](qint64 current, qint64 total) { subTaskProgress(next, current, total); }); @@ -149,7 +148,6 @@ void ConcurrentTask::startSubTask(Task::Ptr next) m_task_progress.insert(next->getUid(), task_progress); updateState(); - updateStepProgress(*task_progress.get(), Operation::ADDED); QMetaObject::invokeMethod(next.get(), &Task::start, Qt::QueuedConnection); } @@ -161,14 +159,14 @@ void ConcurrentTask::subTaskFinished(Task::Ptr task, TaskStepState state) m_doing.remove(task.get()); - auto task_progress = m_task_progress.value(task->getUid()); - task_progress->state = state; + auto task_progress = *m_task_progress.value(task->getUid()); + task_progress.state = state; + m_task_progress.remove(task->getUid()); disconnect(task.get(), 0, this, 0); - emit stepProgress(*task_progress); + emit stepProgress(task_progress); updateState(); - updateStepProgress(*task_progress, Operation::REMOVED); QMetaObject::invokeMethod(this, &ConcurrentTask::executeNextSubTask, Qt::QueuedConnection); } @@ -215,7 +213,6 @@ void ConcurrentTask::subTaskProgress(Task::Ptr task, qint64 current, qint64 tota task_progress->update(current, total); emit stepProgress(*task_progress); - updateStepProgress(*task_progress, Operation::CHANGED); updateState(); if (totalSize() == 1) { @@ -223,52 +220,6 @@ void ConcurrentTask::subTaskProgress(Task::Ptr task, qint64 current, qint64 tota } } -void ConcurrentTask::subTaskStepProgress(Task::Ptr task, TaskStepProgress const& task_progress) -{ - Operation op = Operation::ADDED; - - if (!m_task_progress.contains(task_progress.uid)) { - m_task_progress.insert(task_progress.uid, std::make_shared(task_progress)); - op = Operation::ADDED; - emit stepProgress(task_progress); - updateStepProgress(task_progress, op); - } else { - auto tp = m_task_progress.value(task_progress.uid); - - tp->old_current = tp->current; - tp->old_total = tp->total; - - tp->current = task_progress.current; - tp->total = task_progress.total; - tp->status = task_progress.status; - tp->details = task_progress.details; - - op = Operation::CHANGED; - emit stepProgress(*tp.get()); - updateStepProgress(*tp.get(), op); - } -} - -void ConcurrentTask::updateStepProgress(TaskStepProgress const& changed_progress, Operation op) -{ - switch (op) { - case Operation::ADDED: - m_stepProgress += changed_progress.current; - m_stepTotalProgress += changed_progress.total; - break; - case Operation::REMOVED: - m_stepProgress -= changed_progress.current; - m_stepTotalProgress -= changed_progress.total; - break; - case Operation::CHANGED: - m_stepProgress -= changed_progress.old_current; - m_stepTotalProgress -= changed_progress.old_total; - m_stepProgress += changed_progress.current; - m_stepTotalProgress += changed_progress.total; - break; - } -} - void ConcurrentTask::updateState() { if (totalSize() > 1) { @@ -276,7 +227,6 @@ void ConcurrentTask::updateState() setStatus(tr("Executing %1 task(s) (%2 out of %3 are done)") .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(totalSize()))); } else { - setProgress(m_stepProgress, m_stepTotalProgress); QString status = tr("Please wait..."); if (m_queue.size() > 0) { status = tr("Waiting for a task to start..."); diff --git a/launcher/tasks/ConcurrentTask.h b/launcher/tasks/ConcurrentTask.h index 07ea58575..d988623b9 100644 --- a/launcher/tasks/ConcurrentTask.h +++ b/launcher/tasks/ConcurrentTask.h @@ -48,7 +48,7 @@ class ConcurrentTask : public Task { public: using Ptr = shared_qobject_ptr; - explicit ConcurrentTask(QObject* parent = nullptr, QString task_name = "", int max_concurrent = 6); + explicit ConcurrentTask(QString task_name = "", int max_concurrent = 6); ~ConcurrentTask() override; // safe to call before starting the task @@ -80,23 +80,16 @@ class ConcurrentTask : public Task { void subTaskStatus(Task::Ptr task, const QString& msg); void subTaskDetails(Task::Ptr task, const QString& msg); void subTaskProgress(Task::Ptr task, qint64 current, qint64 total); - void subTaskStepProgress(Task::Ptr task, TaskStepProgress const& task_step_progress); protected: // NOTE: This is not thread-safe. [[nodiscard]] unsigned int totalSize() const { return static_cast(m_queue.size() + m_doing.size() + m_done.size()); } - enum class Operation { ADDED, REMOVED, CHANGED }; - void updateStepProgress(TaskStepProgress const& changed_progress, Operation); - virtual void updateState(); void startSubTask(Task::Ptr task); protected: - QString m_name; - QString m_step_status; - QQueue m_queue; QHash m_doing; @@ -107,7 +100,4 @@ class ConcurrentTask : public Task { QHash> m_task_progress; int m_total_max_size; - - qint64 m_stepProgress = 0; - qint64 m_stepTotalProgress = 100; }; diff --git a/launcher/tasks/MultipleOptionsTask.cpp b/launcher/tasks/MultipleOptionsTask.cpp index 5afe03964..ba0c23542 100644 --- a/launcher/tasks/MultipleOptionsTask.cpp +++ b/launcher/tasks/MultipleOptionsTask.cpp @@ -36,7 +36,7 @@ #include -MultipleOptionsTask::MultipleOptionsTask(QObject* parent, const QString& task_name) : ConcurrentTask(parent, task_name, 1) {} +MultipleOptionsTask::MultipleOptionsTask(const QString& task_name) : ConcurrentTask(task_name, 1) {} void MultipleOptionsTask::executeNextSubTask() { diff --git a/launcher/tasks/MultipleOptionsTask.h b/launcher/tasks/MultipleOptionsTask.h index 9a88a9999..7a19ed6ad 100644 --- a/launcher/tasks/MultipleOptionsTask.h +++ b/launcher/tasks/MultipleOptionsTask.h @@ -42,7 +42,7 @@ class MultipleOptionsTask : public ConcurrentTask { Q_OBJECT public: - explicit MultipleOptionsTask(QObject* parent = nullptr, const QString& task_name = ""); + explicit MultipleOptionsTask(const QString& task_name = ""); ~MultipleOptionsTask() override = default; private slots: diff --git a/launcher/tasks/SequentialTask.cpp b/launcher/tasks/SequentialTask.cpp index 509d91cf7..2e48414f2 100644 --- a/launcher/tasks/SequentialTask.cpp +++ b/launcher/tasks/SequentialTask.cpp @@ -38,7 +38,7 @@ #include #include "tasks/ConcurrentTask.h" -SequentialTask::SequentialTask(QObject* parent, QString task_name) : ConcurrentTask(parent, task_name, 1) {} +SequentialTask::SequentialTask(QString task_name) : ConcurrentTask(task_name, 1) {} void SequentialTask::subTaskFailed(Task::Ptr task, const QString& msg) { diff --git a/launcher/tasks/SequentialTask.h b/launcher/tasks/SequentialTask.h index a7c101ab4..77cd4387f 100644 --- a/launcher/tasks/SequentialTask.h +++ b/launcher/tasks/SequentialTask.h @@ -47,7 +47,7 @@ class SequentialTask : public ConcurrentTask { Q_OBJECT public: - explicit SequentialTask(QObject* parent = nullptr, QString task_name = ""); + explicit SequentialTask(QString task_name = ""); ~SequentialTask() override = default; protected slots: diff --git a/launcher/tasks/Task.cpp b/launcher/tasks/Task.cpp index b17096ca7..1871c5df8 100644 --- a/launcher/tasks/Task.cpp +++ b/launcher/tasks/Task.cpp @@ -40,7 +40,7 @@ Q_LOGGING_CATEGORY(taskLogC, "launcher.task") -Task::Task(QObject* parent, bool show_debug) : QObject(parent), m_show_debug(show_debug) +Task::Task(bool show_debug) : m_show_debug(show_debug) { m_uid = QUuid::createUuid(); setAutoDelete(false); diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index 883408c97..e712700a1 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -87,7 +87,7 @@ class Task : public QObject, public QRunnable { enum class State { Inactive, Running, Succeeded, Failed, AbortedByUser }; public: - explicit Task(QObject* parent = 0, bool show_debug_log = true); + explicit Task(bool show_debug_log = true); virtual ~Task() = default; bool isRunning() const; diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index 39719b125..d03469b78 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -550,7 +550,7 @@ void TranslationsModel::downloadIndex() d->m_index_job.reset(new NetJob("Translations Index", APPLICATION->network())); MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "index_v2.json"); entry->setStale(true); - auto task = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + "index_v2.json"), entry); + auto task = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATION_FILES_URL + "index_v2.json"), entry); d->m_index_task = task.get(); d->m_index_job->addNetAction(task); d->m_index_job->setAskRetry(false); @@ -591,12 +591,13 @@ void TranslationsModel::downloadTranslation(QString key) MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "mmc_" + key + ".qm"); entry->setStale(true); - auto dl = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + lang->file_name), entry); + auto dl = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATION_FILES_URL + lang->file_name), entry); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, lang->file_sha1)); dl->setProgress(dl->getProgress(), lang->file_size); d->m_dl_job.reset(new NetJob("Translation for " + key, APPLICATION->network())); d->m_dl_job->addNetAction(dl); + d->m_dl_job->setAskRetry(false); connect(d->m_dl_job.get(), &NetJob::succeeded, this, &TranslationsModel::dlGood); connect(d->m_dl_job.get(), &NetJob::failed, this, &TranslationsModel::dlFailed); diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index a5ccbc19a..09c47b609 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -233,6 +233,8 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi if (qgetenv("XDG_CURRENT_DESKTOP") == "gamescope") { ui->mainToolBar->addAction(ui->actionCloseWindow); } + + ui->actionViewJavaFolder->setEnabled(BuildConfig.JAVA_DOWNLOADER_ENABLED); } // add the toolbar toggles to the view menu @@ -1223,6 +1225,11 @@ void MainWindow::on_actionViewLogsFolder_triggered() DesktopServices::openPath("logs", true); } +void MainWindow::on_actionViewJavaFolder_triggered() +{ + DesktopServices::openPath(APPLICATION->javaPath(), true); +} + void MainWindow::refreshInstances() { APPLICATION->instances()->loadList(); diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 41bef9980..0e692eda7 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -48,7 +48,6 @@ #include "BaseInstance.h" #include "minecraft/auth/MinecraftAccount.h" -#include "net/NetJob.h" class LaunchController; class NewsChecker; @@ -119,6 +118,7 @@ class MainWindow : public QMainWindow { void on_actionViewCatPackFolder_triggered(); void on_actionViewIconsFolder_triggered(); void on_actionViewLogsFolder_triggered(); + void on_actionViewJavaFolder_triggered(); void on_actionViewSkinsFolder_triggered(); diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index bad8762ad..f20c34206 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -131,7 +131,7 @@ 0 0 800 - 22 + 27 @@ -192,6 +192,7 @@ + @@ -788,6 +789,18 @@ Open the cat packs folder in a file browser. + + + + .. + + + Java + + + Open the Java folder in a file browser. Only available if the built-in Java downloader is used. + + diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp index b652ba991..a8d60aef1 100644 --- a/launcher/ui/dialogs/AboutDialog.cpp +++ b/launcher/ui/dialogs/AboutDialog.cpp @@ -77,7 +77,7 @@ QString getCreditsHtml() 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("

ZekeZ %1

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

cozyGalvinism %1

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

DioEgizio %1

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

flowln %1

\n").arg(getGitHub("flowln")); @@ -101,7 +101,7 @@ QString getCreditsHtml() stream << "

" << QObject::tr("With thanks to", "About Credits") << "

\n"; stream << QString("

Boba %1

\n").arg(getWebsite("https://bobaonline.neocities.org/")); - stream << QString("

Davi Rafael %1

\n").arg(getWebsite("https://auti.one/")); + stream << QString("

AutiOne %1

\n").arg(getWebsite("https://auti.one/")); stream << QString("

Fulmine %1

\n").arg(getWebsite("https://fulmine.xyz/")); stream << QString("

ely %1

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

gon sawa %1

\n").arg(getGitHub("gonsawa")); diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index 5c93053d1..b3b6d2bcc 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -46,11 +46,13 @@ BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, cons : QDialog(parent), ui(new Ui::BlockedModsDialog), m_mods(mods), m_hash_type(hash_type) { m_hashing_task = shared_qobject_ptr( - new ConcurrentTask(this, "MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); + new ConcurrentTask("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); connect(m_hashing_task.get(), &Task::finished, this, &BlockedModsDialog::hashTaskFinished); ui->setupUi(this); + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); m_openMissingButton = ui->buttonBox->addButton(tr("Open Missing"), QDialogButtonBox::ActionRole); connect(m_openMissingButton, &QPushButton::clicked, this, [this]() { openAll(true); }); diff --git a/launcher/ui/dialogs/CopyInstanceDialog.cpp b/launcher/ui/dialogs/CopyInstanceDialog.cpp index 770741a61..e5c2c301b 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.cpp +++ b/launcher/ui/dialogs/CopyInstanceDialog.cpp @@ -109,6 +109,9 @@ CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget* parent) auto HelpButton = ui->buttonBox->button(QDialogButtonBox::Help); connect(HelpButton, &QPushButton::clicked, this, &CopyInstanceDialog::help); + HelpButton->setText(tr("Help")); + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } CopyInstanceDialog::~CopyInstanceDialog() diff --git a/launcher/ui/dialogs/EditAccountDialog.cpp b/launcher/ui/dialogs/EditAccountDialog.cpp index 58036fd82..9d0175bbc 100644 --- a/launcher/ui/dialogs/EditAccountDialog.cpp +++ b/launcher/ui/dialogs/EditAccountDialog.cpp @@ -15,6 +15,7 @@ #include "EditAccountDialog.h" #include +#include #include #include "ui_EditAccountDialog.h" @@ -27,6 +28,9 @@ EditAccountDialog::EditAccountDialog(const QString& text, QWidget* parent, int f ui->userTextBox->setEnabled(flags & UsernameField); ui->passTextBox->setEnabled(flags & PasswordField); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } EditAccountDialog::~EditAccountDialog() diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp index 9f2b3ac42..d25cd32b6 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.cpp +++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -51,6 +51,7 @@ #include #include #include +#include #include #include #include @@ -85,6 +86,9 @@ ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget* parent auto headerView = ui->treeView->header(); headerView->setSectionResizeMode(QHeaderView::ResizeToContents); headerView->setSectionResizeMode(0, QHeaderView::Stretch); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ExportInstanceDialog::~ExportInstanceDialog() diff --git a/launcher/ui/dialogs/ExportPackDialog.cpp b/launcher/ui/dialogs/ExportPackDialog.cpp index 0278c6cb0..ed79a741f 100644 --- a/launcher/ui/dialogs/ExportPackDialog.cpp +++ b/launcher/ui/dialogs/ExportPackDialog.cpp @@ -103,6 +103,9 @@ ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPla QHeaderView* headerView = ui->files->header(); headerView->setSectionResizeMode(QHeaderView::ResizeToContents); headerView->setSectionResizeMode(0, QHeaderView::Stretch); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ExportPackDialog::~ExportPackDialog() diff --git a/launcher/ui/dialogs/ExportToModListDialog.cpp b/launcher/ui/dialogs/ExportToModListDialog.cpp index 1e0ae87a3..c2ba68f7a 100644 --- a/launcher/ui/dialogs/ExportToModListDialog.cpp +++ b/launcher/ui/dialogs/ExportToModListDialog.cpp @@ -64,6 +64,9 @@ ExportToModListDialog::ExportToModListDialog(QString name, QList mods, QWi this->ui->finalText->selectAll(); this->ui->finalText->copy(); }); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Save)->setText(tr("Save")); triggerImp(); } @@ -164,7 +167,12 @@ void ExportToModListDialog::done(int result) if (output.isEmpty()) return; - FS::write(output, ui->finalText->toPlainText().toUtf8()); + + try { + FS::write(output, ui->finalText->toPlainText().toUtf8()); + } catch (const FS::FileSystemException& e) { + qCritical() << "Failed to save mod list file :" << e.cause(); + } } QDialog::done(result); diff --git a/launcher/ui/dialogs/IconPickerDialog.cpp b/launcher/ui/dialogs/IconPickerDialog.cpp index a196fd587..a7b44eab0 100644 --- a/launcher/ui/dialogs/IconPickerDialog.cpp +++ b/launcher/ui/dialogs/IconPickerDialog.cpp @@ -63,6 +63,9 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui auto buttonAdd = ui->buttonBox->addButton(tr("Add Icon"), QDialogButtonBox::ResetRole); buttonRemove = ui->buttonBox->addButton(tr("Remove Icon"), QDialogButtonBox::ResetRole); + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); + connect(buttonAdd, SIGNAL(clicked(bool)), SLOT(addNewIcon())); connect(buttonRemove, SIGNAL(clicked(bool)), SLOT(removeSelectedIcon())); diff --git a/launcher/ui/dialogs/ImportResourceDialog.cpp b/launcher/ui/dialogs/ImportResourceDialog.cpp index 84b692730..e3a1e9a6c 100644 --- a/launcher/ui/dialogs/ImportResourceDialog.cpp +++ b/launcher/ui/dialogs/ImportResourceDialog.cpp @@ -45,6 +45,9 @@ ImportResourceDialog::ImportResourceDialog(QString file_path, PackedResourceType ui->label->setText( tr("Choose the instance you would like to import this %1 to.").arg(ResourceUtils::getPackedTypeName(m_resource_type))); ui->label_file_path->setText(tr("File: %1").arg(m_file_path)); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } void ImportResourceDialog::activated(QModelIndex index) diff --git a/launcher/ui/dialogs/InstallLoaderDialog.cpp b/launcher/ui/dialogs/InstallLoaderDialog.cpp index 541119d10..7082125f2 100644 --- a/launcher/ui/dialogs/InstallLoaderDialog.cpp +++ b/launcher/ui/dialogs/InstallLoaderDialog.cpp @@ -31,6 +31,7 @@ #include "ui/widgets/VersionSelectWidget.h" class InstallLoaderPage : public VersionSelectWidget, public BasePage { + Q_OBJECT public: InstallLoaderPage(const QString& id, const QString& iconName, @@ -103,6 +104,8 @@ InstallLoaderDialog::InstallLoaderDialog(std::shared_ptr profile, c buttons->setOrientation(Qt::Horizontal); buttons->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); + buttons->button(QDialogButtonBox::Ok)->setText(tr("Ok")); + buttons->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); buttonLayout->addWidget(buttons); @@ -164,3 +167,4 @@ void InstallLoaderDialog::done(int result) QDialog::done(result); } +#include "InstallLoaderDialog.moc" \ No newline at end of file diff --git a/launcher/ui/dialogs/MSALoginDialog.cpp b/launcher/ui/dialogs/MSALoginDialog.cpp index 799b5b332..40d1eff1e 100644 --- a/launcher/ui/dialogs/MSALoginDialog.cpp +++ b/launcher/ui/dialogs/MSALoginDialog.cpp @@ -68,6 +68,8 @@ MSALoginDialog::MSALoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::MS } } }); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); } int MSALoginDialog::exec() @@ -78,16 +80,16 @@ int MSALoginDialog::exec() connect(m_authflow_task.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed); connect(m_authflow_task.get(), &Task::succeeded, this, &QDialog::accept); connect(m_authflow_task.get(), &Task::aborted, this, &MSALoginDialog::reject); - connect(m_authflow_task.get(), &Task::status, this, &MSALoginDialog::onTaskStatus); + connect(m_authflow_task.get(), &Task::status, this, &MSALoginDialog::onAuthFlowStatus); connect(m_authflow_task.get(), &AuthFlow::authorizeWithBrowser, this, &MSALoginDialog::authorizeWithBrowser); connect(m_authflow_task.get(), &AuthFlow::authorizeWithBrowserWithExtra, this, &MSALoginDialog::authorizeWithBrowserWithExtra); connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, m_authflow_task.get(), &Task::abort); - m_devicecode_task.reset(new AuthFlow(m_account->accountData(), AuthFlow::Action::DeviceCode, this)); + m_devicecode_task.reset(new AuthFlow(m_account->accountData(), AuthFlow::Action::DeviceCode)); connect(m_devicecode_task.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed); connect(m_devicecode_task.get(), &Task::succeeded, this, &QDialog::accept); connect(m_devicecode_task.get(), &Task::aborted, this, &MSALoginDialog::reject); - connect(m_devicecode_task.get(), &Task::status, this, &MSALoginDialog::onTaskStatus); + connect(m_devicecode_task.get(), &Task::status, this, &MSALoginDialog::onDeviceFlowStatus); connect(m_devicecode_task.get(), &AuthFlow::authorizeWithBrowser, this, &MSALoginDialog::authorizeWithBrowser); connect(m_devicecode_task.get(), &AuthFlow::authorizeWithBrowserWithExtra, this, &MSALoginDialog::authorizeWithBrowserWithExtra); connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, m_devicecode_task.get(), &Task::abort); @@ -132,7 +134,7 @@ void MSALoginDialog::onTaskFailed(QString reason) void MSALoginDialog::authorizeWithBrowser(const QUrl& url) { - ui->stackedWidget->setCurrentIndex(1); + ui->stackedWidget2->setCurrentIndex(1); ui->loginButton->setToolTip(QString("
%1
").arg(url.toString())); m_url = url; } @@ -152,12 +154,18 @@ void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, in } } -void MSALoginDialog::onTaskStatus(QString status) +void MSALoginDialog::onDeviceFlowStatus(QString status) { ui->stackedWidget->setCurrentIndex(0); ui->status->setText(status); } +void MSALoginDialog::onAuthFlowStatus(QString status) +{ + ui->stackedWidget2->setCurrentIndex(0); + ui->status2->setText(status); +} + // Public interface MinecraftAccountPtr MSALoginDialog::newAccount(QWidget* parent) { diff --git a/launcher/ui/dialogs/MSALoginDialog.h b/launcher/ui/dialogs/MSALoginDialog.h index 70f480ca9..375ccc57a 100644 --- a/launcher/ui/dialogs/MSALoginDialog.h +++ b/launcher/ui/dialogs/MSALoginDialog.h @@ -40,7 +40,8 @@ class MSALoginDialog : public QDialog { protected slots: void onTaskFailed(QString reason); - void onTaskStatus(QString status); + void onDeviceFlowStatus(QString status); + void onAuthFlowStatus(QString status); void authorizeWithBrowser(const QUrl& url); void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn); diff --git a/launcher/ui/dialogs/MSALoginDialog.ui b/launcher/ui/dialogs/MSALoginDialog.ui index c6821782f..69cd2e1ab 100644 --- a/launcher/ui/dialogs/MSALoginDialog.ui +++ b/launcher/ui/dialogs/MSALoginDialog.ui @@ -7,7 +7,7 @@ 0 0 440 - 430 + 447 @@ -21,12 +21,12 @@ - + 1 - - + + @@ -41,7 +41,7 @@ - + 16 @@ -61,7 +61,7 @@ - + Status @@ -74,7 +74,7 @@ - + Qt::Vertical @@ -88,8 +88,8 @@ - - + + @@ -136,51 +136,125 @@ + + + + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + + + + + + 16 + + + + Or + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + + + + + + + 1 + + + - - - - - - 0 - 0 - - - - Qt::Horizontal - - - - - - - - 16 - - - - Or - - - Qt::AlignCenter - - - - - - - - 0 - 0 - - - - Qt::Horizontal - - - - + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + 16 + 75 + true + + + + Please wait... + + + Qt::AlignCenter + + + true + + + + + + + Status + + + Qt::AlignCenter + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp index f906cfcea..81f544a90 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -8,6 +8,7 @@ #include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "modplatform/ModIndex.h" #include "modplatform/flame/FlameAPI.h" +#include "tasks/SequentialTask.h" #include "ui_ReviewMessageBox.h" #include "Markdown.h" @@ -45,8 +46,7 @@ ModUpdateDialog::ModUpdateDialog(QWidget* parent, , m_parent(parent) , m_mod_model(mods) , m_candidates(search_for) - , m_second_try_metadata( - new ConcurrentTask(nullptr, "Second Metadata Search", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())) + , m_second_try_metadata(new ConcurrentTask("Second Metadata Search", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())) , m_instance(instance) , m_include_deps(includeDeps) { @@ -89,7 +89,7 @@ void ModUpdateDialog::checkCandidates() auto versions = mcVersions(m_instance); auto loadersList = mcLoadersList(m_instance); - SequentialTask check_task(m_parent, tr("Checking for updates")); + SequentialTask check_task(tr("Checking for updates")); if (!m_modrinth_to_update.empty()) { m_modrinth_check_task.reset(new ModrinthCheckUpdate(m_modrinth_to_update, versions, loadersList, m_mod_model)); @@ -187,7 +187,7 @@ void ModUpdateDialog::checkCandidates() } if (m_include_deps && !APPLICATION->settings()->get("ModDependenciesDisabled").toBool()) { // dependencies - auto depTask = makeShared(this, m_instance, m_mod_model.get(), selectedVers); + auto depTask = makeShared(m_instance, m_mod_model.get(), selectedVers); connect(depTask.get(), &Task::failed, this, [&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); @@ -261,7 +261,7 @@ auto ModUpdateDialog::ensureMetadata() -> bool { auto index_dir = indexDir(); - SequentialTask seq(m_parent, tr("Looking for metadata")); + SequentialTask seq(tr("Looking for metadata")); // A better use of data structures here could remove the need for this QHash QHash should_try_others; @@ -403,9 +403,15 @@ void ModUpdateDialog::onMetadataFailed(Mod* mod, bool try_others, ModPlatform::R connect(task.get(), &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); connect(task.get(), &EnsureMetadataTask::metadataFailed, [this](Mod* candidate) { onMetadataFailed(candidate, false); }); connect(task.get(), &EnsureMetadataTask::failed, - [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); - - m_second_try_metadata->addTask(task); + [this](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + if (task->getHashingTask()) { + auto seq = makeShared(); + seq->addTask(task->getHashingTask()); + seq->addTask(task); + m_second_try_metadata->addTask(seq); + } else { + m_second_try_metadata->addTask(task); + } } else { QString reason{ tr("Couldn't find a valid version on the selected mod provider(s)") }; @@ -418,7 +424,7 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStri auto item_top = new QTreeWidgetItem(ui->modTreeWidget); item_top->setCheckState(0, info.enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); if (!info.enabled) { - item_top->setToolTip(0, tr("Mod was disabled as it may be already instaled.")); + item_top->setToolTip(0, tr("Mod was disabled as it may be already installed.")); } item_top->setText(0, info.name); item_top->setExpanded(true); diff --git a/launcher/ui/dialogs/NewComponentDialog.cpp b/launcher/ui/dialogs/NewComponentDialog.cpp index b47b85ff1..b5f8ff889 100644 --- a/launcher/ui/dialogs/NewComponentDialog.cpp +++ b/launcher/ui/dialogs/NewComponentDialog.cpp @@ -68,6 +68,9 @@ NewComponentDialog::NewComponentDialog(const QString& initialName, const QString ui->nameTextBox->setFocus(); + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); + originalPlaceholderText = ui->uidTextBox->placeholderText(); updateDialogState(); } diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp index 2e799d2a8..d9ea0aafb 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.cpp +++ b/launcher/ui/dialogs/NewInstanceDialog.cpp @@ -36,6 +36,7 @@ #include "NewInstanceDialog.h" #include "Application.h" +#include "ui/pages/modplatform/ModpackProviderBasePage.h" #include "ui/pages/modplatform/import_ftb/ImportFTBPage.h" #include "ui_NewInstanceDialog.h" @@ -108,16 +109,19 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, auto OkButton = m_buttons->button(QDialogButtonBox::Ok); OkButton->setDefault(true); OkButton->setAutoDefault(true); + OkButton->setText(tr("OK")); connect(OkButton, &QPushButton::clicked, this, &NewInstanceDialog::accept); auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel); CancelButton->setDefault(false); CancelButton->setAutoDefault(false); + CancelButton->setText(tr("Cancel")); connect(CancelButton, &QPushButton::clicked, this, &NewInstanceDialog::reject); auto HelpButton = m_buttons->button(QDialogButtonBox::Help); HelpButton->setDefault(false); HelpButton->setAutoDefault(false); + HelpButton->setText(tr("Help")); connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help); if (!url.isEmpty()) { @@ -140,6 +144,8 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, auto geometry = screen->availableSize(); resize(width(), qMin(geometry.height() - 50, 710)); } + + connect(m_container, &PageContainer::selectedPageChanged, this, &NewInstanceDialog::selectedPageChanged); } void NewInstanceDialog::reject() @@ -316,3 +322,16 @@ void NewInstanceDialog::importIconNow() } APPLICATION->settings()->set("NewInstanceGeometry", saveGeometry().toBase64()); } + +void NewInstanceDialog::selectedPageChanged(BasePage* previous, BasePage* selected) +{ + auto prevPage = dynamic_cast(previous); + if (prevPage) { + m_searchTerm = prevPage->getSerachTerm(); + } + + auto nextPage = dynamic_cast(selected); + if (nextPage) { + nextPage->setSearchTerm(m_searchTerm); + } +} diff --git a/launcher/ui/dialogs/NewInstanceDialog.h b/launcher/ui/dialogs/NewInstanceDialog.h index 923579567..e97c9f543 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.h +++ b/launcher/ui/dialogs/NewInstanceDialog.h @@ -82,6 +82,7 @@ class NewInstanceDialog : public QDialog, public BasePageProvider { private slots: void on_iconButton_clicked(); void on_instNameTextBox_textChanged(const QString& arg1); + void selectedPageChanged(BasePage* previous, BasePage* selected); private: Ui::NewInstanceDialog* ui = nullptr; @@ -98,5 +99,7 @@ class NewInstanceDialog : public QDialog, public BasePageProvider { QString importVersion; + QString m_searchTerm; + void importIconNow(); }; diff --git a/launcher/ui/dialogs/OfflineLoginDialog.cpp b/launcher/ui/dialogs/OfflineLoginDialog.cpp index b9d1c2915..d8fbc04fd 100644 --- a/launcher/ui/dialogs/OfflineLoginDialog.cpp +++ b/launcher/ui/dialogs/OfflineLoginDialog.cpp @@ -9,6 +9,9 @@ OfflineLoginDialog::OfflineLoginDialog(QWidget* parent) : QDialog(parent), ui(ne ui->progressBar->setVisible(false); ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); } diff --git a/launcher/ui/dialogs/ProfileSelectDialog.cpp b/launcher/ui/dialogs/ProfileSelectDialog.cpp index fe03e1b6b..95bdf99a9 100644 --- a/launcher/ui/dialogs/ProfileSelectDialog.cpp +++ b/launcher/ui/dialogs/ProfileSelectDialog.cpp @@ -18,6 +18,7 @@ #include #include +#include #include "Application.h" @@ -70,6 +71,9 @@ ProfileSelectDialog::ProfileSelectDialog(const QString& message, int flags, QWid ui->listView->setCurrentIndex(ui->listView->model()->index(0, 0)); connect(ui->listView, SIGNAL(doubleClicked(QModelIndex)), SLOT(on_buttonBox_accepted())); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ProfileSelectDialog::~ProfileSelectDialog() diff --git a/launcher/ui/dialogs/ProfileSetupDialog.cpp b/launcher/ui/dialogs/ProfileSetupDialog.cpp index 385094e23..a6512784f 100644 --- a/launcher/ui/dialogs/ProfileSetupDialog.cpp +++ b/launcher/ui/dialogs/ProfileSetupDialog.cpp @@ -70,6 +70,9 @@ ProfileSetupDialog::ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidg connect(&checkStartTimer, &QTimer::timeout, this, &ProfileSetupDialog::startCheck); setNameStatus(NameStatus::NotSet, QString()); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ProfileSetupDialog::~ProfileSetupDialog() diff --git a/launcher/ui/dialogs/ProgressDialog.cpp b/launcher/ui/dialogs/ProgressDialog.cpp index 0ca3a1bd9..9897687e3 100644 --- a/launcher/ui/dialogs/ProgressDialog.cpp +++ b/launcher/ui/dialogs/ProgressDialog.cpp @@ -90,6 +90,9 @@ void ProgressDialog::on_skipButton_clicked(bool checked) ProgressDialog::~ProgressDialog() { + for (auto conn : this->m_taskConnections) { + disconnect(conn); + } delete ui; } @@ -140,15 +143,15 @@ int ProgressDialog::execWithTask(Task* task) } // Connect signals. - connect(task, &Task::started, this, &ProgressDialog::onTaskStarted); - connect(task, &Task::failed, this, &ProgressDialog::onTaskFailed); - connect(task, &Task::succeeded, this, &ProgressDialog::onTaskSucceeded); - connect(task, &Task::status, this, &ProgressDialog::changeStatus); - connect(task, &Task::details, this, &ProgressDialog::changeStatus); - connect(task, &Task::stepProgress, this, &ProgressDialog::changeStepProgress); - connect(task, &Task::progress, this, &ProgressDialog::changeProgress); - connect(task, &Task::aborted, this, &ProgressDialog::hide); - connect(task, &Task::abortStatusChanged, ui->skipButton, &QPushButton::setEnabled); + this->m_taskConnections.push_back(connect(task, &Task::started, this, &ProgressDialog::onTaskStarted)); + this->m_taskConnections.push_back(connect(task, &Task::failed, this, &ProgressDialog::onTaskFailed)); + this->m_taskConnections.push_back(connect(task, &Task::succeeded, this, &ProgressDialog::onTaskSucceeded)); + this->m_taskConnections.push_back(connect(task, &Task::status, this, &ProgressDialog::changeStatus)); + this->m_taskConnections.push_back(connect(task, &Task::details, this, &ProgressDialog::changeStatus)); + this->m_taskConnections.push_back(connect(task, &Task::stepProgress, this, &ProgressDialog::changeStepProgress)); + this->m_taskConnections.push_back(connect(task, &Task::progress, this, &ProgressDialog::changeProgress)); + this->m_taskConnections.push_back(connect(task, &Task::aborted, this, &ProgressDialog::hide)); + this->m_taskConnections.push_back(connect(task, &Task::abortStatusChanged, ui->skipButton, &QPushButton::setEnabled)); m_is_multi_step = task->isMultiStep(); ui->taskProgressScrollArea->setHidden(!m_is_multi_step); diff --git a/launcher/ui/dialogs/ProgressDialog.h b/launcher/ui/dialogs/ProgressDialog.h index 15eadf4e7..4a696a49d 100644 --- a/launcher/ui/dialogs/ProgressDialog.h +++ b/launcher/ui/dialogs/ProgressDialog.h @@ -93,6 +93,8 @@ class ProgressDialog : public QDialog { Ui::ProgressDialog* ui; Task* m_task; + + QList m_taskConnections; bool m_is_multi_step = false; QHash taskProgress; diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index 90d3b79a9..06fa6dbf3 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -258,7 +258,9 @@ void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* s } // Same effect as having a global search bar - selectedPage()->setSearchTerm(prev_page->getSearchTerm()); + ResourcePage* result = dynamic_cast(selected); + Q_ASSERT(result != nullptr); + result->setSearchTerm(prev_page->getSearchTerm()); } ModDownloadDialog::ModDownloadDialog(QWidget* parent, const std::shared_ptr& mods, BaseInstance* instance) @@ -296,7 +298,7 @@ GetModDependenciesTask::Ptr ModDownloadDialog::getModDependenciesTask() selectedVers.append(std::make_shared(selected->getPack(), selected->getVersion())); } - return makeShared(this, m_instance, model, selectedVers); + return makeShared(m_instance, model, selectedVers); } } return nullptr; diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp index 66c36d400..96cc8149f 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.cpp +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -20,6 +20,9 @@ ReviewMessageBox::ReviewMessageBox(QWidget* parent, [[maybe_unused]] QString con connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &ReviewMessageBox::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &ReviewMessageBox::reject); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ReviewMessageBox::~ReviewMessageBox() @@ -38,7 +41,7 @@ void ReviewMessageBox::appendResource(ResourceInformation&& info) itemTop->setCheckState(0, info.enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); itemTop->setText(0, info.name); if (!info.enabled) { - itemTop->setToolTip(0, tr("Mod was disabled as it may be already instaled.")); + itemTop->setToolTip(0, tr("Mod was disabled as it may be already installed.")); } auto filenameItem = new QTreeWidgetItem(itemTop); diff --git a/launcher/ui/dialogs/ScrollMessageBox.cpp b/launcher/ui/dialogs/ScrollMessageBox.cpp index c04d87842..1cfb848f4 100644 --- a/launcher/ui/dialogs/ScrollMessageBox.cpp +++ b/launcher/ui/dialogs/ScrollMessageBox.cpp @@ -1,4 +1,5 @@ #include "ScrollMessageBox.h" +#include #include "ui_ScrollMessageBox.h" ScrollMessageBox::ScrollMessageBox(QWidget* parent, const QString& title, const QString& text, const QString& body) @@ -8,6 +9,9 @@ ScrollMessageBox::ScrollMessageBox(QWidget* parent, const QString& title, const this->setWindowTitle(title); ui->label->setText(text); ui->textBrowser->setText(body); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ScrollMessageBox::~ScrollMessageBox() diff --git a/launcher/ui/dialogs/VersionSelectDialog.cpp b/launcher/ui/dialogs/VersionSelectDialog.cpp index 876d7470e..30377288b 100644 --- a/launcher/ui/dialogs/VersionSelectDialog.cpp +++ b/launcher/ui/dialogs/VersionSelectDialog.cpp @@ -68,6 +68,9 @@ VersionSelectDialog::VersionSelectDialog(BaseVersionList* vlist, QString title, m_buttonBox->setObjectName(QStringLiteral("buttonBox")); m_buttonBox->setOrientation(Qt::Horizontal); m_buttonBox->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); + + m_buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Ok")); + m_buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); m_horizontalLayout->addWidget(m_buttonBox); m_verticalLayout->addLayout(m_horizontalLayout); diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp index a947af632..f112e1acf 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.cpp +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -93,6 +93,9 @@ SkinManageDialog::SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct) setupCapes(); ui->listView->setCurrentIndex(m_list.index(m_list.getSelectedAccountSkin())); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } SkinManageDialog::~SkinManageDialog() @@ -116,7 +119,7 @@ void SkinManageDialog::selectionChanged(QItemSelection selected, QItemSelection return; m_selected_skin = key; auto skin = m_list.skin(key); - if (!skin) + if (!skin || !skin->isValid()) return; ui->selectedModel->setPixmap(skin->getTexture().scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation)); ui->capeCombo->setCurrentIndex(m_capes_idx.value(skin->getCapeId())); @@ -139,6 +142,9 @@ void SkinManageDialog::on_fileBtn_clicked() { auto filter = QMimeDatabase().mimeTypeForName("image/png").filterString(); QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), filter); + if (raw_path.isNull()) { + return; + } auto message = m_list.installSkin(raw_path, {}); if (!message.isEmpty()) { CustomMessageBox::selectable(this, tr("Selected file is not a valid skin"), message, QMessageBox::Critical)->show(); @@ -212,7 +218,10 @@ void SkinManageDialog::setupCapes() void SkinManageDialog::on_capeCombo_currentIndexChanged(int index) { auto id = ui->capeCombo->currentData(); - ui->capeImage->setPixmap(m_capes.value(id.toString(), {}).scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation)); + auto cape = m_capes.value(id.toString(), {}); + if (!cape.isNull()) { + ui->capeImage->setPixmap(cape.scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation)); + } if (auto skin = m_list.skin(m_selected_skin); skin) { skin->setCapeId(id.toString()); } @@ -505,8 +514,13 @@ void SkinManageDialog::resizeEvent(QResizeEvent* event) QSize s = size() * (1. / 3); if (auto skin = m_list.skin(m_selected_skin); skin) { - ui->selectedModel->setPixmap(skin->getTexture().scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); + if (skin->isValid()) { + ui->selectedModel->setPixmap(skin->getTexture().scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); + } } auto id = ui->capeCombo->currentData(); - ui->capeImage->setPixmap(m_capes.value(id.toString(), {}).scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); + auto cape = m_capes.value(id.toString(), {}); + if (!cape.isNull()) { + ui->capeImage->setPixmap(cape.scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); + } } diff --git a/launcher/ui/java/InstallJavaDialog.cpp b/launcher/ui/java/InstallJavaDialog.cpp index 4fb9fc2d2..5f69b9d46 100644 --- a/launcher/ui/java/InstallJavaDialog.cpp +++ b/launcher/ui/java/InstallJavaDialog.cpp @@ -31,10 +31,12 @@ #include "Filter.h" #include "java/download/ArchiveDownloadTask.h" #include "java/download/ManifestDownloadTask.h" +#include "java/download/SymlinkTask.h" #include "meta/Index.h" #include "meta/VersionList.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" +#include "tasks/SequentialTask.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/java/VersionList.h" @@ -55,13 +57,13 @@ class InstallJavaPage : public QWidget, public BasePage { majorVersionSelect = new VersionSelectWidget(this); majorVersionSelect->selectCurrent(); - majorVersionSelect->setEmptyString(tr("No java versions are currently available in the meta.")); - majorVersionSelect->setEmptyErrorString(tr("Couldn't load or download the java version lists!")); + majorVersionSelect->setEmptyString(tr("No Java versions are currently available in the meta.")); + majorVersionSelect->setEmptyErrorString(tr("Couldn't load or download the Java version lists!")); horizontalLayout->addWidget(majorVersionSelect, 1); javaVersionSelect = new VersionSelectWidget(this); - javaVersionSelect->setEmptyString(tr("No java versions are currently available for your OS.")); - javaVersionSelect->setEmptyErrorString(tr("Couldn't load or download the java version lists!")); + javaVersionSelect->setEmptyString(tr("No Java versions are currently available for your OS.")); + javaVersionSelect->setEmptyErrorString(tr("Couldn't load or download the Java version lists!")); horizontalLayout->addWidget(javaVersionSelect, 4); connect(majorVersionSelect, &VersionSelectWidget::selectedVersionChanged, this, &InstallJavaPage::setSelectedVersion); connect(majorVersionSelect, &VersionSelectWidget::selectedVersionChanged, this, &InstallJavaPage::selectionChanged); @@ -130,9 +132,9 @@ class InstallJavaPage : public QWidget, public BasePage { m_recommended_majors = majors; recommendedFilterChanged(); } - void setRecomend(bool recomend) + void setRecommend(bool recommend) { - m_recommend = recomend; + m_recommend = recommend; recommendedFilterChanged(); } void recommendedFilterChanged() @@ -200,7 +202,7 @@ InstallDialog::InstallDialog(const QString& uid, BaseInstance* instance, QWidget recommendedCheckBox->setCheckState(Qt::CheckState::Checked); connect(recommendedCheckBox, &QCheckBox::stateChanged, this, [this](int state) { for (BasePage* page : container->getPages()) { - pageCast(page)->setRecomend(state == Qt::Checked); + pageCast(page)->setRecommend(state == Qt::Checked); } }); @@ -210,6 +212,7 @@ InstallDialog::InstallDialog(const QString& uid, BaseInstance* instance, QWidget buttons->setOrientation(Qt::Horizontal); buttons->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); buttons->button(QDialogButtonBox::Ok)->setText(tr("Download")); + buttons->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); buttonLayout->addWidget(buttons); @@ -258,7 +261,7 @@ InstallDialog::InstallDialog(const QString& uid, BaseInstance* instance, QWidget container->selectPage(page->id()); auto cast = pageCast(page); - cast->setRecomend(true); + cast->setRecommend(true); connect(cast, &InstallJavaPage::selectionChanged, this, [this, cast] { validate(cast); }); if (!recommendedJavas.isEmpty()) { cast->setRecommendedMajors(recommendedJavas); @@ -313,6 +316,12 @@ void InstallDialog::done(int result) CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show(); deletePath(); } +#if defined(Q_OS_MACOS) + auto seq = makeShared(tr("Install Java")); + seq->addTask(task); + seq->addTask(makeShared(final_path)); + task = seq; +#endif connect(task.get(), &Task::failed, this, [this, &deletePath](QString reason) { QString error = QString("Java download failed: %1").arg(reason); CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show(); @@ -335,4 +344,4 @@ void InstallDialog::done(int result) } // namespace Java -#include "InstallJavaDialog.moc" \ No newline at end of file +#include "InstallJavaDialog.moc" diff --git a/launcher/ui/pagedialog/PageDialog.cpp b/launcher/ui/pagedialog/PageDialog.cpp index 6514217cd..d211cb4d3 100644 --- a/launcher/ui/pagedialog/PageDialog.cpp +++ b/launcher/ui/pagedialog/PageDialog.cpp @@ -39,6 +39,8 @@ PageDialog::PageDialog(BasePageProvider* pageProvider, QString defaultId, QWidge QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Close); buttons->button(QDialogButtonBox::Close)->setDefault(true); + buttons->button(QDialogButtonBox::Close)->setText(tr("Close")); + buttons->button(QDialogButtonBox::Help)->setText(tr("Help")); buttons->setContentsMargins(6, 0, 6, 0); m_container->addButtons(buttons); diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index 82aa76a4f..a137c4cde 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -143,6 +143,7 @@ void APIPage::loadSettings() ui->modrinthToken->setText(modrinthToken); QString customUserAgent = s->get("UserAgentOverride").toString(); ui->userAgentLineEdit->setText(customUserAgent); + ui->technicClientID->setText(s->get("TechnicClientID").toString()); } void APIPage::applySettings() @@ -172,6 +173,7 @@ void APIPage::applySettings() QString modrinthToken = ui->modrinthToken->text(); s->set("ModrinthToken", modrinthToken); s->set("UserAgentOverride", ui->userAgentLineEdit->text()); + s->set("TechnicClientID", ui->technicClientID->text()); } bool APIPage::apply() diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index a7f3f3f72..9c713aa79 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -6,8 +6,8 @@ 0 0 - 800 - 600 + 841 + 620 @@ -288,6 +288,36 @@ + + + + Technic Client ID + + + + + + <html><head/><body><p>Note: you only need to set this to access private data.</p></body></html> + + + + + + + (None) + + + + + + + Enter a custom GUID client ID for Technic here. + + + + + + diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index 6699b00c0..0ae296815 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -67,8 +67,8 @@ JavaPage::JavaPage(QWidget* parent) : QWidget(parent), ui(new Ui::JavaPage) ui->managedJavaList->initialize(new JavaInstallList(this, true)); ui->managedJavaList->setResizeOn(2); ui->managedJavaList->selectCurrent(); - ui->managedJavaList->setEmptyString(tr("No managed java versions are installed")); - ui->managedJavaList->setEmptyErrorString(tr("Couldn't load the managed java list!")); + ui->managedJavaList->setEmptyString(tr("No managed Java versions are installed")); + ui->managedJavaList->setEmptyErrorString(tr("Couldn't load the managed Java list!")); connect(ui->autodetectJavaCheckBox, &QCheckBox::stateChanged, this, [this] { ui->autodownloadCheckBox->setEnabled(ui->autodetectJavaCheckBox->isChecked()); if (!ui->autodetectJavaCheckBox->isChecked()) diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 3cba468ff..a46a6fd5d 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -35,7 +35,7 @@ - QTabWidget::Rounded + QTabWidget::TabShape::Rounded 0 @@ -46,317 +46,339 @@ - - - Update Settings - - - - - - Check for updates automatically - - - - - - - - - Update interval - - - - - - - Set it to 0 to only check on launch - - - h - - - 0 - - - 99999999 - - - - - - - - - - - - Folders + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOff - - - - - &Downloads: - - - downloadsDirTextBox - - - - - - - Browse - - - - - - - - - - - - - &Skins: - - - skinsDirTextBox - - - - - - - &Icons: - - - iconsDirTextBox - - - - - - - When enabled, in addition to the downloads folder, its sub folders will also be searched when looking for resources (e.g. when looking for blocked mods on CurseForge). - - - Check downloads folder recursively - - - - - - - - - - &Java: - - - javaDirTextBox - - - - - - - &Mods: - - - modsDirTextBox - - - - - - - - - - - - - - - - Browse - - - - - - - Browse - - - - - - - Browse - - - - - - - I&nstances: - - - instDirTextBox - - - - - - - Browse - - - - - - - Browse - - - - - - - - - - Mods - - - - - - Disable using metadata provided by mod providers (like Modrinth or CurseForge) for mods. - - - Disable using metadata for mods - - - - - - - <html><head/><body><p><span style=" font-weight:600; color:#f5c211;">Warning</span><span style=" color:#f5c211;">: Disabling mod metadata may also disable some QoL features, such as mod updating!</span></p></body></html> - - - true - - - - - - - Disable the automatic detection, installation, and updating of mod dependencies. - - - Disable automatic mod dependency management - - - - - - - When creating a new modpack instance, do not suggest updating existing instances instead. - - - Skip modpack update prompt - - - - - - - - - - Miscellaneous + + true - - - - - 1 - - - - - - - Number of concurrent tasks - - - - - - - 1 - - - - - - - Number of concurrent downloads - - - - - - - Number of manual retries - - - - - - - 0 - - - - - - - Seconds to wait until the requests are terminated - - - Timeout for HTTP requests - - - - - - - s - - - - + + + + 0 + 0 + 473 + 770 + + + + + + + Update Settings + + + + + + Check for updates automatically + + + + + + + + + Update interval + + + + + + + Set it to 0 to only check on launch + + + h + + + 0 + + + 99999999 + + + + + + + + + + + + Folders + + + + + + &Downloads: + + + downloadsDirTextBox + + + + + + + Browse + + + + + + + + + + + + + &Skins: + + + skinsDirTextBox + + + + + + + &Icons: + + + iconsDirTextBox + + + + + + + When enabled, in addition to the downloads folder, its sub folders will also be searched when looking for resources (e.g. when looking for blocked mods on CurseForge). + + + Check downloads folder recursively + + + + + + + + + + &Java: + + + javaDirTextBox + + + + + + + &Mods: + + + modsDirTextBox + + + + + + + + + + + + + + + + Browse + + + + + + + Browse + + + + + + + Browse + + + + + + + I&nstances: + + + instDirTextBox + + + + + + + Browse + + + + + + + Browse + + + + + + + + + + Mods + + + + + + Disable using metadata provided by mod providers (like Modrinth or CurseForge) for mods. + + + Disable using metadata for mods + + + + + + + <html><head/><body><p><span style=" font-weight:600; color:#f5c211;">Warning</span><span style=" color:#f5c211;">: Disabling mod metadata may also disable some QoL features, such as mod updating!</span></p></body></html> + + + true + + + + + + + Disable the automatic detection, installation, and updating of mod dependencies. + + + Disable automatic mod dependency management + + + + + + + When creating a new modpack instance, do not suggest updating existing instances instead. + + + Skip modpack update prompt + + + + + + + + + + Miscellaneous + + + + + + 1 + + + + + + + Number of concurrent tasks + + + + + + + 1 + + + + + + + Number of concurrent downloads + + + + + + + Number of manual retries + + + + + + + 0 + + + + + + + Seconds to wait until the requests are terminated + + + Timeout for HTTP requests + + + + + + + s + + + + + + + + + + Qt::Orientation::Vertical + + + + 0 + 0 + + + + + + - - - - Qt::Vertical - - - - 0 - 0 - - - - @@ -443,7 +465,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -484,7 +506,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -593,13 +615,13 @@ - Qt::ScrollBarAlwaysOff + Qt::ScrollBarPolicy::ScrollBarAlwaysOff false - Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + Qt::TextInteractionFlag::TextSelectableByKeyboard|Qt::TextInteractionFlag::TextSelectableByMouse @@ -645,15 +667,33 @@
tabWidget + scrollArea autoUpdateCheckBox + updateIntervalSpinBox instDirTextBox instDirBrowseBtn modsDirTextBox modsDirBrowseBtn iconsDirTextBox iconsDirBrowseBtn + javaDirTextBox + javaDirBrowseBtn + skinsDirTextBox + skinsDirBrowseBtn + downloadsDirTextBox + downloadsDirBrowseBtn + downloadsDirWatchRecursiveCheckBox + metadataDisableBtn + dependenciesDisableBtn + skipModpackUpdatePromptBtn + numberOfConcurrentTasksSpinBox + numberOfConcurrentDownloadsSpinBox + numberOfManualRetriesSpinBox + timeoutSecondsSpinBox sortLastLaunchedBtn sortByNameBtn + catOpacitySpinBox + preferMenuBarCheckBox showConsoleCheck autoCloseConsoleCheck showConsoleErrorCheck diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index 19bc9fdf9..cf8d86cd4 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -351,6 +351,9 @@ void InstanceSettingsPage::loadSettings() [this] { ui->javaSettingsGroupBox->setChecked(m_settings->get("OverrideJavaLocation").toBool()); }); ui->javaSettingsGroupBox->setChecked(overrideLocation); ui->javaPathTextBox->setText(m_settings->get("JavaPath").toString()); + connect(m_settings->getSetting("JavaPath").get(), &Setting::SettingChanged, ui->javaSettingsGroupBox, + [this] { ui->javaPathTextBox->setText(m_settings->get("JavaPath").toString()); }); + ui->skipCompatibilityCheckbox->setChecked(m_settings->get("IgnoreJavaCompatibility").toBool()); ui->javaArgumentsGroupBox->setChecked(overrideArgs); diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index 0c22d1de6..6a4888f9c 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -234,7 +234,7 @@ bool LogPage::apply() bool LogPage::shouldDisplay() const { - return m_instance->isRunning() || m_proxy->rowCount() > 0; + return true; } void LogPage::on_btnPaste_clicked() diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index f2feb8c7f..8d03b6d9a 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -124,7 +124,7 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr ui->actionsToolbar->addAction(ui->actionVisitItemPage); connect(ui->actionVisitItemPage, &QAction::triggered, this, &ModFolderPage::visitModPages); - auto changeVersion = new QAction(tr("Change Version")); + auto changeVersion = new QAction(tr("Change Version"), this); changeVersion->setToolTip(tr("Change mod version")); changeVersion->setEnabled(false); ui->actionsToolbar->insertActionAfter(ui->actionUpdateItem, changeVersion); @@ -209,7 +209,7 @@ void ModFolderPage::installMods() ResourceDownload::ModDownloadDialog mdownload(this, m_model, m_instance); if (mdownload.exec()) { - auto tasks = new ConcurrentTask(this, "Download Mods", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + auto tasks = new ConcurrentTask("Download Mods", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); @@ -292,7 +292,7 @@ void ModFolderPage::updateMods(bool includeDeps) } if (update_dialog.exec()) { - auto tasks = new ConcurrentTask(this, "Download Mods", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + auto tasks = new ConcurrentTask("Download Mods", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); @@ -423,7 +423,7 @@ void ModFolderPage::changeModVersion() ResourceDownload::ModDownloadDialog mdownload(this, m_model, m_instance); mdownload.setModMetadata((*mods_list.begin())->metadata()); if (mdownload.exec()) { - auto tasks = new ConcurrentTask(this, "Download Mods", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + auto tasks = new ConcurrentTask("Download Mods", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index 2ec2b402d..41b605991 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -74,6 +74,7 @@ class ModFolderPage : public ExternalResourcesPage { }; class CoreModFolderPage : public ModFolderPage { + Q_OBJECT public: explicit CoreModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent = 0); virtual ~CoreModFolderPage() = default; @@ -87,6 +88,7 @@ class CoreModFolderPage : public ModFolderPage { }; class NilModFolderPage : public ModFolderPage { + Q_OBJECT public: explicit NilModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent = 0); virtual ~NilModFolderPage() = default; diff --git a/launcher/ui/pages/instance/ResourcePackPage.cpp b/launcher/ui/pages/instance/ResourcePackPage.cpp index 85be64256..054bede01 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.cpp +++ b/launcher/ui/pages/instance/ResourcePackPage.cpp @@ -37,8 +37,6 @@ #include "ResourcePackPage.h" -#include "ResourceDownloadTask.h" - #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ResourceDownloadDialog.h" @@ -72,8 +70,7 @@ void ResourcePackPage::downloadRPs() ResourceDownload::ResourcePackDownloadDialog mdownload(this, std::static_pointer_cast(m_model), m_instance); if (mdownload.exec()) { - auto tasks = - new ConcurrentTask(this, "Download Resource Pack", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + auto tasks = new ConcurrentTask("Download Resource Pack", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index c3f955733..b619a07b8 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -242,6 +242,11 @@ ScreenshotsPage::ScreenshotsPage(QString path, QWidget* parent) : QMainWindow(pa m_model->setReadOnly(false); m_model->setNameFilters({ "*.png" }); m_model->setNameFilterDisables(false); + // Sorts by modified date instead of creation date because that column is not available and would require subclassing, this should work + // considering screenshots aren't modified after creation. + constexpr int file_modified_column_index = 3; + m_model->sort(file_modified_column_index, Qt::DescendingOrder); + m_folder = path; m_valid = FS::ensureFolderPathExists(m_folder); diff --git a/launcher/ui/pages/instance/ShaderPackPage.cpp b/launcher/ui/pages/instance/ShaderPackPage.cpp index 40366a1be..61ee01445 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.cpp +++ b/launcher/ui/pages/instance/ShaderPackPage.cpp @@ -65,7 +65,7 @@ void ShaderPackPage::downloadShaders() ResourceDownload::ShaderPackDownloadDialog mdownload(this, std::static_pointer_cast(m_model), m_instance); if (mdownload.exec()) { - auto tasks = new ConcurrentTask(this, "Download Shaders", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + auto tasks = new ConcurrentTask("Download Shaders", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); diff --git a/launcher/ui/pages/instance/TexturePackPage.cpp b/launcher/ui/pages/instance/TexturePackPage.cpp index 7c8d7e061..9c8ee3c78 100644 --- a/launcher/ui/pages/instance/TexturePackPage.cpp +++ b/launcher/ui/pages/instance/TexturePackPage.cpp @@ -74,8 +74,7 @@ void TexturePackPage::downloadTPs() ResourceDownload::TexturePackDownloadDialog mdownload(this, std::static_pointer_cast(m_model), m_instance); if (mdownload.exec()) { - auto tasks = - new ConcurrentTask(this, "Download Texture Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + auto tasks = new ConcurrentTask("Download Texture Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index c55d32efb..ab1c48ed4 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -49,8 +49,12 @@ #include #include #include +#include +#include "QObjectPtr.h" #include "VersionPage.h" +#include "meta/JsonFormat.h" +#include "tasks/SequentialTask.h" #include "ui/dialogs/InstallLoaderDialog.h" #include "ui_VersionPage.h" @@ -63,11 +67,9 @@ #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 "meta/Index.h" #include "meta/VersionList.h" @@ -241,7 +243,7 @@ void VersionPage::updateButtons(int row) 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->actionChange_version->setEnabled(patch && patch->isVersionChangeable(false)); ui->actionEdit->setEnabled(patch && patch->isCustom()); ui->actionCustomize->setEnabled(patch && patch->isCustomizable()); ui->actionRevert->setEnabled(patch && patch->isRevertible()); @@ -370,11 +372,25 @@ void VersionPage::on_actionChange_version_triggered() auto patch = m_profile->getComponent(versionRow); auto name = patch->getName(); auto list = patch->getVersionList(); + list->clearExternalRecommends(); if (!list) { return; } auto uid = list->uid(); + // recommend the correct lwjgl version for the current minecraft version + if (uid == "org.lwjgl" || uid == "org.lwjgl3") { + auto minecraft = m_profile->getComponent("net.minecraft"); + auto lwjglReq = std::find_if(minecraft->m_cachedRequires.cbegin(), minecraft->m_cachedRequires.cend(), + [uid](const Meta::Require& req) -> bool { return req.uid == uid; }); + if (lwjglReq != minecraft->m_cachedRequires.cend()) { + auto lwjglVersion = !lwjglReq->equalsVersion.isEmpty() ? lwjglReq->equalsVersion : lwjglReq->suggests; + if (!lwjglVersion.isEmpty()) { + list->addExternalRecommends({ lwjglVersion }); + } + } + } + VersionSelectDialog vselect(list.get(), tr("Change %1 version").arg(name), this); if (uid == "net.fabricmc.intermediary" || uid == "org.quiltmc.hashed") { vselect.setEmptyString(tr("No intermediary mappings versions are currently available.")); @@ -415,14 +431,18 @@ void VersionPage::on_actionDownload_All_triggered() return; } - auto updateTask = m_inst->createUpdateTask(Net::Mode::Online); - if (!updateTask) { + auto updateTasks = m_inst->createUpdateTask(); + if (updateTasks.isEmpty()) { return; } + auto task = makeShared(); + for (auto t : updateTasks) { + task->addTask(t); + } ProgressDialog tDialog(this); - connect(updateTask.get(), &Task::failed, this, &VersionPage::onGameUpdateError); + connect(task.get(), &Task::failed, this, &VersionPage::onGameUpdateError); // FIXME: unused return value - tDialog.execWithTask(updateTask.get()); + tDialog.execWithTask(task.get()); updateButtons(); m_container->refreshContainer(); } diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index e87a423fa..1f0329321 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -104,18 +104,6 @@ bool checkSide(QString filter, QString value) return filter.isEmpty() || value.isEmpty() || filter == "both" || value == "both" || filter == value; } -bool checkMcVersions(std::list filter, QStringList value) -{ - bool valid = false; - for (auto mcVersion : filter) { - if (value.contains(mcVersion.toString())) { - valid = true; - break; - } - } - return filter.empty() || valid; -} - bool ModModel::checkFilters(ModPlatform::IndexedPack::Ptr pack) { if (!m_filter) @@ -135,7 +123,7 @@ bool ModModel::checkVersionFilters(const ModPlatform::IndexedVersion& v) checkSide(m_filter->side, v.side) && // side (m_filter->releases.empty() || // releases std::find(m_filter->releases.cbegin(), m_filter->releases.cend(), v.version_type) != m_filter->releases.cend()) && - checkMcVersions(m_filter->versions, v.mcVersion)); // mcVersions + m_filter->checkMcVersions(v.mcVersion)); // mcVersions } } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index c9817cdf7..f0cc2df54 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -99,7 +99,7 @@ void ModPage::triggerSearch() updateSelectionButton(); static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), changed); - m_fetch_progress.watch(m_model->activeSearchJob().get()); + m_fetchProgress.watch(m_model->activeSearchJob().get()); } QMap ModPage::urlHandlers() const diff --git a/launcher/ui/pages/modplatform/ModpackProviderBasePage.h b/launcher/ui/pages/modplatform/ModpackProviderBasePage.h new file mode 100644 index 000000000..a3daa9a81 --- /dev/null +++ b/launcher/ui/pages/modplatform/ModpackProviderBasePage.h @@ -0,0 +1,29 @@ +// 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 "ui/pages/BasePage.h" + +class ModpackProviderBasePage : public BasePage { + public: + /** Programatically set the term in the search bar. */ + virtual void setSearchTerm(QString) = 0; + /** Get the current term in the search bar. */ + [[nodiscard]] virtual QString getSerachTerm() const = 0; +}; \ No newline at end of file diff --git a/launcher/ui/pages/modplatform/OptionalModDialog.cpp b/launcher/ui/pages/modplatform/OptionalModDialog.cpp index fc1c8b3cb..5dc53d9dc 100644 --- a/launcher/ui/pages/modplatform/OptionalModDialog.cpp +++ b/launcher/ui/pages/modplatform/OptionalModDialog.cpp @@ -43,6 +43,9 @@ OptionalModDialog::OptionalModDialog(QWidget* parent, const QStringList& mods) : else item->setCheckState(Qt::Checked); }); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } OptionalModDialog::~OptionalModDialog() diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index c8eb91570..50a170fff 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -31,9 +31,9 @@ QHash ResourceModel::s_running_models; ResourceModel::ResourceModel(ResourceAPI* api) : QAbstractListModel(), m_api(api) { s_running_models.insert(this, true); -#ifndef LAUNCHER_TEST - m_current_info_job.setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); -#endif + if (APPLICATION_DYN) { + m_current_info_job.setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + } } ResourceModel::~ResourceModel() @@ -60,11 +60,15 @@ auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant return pack->description; } case Qt::DecorationRole: { - if (auto icon_or_none = const_cast(this)->getIcon(const_cast(index), pack->logoUrl); - icon_or_none.has_value()) - return icon_or_none.value(); - - return APPLICATION->getThemedIcon("screenshot-placeholder"); + if (APPLICATION_DYN) { + if (auto icon_or_none = const_cast(this)->getIcon(const_cast(index), pack->logoUrl); + icon_or_none.has_value()) + return icon_or_none.value(); + + return APPLICATION->getThemedIcon("screenshot-placeholder"); + } else { + return {}; + } } case Qt::SizeHintRole: return QSize(0, 58); diff --git a/launcher/ui/pages/modplatform/ResourcePackPage.cpp b/launcher/ui/pages/modplatform/ResourcePackPage.cpp index 849ea1111..99039476e 100644 --- a/launcher/ui/pages/modplatform/ResourcePackPage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePackPage.cpp @@ -30,7 +30,7 @@ void ResourcePackResourcePage::triggerSearch() updateSelectionButton(); static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt()); - m_fetch_progress.watch(m_model->activeSearchJob().get()); + m_fetchProgress.watch(m_model->activeSearchJob().get()); } QMap ResourcePackResourcePage::urlHandlers() const diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index bed118465..766070f20 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -39,6 +39,7 @@ #include "ResourcePage.h" #include "modplatform/ModIndex.h" +#include "ui/dialogs/CustomMessageBox.h" #include "ui_ResourcePage.h" #include @@ -54,7 +55,7 @@ namespace ResourceDownload { ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_instance) - : QWidget(parent), m_base_instance(base_instance), m_ui(new Ui::ResourcePage), m_parent_dialog(parent), m_fetch_progress(this, false) + : QWidget(parent), m_baseInstance(base_instance), m_ui(new Ui::ResourcePage), m_parentDialog(parent), m_fetchProgress(this, false) { m_ui->setupUi(this); @@ -63,18 +64,18 @@ ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_in m_ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); m_ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); - m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); - m_search_timer.setSingleShot(true); + m_searchTimer.setTimerType(Qt::TimerType::CoarseTimer); + m_searchTimer.setSingleShot(true); - connect(&m_search_timer, &QTimer::timeout, this, &ResourcePage::triggerSearch); + connect(&m_searchTimer, &QTimer::timeout, this, &ResourcePage::triggerSearch); // hide progress bar to prevent weird artifact - m_fetch_progress.hide(); - m_fetch_progress.hideIfInactive(true); - m_fetch_progress.setFixedHeight(24); - m_fetch_progress.progressFormat(""); + m_fetchProgress.hide(); + m_fetchProgress.hideIfInactive(true); + m_fetchProgress.setFixedHeight(24); + m_fetchProgress.progressFormat(""); - m_ui->verticalLayout->insertWidget(1, &m_fetch_progress); + m_ui->verticalLayout->insertWidget(1, &m_fetchProgress); m_ui->packView->setItemDelegate(new ProjectItemDelegate(this)); m_ui->packView->installEventFilter(this); @@ -120,10 +121,10 @@ auto ResourcePage::eventFilter(QObject* watched, QEvent* event) -> bool keyEvent->accept(); return true; } else { - if (m_search_timer.isActive()) - m_search_timer.stop(); + if (m_searchTimer.isActive()) + m_searchTimer.stop(); - m_search_timer.start(350); + m_searchTimer.start(350); } } else if (watched == m_ui->packView) { if (keyEvent->key() == Qt::Key_Return) { @@ -247,14 +248,17 @@ void ResourcePage::updateUi() void ResourcePage::updateSelectionButton() { - if (!isOpened || m_selected_version_index < 0) { + if (!isOpened || m_selectedVersionIndex < 0) { m_ui->resourceSelectionButton->setEnabled(false); return; } m_ui->resourceSelectionButton->setEnabled(true); if (auto current_pack = getCurrentPack(); current_pack) { - if (!current_pack->isVersionSelected(m_selected_version_index)) + if (current_pack->versionsLoaded && current_pack->versions.empty()) { + m_ui->resourceSelectionButton->setEnabled(false); + qWarning() << tr("No version available for the selected pack"); + } else if (!current_pack->isVersionSelected(m_selectedVersionIndex)) m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString())); else m_ui->resourceSelectionButton->setText(tr("Deselect %1 for download").arg(resourceString())); @@ -323,18 +327,18 @@ void ResourcePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI void ResourcePage::onVersionSelectionChanged(int index) { - m_selected_version_index = index; + m_selectedVersionIndex = m_ui->versionSelectionBox->itemData(index).toInt(); updateSelectionButton(); } void ResourcePage::addResourceToDialog(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& version) { - m_parent_dialog->addResource(pack, version); + m_parentDialog->addResource(pack, version); } void ResourcePage::removeResourceFromDialog(const QString& pack_name) { - m_parent_dialog->removeResource(pack_name); + m_parentDialog->removeResource(pack_name); } void ResourcePage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack, @@ -351,14 +355,15 @@ void ResourcePage::removeResourceFromPage(const QString& name) void ResourcePage::onResourceSelected() { - if (m_selected_version_index < 0) + if (m_selectedVersionIndex < 0) return; auto current_pack = getCurrentPack(); - if (!current_pack || !current_pack->versionsLoaded) + if (!current_pack || !current_pack->versionsLoaded || current_pack->versions.size() < m_selectedVersionIndex) return; - auto& version = current_pack->versions[m_selected_version_index]; + auto& version = current_pack->versions[m_selectedVersionIndex]; + Q_ASSERT(!version.downloadUrl.isNull()); if (version.is_currently_selected) removeResourceFromDialog(current_pack->name); else @@ -397,14 +402,14 @@ void ResourcePage::openUrl(const QUrl& url) } } - if (!page.isNull() && !m_do_not_jump_to_mod) { + if (!page.isNull() && !m_doNotJumpToMod) { const QString slug = match.captured(1); // ensure the user isn't opening the same mod if (auto current_pack = getCurrentPack(); current_pack && slug != current_pack->slug) { - m_parent_dialog->selectPage(page); + m_parentDialog->selectPage(page); - auto newPage = m_parent_dialog->selectedPage(); + auto newPage = m_parentDialog->selectedPage(); QLineEdit* searchEdit = newPage->m_ui->searchEdit; auto model = newPage->m_model; @@ -448,7 +453,7 @@ void ResourcePage::openProject(QVariant projectID) m_ui->resourceFilterButton->hide(); m_ui->packView->hide(); m_ui->resourceSelectionButton->hide(); - m_do_not_jump_to_mod = true; + m_doNotJumpToMod = true; auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); @@ -462,20 +467,23 @@ void ResourcePage::openProject(QVariant projectID) auto cancelBtn = buttonBox->button(QDialogButtonBox::Cancel); cancelBtn->setDefault(false); cancelBtn->setAutoDefault(false); + cancelBtn->setText(tr("Cancel")); connect(okBtn, &QPushButton::clicked, this, [this] { onResourceSelected(); - m_parent_dialog->accept(); + m_parentDialog->accept(); }); - connect(cancelBtn, &QPushButton::clicked, m_parent_dialog, &ResourceDownloadDialog::reject); + connect(cancelBtn, &QPushButton::clicked, m_parentDialog, &ResourceDownloadDialog::reject); m_ui->gridLayout_4->addWidget(buttonBox, 1, 2); - auto jump = [this, okBtn] { + connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, + [this, okBtn](int index) { okBtn->setEnabled(m_ui->versionSelectionBox->itemData(index).toInt() >= 0); }); + + auto jump = [this] { for (int row = 0; row < m_model->rowCount({}); row++) { const QModelIndex index = m_model->index(row); m_ui->packView->setCurrentIndex(index); - okBtn->setEnabled(true); return; } m_ui->packDescription->setText(tr("The resource was not found")); diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h index b625240eb..09c512df4 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.h +++ b/launcher/ui/pages/modplatform/ResourcePage.h @@ -62,7 +62,7 @@ class ResourcePage : public QWidget, public BasePage { [[nodiscard]] bool setCurrentPack(ModPlatform::IndexedPack::Ptr); [[nodiscard]] auto getCurrentPack() const -> ModPlatform::IndexedPack::Ptr; - [[nodiscard]] auto getDialog() const -> const ResourceDownloadDialog* { return m_parent_dialog; } + [[nodiscard]] auto getDialog() const -> const ResourceDownloadDialog* { return m_parentDialog; } [[nodiscard]] auto getModel() const -> ResourceModel* { return m_model; } protected: @@ -99,22 +99,22 @@ class ResourcePage : public QWidget, public BasePage { virtual void openUrl(const QUrl&); public: - BaseInstance& m_base_instance; + BaseInstance& m_baseInstance; protected: Ui::ResourcePage* m_ui; - ResourceDownloadDialog* m_parent_dialog = nullptr; + ResourceDownloadDialog* m_parentDialog = nullptr; ResourceModel* m_model = nullptr; - int m_selected_version_index = -1; + int m_selectedVersionIndex = -1; - ProgressWidget m_fetch_progress; + ProgressWidget m_fetchProgress; // Used to do instant searching with a delay to cache quick changes - QTimer m_search_timer; + QTimer m_searchTimer; - bool m_do_not_jump_to_mod = false; + bool m_doNotJumpToMod = false; }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ShaderPackPage.cpp b/launcher/ui/pages/modplatform/ShaderPackPage.cpp index ebd8d4ea2..cedf985fb 100644 --- a/launcher/ui/pages/modplatform/ShaderPackPage.cpp +++ b/launcher/ui/pages/modplatform/ShaderPackPage.cpp @@ -31,7 +31,7 @@ void ShaderPackResourcePage::triggerSearch() updateSelectionButton(); static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt()); - m_fetch_progress.watch(m_model->activeSearchJob().get()); + m_fetchProgress.watch(m_model->activeSearchJob().get()); } QMap ShaderPackResourcePage::urlHandlers() const diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp index d79b7621a..7c69b315e 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -164,3 +164,13 @@ void AtlPage::onVersionSelectionChanged(QString version) selectedVersion = version; suggestCurrent(); } + +void AtlPage::setSearchTerm(QString term) +{ + ui->searchEdit->setText(term); +} + +QString AtlPage::getSerachTerm() const +{ + return ui->searchEdit->text(); +} diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h index 6bc449649..73460232b 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h @@ -42,8 +42,7 @@ #include #include "Application.h" -#include "tasks/Task.h" -#include "ui/pages/BasePage.h" +#include "ui/pages/modplatform/ModpackProviderBasePage.h" namespace Ui { class AtlPage; @@ -51,7 +50,7 @@ class AtlPage; class NewInstanceDialog; -class AtlPage : public QWidget, public BasePage { +class AtlPage : public QWidget, public ModpackProviderBasePage { Q_OBJECT public: @@ -66,6 +65,11 @@ class AtlPage : public QWidget, public BasePage { void openedImpl() override; + /** Programatically set the term in the search bar. */ + virtual void setSearchTerm(QString) override; + /** Get the current term in the search bar. */ + [[nodiscard]] virtual QString getSerachTerm() const override; + private: void suggestCurrent(); diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index a92d5b579..cfdb185ff 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -1,6 +1,7 @@ #include "FlameModel.h" #include #include "Application.h" +#include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" #include "modplatform/flame/FlameAPI.h" #include "ui/widgets/ProjectItem.h" @@ -183,34 +184,28 @@ void ListModel::performPaginatedSearch() return; } } + ResourceAPI::SortingMethod sort{}; + sort.index = currentSort + 1; + auto netJob = makeShared("Flame::Search", APPLICATION->network()); - auto searchUrl = QString( - "https://api.curseforge.com/v1/mods/search?" - "gameId=432&" - "classId=4471&" - "index=%1&" - "pageSize=25&" - "searchFilter=%2&" - "sortField=%3&" - "sortOrder=desc") - .arg(nextSearchOffset) - .arg(currentSearchTerm) - .arg(currentSort + 1); - - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response)); + auto searchUrl = FlameAPI().getSearchURL({ ModPlatform::ResourceType::MODPACK, nextSearchOffset, currentSearchTerm, sort, + m_filter->loaders, m_filter->versions, "", m_filter->categoryIds }); + + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl.value()), response)); jobPtr = netJob; jobPtr->start(); QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished); QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::searchRequestFailed); } -void ListModel::searchWithTerm(const QString& term, int sort) +void ListModel::searchWithTerm(const QString& term, int sort, std::shared_ptr filter, bool filterChanged) { - if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { + if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort && !filterChanged) { return; } currentSearchTerm = term; currentSort = sort; + m_filter = filter; if (hasActiveSearchJob()) { jobPtr->abort(); searchState = ResetRequested; diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.h b/launcher/ui/pages/modplatform/flame/FlameModel.h index 9b6d70fec..026f6d1ee 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameModel.h @@ -14,6 +14,7 @@ #include #include +#include "ui/widgets/ModFilterWidget.h" #include @@ -38,7 +39,7 @@ class ListModel : public QAbstractListModel { void fetchMore(const QModelIndex& parent) override; void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); - void searchWithTerm(const QString& term, int sort); + void searchWithTerm(const QString& term, int sort, std::shared_ptr filter, bool filterChanged); [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } @@ -65,6 +66,7 @@ class ListModel : public QAbstractListModel { QString currentSearchTerm; int currentSort = 0; + std::shared_ptr m_filter; int nextSearchOffset = 0; enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; Task::Ptr jobPtr; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index decb5de3b..de6b3d633 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -34,10 +34,14 @@ */ #include "FlamePage.h" +#include "Version.h" +#include "modplatform/flame/FlamePackIndex.h" #include "ui/dialogs/CustomMessageBox.h" +#include "ui/widgets/ModFilterWidget.h" #include "ui_FlamePage.h" #include +#include #include "Application.h" #include "FlameModel.h" @@ -88,6 +92,7 @@ FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) ui->packView->setItemDelegate(new ProjectItemDelegate(this)); ui->packDescription->setMetaEntry("FlamePacks"); + createFilterWidget(); } FlamePage::~FlamePage() @@ -131,10 +136,25 @@ void FlamePage::openedImpl() void FlamePage::triggerSearch() { - listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); + ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect); + ui->packView->clearSelection(); + ui->packDescription->clear(); + ui->versionSelectionBox->clear(); + listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), + m_filterWidget->changed()); m_fetch_progress.watch(listModel->activeSearchJob().get()); } +bool checkVersionFilters(const Flame::IndexedVersion& v, std::shared_ptr filter) +{ + if (!filter) + return true; + return ((!filter->loaders || !v.loaders || filter->loaders & v.loaders) && // loaders + (filter->releases.empty() || // releases + std::find(filter->releases.cbegin(), filter->releases.cend(), v.version_type) != filter->releases.cend()) && + filter->checkMcVersions({ v.mcVersion })); // mcVersions} +} + void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelIndex prev) { ui->versionSelectionBox->clear(); @@ -148,7 +168,7 @@ void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelInde current = listModel->data(curr, Qt::UserRole).value(); - if (current.versionsLoaded == false) { + if (!current.versionsLoaded || m_filterWidget->changed()) { qDebug() << "Loading flame modpack versions"; auto netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name), APPLICATION->network()); auto response = std::make_shared(); @@ -176,6 +196,16 @@ void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelInde qWarning() << "Error while reading flame modpack version: " << e.cause(); } + auto pred = [this](const Flame::IndexedVersion& v) { return !checkVersionFilters(v, m_filterWidget->getFilter()); }; +#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) + current.versions.removeIf(pred); +#else + for (auto it = current.versions.begin(); it != current.versions.end();) + if (pred(*it)) + it = current.versions.erase(it); + else + ++it; +#endif for (auto version : current.versions) { auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; auto mcVersion = !version.mcVersion.isEmpty() && !version.version.contains(version.mcVersion) @@ -243,7 +273,7 @@ void FlamePage::suggestCurrent() void FlamePage::onVersionSelectionChanged(int index) { bool is_blocked = false; - ui->versionSelectionBox->currentData().toInt(&is_blocked); + ui->versionSelectionBox->itemData(index).toInt(&is_blocked); if (index == -1 || is_blocked) { m_selected_version_index = -1; @@ -299,3 +329,34 @@ void FlamePage::updateUi() ui->packDescription->setHtml(StringUtils::htmlListPatch(text + current.description)); ui->packDescription->flush(); } +QString FlamePage::getSerachTerm() const +{ + return ui->searchEdit->text(); +} + +void FlamePage::setSearchTerm(QString term) +{ + ui->searchEdit->setText(term); +} + +void FlamePage::createFilterWidget() +{ + auto widget = ModFilterWidget::create(nullptr, false, this); + m_filterWidget.swap(widget); + auto old = ui->splitter->replaceWidget(0, m_filterWidget.get()); + // because we replaced the widget we also need to delete it + if (old) { + delete old; + } + + connect(ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); + + connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &FlamePage::triggerSearch); + auto response = std::make_shared(); + m_categoriesTask = FlameAPI::getCategories(response, ModPlatform::ResourceType::MODPACK); + QObject::connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { + auto categories = FlameAPI::loadModCategories(response); + m_filterWidget->setCategories(categories); + }); + m_categoriesTask->start(); +} diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index 7590e1a95..27c96d2f1 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -40,7 +40,8 @@ #include #include #include -#include "ui/pages/BasePage.h" +#include "ui/pages/modplatform/ModpackProviderBasePage.h" +#include "ui/widgets/ModFilterWidget.h" #include "ui/widgets/ProgressWidget.h" namespace Ui { @@ -53,7 +54,7 @@ namespace Flame { class ListModel; } -class FlamePage : public QWidget, public BasePage { +class FlamePage : public QWidget, public ModpackProviderBasePage { Q_OBJECT public: @@ -72,6 +73,11 @@ class FlamePage : public QWidget, public BasePage { bool eventFilter(QObject* watched, QEvent* event) override; + /** Programatically set the term in the search bar. */ + virtual void setSearchTerm(QString) override; + /** Get the current term in the search bar. */ + [[nodiscard]] virtual QString getSerachTerm() const override; + private: void suggestCurrent(); @@ -79,6 +85,7 @@ class FlamePage : public QWidget, public BasePage { void triggerSearch(); void onSelectionChanged(QModelIndex first, QModelIndex second); void onVersionSelectionChanged(int index); + void createFilterWidget(); private: Ui::FlamePage* ui = nullptr; @@ -92,4 +99,7 @@ class FlamePage : public QWidget, public BasePage { // Used to do instant searching with a delay to cache quick changes QTimer m_search_timer; + + unique_qobject_ptr m_filterWidget; + Task::Ptr m_categoriesTask; }; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.ui b/launcher/ui/pages/modplatform/flame/FlamePage.ui index d4ddb37a4..cf882ef1c 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.ui +++ b/launcher/ui/pages/modplatform/flame/FlamePage.ui @@ -30,42 +30,59 @@ - - - Search and filter... - - - - - + - - - Qt::ScrollBarAlwaysOff - - - true - - - - 48 - 48 - + + + Filter options - - - true - - - true + + + Search and filter... + + + + + 0 + 0 + + + + Qt::Horizontal + + + + + Qt::ScrollBarAlwaysOff + + + true + + + + 48 + 48 + + + + + + true + + + true + + + + diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index 62c22902e..4e01f3a65 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -209,17 +209,17 @@ auto FlameShaderPackPage::shouldDisplay() const -> bool unique_qobject_ptr FlameModPage::createFilterWidget() { - return ModFilterWidget::create(&static_cast(m_base_instance), false, this); + return ModFilterWidget::create(&static_cast(m_baseInstance), false, this); } void FlameModPage::prepareProviderCategories() { auto response = std::make_shared(); - auto task = FlameAPI::getModCategories(response); - QObject::connect(task.get(), &Task::succeeded, [this, response]() { + m_categoriesTask = FlameAPI::getModCategories(response); + QObject::connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { auto categories = FlameAPI::loadModCategories(response); m_filter_widget->setCategories(categories); }); - task->start(); + m_categoriesTask->start(); }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h index 6eef3e435..052706549 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h @@ -100,6 +100,9 @@ class FlameModPage : public ModPage { protected: virtual void prepareProviderCategories() override; + + private: + Task::Ptr m_categoriesTask; }; class FlameResourcePackPage : public ResourcePackResourcePage { diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp index db59fe10a..15303bb22 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp @@ -135,4 +135,13 @@ void ImportFTBPage::triggerSearch() currentModel->setSearchTerm(ui->searchEdit->text()); } +void ImportFTBPage::setSearchTerm(QString term) +{ + ui->searchEdit->setText(term); +} + +QString ImportFTBPage::getSerachTerm() const +{ + return ui->searchEdit->text(); +} } // namespace FTBImportAPP diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h index 00f013f6f..7afff5a9d 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h @@ -25,7 +25,7 @@ #include #include "modplatform/import_ftb/PackHelpers.h" -#include "ui/pages/BasePage.h" +#include "ui/pages/modplatform/ModpackProviderBasePage.h" #include "ui/pages/modplatform/import_ftb/ListModel.h" class NewInstanceDialog; @@ -35,7 +35,7 @@ namespace Ui { class ImportFTBPage; } -class ImportFTBPage : public QWidget, public BasePage { +class ImportFTBPage : public QWidget, public ModpackProviderBasePage { Q_OBJECT public: @@ -49,6 +49,11 @@ class ImportFTBPage : public QWidget, public BasePage { void openedImpl() override; void retranslate() override; + /** Programatically set the term in the search bar. */ + virtual void setSearchTerm(QString) override; + /** Get the current term in the search bar. */ + [[nodiscard]] virtual QString getSerachTerm() const override; + private: void suggestCurrent(); void onPackSelectionChanged(Modpack* pack = nullptr); diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui index 18c604ca4..337c3e474 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui @@ -13,6 +13,11 @@ + + + true + + Note: If your FTB instances are not in the default location, select it using the button next to search. diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp index a587b5baf..226a30ee3 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp @@ -369,4 +369,13 @@ void Page::triggerSearch() currentModel->setSearchTerm(ui->searchEdit->text()); } +void Page::setSearchTerm(QString term) +{ + ui->searchEdit->setText(term); +} + +QString Page::getSerachTerm() const +{ + return ui->searchEdit->text(); +} } // namespace LegacyFTB diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.h b/launcher/ui/pages/modplatform/legacy_ftb/Page.h index daef23342..a2dee24e9 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.h +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.h @@ -43,7 +43,7 @@ #include "QObjectPtr.h" #include "modplatform/legacy_ftb/PackFetchTask.h" #include "modplatform/legacy_ftb/PackHelpers.h" -#include "ui/pages/BasePage.h" +#include "ui/pages/modplatform/ModpackProviderBasePage.h" class NewInstanceDialog; @@ -57,7 +57,7 @@ class ListModel; class FilterModel; class PrivatePackManager; -class Page : public QWidget, public BasePage { +class Page : public QWidget, public ModpackProviderBasePage { Q_OBJECT public: @@ -71,6 +71,11 @@ class Page : public QWidget, public BasePage { void openedImpl() override; void retranslate() override; + /** Programatically set the term in the search bar. */ + virtual void setSearchTerm(QString) override; + /** Get the current term in the search bar. */ + [[nodiscard]] virtual QString getSerachTerm() const override; + private: void suggestCurrent(); void onPackSelectionChanged(Modpack* pack = nullptr); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index b53eea4ef..417ff4080 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -152,33 +152,26 @@ void ModpackListModel::performPaginatedSearch() return; } } // TODO: Move to standalone API + ResourceAPI::SortingMethod sort{}; + sort.name = currentSort; + auto searchUrl = ModrinthAPI().getSearchURL({ ModPlatform::ResourceType::MODPACK, nextSearchOffset, currentSearchTerm, sort, + m_filter->loaders, m_filter->versions, "", m_filter->categoryIds }); + 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\"]]") - .arg(nextSearchOffset) - .arg(m_modpacks_per_page) - .arg(currentSearchTerm) - .arg(currentSort); - - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchAllUrl), m_all_response)); + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl.value()), m_allResponse)); QObject::connect(netJob.get(), &NetJob::succeeded, this, [this] { - QJsonParseError parse_error_all{}; + QJsonParseError parseError{}; - 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; + QJsonDocument doc = QJsonDocument::fromJson(*m_allResponse, &parseError); + if (parseError.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from " << debugName() << " at " << parseError.offset + << " reason: " << parseError.errorString(); + qWarning() << *m_allResponse; return; } - searchRequestFinished(doc_all); + searchRequestFinished(doc); }); QObject::connect(netJob.get(), &NetJob::failed, this, &ModpackListModel::searchRequestFailed); @@ -220,19 +213,23 @@ static auto sortFromIndex(int index) -> QString } } -void ModpackListModel::searchWithTerm(const QString& term, const int sort) +void ModpackListModel::searchWithTerm(const QString& term, + const int sort, + std::shared_ptr filter, + bool filterChanged) { if (sort > 5 || sort < 0) return; auto sort_str = sortFromIndex(sort); - if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort_str) { + if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort_str && !filterChanged) { return; } currentSearchTerm = term; currentSort = sort_str; + m_filter = filter; refresh(); } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 514ee4484..640ddf688 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -71,7 +71,7 @@ class ModpackListModel : public QAbstractListModel { /* Ask the API for more information */ void fetchMore(const QModelIndex& parent) override; void refresh(); - void searchWithTerm(const QString& term, int sort); + void searchWithTerm(const QString& term, int sort, std::shared_ptr filter, bool filterChanged); [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } @@ -112,12 +112,13 @@ class ModpackListModel : public QAbstractListModel { QString currentSearchTerm; QString currentSort; + std::shared_ptr m_filter; int nextSearchOffset = 0; enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; Task::Ptr jobPtr; - std::shared_ptr m_all_response = std::make_shared(); + std::shared_ptr m_allResponse = std::make_shared(); QByteArray m_specific_response; int m_modpacks_per_page = 20; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 03461d85a..7d70abec4 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -35,6 +35,8 @@ */ #include "ModrinthPage.h" +#include "Version.h" +#include "modplatform/modrinth/ModrinthAPI.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui_ModrinthPage.h" @@ -58,6 +60,7 @@ ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog), m_fetch_progress(this, false) { ui->setupUi(this); + createFilterWidget(); ui->searchEdit->installEventFilter(this); m_model = new Modrinth::ModpackListModel(this); @@ -126,6 +129,16 @@ bool ModrinthPage::eventFilter(QObject* watched, QEvent* event) return QObject::eventFilter(watched, event); } +bool checkVersionFilters(const Modrinth::ModpackVersion& v, std::shared_ptr filter) +{ + if (!filter) + return true; + return ((!filter->loaders || !v.loaders || filter->loaders & v.loaders) && // loaders + (filter->releases.empty() || // releases + std::find(filter->releases.cbegin(), filter->releases.cend(), v.version_type) != filter->releases.cend()) && + filter->checkMcVersions({ v.gameVersion })); // gameVersion} +} + void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelIndex prev) { ui->versionSelectionBox->clear(); @@ -190,7 +203,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI } else updateUI(); - if (!current.versionsLoaded) { + if (!current.versionsLoaded || m_filterWidget->changed()) { qDebug() << "Loading modrinth modpack versions"; auto netJob = new NetJob(QString("Modrinth::PackVersions(%1)").arg(current.name), APPLICATION->network()); @@ -221,6 +234,16 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI qDebug() << *response; qWarning() << "Error while reading modrinth modpack version: " << e.cause(); } + auto pred = [this](const Modrinth::ModpackVersion& v) { return !checkVersionFilters(v, m_filterWidget->getFilter()); }; +#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) + current.versions.removeIf(pred); +#else + for (auto it = current.versions.begin(); it != current.versions.end();) + if (pred(*it)) + it = current.versions.erase(it); + else + ++it; +#endif for (auto version : current.versions) { auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; auto mcVersion = !version.gameVersion.isEmpty() && !version.name.contains(version.gameVersion) @@ -338,7 +361,11 @@ void ModrinthPage::suggestCurrent() void ModrinthPage::triggerSearch() { - m_model->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); + ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect); + ui->packView->clearSelection(); + ui->packDescription->clear(); + ui->versionSelectionBox->clear(); + m_model->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), m_filterWidget->changed()); m_fetch_progress.watch(m_model->activeSearchJob().get()); } @@ -348,6 +375,38 @@ void ModrinthPage::onVersionSelectionChanged(int index) selectedVersion = ""; return; } - selectedVersion = ui->versionSelectionBox->currentData().toString(); + selectedVersion = ui->versionSelectionBox->itemData(index).toString(); suggestCurrent(); } + +void ModrinthPage::setSearchTerm(QString term) +{ + ui->searchEdit->setText(term); +} + +QString ModrinthPage::getSerachTerm() const +{ + return ui->searchEdit->text(); +} + +void ModrinthPage::createFilterWidget() +{ + auto widget = ModFilterWidget::create(nullptr, true, this); + m_filterWidget.swap(widget); + auto old = ui->splitter->replaceWidget(0, m_filterWidget.get()); + // because we replaced the widget we also need to delete it + if (old) { + delete old; + } + + connect(ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); + + connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &ModrinthPage::triggerSearch); + auto response = std::make_shared(); + m_categoriesTask = ModrinthAPI::getModCategories(response); + QObject::connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { + auto categories = ModrinthAPI::loadCategories(response, "modpack"); + m_filterWidget->setCategories(categories); + }); + m_categoriesTask->start(); +} \ No newline at end of file diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index dadaeb0a0..7f504cdbd 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -38,9 +38,10 @@ #include "Application.h" #include "ui/dialogs/NewInstanceDialog.h" -#include "ui/pages/BasePage.h" #include "modplatform/modrinth/ModrinthPackManifest.h" +#include "ui/pages/modplatform/ModpackProviderBasePage.h" +#include "ui/widgets/ModFilterWidget.h" #include "ui/widgets/ProgressWidget.h" #include @@ -54,7 +55,7 @@ namespace Modrinth { class ModpackListModel; } -class ModrinthPage : public QWidget, public BasePage { +class ModrinthPage : public QWidget, public ModpackProviderBasePage { Q_OBJECT public: @@ -78,10 +79,16 @@ class ModrinthPage : public QWidget, public BasePage { void openedImpl() override; bool eventFilter(QObject* watched, QEvent* event) override; + /** Programatically set the term in the search bar. */ + virtual void setSearchTerm(QString) override; + /** Get the current term in the search bar. */ + [[nodiscard]] virtual QString getSerachTerm() const override; + private slots: void onSelectionChanged(QModelIndex first, QModelIndex second); void onVersionSelectionChanged(int index); void triggerSearch(); + void createFilterWidget(); private: Ui::ModrinthPage* ui; @@ -95,4 +102,7 @@ class ModrinthPage : public QWidget, public BasePage { // Used to do instant searching with a delay to cache quick changes QTimer m_search_timer; + + unique_qobject_ptr m_filterWidget; + Task::Ptr m_categoriesTask; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui index 7f4f903f6..d6e983929 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -12,42 +12,59 @@ - - - Search and filter ... - - - - - + - - - Qt::ScrollBarAlwaysOff - - - true - - - - 48 - 48 - + + + Filter options - - - true - - - true + + + Search and filter... + + + + + 0 + 0 + + + + Qt::Horizontal + + + + + Qt::ScrollBarAlwaysOff + + + true + + + + 48 + 48 + + + + + + true + + + true + + + + diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index 85dcde471..4ee620677 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -144,7 +144,7 @@ auto ModrinthShaderPackPage::shouldDisplay() const -> bool unique_qobject_ptr ModrinthModPage::createFilterWidget() { - return ModFilterWidget::create(&static_cast(m_base_instance), true, this); + return ModFilterWidget::create(&static_cast(m_baseInstance), true, this); } void ModrinthModPage::prepareProviderCategories() diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index 4181edab6..f7e7f4433 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -154,6 +154,10 @@ void Technic::ListModel::performSearch() QString("%1search?build=%2&q=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD, currentSearchTerm); searchMode = List; } + auto clientId = APPLICATION->settings()->get("TechnicClientID").toString(); + if (!clientId.isEmpty()) { + searchUrl += "?cid=" + clientId; + } netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response)); jobPtr = netJob; jobPtr->start(); diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index a8f06619f..50d267b1f 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -342,3 +342,13 @@ void TechnicPage::onVersionSelectionChanged(QString version) selectedVersion = version; selectVersion(); } + +void TechnicPage::setSearchTerm(QString term) +{ + ui->searchEdit->setText(term); +} + +QString TechnicPage::getSerachTerm() const +{ + return ui->searchEdit->text(); +} diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.h b/launcher/ui/pages/modplatform/technic/TechnicPage.h index 01439337d..d1f691b22 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.h +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.h @@ -41,7 +41,7 @@ #include #include "TechnicData.h" #include "net/NetJob.h" -#include "ui/pages/BasePage.h" +#include "ui/pages/modplatform/ModpackProviderBasePage.h" #include "ui/widgets/ProgressWidget.h" namespace Ui { @@ -54,7 +54,7 @@ namespace Technic { class ListModel; } -class TechnicPage : public QWidget, public BasePage { +class TechnicPage : public QWidget, public ModpackProviderBasePage { Q_OBJECT public: @@ -71,6 +71,11 @@ class TechnicPage : public QWidget, public BasePage { bool eventFilter(QObject* watched, QEvent* event) override; + /** Programatically set the term in the search bar. */ + virtual void setSearchTerm(QString) override; + /** Get the current term in the search bar. */ + [[nodiscard]] virtual QString getSerachTerm() const override; + private: void suggestCurrent(); void metadataLoaded(); diff --git a/launcher/ui/setupwizard/AutoJavaWizardPage.cpp b/launcher/ui/setupwizard/AutoJavaWizardPage.cpp new file mode 100644 index 000000000..fd173e71d --- /dev/null +++ b/launcher/ui/setupwizard/AutoJavaWizardPage.cpp @@ -0,0 +1,33 @@ +#include "AutoJavaWizardPage.h" +#include "ui_AutoJavaWizardPage.h" + +#include "Application.h" + +AutoJavaWizardPage::AutoJavaWizardPage(QWidget* parent) : BaseWizardPage(parent), ui(new Ui::AutoJavaWizardPage) +{ + ui->setupUi(this); +} + +AutoJavaWizardPage::~AutoJavaWizardPage() +{ + delete ui; +} + +void AutoJavaWizardPage::initializePage() {} + +bool AutoJavaWizardPage::validatePage() +{ + auto s = APPLICATION->settings(); + + if (!ui->previousSettingsRadioButton->isChecked()) { + s->set("AutomaticJavaSwitch", true); + s->set("AutomaticJavaDownload", true); + } + s->set("UserAskedAboutAutomaticJavaDownload", true); + return true; +} + +void AutoJavaWizardPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/setupwizard/AutoJavaWizardPage.h b/launcher/ui/setupwizard/AutoJavaWizardPage.h new file mode 100644 index 000000000..fcdf5bdf1 --- /dev/null +++ b/launcher/ui/setupwizard/AutoJavaWizardPage.h @@ -0,0 +1,22 @@ +#pragma once +#include +#include "BaseWizardPage.h" + +namespace Ui { +class AutoJavaWizardPage; +} + +class AutoJavaWizardPage : public BaseWizardPage { + Q_OBJECT + + public: + explicit AutoJavaWizardPage(QWidget* parent = nullptr); + ~AutoJavaWizardPage(); + + void initializePage() override; + bool validatePage() override; + void retranslate() override; + + private: + Ui::AutoJavaWizardPage* ui; +}; diff --git a/launcher/ui/setupwizard/AutoJavaWizardPage.ui b/launcher/ui/setupwizard/AutoJavaWizardPage.ui new file mode 100644 index 000000000..a862524b0 --- /dev/null +++ b/launcher/ui/setupwizard/AutoJavaWizardPage.ui @@ -0,0 +1,93 @@ + + + AutoJavaWizardPage + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + <html><head/><body><p><span style=" font-size:14pt; font-weight:600;">New Feature Alert!</span></p></body></html> + + + Qt::RichText + + + true + + + + + + + We've added a feature to automatically download the correct Java version for each version of Minecraft (this can be changed in the Java Settings). Would you like to enable or disable this feature? + + + true + + + + + + + Qt::Horizontal + + + + + + + Enable Auto-Download + + + true + + + buttonGroup + + + + + + + Disable Auto-Download + + + false + + + buttonGroup + + + + + + + Qt::Vertical + + + + 20 + 156 + + + + + + + + + + + + diff --git a/launcher/ui/setupwizard/JavaWizardPage.cpp b/launcher/ui/setupwizard/JavaWizardPage.cpp index a47cebcaa..8caae173c 100644 --- a/launcher/ui/setupwizard/JavaWizardPage.cpp +++ b/launcher/ui/setupwizard/JavaWizardPage.cpp @@ -55,6 +55,7 @@ bool JavaWizardPage::validatePage() auto result = m_java_widget->validate(); settings->set("AutomaticJavaSwitch", m_java_widget->autoDetectJava()); settings->set("AutomaticJavaDownload", m_java_widget->autoDownloadJava()); + settings->set("UserAskedAboutAutomaticJavaDownload", true); switch (result) { default: case JavaSettingsWidget::ValidationStatus::Bad: { @@ -82,7 +83,6 @@ void JavaWizardPage::retranslate() { setTitle(tr("Java")); setSubTitle( - tr("You do not have a working Java set up yet or it went missing.\n" - "Please select one of the following or browse for a Java executable.")); + tr("Please select how much memory to allocate to instances and if Prism Launcher should manage Java automatically or manually.")); m_java_widget->retranslate(); } diff --git a/launcher/ui/setupwizard/JavaWizardPage.h b/launcher/ui/setupwizard/JavaWizardPage.h index dde765f27..9bf9cfa2b 100644 --- a/launcher/ui/setupwizard/JavaWizardPage.h +++ b/launcher/ui/setupwizard/JavaWizardPage.h @@ -9,7 +9,7 @@ class JavaWizardPage : public BaseWizardPage { public: explicit JavaWizardPage(QWidget* parent = Q_NULLPTR); - virtual ~JavaWizardPage() {}; + virtual ~JavaWizardPage() = default; bool wantsRefreshButton() override; void refresh() override; diff --git a/launcher/ui/setupwizard/LoginWizardPage.cpp b/launcher/ui/setupwizard/LoginWizardPage.cpp new file mode 100644 index 000000000..f53e31908 --- /dev/null +++ b/launcher/ui/setupwizard/LoginWizardPage.cpp @@ -0,0 +1,44 @@ +#include "LoginWizardPage.h" +#include "minecraft/auth/AccountList.h" +#include "ui/dialogs/MSALoginDialog.h" +#include "ui_LoginWizardPage.h" + +#include "Application.h" + +LoginWizardPage::LoginWizardPage(QWidget* parent) : BaseWizardPage(parent), ui(new Ui::LoginWizardPage) +{ + ui->setupUi(this); +} + +LoginWizardPage::~LoginWizardPage() +{ + delete ui; +} + +void LoginWizardPage::initializePage() {} + +bool LoginWizardPage::validatePage() +{ + return true; +} + +void LoginWizardPage::retranslate() +{ + ui->retranslateUi(this); +} + +void LoginWizardPage::on_pushButton_clicked() +{ + wizard()->hide(); + auto account = MSALoginDialog::newAccount(nullptr); + wizard()->show(); + if (account) { + APPLICATION->accounts()->addAccount(account); + APPLICATION->accounts()->setDefaultAccount(account); + if (wizard()->currentId() == wizard()->pageIds().last()) { + wizard()->accept(); + } else { + wizard()->next(); + } + } +} diff --git a/launcher/ui/setupwizard/LoginWizardPage.h b/launcher/ui/setupwizard/LoginWizardPage.h new file mode 100644 index 000000000..af71fc183 --- /dev/null +++ b/launcher/ui/setupwizard/LoginWizardPage.h @@ -0,0 +1,24 @@ +#pragma once +#include +#include "BaseWizardPage.h" + +namespace Ui { +class LoginWizardPage; +} + +class LoginWizardPage : public BaseWizardPage { + Q_OBJECT + + public: + explicit LoginWizardPage(QWidget* parent = nullptr); + ~LoginWizardPage(); + + void initializePage() override; + bool validatePage() override; + void retranslate() override; + private slots: + void on_pushButton_clicked(); + + private: + Ui::LoginWizardPage* ui; +}; diff --git a/launcher/ui/setupwizard/LoginWizardPage.ui b/launcher/ui/setupwizard/LoginWizardPage.ui new file mode 100644 index 000000000..191316c4e --- /dev/null +++ b/launcher/ui/setupwizard/LoginWizardPage.ui @@ -0,0 +1,74 @@ + + + LoginWizardPage + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + <html><head/><body><p><span style=" font-size:14pt; font-weight:600;">Add Microsoft account</span></p></body></html> + + + Qt::RichText + + + true + + + + + + + In order to play Minecraft, you must have at least one Microsoft account logged in. Do you want to log in now? + + + true + + + + + + + Qt::Horizontal + + + + + + + Add Microsoft account + + + + + + + Qt::Vertical + + + + 20 + 156 + + + + + + + + + + + + diff --git a/launcher/ui/themes/ThemeManager.cpp b/launcher/ui/themes/ThemeManager.cpp index 293598bc4..8aae73417 100644 --- a/launcher/ui/themes/ThemeManager.cpp +++ b/launcher/ui/themes/ThemeManager.cpp @@ -38,6 +38,9 @@ ThemeManager::ThemeManager() { + QIcon::setFallbackThemeName(QIcon::themeName()); + QIcon::setThemeSearchPaths(QIcon::themeSearchPaths() << m_iconThemeFolder.path()); + themeDebugLog() << "Determining System Widget Theme..."; const auto& style = QApplication::style(); m_defaultStyle = style->objectName(); @@ -95,10 +98,6 @@ void ThemeManager::initializeIcons() // set icon theme search path! themeDebugLog() << "<> Initializing Icon Themes"; - auto searchPaths = QIcon::themeSearchPaths(); - searchPaths.append(m_iconThemeFolder.path()); - QIcon::setThemeSearchPaths(searchPaths); - for (const QString& id : builtinIcons) { IconTheme theme(id, QString(":/icons/%1").arg(id)); if (!theme.load()) { @@ -300,6 +299,7 @@ void ThemeManager::initializeCatPacks() { QList> defaultCats{ { "kitteh", QObject::tr("Background Cat (from MultiMC)") }, { "typescript", QObject::tr("You should have used Typescript") }, + { "miside-screenshot", QObject::tr("MiSide screenshot") }, { "rory", QObject::tr("Rory ID 11 (drawn by Ashtaka)") }, { "rory-flat", QObject::tr("Rory ID 11 (flat edition, drawn by Ashtaka)") }, { "teawie", QObject::tr("Teawie (drawn by SympathyTea)") } }; diff --git a/launcher/ui/themes/ThemeManager.h b/launcher/ui/themes/ThemeManager.h index 3abb298b6..0648c3349 100644 --- a/launcher/ui/themes/ThemeManager.h +++ b/launcher/ui/themes/ThemeManager.h @@ -18,10 +18,12 @@ */ #pragma once +#include +#include #include +#include #include "IconTheme.h" -#include "ui/MainWindow.h" #include "ui/themes/CatPack.h" #include "ui/themes/ITheme.h" diff --git a/launcher/ui/widgets/CustomCommands.ui b/launcher/ui/widgets/CustomCommands.ui index 4a39ff7f7..b485c293e 100644 --- a/launcher/ui/widgets/CustomCommands.ui +++ b/launcher/ui/widgets/CustomCommands.ui @@ -38,19 +38,6 @@ false - - - - P&ost-exit command: - - - postExitCmdTextBox - - - - - - @@ -61,8 +48,8 @@ - - + + @@ -77,6 +64,19 @@ + + + + P&ost-exit command: + + + postExitCmdTextBox + + + + + + diff --git a/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp index 8e2d94556..6efd3f581 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.cpp +++ b/launcher/ui/widgets/JavaSettingsWidget.cpp @@ -3,9 +3,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -34,11 +36,13 @@ JavaSettingsWidget::JavaSettingsWidget(QWidget* parent) : QWidget(parent) goodIcon = APPLICATION->getThemedIcon("status-good"); yellowIcon = APPLICATION->getThemedIcon("status-yellow"); badIcon = APPLICATION->getThemedIcon("status-bad"); + m_memoryTimer = new QTimer(this); setupUi(); - connect(m_minMemSpinBox, SIGNAL(valueChanged(int)), this, SLOT(memoryValueChanged(int))); - connect(m_maxMemSpinBox, SIGNAL(valueChanged(int)), this, SLOT(memoryValueChanged(int))); - connect(m_permGenSpinBox, SIGNAL(valueChanged(int)), this, SLOT(memoryValueChanged(int))); + connect(m_minMemSpinBox, SIGNAL(valueChanged(int)), this, SLOT(onSpinBoxValueChanged(int))); + connect(m_maxMemSpinBox, SIGNAL(valueChanged(int)), this, SLOT(onSpinBoxValueChanged(int))); + connect(m_permGenSpinBox, SIGNAL(valueChanged(int)), this, SLOT(onSpinBoxValueChanged(int))); + connect(m_memoryTimer, &QTimer::timeout, this, &JavaSettingsWidget::memoryValueChanged); connect(m_versionWidget, &VersionSelectWidget::selectedVersionChanged, this, &JavaSettingsWidget::javaVersionSelected); connect(m_javaBrowseBtn, &QPushButton::clicked, this, &JavaSettingsWidget::on_javaBrowseBtn_clicked); connect(m_javaPathTextBox, &QLineEdit::textEdited, this, &JavaSettingsWidget::javaPathEdited); @@ -55,7 +59,6 @@ void JavaSettingsWidget::setupUi() m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); m_versionWidget = new VersionSelectWidget(this); - m_verticalLayout->addWidget(m_versionWidget); m_horizontalLayout = new QHBoxLayout(); m_horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); @@ -73,8 +76,6 @@ void JavaSettingsWidget::setupUi() m_javaStatusBtn->setIcon(yellowIcon); m_horizontalLayout->addWidget(m_javaStatusBtn); - m_verticalLayout->addLayout(m_horizontalLayout); - m_memoryGroupBox = new QGroupBox(this); m_memoryGroupBox->setObjectName(QStringLiteral("memoryGroupBox")); m_gridLayout_2 = new QGridLayout(m_memoryGroupBox); @@ -136,8 +137,6 @@ void JavaSettingsWidget::setupUi() m_horizontalBtnLayout->addWidget(m_javaDownloadBtn); } - m_verticalLayout->addLayout(m_horizontalBtnLayout); - m_autoJavaGroupBox = new QGroupBox(this); m_autoJavaGroupBox->setObjectName(QStringLiteral("autoJavaGroupBox")); m_veriticalJavaLayout = new QVBoxLayout(m_autoJavaGroupBox); @@ -145,21 +144,42 @@ void JavaSettingsWidget::setupUi() m_autodetectJavaCheckBox = new QCheckBox(m_autoJavaGroupBox); m_autodetectJavaCheckBox->setObjectName("autodetectJavaCheckBox"); + m_autodetectJavaCheckBox->setChecked(true); m_veriticalJavaLayout->addWidget(m_autodetectJavaCheckBox); if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { m_autodownloadCheckBox = new QCheckBox(m_autoJavaGroupBox); m_autodownloadCheckBox->setObjectName("autodownloadCheckBox"); - m_autodownloadCheckBox->setEnabled(false); + m_autodownloadCheckBox->setEnabled(m_autodetectJavaCheckBox->isChecked()); m_veriticalJavaLayout->addWidget(m_autodownloadCheckBox); connect(m_autodetectJavaCheckBox, &QCheckBox::stateChanged, this, [this] { m_autodownloadCheckBox->setEnabled(m_autodetectJavaCheckBox->isChecked()); if (!m_autodetectJavaCheckBox->isChecked()) m_autodownloadCheckBox->setChecked(false); }); + + connect(m_autodownloadCheckBox, &QCheckBox::stateChanged, this, [this] { + auto isChecked = m_autodownloadCheckBox->isChecked(); + m_versionWidget->setVisible(!isChecked); + m_javaStatusBtn->setVisible(!isChecked); + m_javaBrowseBtn->setVisible(!isChecked); + m_javaPathTextBox->setVisible(!isChecked); + m_javaDownloadBtn->setVisible(!isChecked); + if (!isChecked) { + m_verticalLayout->removeItem(m_verticalSpacer); + } else { + m_verticalLayout->addSpacerItem(m_verticalSpacer); + } + }); } m_verticalLayout->addWidget(m_autoJavaGroupBox); + m_verticalLayout->addLayout(m_horizontalBtnLayout); + + m_verticalLayout->addWidget(m_versionWidget); + m_verticalLayout->addLayout(m_horizontalLayout); + m_verticalSpacer = new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding); + retranslate(); } @@ -177,23 +197,16 @@ void JavaSettingsWidget::initialize() m_maxMemSpinBox->setValue(observedMaxMemory); m_permGenSpinBox->setValue(observedPermGenMemory); updateThresholds(); - if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { - auto button = - CustomMessageBox::selectable(this, tr("Automatic Java Download"), - tr("%1 can automatically download the correct Java version for each version of Minecraft..\n" - "Do you want to enable Java auto-download?\n") - .arg(BuildConfig.LAUNCHER_DISPLAYNAME), - QMessageBox::Question, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) - ->exec(); - auto checked = button == QMessageBox::Yes; - m_autodetectJavaCheckBox->setChecked(checked); - m_autodownloadCheckBox->setChecked(checked); + m_autodownloadCheckBox->setChecked(true); } } void JavaSettingsWidget::refresh() { + if (BuildConfig.JAVA_DOWNLOADER_ENABLED && m_autodownloadCheckBox->isChecked()) { + return; + } if (JavaUtils::getJavaCheckPath().isEmpty()) { JavaCommon::javaCheckNotFound(this); return; @@ -297,20 +310,21 @@ int JavaSettingsWidget::permGenSize() const return m_permGenSpinBox->value(); } -void JavaSettingsWidget::memoryValueChanged(int) +void JavaSettingsWidget::memoryValueChanged() { bool actuallyChanged = false; unsigned int min = m_minMemSpinBox->value(); unsigned int max = m_maxMemSpinBox->value(); unsigned int permgen = m_permGenSpinBox->value(); - QObject* obj = sender(); - if (obj == m_minMemSpinBox && min != observedMinMemory) { + if (min != observedMinMemory) { observedMinMemory = min; actuallyChanged = true; - } else if (obj == m_maxMemSpinBox && max != observedMaxMemory) { + } + if (max != observedMaxMemory) { observedMaxMemory = max; actuallyChanged = true; - } else if (obj == m_permGenSpinBox && permgen != observedPermGenMemory) { + } + if (permgen != observedPermGenMemory) { observedPermGenMemory = permgen; actuallyChanged = true; } @@ -446,7 +460,7 @@ void JavaSettingsWidget::checkJavaPath(const QString& path) } setJavaStatus(JavaStatus::Pending); m_checker.reset( - new JavaChecker(path, "", minHeapSize(), maxHeapSize(), m_permGenSpinBox->isVisible() ? m_permGenSpinBox->value() : 0, 0, this)); + new JavaChecker(path, "", minHeapSize(), maxHeapSize(), m_permGenSpinBox->isVisible() ? m_permGenSpinBox->value() : 0, 0)); connect(m_checker.get(), &JavaChecker::checkFinished, this, &JavaSettingsWidget::checkFinished); m_checker->start(); } @@ -505,6 +519,9 @@ void JavaSettingsWidget::updateThresholds() } else if (observedMaxMemory < observedMinMemory) { iconName = "status-yellow"; m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation is smaller than the minimum value")); + } else if (BuildConfig.JAVA_DOWNLOADER_ENABLED && m_autodownloadCheckBox->isChecked()) { + iconName = "status-good"; + m_labelMaxMemIcon->setToolTip(""); } else if (observedMaxMemory > 2048 && !m_result.is_64bit) { iconName = "status-bad"; m_labelMaxMemIcon->setToolTip(tr("You are exceeding the maximum allocation supported by 32-bit installations of Java.")); @@ -530,3 +547,13 @@ bool JavaSettingsWidget::autoDetectJava() const { return m_autodetectJavaCheckBox->isChecked(); } + +void JavaSettingsWidget::onSpinBoxValueChanged(int) +{ + m_memoryTimer->start(500); +} + +JavaSettingsWidget::~JavaSettingsWidget() +{ + delete m_verticalSpacer; +}; \ No newline at end of file diff --git a/launcher/ui/widgets/JavaSettingsWidget.h b/launcher/ui/widgets/JavaSettingsWidget.h index 622c473fe..35be48478 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.h +++ b/launcher/ui/widgets/JavaSettingsWidget.h @@ -17,6 +17,7 @@ class QGroupBox; class QGridLayout; class QLabel; class QToolButton; +class QSpacerItem; /** * This is a widget for all the Java settings dialogs and pages. @@ -26,7 +27,7 @@ class JavaSettingsWidget : public QWidget { public: explicit JavaSettingsWidget(QWidget* parent); - virtual ~JavaSettingsWidget() = default; + virtual ~JavaSettingsWidget(); enum class JavaStatus { NotSet, Pending, Good, DoesNotExist, DoesNotStart, ReturnedInvalidData } javaStatus = JavaStatus::NotSet; @@ -48,7 +49,8 @@ class JavaSettingsWidget : public QWidget { void updateThresholds(); protected slots: - void memoryValueChanged(int); + void onSpinBoxValueChanged(int); + void memoryValueChanged(); void javaPathEdited(const QString& path); void javaVersionSelected(BaseVersion::Ptr version); void on_javaBrowseBtn_clicked(); @@ -65,6 +67,7 @@ class JavaSettingsWidget : public QWidget { private: /* data */ VersionSelectWidget* m_versionWidget = nullptr; QVBoxLayout* m_verticalLayout = nullptr; + QSpacerItem* m_verticalSpacer = nullptr; QLineEdit* m_javaPathTextBox = nullptr; QPushButton* m_javaBrowseBtn = nullptr; @@ -99,4 +102,5 @@ class JavaSettingsWidget : public QWidget { uint64_t m_availableMemory = 0ull; shared_qobject_ptr m_checker; JavaChecker::Result m_result; + QTimer* m_memoryTimer; }; diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp index bbb91eac2..68adcdb71 100644 --- a/launcher/ui/widgets/ModFilterWidget.cpp +++ b/launcher/ui/widgets/ModFilterWidget.cpp @@ -64,10 +64,49 @@ class VersionBasicModel : public QIdentityProxyModel { { if (role == Qt::DisplayRole) return QIdentityProxyModel::data(index, BaseVersionList::VersionIdRole); + if (role == Qt::UserRole) + return QIdentityProxyModel::data(index, BaseVersionList::VersionIdRole); return {}; } }; +class AllVersionProxyModel : public QSortFilterProxyModel { + Q_OBJECT + + public: + AllVersionProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {} + + int rowCount(const QModelIndex& parent = QModelIndex()) const override { return QSortFilterProxyModel::rowCount(parent) + 1; } + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override + { + if (!index.isValid()) { + return {}; + } + + if (index.row() == 0) { + if (role == Qt::DisplayRole) { + return tr("All Versions"); + } + if (role == Qt::UserRole) { + return "all"; + } + return {}; + } + + QModelIndex newIndex = QSortFilterProxyModel::index(index.row() - 1, index.column()); + return QSortFilterProxyModel::data(newIndex, role); + } + + Qt::ItemFlags flags(const QModelIndex& index) const override + { + if (index.row() == 0) { + return Qt::ItemIsSelectable | Qt::ItemIsEnabled; + } + return QSortFilterProxyModel::flags(index); + } +}; + ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended, QWidget* parent) : QTabWidget(parent), ui(new Ui::ModFilterWidget), m_instance(instance), m_filter(new Filter()) { @@ -76,14 +115,21 @@ ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended, QWi m_versions_proxy = new VersionProxyModel(this); m_versions_proxy->setFilter(BaseVersionList::TypeRole, new ExactFilter("release")); - auto proxy = new VersionBasicModel(this); + QAbstractProxyModel* proxy = new VersionBasicModel(this); proxy->setSourceModel(m_versions_proxy); if (extended) { + if (!m_instance) { + ui->environmentGroup->hide(); + } ui->versions->setSourceModel(proxy); ui->versions->setSeparator(", "); + ui->versions->setDefaultText(tr("All Versions")); ui->version->hide(); } else { + auto allVersions = new AllVersionProxyModel(this); + allVersions->setSourceModel(proxy); + proxy = allVersions; ui->version->setModel(proxy); ui->versions->hide(); ui->showAllVersions->hide(); @@ -162,18 +208,22 @@ void ModFilterWidget::loadVersionList() void ModFilterWidget::prepareBasicFilter() { - m_filter->hideInstalled = false; - m_filter->side = ""; // or "both" - auto loaders = m_instance->getPackProfile()->getSupportedModLoaders().value(); - ui->neoForge->setChecked(loaders & ModPlatform::NeoForge); - ui->forge->setChecked(loaders & ModPlatform::Forge); - ui->fabric->setChecked(loaders & ModPlatform::Fabric); - ui->quilt->setChecked(loaders & ModPlatform::Quilt); - m_filter->loaders = loaders; - auto def = m_instance->getPackProfile()->getComponentVersion("net.minecraft"); - m_filter->versions.emplace_front(def); - ui->versions->setCheckedItems({ def }); - ui->version->setCurrentIndex(ui->version->findText(def)); + if (m_instance) { + m_filter->hideInstalled = false; + m_filter->side = ""; // or "both" + auto loaders = m_instance->getPackProfile()->getSupportedModLoaders().value(); + ui->neoForge->setChecked(loaders & ModPlatform::NeoForge); + ui->forge->setChecked(loaders & ModPlatform::Forge); + ui->fabric->setChecked(loaders & ModPlatform::Fabric); + ui->quilt->setChecked(loaders & ModPlatform::Quilt); + m_filter->loaders = loaders; + auto def = m_instance->getPackProfile()->getComponentVersion("net.minecraft"); + m_filter->versions.emplace_front(def); + ui->versions->setCheckedItems({ def }); + ui->version->setCurrentIndex(ui->version->findText(def)); + } else { + ui->hideInstalled->hide(); + } } void ModFilterWidget::onShowAllVersionsChanged() @@ -249,7 +299,9 @@ void ModFilterWidget::onHideInstalledFilterChanged() void ModFilterWidget::onVersionFilterTextChanged(const QString& version) { m_filter->versions.clear(); - m_filter->versions.emplace_back(version); + if (ui->version->currentData(Qt::UserRole) != "all") { + m_filter->versions.emplace_back(version); + } m_filter_changed = true; emit filterChanged(); } diff --git a/launcher/ui/widgets/ModFilterWidget.h b/launcher/ui/widgets/ModFilterWidget.h index fdfd2c8bb..50f0e06c9 100644 --- a/launcher/ui/widgets/ModFilterWidget.h +++ b/launcher/ui/widgets/ModFilterWidget.h @@ -71,6 +71,15 @@ class ModFilterWidget : public QTabWidget { releases == other.releases && categoryIds == other.categoryIds; } bool operator!=(const Filter& other) const { return !(*this == other); } + + bool checkMcVersions(QStringList value) + { + for (auto mcVersion : versions) + if (value.contains(mcVersion.toString())) + return true; + + return versions.empty(); + } }; static unique_qobject_ptr create(MinecraftInstance* instance, bool extended, QWidget* parent = nullptr); diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.cpp b/launcher/ui/widgets/ThemeCustomizationWidget.cpp index 7a54bd390..097678b8d 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.cpp +++ b/launcher/ui/widgets/ThemeCustomizationWidget.cpp @@ -87,7 +87,7 @@ void ThemeCustomizationWidget::applyIconTheme(int index) { auto settings = APPLICATION->settings(); auto originalIconTheme = settings->get("IconTheme").toString(); - auto newIconTheme = ui->iconsComboBox->currentData().toString(); + auto newIconTheme = ui->iconsComboBox->itemData(index).toString(); if (originalIconTheme != newIconTheme) { settings->set("IconTheme", newIconTheme); APPLICATION->themeManager()->applyCurrentlySelectedTheme(); @@ -100,7 +100,7 @@ void ThemeCustomizationWidget::applyWidgetTheme(int index) { auto settings = APPLICATION->settings(); auto originalAppTheme = settings->get("ApplicationTheme").toString(); - auto newAppTheme = ui->widgetStyleComboBox->currentData().toString(); + auto newAppTheme = ui->widgetStyleComboBox->itemData(index).toString(); if (originalAppTheme != newAppTheme) { settings->set("ApplicationTheme", newAppTheme); APPLICATION->themeManager()->applyCurrentlySelectedTheme(); @@ -113,7 +113,7 @@ void ThemeCustomizationWidget::applyCatTheme(int index) { auto settings = APPLICATION->settings(); auto originalCat = settings->get("BackgroundCat").toString(); - auto newCat = ui->backgroundCatComboBox->currentData().toString(); + auto newCat = ui->backgroundCatComboBox->itemData(index).toString(); if (originalCat != newCat) { settings->set("BackgroundCat", newCat); } diff --git a/launcher/updater/prismupdater/PrismUpdater.cpp b/launcher/updater/prismupdater/PrismUpdater.cpp index 6b9754864..8bf8cb473 100644 --- a/launcher/updater/prismupdater/PrismUpdater.cpp +++ b/launcher/updater/prismupdater/PrismUpdater.cpp @@ -1210,7 +1210,7 @@ std::optional PrismUpdaterApp::unpackArchive(QFileInfo archive) QProcess proc = QProcess(); proc.start(cmd, args); if (!proc.waitForStarted(5000)) { // wait 5 seconds to start - auto msg = tr("Failed to launcher child process \"%1 %2\".").arg(cmd).arg(args.join(" ")); + auto msg = tr("Failed to launch child process \"%1 %2\".").arg(cmd).arg(args.join(" ")); logUpdate(msg); showFatalErrorMessage(tr("Failed extract archive"), msg); return std::nullopt; @@ -1241,7 +1241,7 @@ bool PrismUpdaterApp::loadPrismVersionFromExe(const QString& exe_path) proc.setReadChannel(QProcess::StandardOutput); proc.start(exe_path, { "--version" }); if (!proc.waitForStarted(5000)) { - showFatalErrorMessage(tr("Failed to Check Version"), tr("Failed to launcher child launcher process to read version.")); + showFatalErrorMessage(tr("Failed to Check Version"), tr("Failed to launch child process to read version.")); return false; } // wait 5 seconds to start if (!proc.waitForFinished(5000)) { diff --git a/launcher/updater/prismupdater/UpdaterDialogs.cpp b/launcher/updater/prismupdater/UpdaterDialogs.cpp index 06dc161b1..eab3e6bbb 100644 --- a/launcher/updater/prismupdater/UpdaterDialogs.cpp +++ b/launcher/updater/prismupdater/UpdaterDialogs.cpp @@ -24,6 +24,7 @@ #include "ui_SelectReleaseDialog.h" +#include #include #include "Markdown.h" #include "StringUtils.h" @@ -55,6 +56,9 @@ SelectReleaseDialog::SelectReleaseDialog(const Version& current_version, const Q connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &SelectReleaseDialog::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &SelectReleaseDialog::reject); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } SelectReleaseDialog::~SelectReleaseDialog() diff --git a/nix/README.md b/nix/README.md index 76cb8bf27..8bb658477 100644 --- a/nix/README.md +++ b/nix/README.md @@ -8,8 +8,8 @@ See [Package variants](#package-variants) for a list of available packages. ## Installing a development release (flake) -We use [garnix](https://garnix.io/) to build and cache our development builds. -If you want to avoid rebuilds you may add the garnix cache to your substitutors, or use `--accept-flake-config` +We use [cachix](https://cachix.org/) to cache our development and release builds. +If you want to avoid rebuilds you may add the Cachix bucket to your substitutors, or use `--accept-flake-config` to temporarily enable it when using `nix` commands. Example (NixOS): @@ -17,12 +17,10 @@ Example (NixOS): ```nix { nix.settings = { - trusted-substituters = [ - "https://cache.garnix.io" - ]; + trusted-substituters = [ "https://prismlauncher.cachix.org" ]; trusted-public-keys = [ - "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" + "prismlauncher.cachix.org-1:9/n/FGyABA2jLUVfY+DEp4hKds/rwO+SCOtbOkDzd+c=" ]; }; } @@ -137,20 +135,18 @@ nix profile install github:PrismLauncher/PrismLauncher ## Installing a development release (without flakes) -We use [garnix](https://garnix.io/) to build and cache our development builds. -If you want to avoid rebuilds you may add the garnix cache to your substitutors. +We use [Cachix](https://cachix.org/) to cache our development and release builds. +If you want to avoid rebuilds you may add the Cachix bucket to your substitutors. Example (NixOS): ```nix { nix.settings = { - trusted-substituters = [ - "https://cache.garnix.io" - ]; + trusted-substituters = [ "https://prismlauncher.cachix.org" ]; trusted-public-keys = [ - "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" + "prismlauncher.cachix.org-1:9/n/FGyABA2jLUVfY+DEp4hKds/rwO+SCOtbOkDzd+c=" ]; }; } diff --git a/nix/checks.nix b/nix/checks.nix index 40a2e272f..ec219d6f8 100644 --- a/nix/checks.nix +++ b/nix/checks.nix @@ -23,7 +23,7 @@ cd ${self} echo "Running clang-format...." - clang-format -i --style='file' --Werror */**.{c,cc,cpp,h,hh,hpp} + clang-format --dry-run --style='file' --Werror */**.{c,cc,cpp,h,hh,hpp} echo "Running deadnix..." deadnix --fail diff --git a/nix/unwrapped.nix b/nix/unwrapped.nix index f75acf1de..1b14886ef 100644 --- a/nix/unwrapped.nix +++ b/nix/unwrapped.nix @@ -9,25 +9,25 @@ ghc_filesystem, jdk17, kdePackages, + libnbtplusplus, ninja, nix-filter, self, stripJavaArchivesHook, tomlplusplus, zlib, + msaClientID ? null, - gamemodeSupport ? stdenv.isLinux, - version, - libnbtplusplus, + gamemodeSupport ? stdenv.hostPlatform.isLinux, }: assert lib.assertMsg ( - gamemodeSupport -> stdenv.isLinux + gamemodeSupport -> stdenv.hostPlatform.isLinux ) "gamemodeSupport is only available on Linux."; stdenv.mkDerivation { pname = "prismlauncher-unwrapped"; - inherit version; + version = self.shortRev or self.dirtyShortRev or "unknown"; src = nix-filter.lib { root = self; @@ -66,20 +66,23 @@ stdenv.mkDerivation { tomlplusplus zlib ] - ++ lib.optionals stdenv.isDarwin [ darwin.apple_sdk.frameworks.Cocoa ] + ++ lib.optionals stdenv.hostPlatform.isDarwin [ darwin.apple_sdk.frameworks.Cocoa ] ++ lib.optional gamemodeSupport gamemode; - hardeningEnable = lib.optionals stdenv.isLinux [ "pie" ]; + hardeningEnable = lib.optionals stdenv.hostPlatform.isLinux [ "pie" ]; cmakeFlags = - [ (lib.cmakeFeature "Launcher_BUILD_PLATFORM" "nixpkgs") ] + [ + # downstream branding + (lib.cmakeFeature "Launcher_BUILD_PLATFORM" "nixpkgs") + ] ++ lib.optionals (msaClientID != null) [ (lib.cmakeFeature "Launcher_MSA_CLIENT_ID" (toString msaClientID)) ] ++ lib.optionals (lib.versionOlder kdePackages.qtbase.version "6") [ (lib.cmakeFeature "Launcher_QT_VERSION_MAJOR" "5") ] - ++ lib.optionals stdenv.isDarwin [ + ++ lib.optionals stdenv.hostPlatform.isDarwin [ # we wrap our binary manually (lib.cmakeFeature "INSTALL_BUNDLE" "nodeps") # disable built-in updater @@ -87,6 +90,8 @@ stdenv.mkDerivation { (lib.cmakeFeature "CMAKE_INSTALL_PREFIX" "${placeholder "out"}/Applications/") ]; + doCheck = true; + dontWrapQtApps = true; meta = { diff --git a/nix/wrapper.nix b/nix/wrapper.nix index 5632d483b..ab9b7144b 100644 --- a/nix/wrapper.nix +++ b/nix/wrapper.nix @@ -1,73 +1,65 @@ { - lib, - stdenv, - symlinkJoin, - prismlauncher-unwrapped, - addOpenGLRunpath, + addDriverRunpath, + alsa-lib, flite, gamemode, - glfw, - glfw-wayland-minecraft, - glxinfo, - jdk8, + glfw3-minecraft, jdk17, jdk21, + jdk8, kdePackages, + lib, libGL, + libX11, + libXcursor, + libXext, + libXrandr, + libXxf86vm, + libjack2, libpulseaudio, libusb1, - makeWrapper, + mesa-demos, openal, pciutils, + pipewire, + prismlauncher-unwrapped, + stdenv, + symlinkJoin, udev, vulkan-loader, - xorg, + xrandr, + additionalLibs ? [ ], additionalPrograms ? [ ], - controllerSupport ? stdenv.isLinux, - gamemodeSupport ? stdenv.isLinux, + controllerSupport ? stdenv.hostPlatform.isLinux, + gamemodeSupport ? stdenv.hostPlatform.isLinux, jdks ? [ jdk21 jdk17 jdk8 ], msaClientID ? null, - textToSpeechSupport ? stdenv.isLinux, - # Adds `glfw-wayland-minecraft` to `LD_LIBRARY_PATH` - # when launched on wayland, allowing for the game to be run natively. - # Make sure to enable "Use system installation of GLFW" in instance settings - # for this to take effect - # - # Warning: This build of glfw may be unstable, and the launcher - # itself can take slightly longer to start - withWaylandGLFW ? false, + textToSpeechSupport ? stdenv.hostPlatform.isLinux, }: assert lib.assertMsg ( - controllerSupport -> stdenv.isLinux + controllerSupport -> stdenv.hostPlatform.isLinux ) "controllerSupport only has an effect on Linux."; assert lib.assertMsg ( - textToSpeechSupport -> stdenv.isLinux + textToSpeechSupport -> stdenv.hostPlatform.isLinux ) "textToSpeechSupport only has an effect on Linux."; -assert lib.assertMsg ( - withWaylandGLFW -> stdenv.isLinux -) "withWaylandGLFW is only available on Linux."; - let prismlauncher' = prismlauncher-unwrapped.override { inherit msaClientID gamemodeSupport; }; in + symlinkJoin { name = "prismlauncher-${prismlauncher'.version}"; paths = [ prismlauncher' ]; - nativeBuildInputs = - [ kdePackages.wrapQtAppsHook ] - # purposefully using a shell wrapper here for variable expansion - # see https://github.com/NixOS/nixpkgs/issues/172583 - ++ lib.optional withWaylandGLFW makeWrapper; + nativeBuildInputs = [ kdePackages.wrapQtAppsHook ]; buildInputs = [ @@ -75,45 +67,39 @@ symlinkJoin { kdePackages.qtsvg ] ++ lib.optional ( - lib.versionAtLeast kdePackages.qtbase.version "6" && stdenv.isLinux + lib.versionAtLeast kdePackages.qtbase.version "6" && stdenv.hostPlatform.isLinux ) kdePackages.qtwayland; - env = { - waylandPreExec = lib.optionalString withWaylandGLFW '' - if [ -n "$WAYLAND_DISPLAY" ]; then - export LD_LIBRARY_PATH=${lib.getLib glfw-wayland-minecraft}/lib:"$LD_LIBRARY_PATH" - fi - ''; - }; - - postBuild = - lib.optionalString withWaylandGLFW '' - qtWrapperArgs+=(--run "$waylandPreExec") - '' - + '' - wrapQtAppsHook - ''; + postBuild = '' + wrapQtAppsHook + ''; qtWrapperArgs = let runtimeLibs = [ - # lwjgl - glfw - libpulseaudio - libGL - openal stdenv.cc.cc.lib + ## native versions + glfw3-minecraft + openal - vulkan-loader # VulkanMod's lwjgl + ## openal + alsa-lib + libjack2 + libpulseaudio + pipewire + + ## glfw + libGL + libX11 + libXcursor + libXext + libXrandr + libXxf86vm udev # oshi - xorg.libX11 - xorg.libXext - xorg.libXcursor - xorg.libXrandr - xorg.libXxf86vm + vulkan-loader # VulkanMod's lwjgl ] ++ lib.optional textToSpeechSupport flite ++ lib.optional gamemodeSupport gamemode.lib @@ -121,14 +107,15 @@ symlinkJoin { ++ additionalLibs; runtimePrograms = [ - glxinfo + mesa-demos pciutils # need lspci - xorg.xrandr # needed for LWJGL [2.9.2, 3) https://github.com/LWJGL/lwjgl/issues/128 + xrandr # needed for LWJGL [2.9.2, 3) https://github.com/LWJGL/lwjgl/issues/128 ] ++ additionalPrograms; + in [ "--prefix PRISMLAUNCHER_JAVA_PATHS : ${lib.makeSearchPath "bin/java" jdks}" ] - ++ lib.optionals stdenv.isLinux [ - "--set LD_LIBRARY_PATH ${addOpenGLRunpath.driverLink}/lib:${lib.makeLibraryPath runtimeLibs}" + ++ lib.optionals stdenv.hostPlatform.isLinux [ + "--set LD_LIBRARY_PATH ${addDriverRunpath.driverLink}/lib:${lib.makeLibraryPath runtimeLibs}" "--prefix PATH : ${lib.makeBinPath runtimePrograms}" ]; diff --git a/program_info/CMakeLists.txt b/program_info/CMakeLists.txt index ac24e65f0..14a558346 100644 --- a/program_info/CMakeLists.txt +++ b/program_info/CMakeLists.txt @@ -41,6 +41,31 @@ configure_file(org.freesmTeam.freesmlauncher.metainfo.xml.in org.freesmTeam.free configure_file(freesmlauncher.rc.in freesmlauncher.rc @ONLY) configure_file(freesmlauncher.manifest.in freesmlauncher.manifest @ONLY) configure_file(freesmlauncher.ico freesmlauncher.ico COPYONLY) + +if(MSVC) + set(Launcher_MSVC_Redist_NSIS_Section [=[ +!ifdef haveNScurl +Section "Visual Studio Runtime" + Var /GLOBAL vc_redist_exe + ${If} ${IsNativeARM64} + StrCpy $vc_redist_exe "vc_redist.arm64.exe" + ${Else} + StrCpy $vc_redist_exe "vc_redist.x64.exe" + ${EndIf} + DetailPrint 'Downloading Microsoft Visual C++ Redistributable...' + NScurl::http GET "https://aka.ms/vs/17/release/$vc_redist_exe" "$INSTDIR\vc_redist\$vc_redist_exe" /INSIST /CANCEL /Zone.Identifier /END + Pop $0 + ${If} $0 == "OK" + DetailPrint "Download successful" + ExecWait "$INSTDIR\vc_redist\$vc_redist_exe /install /passive /norestart" + ${Else} + DetailPrint "Download failed with error $0" + ${EndIf} +SectionEnd +!endif +]=]) +endif() + configure_file(win_install.nsi.in win_install.nsi @ONLY) if(SCDOC_FOUND) diff --git a/program_info/freesmlauncher.ico b/program_info/freesmlauncher.ico index 2f0fa67ff..573e943fc 100644 Binary files a/program_info/freesmlauncher.ico and b/program_info/freesmlauncher.ico differ diff --git a/program_info/org.freesmTeam.freesmlauncher.svg b/program_info/org.freesmTeam.freesmlauncher.svg index 949cabdf3..245cc9703 100644 --- a/program_info/org.freesmTeam.freesmlauncher.svg +++ b/program_info/org.freesmTeam.freesmlauncher.svg @@ -1,57 +1,23 @@ - - - - Freesm Launcher Logo - - - - - - - - - - - - - - - + + + Prism Launcher Logo + + + + + + + + + + + - - - - - - - Freesm Launcher Logo - 19/10/2022 - - - Freesm Launcher - - - - - NotWindstone, s0me1newithhand7s - - - https://github.com/FreesmTeam/FreesmLauncher - - - Freesm Launcher - - - - - - - - - - - - - - - + + + + + + + + \ No newline at end of file diff --git a/program_info/win_install.nsi.in b/program_info/win_install.nsi.in index e03649763..c00ff86f1 100644 --- a/program_info/win_install.nsi.in +++ b/program_info/win_install.nsi.in @@ -2,6 +2,8 @@ !include "LogicLib.nsh" !include "MUI2.nsh" +!include "x64.nsh" + Unicode true Name "@Launcher_DisplayName@" @@ -112,6 +114,16 @@ VIAddVersionKey /LANG=${LANG_ENGLISH} "LegalCopyright" "@Launcher_Copyright@" VIAddVersionKey /LANG=${LANG_ENGLISH} "FileVersion" "@Launcher_VERSION_NAME4@" VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductVersion" "@Launcher_VERSION_NAME4@" +;-------------------------------- +; Conditional comp with file exist + +!macro CompileTimeIfFileExist path define +!tempfile tmpinc +!system 'IF EXIST "${path}" echo !define ${define} > "${tmpinc}"' +!include "${tmpinc}" +!delfile "${tmpinc}" +!undef tmpinc +!macroend ;-------------------------------- ; Shell Associate Macros @@ -175,7 +187,7 @@ VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductVersion" "@Launcher_VERSION_NAME4@ !macroend -!macro APP_UNASSOCIATE EXT APP_ID +!macro APP_UNASSOCIATE EXT APP_ID APP_EXE # Unregister file type ClearErrors @@ -336,6 +348,19 @@ Section "" UninstallPrevious SectionEnd +;------------------------------------ +; include nice plugins + +; NScurl - curl in NSIS +; used for MSVS redist download +; extract to ../NSISPlugins/NScurl +; https://github.com/negrutiu/nsis-nscurl/releases/latest/download/NScurl.zip +!insertmacro CompileTimeIfFileExist "../NSISPlugins/NScurl/Plugins/" haveNScurl +!ifdef haveNScurl +!AddPluginDir /x86-unicode "../NSISPlugins/NScurl/Plugins/x86-unicode" +!AddPluginDir /x86-ansi "../NSISPlugins/NScurl/Plugins/x86-ansi" +!AddPluginDir /amd64-unicode "../NSISPlugins/NScurl/Plugins/amd64-unicode" +!endif ;------------------------------------ @@ -396,6 +421,8 @@ Section "@Launcher_DisplayName@" SectionEnd +@Launcher_MSVC_Redist_NSIS_Section@ + Section "Start Menu Shortcut" SM_SHORTCUTS CreateShortcut "$SMPROGRAMS\@Launcher_DisplayName@.lnk" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" 0 @@ -464,8 +491,8 @@ Section -un.ShellAssoc !insertmacro APP_TEARDOWN_DEFAULT `${APPID}` `${APPNAME}` `${APPEXE}` - !insertmacro APP_UNASSOCIATE ".zip" `${APPID}` - !insertmacro APP_UNASSOCIATE ".mrpack" `${APPID}` + !insertmacro APP_UNASSOCIATE ".zip" `${APPID}` `${APPEXE}` + !insertmacro APP_UNASSOCIATE ".mrpack" `${APPID}` `${APPEXE}` !insertmacro NotifyShell_AssocChanged SectionEnd diff --git a/tests/ResourceModel_test.cpp b/tests/ResourceModel_test.cpp index 6d8c25535..4df8c32f5 100644 --- a/tests/ResourceModel_test.cpp +++ b/tests/ResourceModel_test.cpp @@ -59,10 +59,7 @@ class DummyResourceModel : public ResourceModel { class ResourceModelTest : public QObject { Q_OBJECT private slots: - void test_abstract_item_model() - { - - } + void test_abstract_item_model() { int placeholder = 1; } void test_search() { diff --git a/tests/Task_test.cpp b/tests/Task_test.cpp index 0740ba0a3..463e78b42 100644 --- a/tests/Task_test.cpp +++ b/tests/Task_test.cpp @@ -16,7 +16,7 @@ class BasicTask : public Task { friend class TaskTest; public: - BasicTask(bool show_debug_log = true) : Task(nullptr, show_debug_log) {} + BasicTask(bool show_debug_log = true) : Task(show_debug_log) {} private: void executeTask() override { emitSucceeded(); }