diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 60bd86eec..4146cddf4 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -25,7 +25,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} - name: Create backport PRs - uses: korthout/backport-action@v2.5.0 + uses: korthout/backport-action@v3.1.0 with: # Config README: https://github.com/korthout/backport-action#backport-action pull_description: |- diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f3896550f..ccba62541 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,14 +54,17 @@ jobs: include: - os: ubuntu-20.04 qt_ver: 5 + qt_host: linux + qt_arch: "" + qt_version: "5.12.8" + qt_modules: "qtnetworkauth" - os: ubuntu-20.04 qt_ver: 6 qt_host: linux qt_arch: "" qt_version: "6.2.4" - qt_modules: "qt5compat qtimageformats" - qt_tools: "" + qt_modules: "qt5compat qtimageformats qtnetworkauth" - os: windows-2022 name: "Windows-MinGW-w64" @@ -75,10 +78,9 @@ jobs: vcvars_arch: "amd64" qt_ver: 6 qt_host: windows - qt_arch: '' - qt_version: '6.7.0' - qt_modules: 'qt5compat qtimageformats' - qt_tools: '' + qt_arch: "" + qt_version: "6.7.2" + qt_modules: "qt5compat qtimageformats qtnetworkauth" - os: windows-2022 name: "Windows-MSVC-arm64" @@ -87,29 +89,26 @@ jobs: vcvars_arch: "amd64_arm64" qt_ver: 6 qt_host: windows - qt_arch: 'win64_msvc2019_arm64' - qt_version: '6.7.0' - qt_modules: 'qt5compat qtimageformats' - qt_tools: '' + qt_arch: "win64_msvc2019_arm64" + qt_version: "6.7.2" + qt_modules: "qt5compat qtimageformats qtnetworkauth" - - os: macos-12 + - os: macos-14 name: macOS macosx_deployment_target: 11.0 qt_ver: 6 qt_host: mac - qt_arch: '' - qt_version: '6.7.0' - qt_modules: 'qt5compat qtimageformats' - qt_tools: '' + qt_arch: "" + qt_version: "6.7.2" + qt_modules: "qt5compat qtimageformats qtnetworkauth" - - os: macos-12 + - os: macos-14 name: macOS-Legacy macosx_deployment_target: 10.13 qt_ver: 5 qt_host: mac qt_version: "5.15.2" - qt_modules: "" - qt_tools: "" + qt_modules: "qtnetworkauth" runs-on: ${{ matrix.os }} @@ -151,6 +150,7 @@ jobs: quazip-qt6:p ccache:p qt6-5compat:p + qt6-networkauth:p cmark:p - name: Force newer ccache @@ -160,7 +160,7 @@ jobs: - name: Setup ccache if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug' - uses: hendrikmuhs/ccache-action@v1.2.12 + uses: hendrikmuhs/ccache-action@v1.2.14 with: key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }} @@ -207,11 +207,6 @@ jobs: brew update brew install ninja extra-cmake-modules - - name: Install Qt (Linux) - if: runner.os == 'Linux' && matrix.qt_ver != 6 - run: | - sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 - - name: Install host Qt (Windows MSVC arm64) if: runner.os == 'Windows' && matrix.architecture == 'arm64' uses: jurplel/install-qt-action@v3 @@ -223,20 +218,18 @@ jobs: target: "desktop" arch: "" modules: ${{ matrix.qt_modules }} - tools: ${{ matrix.qt_tools }} cache: ${{ inputs.is_qt_cached }} cache-key-prefix: host-qt-arm64-windows dir: ${{ github.workspace }}\HostQt set-env: false - - name: Install Qt (macOS, Linux, Qt 6 & Windows MSVC) - if: runner.os == 'Linux' && matrix.qt_ver == 6 || runner.os == 'macOS' || (runner.os == 'Windows' && matrix.msystem == '') + - name: Install Qt (macOS, Linux & Windows MSVC) + if: matrix.msystem == '' uses: jurplel/install-qt-action@v3 with: aqtversion: "==3.1.*" py7zrversion: ">=0.20.2" version: ${{ matrix.qt_version }} - host: ${{ matrix.qt_host }} target: "desktop" arch: ${{ matrix.qt_arch }} modules: ${{ matrix.qt_modules }} @@ -266,6 +259,12 @@ jobs: run: | echo "QT_HOST_PATH=${{ github.workspace }}\HostQt\Qt\${{ matrix.qt_version }}\msvc2019_64" >> $env:GITHUB_ENV + - name: Setup java (macOS) + if: runner.os == 'macOS' + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "17" ## # CONFIGURE ## @@ -273,23 +272,23 @@ jobs: - name: Configure CMake (macOS) if: runner.os == 'macOS' && matrix.qt_ver == 6 run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=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 -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DMACOSX_SPARKLE_UPDATE_PUBLIC_KEY="" -DMACOSX_SPARKLE_UPDATE_FEED_URL="" -G Ninja + 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 -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 -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 }}") { @@ -304,7 +303,7 @@ jobs: - name: Configure CMake (Linux) if: runner.os == 'Linux' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=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 @@ -432,12 +431,6 @@ jobs: run: | cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build_type }} - cd ${{ env.INSTALL_DIR }} - if ("${{ matrix.qt_ver }}" -eq "5") - { - Copy-Item ${{ runner.workspace }}/Qt/Tools/OpenSSL/Win_x86/bin/libcrypto-1_1.dll -Destination libcrypto-1_1.dll - Copy-Item ${{ runner.workspace }}/Qt/Tools/OpenSSL/Win_x86/bin/libssl-1_1.dll -Destination libssl-1_1.dll - } cd ${{ github.workspace }} Get-ChildItem ${{ env.INSTALL_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt @@ -490,28 +483,6 @@ jobs: ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY } - - name: Package (Linux) - if: runner.os == 'Linux' - run: | - cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_DIR }} - for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_DIR }}/manifest.txt - cd ${{ env.INSTALL_DIR }} - tar --owner root --group root -czf ../PrismLauncher.tar.gz * - - - name: Package (Linux, portable) - if: runner.os == 'Linux' - run: | - cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} - cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable - - # workaround to make portable installs to work on fedora - mkdir ${{ env.INSTALL_PORTABLE_DIR }}/lib - cp /lib/x86_64-linux-gnu/libbz2.so.1.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib - - for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt - cd ${{ env.INSTALL_PORTABLE_DIR }} - tar -czf ../PrismLauncher-portable.tar.gz * - - name: Package AppImage (Linux) if: runner.os == 'Linux' && matrix.qt_ver != 5 shell: bash @@ -558,6 +529,25 @@ jobs: mv "PrismLauncher-Linux-x86_64.AppImage" "PrismLauncher-Linux-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage" + - name: Package (Linux, portable) + if: runner.os == 'Linux' + run: | + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_PORTABLE_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DLauncher_BUILD_ARTIFACT=Linux-Qt${{ matrix.qt_ver }} -DINSTALL_BUNDLE=full -G Ninja + cmake --install ${{ env.BUILD_DIR }} + cmake --install ${{ env.BUILD_DIR }} --component portable + + mkdir ${{ env.INSTALL_PORTABLE_DIR }}/lib + cp /lib/x86_64-linux-gnu/libbz2.so.1.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib + cp /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib + cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 ${{ env.INSTALL_PORTABLE_DIR }}/lib + cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_PORTABLE_DIR }}/lib + cp /usr/lib/x86_64-linux-gnu/libffi.so.7 ${{ env.INSTALL_PORTABLE_DIR }}/lib + mv ${{ env.INSTALL_PORTABLE_DIR }}/bin/*.so* ${{ env.INSTALL_PORTABLE_DIR }}/lib + + for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt + cd ${{ env.INSTALL_PORTABLE_DIR }} + tar -czf ../PrismLauncher-portable.tar.gz * + ## # UPLOAD BUILDS ## @@ -590,13 +580,6 @@ jobs: name: PrismLauncher-${{ matrix.name }}-Setup-${{ env.VERSION }}-${{ inputs.build_type }} path: PrismLauncher-Setup.exe - - name: Upload binary tarball (Linux, Qt 5) - if: runner.os == 'Linux' && matrix.qt_ver != 6 - uses: actions/upload-artifact@v4 - with: - name: PrismLauncher-${{ runner.os }}-Qt5-${{ env.VERSION }}-${{ inputs.build_type }} - path: PrismLauncher.tar.gz - - name: Upload binary tarball (Linux, portable, Qt 5) if: runner.os == 'Linux' && matrix.qt_ver != 6 uses: actions/upload-artifact@v4 @@ -604,13 +587,6 @@ jobs: name: PrismLauncher-${{ runner.os }}-Qt5-Portable-${{ env.VERSION }}-${{ inputs.build_type }} path: PrismLauncher-portable.tar.gz - - name: Upload binary tarball (Linux, Qt 6) - if: runner.os == 'Linux' && matrix.qt_ver !=5 - uses: actions/upload-artifact@v4 - with: - name: PrismLauncher-${{ runner.os }}-Qt6-${{ env.VERSION }}-${{ inputs.build_type }} - path: PrismLauncher.tar.gz - - name: Upload binary tarball (Linux, portable, Qt 6) if: runner.os == 'Linux' && matrix.qt_ver != 5 uses: actions/upload-artifact@v4 @@ -641,7 +617,7 @@ jobs: flatpak: runs-on: ubuntu-latest container: - image: bilelmoussaoui/flatpak-github-actions:kde-5.15-23.08 + image: bilelmoussaoui/flatpak-github-actions:kde-6.7 options: --privileged steps: - name: Checkout diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d40d7eb68..5255f865b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -23,7 +23,7 @@ jobs: run: sudo apt-get -y update - sudo apt-get -y install ninja-build extra-cmake-modules scdoc qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 + sudo apt-get -y install ninja-build extra-cmake-modules scdoc qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 libqt5networkauth5 libqt5networkauth5-dev - name: Configure and Build run: | diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index fa22c96d5..134281b2c 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -46,9 +46,7 @@ jobs: run: | mv ${{ github.workspace }}/PrismLauncher-source PrismLauncher-${{ env.VERSION }} mv PrismLauncher-Linux-Qt6-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz - mv PrismLauncher-Linux-Qt6*/PrismLauncher.tar.gz PrismLauncher-Linux-Qt6-${{ env.VERSION }}.tar.gz mv PrismLauncher-Linux-Qt5-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt5-Portable-${{ env.VERSION }}.tar.gz - mv PrismLauncher-Linux-Qt5*/PrismLauncher.tar.gz PrismLauncher-Linux-Qt5-${{ env.VERSION }}.tar.gz mv PrismLauncher-*.AppImage/PrismLauncher-*.AppImage PrismLauncher-Linux-x86_64.AppImage mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync mv PrismLauncher-macOS-Legacy*/PrismLauncher.zip PrismLauncher-macOS-Legacy-${{ env.VERSION }}.zip @@ -92,11 +90,9 @@ jobs: draft: true prerelease: false files: | - PrismLauncher-Linux-Qt5-${{ env.VERSION }}.tar.gz PrismLauncher-Linux-Qt5-Portable-${{ env.VERSION }}.tar.gz PrismLauncher-Linux-x86_64.AppImage PrismLauncher-Linux-x86_64.AppImage.zsync - PrismLauncher-Linux-Qt6-${{ env.VERSION }}.tar.gz PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz PrismLauncher-Windows-MinGW-w64-${{ env.VERSION }}.zip PrismLauncher-Windows-MinGW-w64-Portable-${{ env.VERSION }}.zip diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index 855b105ea..ecc38ff28 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@8887e596b4ee1134dae06b98d573bd674693f47c # v26 + - uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27 - - uses: DeterminateSystems/update-flake-lock@v21 + - uses: DeterminateSystems/update-flake-lock@v24 with: commit-msg: "chore(nix): update lockfile" pr-title: "chore(nix): update lockfile" diff --git a/BUILD.md b/BUILD.md deleted file mode 100644 index a139039df..000000000 --- a/BUILD.md +++ /dev/null @@ -1,3 +0,0 @@ -# Build Instructions - -Full build instructions are available on [the website](https://prismlauncher.org/wiki/development/build-instructions/). diff --git a/CMakeLists.txt b/CMakeLists.txt index 2e46bb605..3d70fe79b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -176,6 +176,8 @@ 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) @@ -205,6 +207,7 @@ set(Launcher_BUG_TRACKER_URL "https://github.com/PrismLauncher/PrismLauncher/iss # Translations Platform URL 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://prismlauncher.org/matrix" CACHE STRING "URL to the Matrix Space") @@ -219,6 +222,19 @@ set(Launcher_SUBREDDIT_URL "https://prismlauncher.org/reddit" CACHE STRING "URL 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(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT 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(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT OFF) +endif() + +# Java downloader +option(Launcher_ENABLE_JAVA_DOWNLOADER "Build the java downloader feature" ${Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT}) + # Native libraries if(UNIX AND APPLE) set(Launcher_GLFW_LIBRARY_NAME "libglfw.dylib" CACHE STRING "Name of native glfw library") @@ -282,7 +298,7 @@ endif() include(QtVersionlessBackport) if(Launcher_QT_VERSION_MAJOR EQUAL 5) set(QT_VERSION_MAJOR 5) - find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml) + find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml NetworkAuth) if(NOT Launcher_FORCE_BUNDLED_LIBS) find_package(QuaZip-Qt5 1.3 QUIET) @@ -296,7 +312,7 @@ if(Launcher_QT_VERSION_MAJOR EQUAL 5) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DUNICODE -D_UNICODE") elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) set(QT_VERSION_MAJOR 6) - find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat) + find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat NetworkAuth) list(APPEND Launcher_QT_LIBS Qt6::Core5Compat) if(NOT Launcher_FORCE_BUNDLED_LIBS) @@ -417,7 +433,19 @@ elseif(UNIX) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_mrpack_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR}) install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "share/${Launcher_Name}") + + if (INSTALL_BUNDLE STREQUAL full) + 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() + if(Launcher_ManPage) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_ManPage} DESTINATION "${KDE_INSTALL_MANDIR}/man6") endif() @@ -511,7 +539,6 @@ if(NOT cmark_FOUND) else() message(STATUS "Using system cmark") endif() -add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too much add_subdirectory(libraries/gamemode) add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API if (NOT ghc_filesystem_FOUND) diff --git a/COPYING.md b/COPYING.md index f14e2958e..111587060 100644 --- a/COPYING.md +++ b/COPYING.md @@ -333,32 +333,6 @@ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -## O2 (Katabasis fork) - - Copyright (c) 2012, Akos Polster - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ## Gamemode Copyright (c) 2017-2022, Feral Interactive diff --git a/README.md b/README.md index b32132d49..9c4909509 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,12 @@ The translation effort for Prism Launcher is hosted on [Weblate](https://hosted. ## Building -If you want to build Prism Launcher yourself, check the [Build Instructions](https://prismlauncher.org/wiki/development/build-instructions/). +If you want to build Prism Launcher yourself, check the build instructions: + +- [Windows](https://prismlauncher.org/wiki/development/build-instructions/windows/) +- [Linux](https://prismlauncher.org/wiki/development/build-instructions/linux/) +- [MacOS](https://prismlauncher.org/wiki/development/build-instructions/macos/) +- [OpenBSD](https://prismlauncher.org/wiki/development/build-instructions/openbsd/) ## Sponsors & Partners diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index b40cacb0f..b48232b43 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -81,6 +81,9 @@ Config::Config() UPDATER_ENABLED = true; } + #cmakedefine01 Launcher_ENABLE_JAVA_DOWNLOADER + JAVA_DOWNLOADER_ENABLED = Launcher_ENABLE_JAVA_DOWNLOADER; + GIT_COMMIT = "@Launcher_GIT_COMMIT@"; GIT_TAG = "@Launcher_GIT_TAG@"; GIT_REFSPEC = "@Launcher_GIT_REFSPEC@"; @@ -113,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 77b6eef54..ae705d098 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -67,6 +67,7 @@ class Config { QString VERSION_CHANNEL; bool UPDATER_ENABLED = false; + bool JAVA_DOWNLOADER_ENABLED = false; /// A short string identifying this build's platform or distribution. QString BUILD_PLATFORM; @@ -132,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 */ @@ -163,10 +169,9 @@ class Config { QString RESOURCE_BASE = "https://resources.download.minecraft.net/"; QString LIBRARY_BASE = "https://libraries.minecraft.net/"; - QString AUTH_BASE = "https://authserver.mojang.com/"; 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 d36ac3e8f..6d3845dfc 100644 --- a/cmake/MacOSXBundleInfo.plist.in +++ b/cmake/MacOSXBundleInfo.plist.in @@ -6,6 +6,8 @@ A Minecraft mod wants to access your camera. NSMicrophoneUsageDescription A Minecraft mod wants to access your microphone. + NSDownloadsFolderUsageDescription + Prism 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 Prism scans for downloaded mods in Settings or the prompt that appears. NSPrincipalClass NSApplication NSHighResolutionCapable @@ -77,6 +79,14 @@ curseforge + + CFBundleURLName + Prismlauncher + CFBundleURLSchemes + + prismlauncher + + diff --git a/default.nix b/default.nix index c7d0c267d..6466507b7 100644 --- a/default.nix +++ b/default.nix @@ -1,14 +1,9 @@ -( - import - ( - let - lock = builtins.fromJSON (builtins.readFile ./flake.lock); - in - fetchTarball { - url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; - sha256 = lock.nodes.flake-compat.locked.narHash; - } - ) - {src = ./.;} -) -.defaultNix +(import ( + let + lock = builtins.fromJSON (builtins.readFile ./flake.lock); + in + fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } +) { src = ./.; }).defaultNix diff --git a/flake.lock b/flake.lock index 0fbfca9cc..58bc4e30f 100644 --- a/flake.lock +++ b/flake.lock @@ -16,65 +16,6 @@ "type": "github" } }, - "flake-parts": { - "inputs": { - "nixpkgs-lib": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1712014858, - "narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "9126214d0a59633752a136528f5f3b9aa8565b7d", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "flake-parts", - "type": "github" - } - }, - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "gitignore": { - "inputs": { - "nixpkgs": [ - "pre-commit-hooks", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1709087332, - "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", - "type": "github" - } - }, "libnbtplusplus": { "flake": false, "locked": { @@ -91,72 +32,43 @@ "type": "github" } }, - "nixpkgs": { + "nix-filter": { "locked": { - "lastModified": 1713596654, - "narHash": "sha256-LJbHQQ5aX1LVth2ST+Kkse/DRzgxlVhTL1rxthvyhZc=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "fd16bb6d3bcca96039b11aa52038fafeb6e4f4be", + "lastModified": 1710156097, + "narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=", + "owner": "numtide", + "repo": "nix-filter", + "rev": "3342559a24e85fc164b295c3444e8a139924675b", "type": "github" }, "original": { - "owner": "nixos", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", + "owner": "numtide", + "repo": "nix-filter", "type": "github" } }, - "pre-commit-hooks": { - "inputs": { - "flake-compat": [ - "flake-compat" - ], - "flake-utils": "flake-utils", - "gitignore": "gitignore", - "nixpkgs": [ - "nixpkgs" - ], - "nixpkgs-stable": [ - "nixpkgs" - ] - }, + "nixpkgs": { "locked": { - "lastModified": 1712897695, - "narHash": "sha256-nMirxrGteNAl9sWiOhoN5tIHyjBbVi5e2tgZUgZlK3Y=", - "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "rev": "40e6053ecb65fcbf12863338a6dcefb3f55f1bf8", + "lastModified": 1726062873, + "narHash": "sha256-IiA3jfbR7K/B5+9byVi9BZGWTD4VSbWe8VLpp9B/iYk=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "4f807e8940284ad7925ebd0a0993d2a1791acb2f", "type": "github" }, "original": { - "owner": "cachix", - "repo": "pre-commit-hooks.nix", + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { "flake-compat": "flake-compat", - "flake-parts": "flake-parts", "libnbtplusplus": "libnbtplusplus", - "nixpkgs": "nixpkgs", - "pre-commit-hooks": "pre-commit-hooks" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" + "nix-filter": "nix-filter", + "nixpkgs": "nixpkgs" } } }, diff --git a/flake.nix b/flake.nix index e16c76699..987fc0eda 100644 --- a/flake.nix +++ b/flake.nix @@ -2,52 +2,121 @@ 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://cache.garnix.io" ]; + extra-trusted-public-keys = [ "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" ]; }; inputs = { - nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; - flake-parts = { - url = "github:hercules-ci/flake-parts"; - inputs.nixpkgs-lib.follows = "nixpkgs"; - }; - pre-commit-hooks = { - url = "github:cachix/pre-commit-hooks.nix"; - inputs = { - nixpkgs.follows = "nixpkgs"; - nixpkgs-stable.follows = "nixpkgs"; - flake-compat.follows = "flake-compat"; - }; - }; - flake-compat = { - url = "github:edolstra/flake-compat"; - flake = false; - }; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + libnbtplusplus = { url = "github:PrismLauncher/libnbtplusplus"; flake = false; }; + + nix-filter.url = "github:numtide/nix-filter"; + + /* + Inputs below this are optional and can be removed + + ``` + { + inputs.prismlauncher = { + url = "github:PrismLauncher/PrismLauncher"; + inputs = { + flake-compat.follows = ""; + }; + }; + } + ``` + */ + + flake-compat = { + url = "github:edolstra/flake-compat"; + flake = false; + }; }; - outputs = { - flake-parts, - pre-commit-hooks, - ... - } @ inputs: - flake-parts.lib.mkFlake {inherit inputs;} { - imports = [ - pre-commit-hooks.flakeModule + outputs = + { + self, + nixpkgs, + libnbtplusplus, + nix-filter, + ... + }: + let + inherit (nixpkgs) lib; - ./nix/dev.nix - ./nix/distribution.nix - ]; + # While we only officially support aarch and x86_64 on Linux and MacOS, + # we expose a reasonable amount of other systems for users who want to + # build for most exotic platforms + systems = lib.systems.flakeExposed; - systems = [ - "x86_64-linux" - "aarch64-linux" - "x86_64-darwin" - "aarch64-darwin" - ]; + forAllSystems = lib.genAttrs systems; + nixpkgsFor = forAllSystems (system: nixpkgs.legacyPackages.${system}); + in + { + checks = forAllSystems ( + system: + let + checks' = nixpkgsFor.${system}.callPackage ./nix/checks.nix { inherit self; }; + in + lib.filterAttrs (_: lib.isDerivation) checks' + ); + + devShells = forAllSystems ( + system: + let + pkgs = nixpkgsFor.${system}; + in + { + default = pkgs.mkShell { + inputsFrom = [ self.packages.${system}.prismlauncher-unwrapped ]; + buildInputs = with pkgs; [ + ccache + ninja + ]; + }; + } + ); + + 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 { }; + }; + + packages = forAllSystems ( + system: + let + pkgs = nixpkgsFor.${system}; + + # Build a scope from our overlay + prismPackages = lib.makeScope pkgs.newScope (final: self.overlays.default final pkgs); + + # Grab our packages from it and set the default + packages = { + inherit (prismPackages) prismlauncher-unwrapped prismlauncher; + default = prismPackages.prismlauncher; + }; + in + # Only output them if they're available on the current system + lib.filterAttrs (_: lib.meta.availableOn pkgs.stdenv.hostPlatform) packages + ); }; } diff --git a/flatpak/org.prismlauncher.PrismLauncher.yml b/flatpak/org.prismlauncher.PrismLauncher.yml index b4c6e8143..bd09f7fd8 100644 --- a/flatpak/org.prismlauncher.PrismLauncher.yml +++ b/flatpak/org.prismlauncher.PrismLauncher.yml @@ -1,6 +1,6 @@ id: org.prismlauncher.PrismLauncher runtime: org.kde.Platform -runtime-version: 5.15-23.08 +runtime-version: 6.7 sdk: org.kde.Sdk sdk-extensions: - org.freedesktop.Sdk.Extension.openjdk21 @@ -38,7 +38,6 @@ modules: config-opts: - -DLauncher_BUILD_PLATFORM=flatpak - -DCMAKE_BUILD_TYPE=RelWithDebInfo - - -DLauncher_QT_VERSION_MAJOR=5 build-options: env: JAVA_HOME: /usr/lib/sdk/openjdk17/jvm/openjdk-17 @@ -65,7 +64,8 @@ modules: config-opts: - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DBUILD_SHARED_LIBS:BOOL=ON - - -DGLFW_USE_WAYLAND=ON + - -DGLFW_USE_WAYLAND:BOOL=ON + - -DGLFW_BUILD_DOCS:BOOL=OFF sources: - type: git url: https://github.com/glfw/glfw.git diff --git a/garnix.yaml b/garnix.yaml index 6cf8f7214..a7c1b48a9 100644 --- a/garnix.yaml +++ b/garnix.yaml @@ -1,7 +1,10 @@ builds: exclude: + # Currently broken on Garnix's end - "*.x86_64-darwin.*" include: - "checks.x86_64-linux.*" - - "devShells.*.*" - - "packages.*.*" + - "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 bb8751ccc..3bed11db2 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -44,10 +44,11 @@ #include "BuildConfig.h" #include "DataMigrationTask.h" +#include "java/JavaInstallList.h" #include "net/PasteUpload.h" #include "pathmatcher/MultiMatcher.h" #include "pathmatcher/SimplePrefixMatcher.h" -#include "settings/INIFile.h" +#include "tools/GenericProfiler.h" #include "ui/InstanceWindow.h" #include "ui/MainWindow.h" @@ -66,8 +67,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" @@ -105,7 +108,7 @@ #include "icons/IconList.h" #include "net/HttpMetaCache.h" -#include "java/JavaUtils.h" +#include "java/JavaInstallList.h" #include "updater/ExternalUpdater.h" @@ -125,6 +128,7 @@ #include #include +#include "SysInfo.h" #ifdef Q_OS_LINUX #include @@ -150,6 +154,7 @@ #endif #if defined Q_OS_WIN32 +#include #include "WindowsConsole.h" #endif @@ -235,6 +240,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) { { { "d", "dir" }, "Use a custom path as application root (use '.' for current directory)", "directory" }, { { "l", "launch" }, "Launch the specified instance (by instance ID)", "instance" }, { { "s", "server" }, "Join the specified server on launch (only valid in combination with --launch)", "address" }, + { { "w", "world" }, "Join the specified world on launch (only valid in combination with --launch)", "world" }, { { "a", "profile" }, "Use the account specified by its profile name (only valid in combination with --launch)", "profile" }, { "alive", "Write a small '" + liveCheckFile + "' file after the launcher starts" }, { { "I", "import" }, "Import instance or resource from specified local path or URL", "url" }, @@ -249,6 +255,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_instanceIdToLaunch = parser.value("launch"); m_serverToJoin = parser.value("server"); + m_worldToJoin = parser.value("world"); m_profileToUse = parser.value("profile"); m_liveCheck = parser.isSet("alive"); @@ -264,7 +271,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } // error if --launch is missing with --server or --profile - if ((!m_serverToJoin.isEmpty() || !m_profileToUse.isEmpty()) && m_instanceIdToLaunch.isEmpty()) { + if (((!m_serverToJoin.isEmpty() || !m_worldToJoin.isEmpty()) || !m_profileToUse.isEmpty()) && m_instanceIdToLaunch.isEmpty()) { std::cerr << "--server and --profile can only be used in combination with --launch!" << std::endl; m_status = Application::Failed; return; @@ -291,12 +298,17 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) QString adjustedBy; QString dataPath; // change folder + QString dataDirEnv; QString dirParam = parser.value("dir"); if (!dirParam.isEmpty()) { // the dir param. it makes multimc data path point to whatever the user specified // on command line adjustedBy = "Command line"; dataPath = dirParam; + } else if (dataDirEnv = QProcessEnvironment::systemEnvironment().value(QString("%1_DATA_DIR").arg(BuildConfig.LAUNCHER_NAME.toUpper())); + !dataDirEnv.isEmpty()) { + adjustedBy = "System environment"; + dataPath = dataDirEnv; } else { QDir foo; if (DesktopServices::isSnap()) { @@ -379,6 +391,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) if (!m_serverToJoin.isEmpty()) { launch.args["server"] = m_serverToJoin; + } else if (!m_worldToJoin.isEmpty()) { + launch.args["world"] = m_worldToJoin; } if (!m_profileToUse.isEmpty()) { launch.args["profile"] = m_profileToUse; @@ -394,20 +408,15 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) { static const QString baseLogFile = BuildConfig.LAUNCHER_NAME + "-%0.log"; static const QString logBase = FS::PathCombine("logs", baseLogFile); - auto moveFile = [](const QString& oldName, const QString& newName) { - QFile::remove(newName); - QFile::copy(oldName, newName); - QFile::remove(oldName); - }; if (FS::ensureFolderPathExists("logs")) { // if this did not fail for (auto i = 0; i <= 4; i++) if (auto oldName = baseLogFile.arg(i); QFile::exists(oldName)) // do not pointlessly delete new files if the old ones are not there - moveFile(oldName, logBase.arg(i)); + FS::move(oldName, logBase.arg(i)); } for (auto i = 4; i > 0; i--) - moveFile(logBase.arg(i - 1), logBase.arg(i)); + FS::move(logBase.arg(i - 1), logBase.arg(i)); logFile = std::unique_ptr(new QFile(logBase.arg(0))); if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { @@ -447,7 +456,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // search the dataPath() // seach app data standard path - if (!foundLoggingRules && !isPortable() && dirParam.isEmpty()) { + if (!foundLoggingRules && !isPortable() && dirParam.isEmpty() && dataDirEnv.isEmpty()) { logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile)); if (!logRulesPath.isEmpty()) { qDebug() << "Found" << logRulesPath << "..."; @@ -522,6 +531,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } if (!m_serverToJoin.isEmpty()) { qDebug() << "Address of server to join :" << m_serverToJoin; + } else if (!m_worldToJoin.isEmpty()) { + qDebug() << "Name of the world to join :" << m_worldToJoin; } qDebug() << "<> Paths set."; } @@ -558,6 +569,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("NumberOfConcurrentTasks", 10); m_settings->registerSetting("NumberOfConcurrentDownloads", 6); + m_settings->registerSetting("NumberOfManualRetries", 1); + m_settings->registerSetting("RequestTimeout", 60); QString defaultMonospace; int defaultSize = 11; @@ -592,6 +605,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("IconsDir", "icons"); m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); m_settings->registerSetting("DownloadsDirWatchRecursive", false); + m_settings->registerSetting("SkinsDir", "skins"); + m_settings->registerSetting("JavaDir", "java"); // Editors m_settings->registerSetting("JsonEditor", QString()); @@ -620,7 +635,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // Memory m_settings->registerSetting({ "MinMemAlloc", "MinMemoryAlloc" }, 512); - m_settings->registerSetting({ "MaxMemAlloc", "MaxMemoryAlloc" }, suitableMaxMem()); + m_settings->registerSetting({ "MaxMemAlloc", "MaxMemoryAlloc" }, SysInfo::suitableMaxMem()); m_settings->registerSetting("PermGen", 128); // Java Settings @@ -634,6 +649,10 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("JvmArgs", ""); m_settings->registerSetting("IgnoreJavaCompatibility", false); m_settings->registerSetting("IgnoreJavaWizard", false); + 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); @@ -659,6 +678,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // Minecraft mods m_settings->registerSetting("ModMetadataDisabled", false); m_settings->registerSetting("ModDependenciesDisabled", false); + m_settings->registerSetting("SkipModpackUpdatePrompt", false); // Minecraft offline player name m_settings->registerSetting("LastOfflinePlayerName", ""); @@ -813,7 +833,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_icons.reset(new IconList(instFolders, setting->get().toString())); connect(setting.get(), &Setting::SettingChanged, [&](const Setting&, QVariant value) { m_icons->directoryChanged(value.toString()); }); - qDebug() << "<> Instance icons intialized."; + qDebug() << "<> Instance icons initialized."; } // Themes @@ -850,25 +870,19 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) { m_metacache.reset(new HttpMetaCache("metacache")); m_metacache->addBase("asset_indexes", QDir("assets/indexes").absolutePath()); - m_metacache->addBase("asset_objects", QDir("assets/objects").absolutePath()); - m_metacache->addBase("versions", QDir("versions").absolutePath()); m_metacache->addBase("libraries", QDir("libraries").absolutePath()); - m_metacache->addBase("minecraftforge", QDir("mods/minecraftforge").absolutePath()); m_metacache->addBase("fmllibs", QDir("mods/minecraftforge/libs").absolutePath()); - m_metacache->addBase("liteloader", QDir("mods/liteloader").absolutePath()); m_metacache->addBase("general", QDir("cache").absolutePath()); m_metacache->addBase("ATLauncherPacks", QDir("cache/ATLauncherPacks").absolutePath()); m_metacache->addBase("FTBPacks", QDir("cache/FTBPacks").absolutePath()); - m_metacache->addBase("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath()); m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath()); m_metacache->addBase("FlamePacks", QDir("cache/FlamePacks").absolutePath()); m_metacache->addBase("FlameMods", QDir("cache/FlameMods").absolutePath()); m_metacache->addBase("ModrinthPacks", QDir("cache/ModrinthPacks").absolutePath()); m_metacache->addBase("ModrinthModpacks", QDir("cache/ModrinthModpacks").absolutePath()); - m_metacache->addBase("root", QDir::currentPath()); m_metacache->addBase("translations", QDir("translations").absolutePath()); - m_metacache->addBase("icons", QDir("cache/icons").absolutePath()); m_metacache->addBase("meta", QDir("meta").absolutePath()); + m_metacache->addBase("java", QDir("cache/java").absolutePath()); m_metacache->Load(); qDebug() << "<> Cache initialized."; } @@ -879,6 +893,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // FIXME: what to do with these? m_profilers.insert("jprofiler", std::shared_ptr(new JProfilerFactory())); m_profilers.insert("jvisualvm", std::shared_ptr(new JVisualVMFactory())); + m_profilers.insert("generic", std::shared_ptr(new GenericProfilerFactory())); for (auto profiler : m_profilers.values()) { profiler->registerSettings(m_settings); } @@ -947,8 +962,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) [[fallthrough]]; default: { qDebug() << "Exiting because update lockfile is present"; - QMetaObject::invokeMethod( - this, []() { exit(1); }, Qt::QueuedConnection); + QMetaObject::invokeMethod(this, []() { exit(1); }, Qt::QueuedConnection); return; } } @@ -980,8 +994,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) [[fallthrough]]; default: { qDebug() << "Exiting because update lockfile is present"; - QMetaObject::invokeMethod( - this, []() { exit(1); }, Qt::QueuedConnection); + QMetaObject::invokeMethod(this, []() { exit(1); }, Qt::QueuedConnection); return; } } @@ -993,7 +1006,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) "\n" "You are now running %1 .\n" "Check the Prism Launcher updater log at: \n" - "%1\n" + "%2\n" "for details.") .arg(BuildConfig.printableVersionString()) .arg(update_log_path); @@ -1067,13 +1080,15 @@ bool Application::createSetupWizard() } return false; }(); + 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) @@ -1090,6 +1105,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) { @@ -1100,11 +1117,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() @@ -1160,14 +1180,17 @@ void Application::performMainStartupAction() if (!m_instanceIdToLaunch.isEmpty()) { auto inst = instances()->getInstanceById(m_instanceIdToLaunch); if (inst) { - MinecraftServerTargetPtr serverToJoin = nullptr; + MinecraftTarget::Ptr targetToJoin = nullptr; MinecraftAccountPtr accountToUse = nullptr; qDebug() << "<> Instance" << m_instanceIdToLaunch << "launching"; if (!m_serverToJoin.isEmpty()) { // FIXME: validate the server string - serverToJoin.reset(new MinecraftServerTarget(MinecraftServerTarget::parse(m_serverToJoin))); + targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(m_serverToJoin, false))); qDebug() << " Launching with server" << m_serverToJoin; + } else if (!m_worldToJoin.isEmpty()) { + targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(m_worldToJoin, true))); + qDebug() << " Launching with world" << m_worldToJoin; } if (!m_profileToUse.isEmpty()) { @@ -1178,7 +1201,7 @@ void Application::performMainStartupAction() qDebug() << " Launching with account" << m_profileToUse; } - launch(inst, true, false, serverToJoin, accountToUse); + launch(inst, true, false, targetToJoin, accountToUse); return; } } @@ -1209,6 +1232,12 @@ void Application::performMainStartupAction() qDebug() << "<> Updater started."; } + { // delete instances tmp dirctory + auto instDir = m_settings->get("InstanceDir").toString(); + const QString tempRoot = FS::PathCombine(instDir, ".tmp"); + FS::deletePath(tempRoot); + } + if (!m_urlsToImport.isEmpty()) { qDebug() << "<> Importing from url:" << m_urlsToImport; m_mainWindow->processURLs(m_urlsToImport); @@ -1240,16 +1269,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") { @@ -1262,6 +1298,7 @@ void Application::messageReceived(const QByteArray& message) } else if (command == "launch") { QString id = received.args["id"]; QString server = received.args["server"]; + QString world = received.args["world"]; QString profile = received.args["profile"]; InstancePtr instance; @@ -1276,11 +1313,12 @@ void Application::messageReceived(const QByteArray& message) return; } - MinecraftServerTargetPtr serverObject = nullptr; + MinecraftTarget::Ptr serverObject = nullptr; if (!server.isEmpty()) { - serverObject = std::make_shared(MinecraftServerTarget::parse(server)); + serverObject = std::make_shared(MinecraftTarget::parse(server, false)); + } else if (!world.isEmpty()) { + serverObject = std::make_shared(MinecraftTarget::parse(world, true)); } - MinecraftAccountPtr accountObject; if (!profile.isEmpty()) { accountObject = accounts()->getAccountByProfileName(profile); @@ -1329,11 +1367,7 @@ bool Application::openJsonEditor(const QString& filename) } } -bool Application::launch(InstancePtr instance, - bool online, - bool demo, - MinecraftServerTargetPtr serverToJoin, - MinecraftAccountPtr accountToUse) +bool Application::launch(InstancePtr instance, bool online, bool demo, MinecraftTarget::Ptr targetToJoin, MinecraftAccountPtr accountToUse) { if (m_updateRunning) { qDebug() << "Cannot launch instances while an update is running. Please try again when updates are completed."; @@ -1351,7 +1385,7 @@ bool Application::launch(InstancePtr instance, controller->setOnline(online); controller->setDemo(demo); controller->setProfiler(profilers().value(instance->settings()->get("Profiler").toString(), nullptr).get()); - controller->setServerToJoin(serverToJoin); + controller->setTargetToJoin(targetToJoin); controller->setAccountToUse(accountToUse); if (window) { controller->setParentWidget(window); @@ -1669,8 +1703,7 @@ QString Application::getJarPath(QString jarFile) #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME), #endif - FS::PathCombine(m_rootPath, "jars"), - FS::PathCombine(applicationDirPath(), "jars"), + FS::PathCombine(m_rootPath, "jars"), FS::PathCombine(applicationDirPath(), "jars"), FS::PathCombine(applicationDirPath(), "..", "jars") // from inside build dir, for debuging }; for (QString p : potentialPaths) { @@ -1731,20 +1764,6 @@ QString Application::getUserAgentUncached() return BuildConfig.USER_AGENT_UNCACHED; } -int Application::suitableMaxMem() -{ - float totalRAM = (float)Sys::getSystemRam() / (float)Sys::mebibyte; - int maxMemoryAlloc; - - // If totalRAM < 6GB, use (totalRAM / 1.5), else 4GB - if (totalRAM < (4096 * 1.5)) - maxMemoryAlloc = (int)(totalRAM / 1.5); - else - maxMemoryAlloc = 4096; - - return maxMemoryAlloc; -} - bool Application::handleDataMigration(const QString& currentData, const QString& oldData, const QString& name, @@ -1851,3 +1870,8 @@ QUrl Application::normalizeImportUrl(QString const& url) return QUrl::fromUserInput(url); } } + +const QString Application::javaPath() +{ + return m_settings->get("JavaDir").toString(); +} diff --git a/launcher/Application.h b/launcher/Application.h index 7669e08ec..7432c9683 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -47,8 +47,7 @@ #include -#include "minecraft/launch/MinecraftServerTarget.h" -#include "ui/themes/CatPack.h" +#include "minecraft/launch/MinecraftTarget.h" class LaunchController; class LocalPeer; @@ -162,6 +161,9 @@ class Application : public QApplication { /// the data path the application is using const QString& dataRoot() { return m_dataPath; } + /// the java installed path the application is using + const QString javaPath(); + bool isPortable() { return m_portable; } const Capabilities capabilities() { return m_capabilities; } @@ -180,8 +182,6 @@ class Application : public QApplication { void ShowGlobalSettings(class QWidget* parent, QString open_page = QString()); - int suitableMaxMem(); - bool updaterEnabled(); QString updaterBinaryName(); @@ -193,6 +193,8 @@ class Application : public QApplication { void globalSettingsClosed(); int currentCatChanged(int index); + void oauthReplyRecieved(QVariantMap); + #ifdef Q_OS_MACOS void clickedOnDock(); #endif @@ -201,7 +203,7 @@ class Application : public QApplication { bool launch(InstancePtr instance, bool online = true, bool demo = false, - MinecraftServerTargetPtr serverToJoin = nullptr, + MinecraftTarget::Ptr targetToJoin = nullptr, MinecraftAccountPtr accountToUse = nullptr); bool kill(InstancePtr instance); void closeCurrentWindow(); @@ -289,6 +291,7 @@ class Application : public QApplication { QString m_detectedOpenALPath; QString m_instanceIdToLaunch; QString m_serverToJoin; + QString m_worldToJoin; QString m_profileToUse; bool m_liveCheck = false; QList m_urlsToImport; diff --git a/launcher/BaseInstaller.cpp b/launcher/BaseInstaller.cpp index 1ff86ed40..96a3b5ebe 100644 --- a/launcher/BaseInstaller.cpp +++ b/launcher/BaseInstaller.cpp @@ -16,6 +16,7 @@ #include #include "BaseInstaller.h" +#include "FileSystem.h" #include "minecraft/MinecraftInstance.h" BaseInstaller::BaseInstaller() {} @@ -42,7 +43,7 @@ bool BaseInstaller::add(MinecraftInstance* to) bool BaseInstaller::remove(MinecraftInstance* from) { - return QFile::remove(filename(from->instanceRoot())); + return FS::deletePath(filename(from->instanceRoot())); } QString BaseInstaller::filename(const QString& root) const diff --git a/launcher/BaseInstaller.h b/launcher/BaseInstaller.h index 6244ced7d..1cf7d65f5 100644 --- a/launcher/BaseInstaller.h +++ b/launcher/BaseInstaller.h @@ -29,7 +29,7 @@ class BaseVersion; class BaseInstaller { public: BaseInstaller(); - virtual ~BaseInstaller(){}; + virtual ~BaseInstaller() {}; bool isApplied(MinecraftInstance* on); virtual bool add(MinecraftInstance* to); diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index cda44b454..69cf95e3c 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -269,13 +269,18 @@ void BaseInstance::setRunning(bool running) m_isRunning = running; - if (!m_settings->get("RecordGameTime").toBool()) { - emit runningStatusChanged(running); + emit runningStatusChanged(running); +} + +void BaseInstance::setMinecraftRunning(bool running) +{ + if (!settings()->get("RecordGameTime").toBool()) { return; } if (running) { m_timeStarted = QDateTime::currentDateTime(); + setLastLaunch(m_timeStarted.toMSecsSinceEpoch()); } else { QDateTime timeEnded = QDateTime::currentDateTime(); @@ -285,8 +290,6 @@ void BaseInstance::setRunning(bool running) emit propertiesChanged(this); } - - emit runningStatusChanged(running); } int64_t BaseInstance::totalTimePlayed() const diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index f4ed9113c..2be28d1ec 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -56,7 +56,7 @@ #include "net/Mode.h" #include "RuntimeContext.h" -#include "minecraft/launch/MinecraftServerTarget.h" +#include "minecraft/launch/MinecraftTarget.h" class QDir; class Task; @@ -104,6 +104,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, MinecraftServerTargetPtr serverToJoin) = 0; + virtual shared_qobject_ptr createLaunchTask(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) = 0; /// returns the current launch task (if any) shared_qobject_ptr getLaunchTask(); @@ -214,7 +215,7 @@ class BaseInstance : public QObject, public std::enable_shared_from_thistypeString(); + case JavaMajorRole: { + auto major = version->name(); + if (major.startsWith("java")) { + major = "Java " + major.mid(4); + } + return major; + } + default: return QVariant(); } @@ -110,6 +118,8 @@ QHash BaseVersionList::roleNames() const roles.insert(TypeRole, "type"); roles.insert(BranchRole, "branch"); roles.insert(PathRole, "path"); - roles.insert(ArchitectureRole, "architecture"); + roles.insert(JavaNameRole, "javaName"); + roles.insert(CPUArchitectureRole, "architecture"); + roles.insert(JavaMajorRole, "javaMajor"); return roles; } diff --git a/launcher/BaseVersionList.h b/launcher/BaseVersionList.h index 231887c4e..673d13562 100644 --- a/launcher/BaseVersionList.h +++ b/launcher/BaseVersionList.h @@ -48,7 +48,9 @@ class BaseVersionList : public QAbstractListModel { TypeRole, BranchRole, PathRole, - ArchitectureRole, + JavaNameRole, + JavaMajorRole, + CPUArchitectureRole, SortRole }; using RoleList = QList; diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index bc48abdef..3d1f38c03 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -24,6 +24,8 @@ set(CORE_SOURCES NullInstance.h MMCZip.h MMCZip.cpp + Untar.h + Untar.cpp StringUtils.h StringUtils.cpp QVariantUtils.h @@ -126,7 +128,6 @@ set(NET_SOURCES net/MetaCacheSink.h net/Logging.h net/Logging.cpp - net/NetAction.h net/NetJob.cpp net/NetJob.h net/NetUtils.h @@ -139,7 +140,6 @@ set(NET_SOURCES net/HeaderProxy.h net/RawHeaderProxy.h net/ApiHeaderProxy.h - net/StaticHeaderProxy.h net/ApiDownload.h net/ApiDownload.cpp net/ApiUpload.cpp @@ -160,16 +160,18 @@ set(LAUNCH_SOURCES launch/steps/PreLaunchCommand.h launch/steps/TextPrint.cpp launch/steps/TextPrint.h - launch/steps/Update.cpp - launch/steps/Update.h launch/steps/QuitAfterGameStop.cpp launch/steps/QuitAfterGameStop.h + launch/steps/PrintServers.cpp + launch/steps/PrintServers.h launch/LaunchStep.cpp launch/LaunchStep.h launch/LaunchTask.cpp launch/LaunchTask.h launch/LogModel.cpp launch/LogModel.h + launch/TaskStepWrapper.cpp + launch/TaskStepWrapper.h ) # Old update system @@ -205,33 +207,27 @@ set(ICONS_SOURCES # Support for Minecraft instances and launch set(MINECRAFT_SOURCES + + # Logging + minecraft/Logging.h + minecraft/Logging.cpp + # Minecraft support minecraft/auth/AccountData.cpp minecraft/auth/AccountData.h minecraft/auth/AccountList.cpp minecraft/auth/AccountList.h - minecraft/auth/AccountTask.cpp - minecraft/auth/AccountTask.h - minecraft/auth/AuthRequest.cpp - minecraft/auth/AuthRequest.h minecraft/auth/AuthSession.cpp minecraft/auth/AuthSession.h - minecraft/auth/AuthStep.cpp minecraft/auth/AuthStep.h minecraft/auth/MinecraftAccount.cpp minecraft/auth/MinecraftAccount.h minecraft/auth/Parsers.cpp minecraft/auth/Parsers.h - minecraft/auth/flows/AuthFlow.cpp - minecraft/auth/flows/AuthFlow.h - minecraft/auth/flows/MSA.cpp - minecraft/auth/flows/MSA.h - minecraft/auth/flows/Offline.cpp - minecraft/auth/flows/Offline.h + minecraft/auth/AuthFlow.cpp + minecraft/auth/AuthFlow.h - minecraft/auth/steps/OfflineStep.cpp - minecraft/auth/steps/OfflineStep.h minecraft/auth/steps/EntitlementsStep.cpp minecraft/auth/steps/EntitlementsStep.h minecraft/auth/steps/GetSkinStep.cpp @@ -240,6 +236,8 @@ set(MINECRAFT_SOURCES minecraft/auth/steps/LauncherLoginStep.h minecraft/auth/steps/MinecraftProfileStep.cpp minecraft/auth/steps/MinecraftProfileStep.h + minecraft/auth/steps/MSADeviceCodeStep.cpp + minecraft/auth/steps/MSADeviceCodeStep.h minecraft/auth/steps/MSAStep.cpp minecraft/auth/steps/MSAStep.h minecraft/auth/steps/XboxAuthorizationStep.cpp @@ -271,8 +269,8 @@ set(MINECRAFT_SOURCES minecraft/launch/ExtractNatives.h minecraft/launch/LauncherPartLaunch.cpp minecraft/launch/LauncherPartLaunch.h - minecraft/launch/MinecraftServerTarget.cpp - minecraft/launch/MinecraftServerTarget.h + minecraft/launch/MinecraftTarget.cpp + minecraft/launch/MinecraftTarget.h minecraft/launch/PrintInstanceInfo.cpp minecraft/launch/PrintInstanceInfo.h minecraft/launch/ReconstructAssets.cpp @@ -281,6 +279,8 @@ set(MINECRAFT_SOURCES minecraft/launch/ScanModFolders.h minecraft/launch/VerifyJavaInstall.cpp minecraft/launch/VerifyJavaInstall.h + minecraft/launch/AutoInstallJava.cpp + minecraft/launch/AutoInstallJava.h minecraft/GradleSpecifier.h minecraft/MinecraftInstance.cpp @@ -295,8 +295,6 @@ set(MINECRAFT_SOURCES minecraft/ComponentUpdateTask.h minecraft/MinecraftLoadAndCheck.h minecraft/MinecraftLoadAndCheck.cpp - minecraft/MinecraftUpdate.h - minecraft/MinecraftUpdate.cpp minecraft/MojangVersionFormat.cpp minecraft/MojangVersionFormat.h minecraft/Rule.cpp @@ -371,13 +369,17 @@ set(MINECRAFT_SOURCES minecraft/AssetsUtils.h minecraft/AssetsUtils.cpp - # Minecraft services - minecraft/services/CapeChange.cpp - minecraft/services/CapeChange.h - minecraft/services/SkinUpload.cpp - minecraft/services/SkinUpload.h - minecraft/services/SkinDelete.cpp - minecraft/services/SkinDelete.h + # Minecraft skins + minecraft/skins/CapeChange.cpp + minecraft/skins/CapeChange.h + minecraft/skins/SkinUpload.cpp + minecraft/skins/SkinUpload.h + minecraft/skins/SkinDelete.cpp + minecraft/skins/SkinDelete.h + minecraft/skins/SkinModel.cpp + minecraft/skins/SkinModel.h + minecraft/skins/SkinList.cpp + minecraft/skins/SkinList.h minecraft/Agent.h) @@ -421,8 +423,6 @@ set(SETTINGS_SOURCES set(JAVA_SOURCES java/JavaChecker.h java/JavaChecker.cpp - java/JavaCheckerJob.h - java/JavaCheckerJob.cpp java/JavaInstall.h java/JavaInstall.cpp java/JavaInstallList.h @@ -431,6 +431,20 @@ set(JAVA_SOURCES java/JavaUtils.cpp java/JavaVersion.h java/JavaVersion.cpp + + java/JavaMetadata.h + java/JavaMetadata.cpp + java/download/ArchiveDownloadTask.cpp + java/download/ArchiveDownloadTask.h + java/download/ManifestDownloadTask.cpp + java/download/ManifestDownloadTask.h + java/download/SymlinkTask.cpp + java/download/SymlinkTask.h + + ui/java/InstallJavaDialog.h + ui/java/InstallJavaDialog.cpp + ui/java/VersionList.h + ui/java/VersionList.cpp ) set(TRANSLATIONS_SOURCES @@ -452,6 +466,8 @@ set(TOOLS_SOURCES tools/JVisualVM.h tools/MCEditTool.cpp tools/MCEditTool.h + tools/GenericProfiler.cpp + tools/GenericProfiler.h ) set(META_SOURCES @@ -623,7 +639,6 @@ set(PRISMUPDATER_SOURCES net/HttpMetaCache.h net/Logging.h net/Logging.cpp - net/NetAction.h net/NetRequest.cpp net/NetRequest.h net/NetJob.cpp @@ -652,6 +667,22 @@ ecm_qt_declare_logging_category(CORE_SOURCES EXPORT "${Launcher_Name}" ) +ecm_qt_export_logging_category( + IDENTIFIER instanceProfileC + CATEGORY_NAME "launcher.instance.profile" + DEFAULT_SEVERITY Debug + DESCRIPTION "Profile actions" + EXPORT "${Launcher_Name}" +) + +ecm_qt_export_logging_category( + IDENTIFIER instanceProfileResolveC + CATEGORY_NAME "launcher.instance.profile.resolve" + DEFAULT_SEVERITY Debug + DESCRIPTION "Profile component resolusion actions" + EXPORT "${Launcher_Name}" +) + ecm_qt_export_logging_category( IDENTIFIER taskLogC CATEGORY_NAME "launcher.task" @@ -664,7 +695,7 @@ ecm_qt_export_logging_category( IDENTIFIER taskNetLogC CATEGORY_NAME "launcher.task.net" DEFAULT_SEVERITY Debug - DESCRIPTION "task network action" + DESCRIPTION "Task network action" EXPORT "${Launcher_Name}" ) @@ -672,14 +703,14 @@ ecm_qt_export_logging_category( IDENTIFIER taskDownloadLogC CATEGORY_NAME "launcher.task.net.download" DEFAULT_SEVERITY Debug - DESCRIPTION "task network download actions" + DESCRIPTION "Task network download actions" EXPORT "${Launcher_Name}" ) ecm_qt_export_logging_category( IDENTIFIER taskUploadLogC CATEGORY_NAME "launcher.task.net.upload" DEFAULT_SEVERITY Debug - DESCRIPTION "task network upload actions" + DESCRIPTION "Task network upload actions" EXPORT "${Launcher_Name}" ) @@ -749,6 +780,8 @@ SET(LAUNCHER_SOURCES DataMigrationTask.cpp ApplicationMessage.h ApplicationMessage.cpp + SysInfo.h + SysInfo.cpp # GUI - general utilities DesktopServices.h @@ -787,16 +820,12 @@ SET(LAUNCHER_SOURCES # GUI - windows ui/GuiUtil.h ui/GuiUtil.cpp - ui/ColorCache.h - ui/ColorCache.cpp ui/MainWindow.h ui/MainWindow.cpp ui/InstanceWindow.h ui/InstanceWindow.cpp # FIXME: maybe find a better home for this. - SkinUtils.cpp - SkinUtils.h FileIgnoreProxy.cpp FileIgnoreProxy.h FastFileIconProvider.cpp @@ -814,6 +843,10 @@ SET(LAUNCHER_SOURCES ui/setupwizard/PasteWizardPage.h ui/setupwizard/ThemeWizardPage.cpp ui/setupwizard/ThemeWizardPage.h + ui/setupwizard/AutoJavaWizardPage.cpp + ui/setupwizard/AutoJavaWizardPage.h + ui/setupwizard/LoginWizardPage.cpp + ui/setupwizard/LoginWizardPage.h # GUI - themes ui/themes/FusionTheme.cpp @@ -826,6 +859,8 @@ SET(LAUNCHER_SOURCES ui/themes/DarkTheme.h ui/themes/ITheme.cpp ui/themes/ITheme.h + ui/themes/HintOverrideProxyStyle.cpp + ui/themes/HintOverrideProxyStyle.h ui/themes/SystemTheme.cpp ui/themes/SystemTheme.h ui/themes/IconTheme.cpp @@ -1021,8 +1056,6 @@ SET(LAUNCHER_SOURCES ui/dialogs/ReviewMessageBox.h ui/dialogs/VersionSelectDialog.cpp ui/dialogs/VersionSelectDialog.h - ui/dialogs/SkinUploadDialog.cpp - ui/dialogs/SkinUploadDialog.h ui/dialogs/ResourceDownloadDialog.cpp ui/dialogs/ResourceDownloadDialog.h ui/dialogs/ScrollMessageBox.cpp @@ -1036,7 +1069,12 @@ SET(LAUNCHER_SOURCES ui/dialogs/InstallLoaderDialog.cpp ui/dialogs/InstallLoaderDialog.h + ui/dialogs/skins/SkinManageDialog.cpp + ui/dialogs/skins/SkinManageDialog.h + # GUI - widgets + ui/widgets/CheckComboBox.cpp + ui/widgets/CheckComboBox.h ui/widgets/Common.cpp ui/widgets/Common.h ui/widgets/CustomCommands.cpp @@ -1121,6 +1159,8 @@ endif() qt_wrap_ui(LAUNCHER_UI ui/MainWindow.ui ui/setupwizard/PasteWizardPage.ui + ui/setupwizard/AutoJavaWizardPage.ui + ui/setupwizard/LoginWizardPage.ui ui/setupwizard/ThemeWizardPage.ui ui/pages/global/AccountListPage.ui ui/pages/global/JavaPage.ui @@ -1165,7 +1205,6 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/NewComponentDialog.ui ui/dialogs/NewsDialog.ui ui/dialogs/ProfileSelectDialog.ui - ui/dialogs/SkinUploadDialog.ui ui/dialogs/ExportInstanceDialog.ui ui/dialogs/ExportPackDialog.ui ui/dialogs/ExportToModListDialog.ui @@ -1179,6 +1218,8 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/ScrollMessageBox.ui ui/dialogs/BlockedModsDialog.ui ui/dialogs/ChooseProviderDialog.ui + + ui/dialogs/skins/SkinManageDialog.ui ) qt_wrap_ui(PRISM_UPDATE_UI @@ -1238,7 +1279,6 @@ target_link_libraries(Launcher_logic tomlplusplus::tomlplusplus qdcss BuildConfig - Katabasis Qt${QT_VERSION_MAJOR}::Widgets ghcFilesystem::ghc_filesystem ) @@ -1256,6 +1296,7 @@ target_link_libraries(Launcher_logic Qt${QT_VERSION_MAJOR}::Concurrent Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Widgets + Qt${QT_VERSION_MAJOR}::NetworkAuth ${Launcher_QT_LIBS} ) target_link_libraries(Launcher_logic @@ -1326,7 +1367,6 @@ if(Launcher_BUILD_UPDATER) Qt${QT_VERSION_MAJOR}::Network ${Launcher_QT_LIBS} cmark::cmark - Katabasis ) add_executable("${Launcher_Name}_updater" WIN32 updater/prismupdater/updater_main.cpp) @@ -1491,7 +1531,6 @@ if(INSTALL_BUNDLE STREQUAL "full") CONFIGURATIONS Debug RelWithDebInfo "" DESTINATION ${PLUGIN_DEST_DIR} COMPONENT Runtime - PATTERN "*qopensslbackend*" EXCLUDE PATTERN "*qcertonlybackend*" EXCLUDE ) install( @@ -1502,10 +1541,78 @@ if(INSTALL_BUNDLE STREQUAL "full") REGEX "dd\\." EXCLUDE REGEX "_debug\\." EXCLUDE REGEX "\\.dSYM" EXCLUDE - PATTERN "*qopensslbackend*" EXCLUDE PATTERN "*qcertonlybackend*" EXCLUDE ) endif() + # Wayland support + if(EXISTS "${QT_PLUGINS_DIR}/wayland-graphics-integration-client") + install( + DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-client" + CONFIGURATIONS Debug RelWithDebInfo "" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + ) + install( + DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-client" + CONFIGURATIONS Release MinSizeRel + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "dd\\." EXCLUDE + REGEX "_debug\\." EXCLUDE + REGEX "\\.dSYM" EXCLUDE + ) + endif() + if(EXISTS "${QT_PLUGINS_DIR}/wayland-graphics-integration-server") + install( + DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-server" + CONFIGURATIONS Debug RelWithDebInfo "" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + ) + install( + DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-server" + CONFIGURATIONS Release MinSizeRel + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "dd\\." EXCLUDE + REGEX "_debug\\." EXCLUDE + REGEX "\\.dSYM" EXCLUDE + ) + endif() + if(EXISTS "${QT_PLUGINS_DIR}/wayland-decoration-client") + install( + DIRECTORY "${QT_PLUGINS_DIR}/wayland-decoration-client" + CONFIGURATIONS Debug RelWithDebInfo "" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + ) + install( + DIRECTORY "${QT_PLUGINS_DIR}/wayland-decoration-client" + CONFIGURATIONS Release MinSizeRel + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "dd\\." EXCLUDE + REGEX "_debug\\." EXCLUDE + REGEX "\\.dSYM" EXCLUDE + ) + endif() + if(EXISTS "${QT_PLUGINS_DIR}/wayland-shell-integration") + install( + DIRECTORY "${QT_PLUGINS_DIR}/wayland-shell-integration" + CONFIGURATIONS Debug RelWithDebInfo "" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + ) + install( + DIRECTORY "${QT_PLUGINS_DIR}/wayland-shell-integration" + CONFIGURATIONS Release MinSizeRel + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "dd\\." EXCLUDE + REGEX "_debug\\." EXCLUDE + REGEX "\\.dSYM" EXCLUDE + ) + endif() configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/install_prereqs.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/install_prereqs.cmake" diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 70704e1d3..7f38cff04 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -276,6 +276,9 @@ bool ensureFolderPathExists(const QFileInfo folderPath) { QDir dir; QString ensuredPath = folderPath.filePath(); + if (folderPath.exists()) + return true; + bool success = dir.mkpath(ensuredPath); return success; } @@ -647,6 +650,19 @@ void ExternalLinkFileProcess::runLinkFile() qDebug() << "Process exited"; } +bool moveByCopy(const QString& source, const QString& dest) +{ + if (!copy(source, dest)()) { // copy + qDebug() << "Copy of" << source << "to" << dest << "failed!"; + return false; + } + if (!deletePath(source)) { // remove original + qDebug() << "Deletion of" << source << "failed!"; + return false; + }; + return true; +} + bool move(const QString& source, const QString& dest) { std::error_code err; @@ -654,13 +670,14 @@ bool move(const QString& source, const QString& dest) ensureFilePathExists(dest); fs::rename(StringUtils::toStdString(source), StringUtils::toStdString(dest), err); - if (err) { - qWarning() << "Failed to move file:" << QString::fromStdString(err.message()); - qDebug() << "Source file:" << source; - qDebug() << "Destination file:" << dest; + if (err.value() != 0) { + if (moveByCopy(source, dest)) + return true; + qDebug() << "Move of" << source << "to" << dest << "failed!"; + qWarning() << "Failed to move file:" << QString::fromStdString(err.message()) << QString::number(err.value()); + return false; } - - return err.value() == 0; + return true; } bool deletePath(QString path) @@ -801,25 +818,68 @@ QString NormalizePath(QString path) } } -static const QString BAD_PATH_CHARS = "\"?<>:;*|!+\r\n"; -static const QString BAD_FILENAME_CHARS = BAD_PATH_CHARS + "\\/"; +static const QString BAD_WIN_CHARS = "<>:\"|?*\r\n"; +static const QString BAD_NTFS_CHARS = "<>:\"|?*"; +static const QString BAD_HFS_CHARS = ":"; + +static const QString BAD_FILENAME_CHARS = BAD_WIN_CHARS + "\\/"; QString RemoveInvalidFilenameChars(QString string, QChar replaceWith) { for (int i = 0; i < string.length(); i++) if (string.at(i) < ' ' || BAD_FILENAME_CHARS.contains(string.at(i))) string[i] = replaceWith; - return string; } -QString RemoveInvalidPathChars(QString string, QChar replaceWith) +QString RemoveInvalidPathChars(QString path, QChar replaceWith) { - for (int i = 0; i < string.length(); i++) - if (string.at(i) < ' ' || BAD_PATH_CHARS.contains(string.at(i))) - string[i] = replaceWith; + QString invalidChars; +#ifdef Q_OS_WIN + invalidChars = BAD_WIN_CHARS; +#endif - return string; + // the null character is ignored in this check as it was not a problem until now + switch (statFS(path).fsType) { + case FilesystemType::FAT: // similar to NTFS + /* fallthrough */ + case FilesystemType::NTFS: + /* fallthrough */ + case FilesystemType::REFS: // similar to NTFS(should be available only on windows) + invalidChars += BAD_NTFS_CHARS; + break; + // case FilesystemType::EXT: + // case FilesystemType::EXT_2_OLD: + // case FilesystemType::EXT_2_3_4: + // case FilesystemType::XFS: + // case FilesystemType::BTRFS: + // case FilesystemType::NFS: + // case FilesystemType::ZFS: + case FilesystemType::APFS: + /* fallthrough */ + case FilesystemType::HFS: + /* fallthrough */ + case FilesystemType::HFSPLUS: + /* fallthrough */ + case FilesystemType::HFSX: + invalidChars += BAD_HFS_CHARS; + break; + // case FilesystemType::FUSEBLK: + // case FilesystemType::F2FS: + // case FilesystemType::UNKNOWN: + default: + break; + } + + if (invalidChars.size() != 0) { + for (int i = 0; i < path.length(); i++) { + if (path.at(i) < ' ' || invalidChars.contains(path.at(i))) { + path[i] = replaceWith; + } + } + } + + return path; } QString DirNameFromString(QString string, QString inDir) @@ -861,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 = @@ -1634,4 +1698,30 @@ QString getPathNameInLocal8bit(const QString& file) } #endif +QString getUniqueResourceName(const QString& filePath) +{ + auto newFileName = filePath; + if (!newFileName.endsWith(".disabled")) { + return newFileName; // prioritize enabled mods + } + newFileName.chop(9); + if (!QFile::exists(newFileName)) { + return filePath; + } + QFileInfo fileInfo(filePath); + auto baseName = fileInfo.completeBaseName(); + auto path = fileInfo.absolutePath(); + + int counter = 1; + do { + if (counter == 1) { + newFileName = FS::PathCombine(path, baseName + ".duplicate"); + } else { + newFileName = FS::PathCombine(path, baseName + ".duplicate" + QString::number(counter)); + } + counter++; + } while (QFile::exists(newFileName)); + + return newFileName; +} } // namespace FS diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index 5496c3795..c5beef7bd 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -72,7 +72,7 @@ void appendSafe(const QString& filename, const QByteArray& data); void append(const QString& filename, const QByteArray& data); /** - * read data from a file safely\ + * read data from a file safely */ QByteArray read(const QString& filename); @@ -240,6 +240,7 @@ class create_link : public QObject { bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); } int totalLinked() { return m_linked; } + int totalToLink() { return static_cast(m_links_to_make.size()); } void runPrivileged() { runPrivileged(QString()); } void runPrivileged(const QString& offset); @@ -378,6 +379,7 @@ enum class FilesystemType { HFSX, FUSEBLK, F2FS, + BCACHEFS, UNKNOWN }; @@ -406,6 +408,7 @@ static const QMap s_filesystem_type_names = { { Fil { FilesystemType::HFSX, { "HFSX" } }, { FilesystemType::FUSEBLK, { "FUSEBLK" } }, { FilesystemType::F2FS, { "F2FS" } }, + { FilesystemType::BCACHEFS, { "BCACHEFS" } }, { FilesystemType::UNKNOWN, { "UNKNOWN" } } }; /** @@ -458,7 +461,7 @@ QString nearestExistentAncestor(const QString& path); FilesystemInfo statFS(const QString& path); static const QList s_clone_filesystems = { FilesystemType::BTRFS, FilesystemType::APFS, FilesystemType::ZFS, - FilesystemType::XFS, FilesystemType::REFS }; + FilesystemType::XFS, FilesystemType::REFS, FilesystemType::BCACHEFS }; /** * @brief if the Filesystem is reflink/clone capable @@ -557,4 +560,6 @@ uintmax_t hardLinkCount(const QString& path); QString getPathNameInLocal8bit(const QString& file); #endif +QString getUniqueResourceName(const QString& filePath); + } // namespace FS diff --git a/launcher/Filter.cpp b/launcher/Filter.cpp index fc1c42344..adeb2209e 100644 --- a/launcher/Filter.cpp +++ b/launcher/Filter.cpp @@ -1,16 +1,12 @@ #include "Filter.h" -Filter::~Filter() {} - ContainsFilter::ContainsFilter(const QString& pattern) : pattern(pattern) {} -ContainsFilter::~ContainsFilter() {} bool ContainsFilter::accepts(const QString& value) { return value.contains(pattern); } ExactFilter::ExactFilter(const QString& pattern) : pattern(pattern) {} -ExactFilter::~ExactFilter() {} bool ExactFilter::accepts(const QString& value) { return value == pattern; @@ -27,10 +23,15 @@ RegexpFilter::RegexpFilter(const QString& regexp, bool invert) : invert(invert) pattern.setPattern(regexp); pattern.optimize(); } -RegexpFilter::~RegexpFilter() {} bool RegexpFilter::accepts(const QString& value) { auto match = pattern.match(value); bool matched = match.hasMatch(); return invert ? (!matched) : (matched); } + +ExactListFilter::ExactListFilter(const QStringList& pattern) : m_pattern(pattern) {} +bool ExactListFilter::accepts(const QString& value) +{ + return m_pattern.isEmpty() || m_pattern.contains(value); +} \ No newline at end of file diff --git a/launcher/Filter.h b/launcher/Filter.h index 089c844d4..ae835e724 100644 --- a/launcher/Filter.h +++ b/launcher/Filter.h @@ -5,14 +5,14 @@ class Filter { public: - virtual ~Filter(); + virtual ~Filter() = default; virtual bool accepts(const QString& value) = 0; }; class ContainsFilter : public Filter { public: ContainsFilter(const QString& pattern); - virtual ~ContainsFilter(); + virtual ~ContainsFilter() = default; bool accepts(const QString& value) override; private: @@ -22,7 +22,7 @@ class ContainsFilter : public Filter { class ExactFilter : public Filter { public: ExactFilter(const QString& pattern); - virtual ~ExactFilter(); + virtual ~ExactFilter() = default; bool accepts(const QString& value) override; private: @@ -32,7 +32,7 @@ class ExactFilter : public Filter { class ExactIfPresentFilter : public Filter { public: ExactIfPresentFilter(const QString& pattern); - ~ExactIfPresentFilter() override = default; + virtual ~ExactIfPresentFilter() override = default; bool accepts(const QString& value) override; private: @@ -42,10 +42,20 @@ class ExactIfPresentFilter : public Filter { class RegexpFilter : public Filter { public: RegexpFilter(const QString& regexp, bool invert); - virtual ~RegexpFilter(); + virtual ~RegexpFilter() = default; bool accepts(const QString& value) override; private: QRegularExpression pattern; bool invert = false; }; + +class ExactListFilter : public Filter { + public: + ExactListFilter(const QStringList& pattern = {}); + virtual ~ExactListFilter() = default; + bool accepts(const QString& value) override; + + private: + QStringList m_pattern; +}; diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp index 52eb7d879..0220a4144 100644 --- a/launcher/InstanceCopyTask.cpp +++ b/launcher/InstanceCopyTask.cpp @@ -1,10 +1,12 @@ #include "InstanceCopyTask.h" #include #include +#include #include "FileSystem.h" #include "NullInstance.h" #include "pathmatcher/RegexpMatcher.h" #include "settings/INISettingsObject.h" +#include "tasks/Task.h" InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs) { @@ -38,38 +40,50 @@ void InstanceCopyTask::executeTask() { setStatus(tr("Copying instance %1").arg(m_origInstance->name())); - auto copySaves = [&]() { - QFileInfo mcDir(FS::PathCombine(m_stagingPath, "minecraft")); - QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft")); - - QString staging_mc_dir; - if (dotMCDir.exists() && !mcDir.exists()) - staging_mc_dir = dotMCDir.filePath(); - else - staging_mc_dir = mcDir.filePath(); - - FS::copy savesCopy(FS::PathCombine(m_origInstance->gameRoot(), "saves"), FS::PathCombine(staging_mc_dir, "saves")); - savesCopy.followSymlinks(true); - - return savesCopy(); - }; - - m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this, copySaves] { + m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { if (m_useClone) { FS::clone folderClone(m_origInstance->instanceRoot(), m_stagingPath); folderClone.matcher(m_matcher.get()); + folderClone(true); + setProgress(0, folderClone.totalCloned()); + connect(&folderClone, &FS::clone::fileCloned, + [this](QString src, QString dst) { setProgress(m_progress + 1, m_progressTotal); }); return folderClone(); - } else if (m_useLinks || m_useHardLinks) { + } + if (m_useLinks || m_useHardLinks) { + std::unique_ptr savesCopy; + if (m_copySaves) { + QFileInfo mcDir(FS::PathCombine(m_stagingPath, "minecraft")); + QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft")); + + QString staging_mc_dir; + if (dotMCDir.exists() && !mcDir.exists()) + staging_mc_dir = dotMCDir.filePath(); + else + staging_mc_dir = mcDir.filePath(); + + savesCopy = std::make_unique(FS::PathCombine(m_origInstance->gameRoot(), "saves"), + FS::PathCombine(staging_mc_dir, "saves")); + savesCopy->followSymlinks(true); + (*savesCopy)(true); + setProgress(0, savesCopy->totalCopied()); + connect(savesCopy.get(), &FS::copy::fileCopied, [this](QString src) { setProgress(m_progress + 1, m_progressTotal); }); + } FS::create_link folderLink(m_origInstance->instanceRoot(), m_stagingPath); int depth = m_linkRecursively ? -1 : 0; // we need to at least link the top level instead of the instance folder folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher.get()); + folderLink(true); + setProgress(0, m_progressTotal + folderLink.totalToLink()); + connect(&folderLink, &FS::create_link::fileLinked, + [this](QString src, QString dst) { setProgress(m_progress + 1, m_progressTotal); }); bool there_were_errors = false; if (!folderLink()) { #if defined Q_OS_WIN32 if (!m_useHardLinks) { + setProgress(0, m_progressTotal); qDebug() << "EXPECTED: Link failure, Windows requires permissions for symlinks"; qDebug() << "attempting to run with privelage"; @@ -94,13 +108,11 @@ void InstanceCopyTask::executeTask() } } - if (m_copySaves) { - there_were_errors |= !copySaves(); + if (savesCopy) { + there_were_errors |= !(*savesCopy)(); } return got_priv_results && !there_were_errors; - } else { - qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str(); } #else qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str(); @@ -108,17 +120,19 @@ void InstanceCopyTask::executeTask() return false; } - if (m_copySaves) { - there_were_errors |= !copySaves(); + if (savesCopy) { + there_were_errors |= !(*savesCopy)(); } return !there_were_errors; - } else { - FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath); - folderCopy.followSymlinks(false).matcher(m_matcher.get()); - - return folderCopy(); } + FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath); + folderCopy.followSymlinks(false).matcher(m_matcher.get()); + + folderCopy(true); + setProgress(0, folderCopy.totalCopied()); + connect(&folderCopy, &FS::copy::fileCopied, [this](QString src) { setProgress(m_progress + 1, m_progressTotal); }); + return folderCopy(); }); connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &InstanceCopyTask::copyFinished); connect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &InstanceCopyTask::copyAborted); @@ -159,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(); @@ -170,3 +188,14 @@ void InstanceCopyTask::copyAborted() emitFailed(tr("Instance folder copy has been aborted.")); return; } + +bool InstanceCopyTask::abort() +{ + if (m_copyFutureWatcher.isRunning()) { + m_copyFutureWatcher.cancel(); + // NOTE: Here we don't do `emitAborted()` because it will be done when `m_copyFutureWatcher` actually cancels, which may not occur + // immediately. + return true; + } + return false; +} \ No newline at end of file diff --git a/launcher/InstanceCopyTask.h b/launcher/InstanceCopyTask.h index 357c6df0b..0f7f1020d 100644 --- a/launcher/InstanceCopyTask.h +++ b/launcher/InstanceCopyTask.h @@ -19,6 +19,7 @@ class InstanceCopyTask : public InstanceTask { protected: //! Entry point for tasks. virtual void executeTask() override; + bool abort() override; void copyFinished(); void copyAborted(); diff --git a/launcher/InstanceCreationTask.cpp b/launcher/InstanceCreationTask.cpp index 73dc17891..9c17dfc9f 100644 --- a/launcher/InstanceCreationTask.cpp +++ b/launcher/InstanceCreationTask.cpp @@ -2,8 +2,7 @@ #include #include - -InstanceCreationTask::InstanceCreationTask() = default; +#include "FileSystem.h" void InstanceCreationTask::executeTask() { @@ -47,7 +46,7 @@ void InstanceCreationTask::executeTask() if (!QFile::exists(path)) continue; qDebug() << "Removing" << path; - if (!QFile::remove(path)) { + if (!FS::deletePath(path)) { qCritical() << "Couldn't remove the old conflicting files."; emitFailed(tr("Failed to remove old conflicting files.")); return; diff --git a/launcher/InstanceCreationTask.h b/launcher/InstanceCreationTask.h index 380fdf8a4..84fb2a145 100644 --- a/launcher/InstanceCreationTask.h +++ b/launcher/InstanceCreationTask.h @@ -6,7 +6,7 @@ class InstanceCreationTask : public InstanceTask { Q_OBJECT public: - InstanceCreationTask(); + InstanceCreationTask() = default; virtual ~InstanceCreationTask() = default; protected: diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index d4676f358..57cc77527 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -56,6 +56,7 @@ #include #include +#include #include @@ -68,15 +69,8 @@ bool InstanceImportTask::abort() if (!canAbort()) return false; - if (m_filesNetJob) - m_filesNetJob->abort(); - if (m_extractFuture.isRunning()) { - // NOTE: The tasks created by QtConcurrent::run() can't actually get cancelled, - // but we can use this call to check the state when the extraction finishes. - m_extractFuture.cancel(); - m_extractFuture.waitForFinished(); - } - + if (task) + task->abort(); return Task::abort(); } @@ -89,7 +83,6 @@ void InstanceImportTask::executeTask() processZipPack(); } else { setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString())); - m_downloadRequired = true; downloadFromUrl(); } @@ -97,115 +90,132 @@ void InstanceImportTask::executeTask() void InstanceImportTask::downloadFromUrl() { - const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path(); + const QString path(m_sourceUrl.host() + '/' + m_sourceUrl.path()); + auto entry = APPLICATION->metacache()->resolveEntry("general", path); entry->setStale(true); - m_filesNetJob.reset(new NetJob(tr("Modpack download"), APPLICATION->network())); - m_filesNetJob->addNetAction(Net::ApiDownload::makeCached(m_sourceUrl, entry)); m_archivePath = entry->getFullPath(); - connect(m_filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded); - connect(m_filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged); - connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propagateStepProgress); - connect(m_filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::downloadFailed); - connect(m_filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::downloadAborted); - m_filesNetJob->start(); + auto filesNetJob = makeShared(tr("Modpack download"), APPLICATION->network()); + filesNetJob->addNetAction(Net::ApiDownload::makeCached(m_sourceUrl, entry)); + + connect(filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::processZipPack); + connect(filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::setProgress); + 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); + filesNetJob->start(); } -void InstanceImportTask::downloadSucceeded() +QString InstanceImportTask::getRootFromZip(QuaZip* zip, const QString& root) { - processZipPack(); - m_filesNetJob.reset(); -} + if (!isRunning()) { + return {}; + } + QuaZipDir rootDir(zip, root); + for (auto&& fileName : rootDir.entryList(QDir::Files)) { + setDetails(fileName); + if (fileName == "instance.cfg") { + qDebug() << "MultiMC:" << true; + m_modpackType = ModpackType::MultiMC; + return root; + } + if (fileName == "manifest.json") { + qDebug() << "Flame:" << true; + m_modpackType = ModpackType::Flame; + return root; + } -void InstanceImportTask::downloadFailed(QString reason) -{ - emitFailed(reason); - m_filesNetJob.reset(); -} + QCoreApplication::processEvents(); + } -void InstanceImportTask::downloadProgressChanged(qint64 current, qint64 total) -{ - setProgress(current, total); -} + // Recurse the search to non-ignored subfolders + for (auto&& fileName : rootDir.entryList(QDir::Dirs)) { + if ("overrides/" == fileName) + continue; -void InstanceImportTask::downloadAborted() -{ - emitAborted(); - m_filesNetJob.reset(); + QString result = getRootFromZip(zip, root + fileName); + if (!result.isEmpty()) + return result; + } + + return {}; } void InstanceImportTask::processZipPack() { - setStatus(tr("Extracting modpack")); + setStatus(tr("Attempting to determine instance type")); QDir extractDir(m_stagingPath); qDebug() << "Attempting to create instance from" << m_archivePath; // open the zip and find relevant files in it - m_packZip.reset(new QuaZip(m_archivePath)); - if (!m_packZip->open(QuaZip::mdUnzip)) { + auto packZip = std::make_shared(m_archivePath); + if (!packZip->open(QuaZip::mdUnzip)) { emitFailed(tr("Unable to open supplied modpack zip file.")); return; } - QuaZipDir packZipDir(m_packZip.get()); + QuaZipDir packZipDir(packZip.get()); + qDebug() << "Attempting to determine instance type"; - // https://docs.modrinth.com/docs/modpacks/format_definition/#storage - bool modrinthFound = packZipDir.exists("/modrinth.index.json"); - bool technicFound = packZipDir.exists("/bin/modpack.jar") || packZipDir.exists("/bin/version.json"); QString root; // NOTE: Prioritize modpack platforms that aren't searched for recursively. // Especially Flame has a very common filename for its manifest, which may appear inside overrides for example - if (modrinthFound) { + // https://docs.modrinth.com/docs/modpacks/format_definition/#storage + if (packZipDir.exists("/modrinth.index.json")) { // process as Modrinth pack - qDebug() << "Modrinth:" << modrinthFound; + qDebug() << "Modrinth:" << true; m_modpackType = ModpackType::Modrinth; - } else if (technicFound) { + } else if (packZipDir.exists("/bin/modpack.jar") || packZipDir.exists("/bin/version.json")) { // process as Technic pack - qDebug() << "Technic:" << technicFound; + qDebug() << "Technic:" << true; extractDir.mkpath("minecraft"); extractDir.cd("minecraft"); m_modpackType = ModpackType::Technic; } else { - QStringList paths_to_ignore{ "overrides/" }; - - if (QString mmcRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg", paths_to_ignore); !mmcRoot.isNull()) { - // process as MultiMC instance/pack - qDebug() << "MultiMC:" << mmcRoot; - root = mmcRoot; - m_modpackType = ModpackType::MultiMC; - } else if (QString flameRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json", paths_to_ignore); - !flameRoot.isNull()) { - // process as Flame pack - qDebug() << "Flame:" << flameRoot; - root = flameRoot; - m_modpackType = ModpackType::Flame; - } + root = getRootFromZip(packZip.get()); + setDetails(""); } if (m_modpackType == ModpackType::Unknown) { emitFailed(tr("Archive does not contain a recognized modpack type.")); return; } + setStatus(tr("Extracting modpack")); // make sure we extract just the pack - m_extractFuture = - QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), root, extractDir.absolutePath()); - connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, &InstanceImportTask::extractFinished); - m_extractFutureWatcher.setFuture(m_extractFuture); + auto zipTask = makeShared(packZip, extractDir, root); + + auto progressStep = std::make_shared(); + connect(zipTask.get(), &Task::finished, this, [this, progressStep] { + progressStep->state = TaskStepState::Succeeded; + stepProgress(*progressStep); + }); + + connect(zipTask.get(), &Task::succeeded, this, &InstanceImportTask::extractFinished); + connect(zipTask.get(), &Task::aborted, this, &InstanceImportTask::emitAborted); + connect(zipTask.get(), &Task::failed, this, [this, progressStep](QString reason) { + progressStep->state = TaskStepState::Failed; + stepProgress(*progressStep); + emitFailed(reason); + }); + connect(zipTask.get(), &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress); + + connect(zipTask.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) { + progressStep->update(current, total); + stepProgress(*progressStep); + }); + connect(zipTask.get(), &Task::status, this, [this, progressStep](QString status) { + progressStep->status = status; + stepProgress(*progressStep); + }); + task.reset(zipTask); + zipTask->start(); } void InstanceImportTask::extractFinished() { - m_packZip.reset(); - - if (m_extractFuture.isCanceled()) - return; - if (!m_extractFuture.result().has_value()) { - emitFailed(tr("Failed to extract modpack")); - return; - } - QDir extractDir(m_stagingPath); qDebug() << "Fixing permissions for extracted pack files..."; @@ -324,13 +334,15 @@ void InstanceImportTask::processMultiMC() m_instIcon = instance.iconKey(); auto importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), m_instIcon); + if (importIconPath.isNull() || !QFile::exists(importIconPath)) + importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), "icon.png"); if (!importIconPath.isNull() && QFile::exists(importIconPath)) { // import icon auto iconList = APPLICATION->icons(); if (iconList->iconFileExists(m_instIcon)) { iconList->deleteIcon(m_instIcon); } - iconList->installIcons({ importIconPath }); + iconList->installIcon(importIconPath, m_instIcon); } } emitSucceeded(); diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index 28efd7ec5..cf86af4ea 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -39,11 +39,8 @@ #include #include #include "InstanceTask.h" -#include "QObjectPtr.h" -#include "modplatform/flame/PackManifest.h" -#include "net/NetJob.h" -#include "settings/SettingsObject.h" +#include #include class QuaZip; @@ -54,35 +51,26 @@ class InstanceImportTask : public InstanceTask { explicit InstanceImportTask(const QUrl& sourceUrl, QWidget* parent = nullptr, QMap&& extra_info = {}); bool abort() override; - const QVector& getBlockedFiles() const { return m_blockedMods; } protected: //! Entry point for tasks. virtual void executeTask() override; private: - void processZipPack(); void processMultiMC(); void processTechnic(); void processFlame(); void processModrinth(); + QString getRootFromZip(QuaZip* zip, const QString& root = ""); private slots: - void downloadSucceeded(); - void downloadFailed(QString reason); - void downloadProgressChanged(qint64 current, qint64 total); - void downloadAborted(); + void processZipPack(); void extractFinished(); private: /* data */ - NetJob::Ptr m_filesNetJob; QUrl m_sourceUrl; QString m_archivePath; - bool m_downloadRequired = false; - std::unique_ptr m_packZip; - QFuture> m_extractFuture; - QFutureWatcher> m_extractFutureWatcher; - QVector m_blockedMods; + Task::Ptr task; enum class ModpackType { Unknown, MultiMC, diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 5e4abf020..e1fa755dd 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -372,13 +372,13 @@ void InstanceList::undoTrashInstance() auto top = m_trashHistory.pop(); - while (QDir(top.polyPath).exists()) { + while (QDir(top.path).exists()) { top.id += "1"; - top.polyPath += "1"; + top.path += "1"; } - qDebug() << "Moving" << top.trashPath << "back to" << top.polyPath; - QFile(top.trashPath).rename(top.polyPath); + qDebug() << "Moving" << top.trashPath << "back to" << top.path; + QFile(top.trashPath).rename(top.path); m_instanceGroupIndex[top.id] = top.groupName; increaseGroupCount(top.groupName); @@ -635,8 +635,8 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id) QString inst_type = instanceSettings->get("InstanceType").toString(); - // NOTE: Some PolyMC versions didn't save the InstanceType properly. We will just bank on the probability that this is probably a OneSix - // instance + // NOTE: Some launcher versions didn't save the InstanceType properly. We will just bank on the probability that this is probably a + // OneSix instance if (inst_type == "OneSix" || inst_type.isEmpty()) { inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot)); } else { @@ -710,6 +710,12 @@ void InstanceList::saveGroupList() groupsArr.insert(name, groupObj); } toplevel.insert("groups", groupsArr); + // empty string represents ungrouped "group" + if (m_collapsedGroups.contains("")) { + QJsonObject ungrouped; + ungrouped.insert("hidden", QJsonValue(true)); + toplevel.insert("ungrouped", ungrouped); + } QJsonDocument doc(toplevel); try { FS::write(groupFileName, doc.toJson()); @@ -805,6 +811,16 @@ void InstanceList::loadGroupList() increaseGroupCount(groupName); } } + + bool ungroupedHidden = false; + if (rootObj.value("ungrouped").isObject()) { + QJsonObject ungrouped = rootObj.value("ungrouped").toObject(); + ungroupedHidden = ungrouped.value("hidden").toBool(false); + } + if (ungroupedHidden) { + // empty string represents ungrouped "group" + m_collapsedGroups.insert(""); + } m_groupsLoaded = true; qDebug() << "Group list loaded."; } @@ -972,7 +988,6 @@ bool InstanceList::commitStagedInstance(const QString& path, if (groupName.isEmpty() && !groupName.isNull()) groupName = QString(); - QDir dir; QString instID; InstancePtr inst; @@ -996,7 +1011,7 @@ bool InstanceList::commitStagedInstance(const QString& path, return false; } } else { - if (!dir.rename(path, destination)) { + if (!FS::move(path, destination)) { qWarning() << "Failed to move" << path << "to" << destination; return false; } diff --git a/launcher/InstanceList.h b/launcher/InstanceList.h index 5ddddee95..c85fe55c7 100644 --- a/launcher/InstanceList.h +++ b/launcher/InstanceList.h @@ -58,7 +58,7 @@ enum class GroupsState { NotLoaded, Steady, Dirty }; struct TrashHistoryItem { QString id; - QString polyPath; + QString path; QString trashPath; QString groupName; }; diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h index 66d2b6750..174041f89 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -22,7 +22,7 @@ class InstancePageProvider : protected QObject, public BasePageProvider { public: explicit InstancePageProvider(InstancePtr parent) { inst = parent; } - virtual ~InstancePageProvider(){}; + virtual ~InstancePageProvider() = default; virtual QList getPages() override { QList values; @@ -39,7 +39,7 @@ class InstancePageProvider : protected QObject, public BasePageProvider { values.append(new TexturePackPage(onesix.get(), onesix->texturePackList())); values.append(new ShaderPackPage(onesix.get(), onesix->shaderPackList())); values.append(new NotesPage(onesix.get())); - values.append(new WorldListPage(onesix.get(), onesix->worldList())); + values.append(new WorldListPage(onesix, onesix->worldList())); values.append(new ServersPage(onesix)); // values.append(new GameOptionsPage(onesix.get())); values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots"))); diff --git a/launcher/InstanceTask.cpp b/launcher/InstanceTask.cpp index 53476897c..be10bbe07 100644 --- a/launcher/InstanceTask.cpp +++ b/launcher/InstanceTask.cpp @@ -1,5 +1,7 @@ #include "InstanceTask.h" +#include "Application.h" +#include "settings/SettingsObject.h" #include "ui/dialogs/CustomMessageBox.h" #include @@ -22,6 +24,9 @@ InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& ol ShouldUpdate askIfShouldUpdate(QWidget* parent, QString original_version_name) { + if (APPLICATION->settings()->get("SkipModpackUpdatePrompt").toBool()) + return ShouldUpdate::SkipUpdating; + auto info = CustomMessageBox::selectable( parent, QObject::tr("Similar modpack was found!"), QObject::tr( diff --git a/launcher/JavaCommon.cpp b/launcher/JavaCommon.cpp index e16ac9255..3cbf9f9d5 100644 --- a/launcher/JavaCommon.cpp +++ b/launcher/JavaCommon.cpp @@ -63,7 +63,7 @@ bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget* parent) return true; } -void JavaCommon::javaWasOk(QWidget* parent, const JavaCheckResult& result) +void JavaCommon::javaWasOk(QWidget* parent, const JavaChecker::Result& result) { QString text; text += QObject::tr( @@ -79,7 +79,7 @@ void JavaCommon::javaWasOk(QWidget* parent, const JavaCheckResult& result) CustomMessageBox::selectable(parent, QObject::tr("Java test success"), text, QMessageBox::Information)->show(); } -void JavaCommon::javaArgsWereBad(QWidget* parent, const JavaCheckResult& result) +void JavaCommon::javaArgsWereBad(QWidget* parent, const JavaChecker::Result& result) { auto htmlError = result.errorLog; QString text; @@ -89,7 +89,7 @@ void JavaCommon::javaArgsWereBad(QWidget* parent, const JavaCheckResult& result) CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show(); } -void JavaCommon::javaBinaryWasBad(QWidget* parent, const JavaCheckResult& result) +void JavaCommon::javaBinaryWasBad(QWidget* parent, const JavaChecker::Result& result) { QString text; text += QObject::tr( @@ -116,34 +116,26 @@ void JavaCommon::TestCheck::run() emit finished(); return; } - checker.reset(new JavaChecker()); + checker.reset(new JavaChecker(m_path, "", 0, 0, 0, 0, this)); connect(checker.get(), &JavaChecker::checkFinished, this, &JavaCommon::TestCheck::checkFinished); - checker->m_path = m_path; - checker->performCheck(); + checker->start(); } -void JavaCommon::TestCheck::checkFinished(JavaCheckResult result) +void JavaCommon::TestCheck::checkFinished(const JavaChecker::Result& result) { - if (result.validity != JavaCheckResult::Validity::Valid) { + if (result.validity != JavaChecker::Result::Validity::Valid) { javaBinaryWasBad(m_parent, result); emit finished(); return; } - checker.reset(new JavaChecker()); + checker.reset(new JavaChecker(m_path, m_args, m_maxMem, m_maxMem, result.javaVersion.requiresPermGen() ? m_permGen : 0, 0, this)); connect(checker.get(), &JavaChecker::checkFinished, this, &JavaCommon::TestCheck::checkFinishedWithArgs); - checker->m_path = m_path; - checker->m_args = m_args; - checker->m_minMem = m_minMem; - checker->m_maxMem = m_maxMem; - if (result.javaVersion.requiresPermGen()) { - checker->m_permGen = m_permGen; - } - checker->performCheck(); + checker->start(); } -void JavaCommon::TestCheck::checkFinishedWithArgs(JavaCheckResult result) +void JavaCommon::TestCheck::checkFinishedWithArgs(const JavaChecker::Result& result) { - if (result.validity == JavaCheckResult::Validity::Valid) { + if (result.validity == JavaChecker::Result::Validity::Valid) { javaWasOk(m_parent, result); emit finished(); return; diff --git a/launcher/JavaCommon.h b/launcher/JavaCommon.h index c96f7a985..a21b5a494 100644 --- a/launcher/JavaCommon.h +++ b/launcher/JavaCommon.h @@ -10,11 +10,11 @@ namespace JavaCommon { bool checkJVMArgs(QString args, QWidget* parent); // Show a dialog saying that the Java binary was usable -void javaWasOk(QWidget* parent, const JavaCheckResult& result); +void javaWasOk(QWidget* parent, const JavaChecker::Result& result); // Show a dialog saying that the Java binary was not usable because of bad options -void javaArgsWereBad(QWidget* parent, const JavaCheckResult& result); +void javaArgsWereBad(QWidget* parent, const JavaChecker::Result& result); // Show a dialog saying that the Java binary was not usable -void javaBinaryWasBad(QWidget* parent, const JavaCheckResult& result); +void javaBinaryWasBad(QWidget* parent, const JavaChecker::Result& result); // Show a dialog if we couldn't find Java Checker void javaCheckNotFound(QWidget* parent); @@ -24,7 +24,7 @@ class TestCheck : public QObject { TestCheck(QWidget* parent, QString path, QString args, int minMem, int maxMem, int permGen) : m_parent(parent), m_path(path), m_args(args), m_minMem(minMem), m_maxMem(maxMem), m_permGen(permGen) {} - virtual ~TestCheck(){}; + virtual ~TestCheck() {}; void run(); @@ -32,11 +32,11 @@ class TestCheck : public QObject { void finished(); private slots: - void checkFinished(JavaCheckResult result); - void checkFinishedWithArgs(JavaCheckResult result); + void checkFinished(const JavaChecker::Result& result); + void checkFinishedWithArgs(const JavaChecker::Result& result); private: - std::shared_ptr checker; + JavaChecker::Ptr checker; QWidget* m_parent = nullptr; QString m_path; QString m_args; diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index ff8558ce7..687da1322 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -36,6 +36,7 @@ #include "LaunchController.h" #include "Application.h" +#include "launch/steps/PrintServers.h" #include "minecraft/auth/AccountData.h" #include "minecraft/auth/AccountList.h" @@ -52,12 +53,12 @@ #include #include #include +#include #include #include "BuildConfig.h" #include "JavaCommon.h" #include "launch/steps/TextPrint.h" -#include "minecraft/auth/AccountTask.h" #include "tasks/Task.h" LaunchController::LaunchController(QObject* parent) : Task(parent) {} @@ -85,7 +86,7 @@ void LaunchController::decideAccount() // Find an account to use. auto accounts = APPLICATION->accounts(); - if (accounts->count() <= 0) { + if (accounts->count() <= 0 || !accounts->anyAccountIsValid()) { // Tell the user they need to log in at least one account in order to play. auto reply = CustomMessageBox::selectable(m_parentWidget, tr("No Accounts"), tr("In order to play Minecraft, you must have at least one Microsoft " @@ -129,12 +130,63 @@ void LaunchController::decideAccount() } } +bool LaunchController::askPlayDemo() +{ + QMessageBox box(m_parentWidget); + box.setWindowTitle(tr("Play demo?")); + box.setText( + tr("This account does not own Minecraft.\nYou need to purchase the game first to play it.\n\nDo you want to play " + "the demo?")); + box.setIcon(QMessageBox::Warning); + auto demoButton = box.addButton(tr("Play Demo"), QMessageBox::ButtonRole::YesRole); + auto cancelButton = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::NoRole); + box.setDefaultButton(cancelButton); + + box.exec(); + return box.clickedButton() == demoButton; +} + +QString LaunchController::askOfflineName(QString playerName, bool demo, bool& ok) +{ + // we ask the user for a player name + QString message = tr("Choose your offline mode player name."); + if (demo) { + message = tr("Choose your demo mode player name."); + } + + QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString(); + QString usedname = lastOfflinePlayerName.isEmpty() ? playerName : lastOfflinePlayerName; + QString name = QInputDialog::getText(m_parentWidget, tr("Player name"), message, QLineEdit::Normal, usedname, &ok); + if (!ok) + return {}; + if (name.length()) { + usedname = name; + APPLICATION->settings()->set("LastOfflinePlayerName", usedname); + } + return usedname; +} + void LaunchController::login() { decideAccount(); - // if no account is selected, we bail if (!m_accountToUse) { + // if no account is selected, ask about demo + if (!m_demo) { + m_demo = askPlayDemo(); + } + if (m_demo) { + // we ask the user for a player name + bool ok = false; + auto name = askOfflineName("Player", m_demo, ok); + if (ok) { + m_session = std::make_shared(); + m_session->MakeDemo(name, MinecraftAccount::uuidFromUsername(name).toString().remove(QRegularExpression("[{}-]"))); + launchInstance(); + return; + } + } + // if no account is selected, we bail emitFailed(tr("No account selected for launch.")); return; } @@ -143,7 +195,8 @@ void LaunchController::login() bool tryagain = true; unsigned int tries = 0; - if (m_accountToUse->accountType() != AccountType::Offline && m_accountToUse->accountState() == AccountState::Offline) { + if ((m_accountToUse->accountType() != AccountType::Offline && m_accountToUse->accountState() == AccountState::Offline) || + m_accountToUse->shouldRefresh()) { // Force account refresh on the account used to launch the instance updating the AccountState // only on first try and if it is not meant to be offline auto accounts = APPLICATION->accounts(); @@ -181,24 +234,12 @@ void LaunchController::login() if (!m_session->wants_online) { // we ask the user for a player name bool ok = false; - - QString message = tr("Choose your offline mode player name."); - if (m_session->demo) { - message = tr("Choose your demo mode player name."); - } - - QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString(); - QString usedname = lastOfflinePlayerName.isEmpty() ? m_session->player_name : lastOfflinePlayerName; - QString name = QInputDialog::getText(m_parentWidget, tr("Player name"), message, QLineEdit::Normal, usedname, &ok); + auto name = askOfflineName(m_session->player_name, m_session->demo, ok); if (!ok) { tryagain = false; break; } - if (name.length()) { - usedname = name; - APPLICATION->settings()->set("LastOfflinePlayerName", usedname); - } - m_session->MakeOffline(usedname); + m_session->MakeOffline(name); // offline flavored game from here :3 } if (m_accountToUse->ownsMinecraft()) { @@ -218,20 +259,10 @@ void LaunchController::login() return; } else { // play demo ? - QMessageBox box(m_parentWidget); - box.setWindowTitle(tr("Play demo?")); - box.setText( - tr("This account does not own Minecraft.\nYou need to purchase the game first to play it.\n\nDo you want to play " - "the demo?")); - box.setIcon(QMessageBox::Warning); - auto demoButton = box.addButton(tr("Play Demo"), QMessageBox::ButtonRole::YesRole); - auto cancelButton = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::NoRole); - box.setDefaultButton(cancelButton); - - box.exec(); - if (box.clickedButton() == demoButton) { - // play demo here - m_session->MakeDemo(); + if (!m_session->demo) { + m_session->demo = askPlayDemo(); + } + if (m_session->demo) { // play demo here launchInstance(); } else { emitFailed(tr("Launch cancelled - account does not own Minecraft.")); @@ -294,7 +325,7 @@ void LaunchController::launchInstance() return; } - m_launcher = m_instance->createLaunchTask(m_session, m_serverToJoin); + m_launcher = m_instance->createLaunchTask(m_session, m_targetToJoin); if (!m_launcher) { emitFailed(tr("Couldn't instantiate a launcher.")); return; @@ -316,26 +347,9 @@ void LaunchController::launchInstance() online_mode = "online"; // Prepend Server Status - QStringList servers = { "session.minecraft.net", "textures.minecraft.net", "api.mojang.com" }; - QString resolved_servers = ""; - QHostInfo host_info; + QStringList servers = { "login.microsoftonline.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com" }; - for (QString server : servers) { - host_info = QHostInfo::fromName(server); - resolved_servers = resolved_servers + server + " resolves to:\n ["; - if (!host_info.addresses().isEmpty()) { - for (QHostAddress address : host_info.addresses()) { - resolved_servers = resolved_servers + address.toString(); - if (!host_info.addresses().endsWith(address)) { - resolved_servers = resolved_servers + ", "; - } - } - } else { - resolved_servers = resolved_servers + "N/A"; - } - resolved_servers = resolved_servers + "]\n\n"; - } - m_launcher->prependStep(makeShared(m_launcher.get(), resolved_servers, MessageLevel::Launcher)); + m_launcher->prependStep(makeShared(m_launcher.get(), servers)); } else { online_mode = m_demo ? "demo" : "offline"; } diff --git a/launcher/LaunchController.h b/launcher/LaunchController.h index f1c88afb7..6e2a94258 100644 --- a/launcher/LaunchController.h +++ b/launcher/LaunchController.h @@ -39,7 +39,7 @@ #include #include "minecraft/auth/MinecraftAccount.h" -#include "minecraft/launch/MinecraftServerTarget.h" +#include "minecraft/launch/MinecraftTarget.h" class InstanceWindow; class LaunchController : public Task { @@ -48,7 +48,7 @@ class LaunchController : public Task { void executeTask() override; LaunchController(QObject* parent = nullptr); - virtual ~LaunchController(){}; + virtual ~LaunchController() = default; void setInstance(InstancePtr instance) { m_instance = instance; } @@ -62,7 +62,7 @@ class LaunchController : public Task { void setParentWidget(QWidget* widget) { m_parentWidget = widget; } - void setServerToJoin(MinecraftServerTargetPtr serverToJoin) { m_serverToJoin = std::move(serverToJoin); } + void setTargetToJoin(MinecraftTarget::Ptr targetToJoin) { m_targetToJoin = std::move(targetToJoin); } void setAccountToUse(MinecraftAccountPtr accountToUse) { m_accountToUse = std::move(accountToUse); } @@ -74,6 +74,8 @@ class LaunchController : public Task { void login(); void launchInstance(); void decideAccount(); + bool askPlayDemo(); + QString askOfflineName(QString playerName, bool demo, bool& ok); private slots: void readyForLaunch(); @@ -92,5 +94,5 @@ class LaunchController : public Task { MinecraftAccountPtr m_accountToUse = nullptr; AuthSessionPtr m_session; shared_qobject_ptr m_launcher; - MinecraftServerTargetPtr m_serverToJoin; + MinecraftTarget::Ptr m_targetToJoin; }; diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index 9a5ae7a9d..dcf3d566f 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -2,7 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (c) 2023 Trial97 + * 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 @@ -42,6 +42,7 @@ #include #include +#include #include #if defined(LAUNCHER_APPLICATION) @@ -122,7 +123,7 @@ bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, zip.setUtf8Enabled(true); QDir().mkpath(QFileInfo(fileCompressed).absolutePath()); if (!zip.open(QuaZip::mdCreate)) { - QFile::remove(fileCompressed); + FS::deletePath(fileCompressed); return false; } @@ -130,7 +131,7 @@ bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, zip.close(); if (zip.getZipError() != 0) { - QFile::remove(fileCompressed); + FS::deletePath(fileCompressed); return false; } @@ -144,7 +145,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QListtype() == ResourceType::ZIPFILE) { if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles)) { zipOut.close(); - QFile::remove(targetJarPath); + FS::deletePath(targetJarPath); qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; return false; } @@ -171,7 +172,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QListfileinfo(); if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) { zipOut.close(); - QFile::remove(targetJarPath); + FS::deletePath(targetJarPath); qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; return false; } @@ -194,7 +195,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QListfileinfo().fileName() << "to the jar."; return false; } @@ -202,7 +203,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QListfileinfo().fileName() << "to the jar."; return false; } @@ -210,7 +211,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList extractSubDir(QuaZip* zip, const QString& subdir, con do { QString file_name = zip->getCurrentFileName(); -#ifdef Q_OS_WIN file_name = FS::RemoveInvalidPathChars(file_name); -#endif if (!file_name.startsWith(subdir)) continue; @@ -332,9 +331,31 @@ std::optional extractSubDir(QuaZip* zip, const QString& subdir, con } extracted.append(target_file_path); - QFile::setPermissions(target_file_path, - QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser); + auto fileInfo = QFileInfo(target_file_path); + if (fileInfo.isFile()) { + auto permissions = fileInfo.permissions(); + auto maxPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser | + QFileDevice::Permission::ReadGroup | QFileDevice::Permission::ReadOther; + auto minPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; + auto newPermisions = (permissions & maxPermisions) | minPermisions; + if (newPermisions != permissions) { + if (!QFile::setPermissions(target_file_path, newPermisions)) { + qWarning() << (QObject::tr("Could not fix permissions for %1").arg(target_file_path)); + } + } + } else if (fileInfo.isDir()) { + // Ensure the folder has the minimal required permissions + QFile::Permissions minimalPermissions = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner | QFile::ReadGroup | + QFile::ExeGroup | QFile::ReadOther | QFile::ExeOther; + + QFile::Permissions currentPermissions = fileInfo.permissions(); + if ((currentPermissions & minimalPermissions) != minimalPermissions) { + if (!QFile::setPermissions(target_file_path, minimalPermissions)) { + qWarning() << (QObject::tr("Could not fix permissions for %1").arg(target_file_path)); + } + } + } qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; } while (zip->goToNextFile()); @@ -492,10 +513,10 @@ auto ExportToZipTask::exportZip() -> ZipResult void ExportToZipTask::finish() { if (m_build_zip_future.isCanceled()) { - QFile::remove(m_output_path); + FS::deletePath(m_output_path); emitAborted(); } else if (auto result = m_build_zip_future.result(); result.has_value()) { - QFile::remove(m_output_path); + FS::deletePath(m_output_path); emitFailed(result.value()); } else { emitSucceeded(); @@ -512,6 +533,138 @@ bool ExportToZipTask::abort() } return false; } -#endif +void ExtractZipTask::executeTask() +{ + if (!m_input->isOpen() && !m_input->open(QuaZip::mdUnzip)) { + emitFailed(tr("Unable to open supplied zip file.")); + return; + } + m_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return extractZip(); }); + connect(&m_zip_watcher, &QFutureWatcher::finished, this, &ExtractZipTask::finish); + m_zip_watcher.setFuture(m_zip_future); +} + +auto ExtractZipTask::extractZip() -> ZipResult +{ + auto target = m_output_dir.absolutePath(); + auto target_top_dir = QUrl::fromLocalFile(target); + + QStringList extracted; + + qDebug() << "Extracting subdir" << m_subdirectory << "from" << m_input->getZipName() << "to" << target; + auto numEntries = m_input->getEntriesCount(); + if (numEntries < 0) { + return ZipResult(tr("Failed to enumerate files in archive")); + } + if (numEntries == 0) { + logWarning(tr("Extracting empty archives seems odd...")); + return ZipResult(); + } + if (!m_input->goToFirstFile()) { + return ZipResult(tr("Failed to seek to first file in zip")); + } + + setStatus("Extracting files..."); + setProgress(0, numEntries); + do { + if (m_zip_future.isCanceled()) + return ZipResult(); + setProgress(m_progress + 1, m_progressTotal); + QString file_name = m_input->getCurrentFileName(); + if (!file_name.startsWith(m_subdirectory)) + continue; + + auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(m_subdirectory.size())); + auto original_name = relative_file_name; + setStatus("Unziping: " + relative_file_name); + + // Fix subdirs/files ending with a / getting transformed into absolute paths + if (relative_file_name.startsWith('/')) + relative_file_name = relative_file_name.mid(1); + + // Fix weird "folders with a single file get squashed" thing + QString sub_path; + if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) { + sub_path = relative_file_name.section('/', 0, -2) + '/'; + FS::ensureFolderPathExists(FS::PathCombine(target, sub_path)); + + relative_file_name = relative_file_name.split('/').last(); + } + + QString target_file_path; + if (relative_file_name.isEmpty()) { + target_file_path = target + '/'; + } else { + target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name); + if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/')) + target_file_path += '/'; + } + + if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) { + return ZipResult(tr("Extracting %1 was cancelled, because it was effectively outside of the target path %2") + .arg(relative_file_name, target)); + } + + if (!JlCompress::extractFile(m_input.get(), "", target_file_path)) { + JlCompress::removeFile(extracted); + return ZipResult(tr("Failed to extract file %1 to %2").arg(original_name, target_file_path)); + } + + extracted.append(target_file_path); + auto fileInfo = QFileInfo(target_file_path); + if (fileInfo.isFile()) { + auto permissions = fileInfo.permissions(); + auto maxPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser | + QFileDevice::Permission::ReadGroup | QFileDevice::Permission::ReadOther; + auto minPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; + + auto newPermisions = (permissions & maxPermisions) | minPermisions; + if (newPermisions != permissions) { + if (!QFile::setPermissions(target_file_path, newPermisions)) { + logWarning(tr("Could not fix permissions for %1").arg(target_file_path)); + } + } + } else if (fileInfo.isDir()) { + // Ensure the folder has the minimal required permissions + QFile::Permissions minimalPermissions = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner | QFile::ReadGroup | + QFile::ExeGroup | QFile::ReadOther | QFile::ExeOther; + + QFile::Permissions currentPermissions = fileInfo.permissions(); + if ((currentPermissions & minimalPermissions) != minimalPermissions) { + if (!QFile::setPermissions(target_file_path, minimalPermissions)) { + logWarning(tr("Could not fix permissions for %1").arg(target_file_path)); + } + } + } + + qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; + } while (m_input->goToNextFile()); + + return ZipResult(); +} + +void ExtractZipTask::finish() +{ + if (m_zip_future.isCanceled()) { + emitAborted(); + } else if (auto result = m_zip_future.result(); result.has_value()) { + emitFailed(result.value()); + } else { + emitSucceeded(); + } +} + +bool ExtractZipTask::abort() +{ + if (m_zip_future.isRunning()) { + m_zip_future.cancel(); + // NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur + // immediately. + return true; + } + return false; +} + +#endif } // namespace MMCZip diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h index 43b4ab933..1635f8b32 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -2,7 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (c) 2023 Trial97 + * 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 @@ -154,7 +154,12 @@ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, Q #if defined(LAUNCHER_APPLICATION) class ExportToZipTask : public Task { public: - ExportToZipTask(QString outputPath, QDir dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false) + ExportToZipTask(QString outputPath, + QDir dir, + QFileInfoList files, + QString destinationPrefix = "", + bool followSymlinks = false, + bool utf8Enabled = false) : m_output_path(outputPath) , m_output(outputPath) , m_dir(dir) @@ -163,10 +168,15 @@ class ExportToZipTask : public Task { , m_follow_symlinks(followSymlinks) { setAbortable(true); - m_output.setUtf8Enabled(true); + m_output.setUtf8Enabled(utf8Enabled); }; - ExportToZipTask(QString outputPath, QString dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false) - : ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks){}; + ExportToZipTask(QString outputPath, + QString dir, + QFileInfoList files, + QString destinationPrefix = "", + bool followSymlinks = false, + bool utf8Enabled = false) + : ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks, utf8Enabled) {}; virtual ~ExportToZipTask() = default; @@ -195,5 +205,33 @@ class ExportToZipTask : public Task { QFuture m_build_zip_future; QFutureWatcher m_build_zip_watcher; }; + +class ExtractZipTask : public Task { + public: + ExtractZipTask(QString input, QDir outputDir, QString subdirectory = "") + : ExtractZipTask(std::make_shared(input), outputDir, subdirectory) + {} + ExtractZipTask(std::shared_ptr input, QDir outputDir, QString subdirectory = "") + : m_input(input), m_output_dir(outputDir), m_subdirectory(subdirectory) + {} + virtual ~ExtractZipTask() = default; + + using ZipResult = std::optional; + + protected: + virtual void executeTask() override; + bool abort() override; + + ZipResult extractZip(); + void finish(); + + private: + std::shared_ptr m_input; + QDir m_output_dir; + QString m_subdirectory; + + QFuture m_zip_future; + QFutureWatcher m_zip_watcher; +}; #endif } // namespace MMCZip diff --git a/launcher/MangoHud.cpp b/launcher/MangoHud.cpp index ab79f418b..29a7c63d9 100644 --- a/launcher/MangoHud.cpp +++ b/launcher/MangoHud.cpp @@ -40,8 +40,8 @@ namespace MangoHud { QString getLibraryString() { - /* - * Check for vulkan layers in this order: + /** + * Guess MangoHud install location by searching for vulkan layers in this order: * * $VK_LAYER_PATH * $XDG_DATA_DIRS (/usr/local/share/:/usr/share/) @@ -49,8 +49,9 @@ QString getLibraryString() * /etc * $XDG_CONFIG_DIRS (/etc/xdg) * $XDG_CONFIG_HOME (~/.config) + * + * @returns Absolute path of libMangoHud.so if found and empty QString otherwise. */ - QStringList vkLayerList; { QString home = QDir::homePath(); @@ -85,7 +86,7 @@ QString getLibraryString() vkLayerList << FS::PathCombine(xdgConfigHome, "vulkan", "implicit_layer.d"); } - for (QString vkLayer : vkLayerList) { + for (const QString& vkLayer : vkLayerList) { // prefer to use architecture specific vulkan layers QString currentArch = QSysInfo::currentCpuArchitecture(); @@ -95,8 +96,8 @@ QString getLibraryString() QStringList manifestNames = { QString("MangoHud.%1.json").arg(currentArch), "MangoHud.json" }; - QString filePath = ""; - for (QString manifestName : manifestNames) { + QString filePath{}; + for (const QString& manifestName : manifestNames) { QString tryPath = FS::PathCombine(vkLayer, manifestName); if (QFile::exists(tryPath)) { filePath = tryPath; @@ -107,14 +108,34 @@ QString getLibraryString() if (filePath.isEmpty()) { continue; } + try { + auto conf = Json::requireDocument(filePath, vkLayer); + auto confObject = Json::requireObject(conf, vkLayer); + auto layer = Json::ensureObject(confObject, "layer"); + QString libraryName = Json::ensureString(layer, "library_path"); - auto conf = Json::requireDocument(filePath, vkLayer); - auto confObject = Json::requireObject(conf, vkLayer); - auto layer = Json::ensureObject(confObject, "layer"); - return Json::ensureString(layer, "library_path"); + if (libraryName.isEmpty()) { + continue; + } + if (QFileInfo(libraryName).isAbsolute()) { + return libraryName; + } + +#ifdef __GLIBC__ + // Check whether mangohud is usable on a glibc based system + QString libraryPath = findLibrary(libraryName); + if (!libraryPath.isEmpty()) { + return libraryPath; + } +#else + // Without glibc return recorded shared library as-is. + return libraryName; +#endif + } catch (const Exception& e) { + } } - return QString(); + return {}; } QString findLibrary(QString libName) diff --git a/launcher/NullInstance.h b/launcher/NullInstance.h index c79600e7d..3d01c9d33 100644 --- a/launcher/NullInstance.h +++ b/launcher/NullInstance.h @@ -46,14 +46,14 @@ class NullInstance : public BaseInstance { { setVersionBroken(true); } - virtual ~NullInstance(){}; + virtual ~NullInstance() = default; void saveNow() override {} void loadSpecificSettings() override { setSpecificSettingsLoaded(true); } QString getStatusbarDescription() override { return tr("Unknown instance type"); }; QSet traits() const override { return {}; }; QString instanceConfigFolder() const override { return instanceRoot(); }; - shared_qobject_ptr createLaunchTask(AuthSessionPtr, MinecraftServerTargetPtr) override { return nullptr; } - shared_qobject_ptr createUpdateTask([[maybe_unused]] Net::Mode mode) override { return nullptr; } + shared_qobject_ptr createLaunchTask(AuthSessionPtr, MinecraftTarget::Ptr) override { return nullptr; } + QList createUpdateTask() override { return {}; } QProcessEnvironment createEnvironment() override { return QProcessEnvironment(); } QProcessEnvironment createLaunchEnvironment() override { return QProcessEnvironment(); } QMap getVariables() override { return QMap(); } @@ -64,7 +64,7 @@ class NullInstance : public BaseInstance { bool canEdit() const override { return false; } bool canLaunch() const override { return false; } void populateLaunchMenu(QMenu* menu) override {} - QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override + QStringList verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) override { QStringList out; out << "Null instance - placeholder."; diff --git a/launcher/ResourceDownloadTask.cpp b/launcher/ResourceDownloadTask.cpp index e5828b569..0fe082ac4 100644 --- a/launcher/ResourceDownloadTask.cpp +++ b/launcher/ResourceDownloadTask.cpp @@ -24,7 +24,9 @@ #include "minecraft/mod/ModFolderModel.h" #include "minecraft/mod/ResourceFolderModel.h" +#include "modplatform/helpers/HashUtils.h" #include "net/ApiDownload.h" +#include "net/ChecksumValidator.h" ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion version, @@ -53,7 +55,29 @@ ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, } } - m_filesNetJob->addNetAction(Net::ApiDownload::makeFile(m_pack_version.downloadUrl, dir.absoluteFilePath(getFilename()))); + auto action = Net::ApiDownload::makeFile(m_pack_version.downloadUrl, dir.absoluteFilePath(getFilename())); + if (!m_pack_version.hash_type.isEmpty() && !m_pack_version.hash.isEmpty()) { + switch (Hashing::algorithmFromString(m_pack_version.hash_type)) { + case Hashing::Algorithm::Md4: + action->addValidator(new Net::ChecksumValidator(QCryptographicHash::Algorithm::Md4, m_pack_version.hash)); + break; + case Hashing::Algorithm::Md5: + action->addValidator(new Net::ChecksumValidator(QCryptographicHash::Algorithm::Md5, m_pack_version.hash)); + break; + case Hashing::Algorithm::Sha1: + action->addValidator(new Net::ChecksumValidator(QCryptographicHash::Algorithm::Sha1, m_pack_version.hash)); + break; + case Hashing::Algorithm::Sha256: + action->addValidator(new Net::ChecksumValidator(QCryptographicHash::Algorithm::Sha256, m_pack_version.hash)); + break; + case Hashing::Algorithm::Sha512: + action->addValidator(new Net::ChecksumValidator(QCryptographicHash::Algorithm::Sha512, m_pack_version.hash)); + break; + default: + break; + } + } + m_filesNetJob->addNetAction(action); connect(m_filesNetJob.get(), &NetJob::succeeded, this, &ResourceDownloadTask::downloadSucceeded); connect(m_filesNetJob.get(), &NetJob::progress, this, &ResourceDownloadTask::downloadProgressChanged); connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &ResourceDownloadTask::propagateStepProgress); 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/SkinUtils.cpp b/launcher/SkinUtils.cpp deleted file mode 100644 index 989114ad5..000000000 --- a/launcher/SkinUtils.cpp +++ /dev/null @@ -1,52 +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 "SkinUtils.h" -#include "Application.h" -#include "net/HttpMetaCache.h" - -#include -#include -#include -#include -#include - -namespace SkinUtils { -/* - * Given a username, return a pixmap of the cached skin (if it exists), QPixmap() otherwise - */ -QPixmap getFaceFromCache(QString username, int height, int width) -{ - QFile fskin(APPLICATION->metacache()->resolveEntry("skins", username + ".png")->getFullPath()); - - if (fskin.exists()) { - QPixmap skinTexture(fskin.fileName()); - if (!skinTexture.isNull()) { - QPixmap skin = QPixmap(8, 8); -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - skin.fill(QColorConstants::Transparent); -#else - skin.fill(QColor(0, 0, 0, 0)); -#endif - QPainter painter(&skin); - painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8)); - painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8)); - return skin.scaled(height, width, Qt::KeepAspectRatio); - } - } - - return QPixmap(); -} -} // namespace SkinUtils diff --git a/launcher/SkinUtils.h b/launcher/SkinUtils.h deleted file mode 100644 index 11bc8bc6f..000000000 --- a/launcher/SkinUtils.h +++ /dev/null @@ -1,22 +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 - -namespace SkinUtils { -QPixmap getFaceFromCache(QString id, int height = 64, int width = 64); -} diff --git a/launcher/StringUtils.cpp b/launcher/StringUtils.cpp index 72ccdfbff..edda9f247 100644 --- a/launcher/StringUtils.cpp +++ b/launcher/StringUtils.cpp @@ -212,3 +212,25 @@ QPair StringUtils::splitFirst(const QString& s, const QRegular right = s.mid(end); return qMakePair(left, right); } + +static const QRegularExpression ulMatcher("<\\s*/\\s*ul\\s*>"); + +QString StringUtils::htmlListPatch(QString htmlStr) +{ + int pos = htmlStr.indexOf(ulMatcher); + int imgPos; + while (pos != -1) { + pos = htmlStr.indexOf(">", pos) + 1; // Get the size of the tag. Add one for zeroeth index + imgPos = htmlStr.indexOf(""); + + pos = htmlStr.indexOf(ulMatcher, pos); + } + return htmlStr; +} \ No newline at end of file diff --git a/launcher/StringUtils.h b/launcher/StringUtils.h index 9d2bdd85e..624ee41a3 100644 --- a/launcher/StringUtils.h +++ b/launcher/StringUtils.h @@ -85,4 +85,6 @@ QPair splitFirst(const QString& s, const QString& sep, Qt::Cas QPair splitFirst(const QString& s, QChar sep, Qt::CaseSensitivity cs = Qt::CaseSensitive); QPair splitFirst(const QString& s, const QRegularExpression& re); +QString htmlListPatch(QString htmlStr); + } // namespace StringUtils diff --git a/launcher/SysInfo.cpp b/launcher/SysInfo.cpp new file mode 100644 index 000000000..0dfa74de7 --- /dev/null +++ b/launcher/SysInfo.cpp @@ -0,0 +1,99 @@ +#include +#include +#include "sys.h" +#ifdef Q_OS_MACOS +#include +#endif +#include +#include +#include +#include + +#ifdef Q_OS_MACOS +bool rosettaDetect() +{ + int ret = 0; + size_t size = sizeof(ret); + if (sysctlbyname("sysctl.proc_translated", &ret, &size, NULL, 0) == -1) { + return false; + } + return ret == 1; +} +#endif + +namespace SysInfo { +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 +} + +QString useQTForArch() +{ +#if defined(Q_OS_MACOS) && !defined(Q_PROCESSOR_ARM) + if (rosettaDetect()) { + return "arm64"; + } else { + return "x86_64"; + } +#endif + return QSysInfo::currentCpuArchitecture(); +} + +int suitableMaxMem() +{ + float totalRAM = (float)Sys::getSystemRam() / (float)Sys::mebibyte; + int maxMemoryAlloc; + + // If totalRAM < 6GB, use (totalRAM / 1.5), else 4GB + if (totalRAM < (4096 * 1.5)) + maxMemoryAlloc = (int)(totalRAM / 1.5); + else + maxMemoryAlloc = 4096; + + return maxMemoryAlloc; +} + +QString getSupportedJavaArchitecture() +{ + auto sys = currentSystem(); + auto arch = useQTForArch(); + if (sys == "windows") { + if (arch == "x86_64") + return "windows-x64"; + if (arch == "i386") + return "windows-x86"; + // Unknown, maybe arm, appending arch + return "windows-" + arch; + } + if (sys == "osx") { + if (arch == "arm64") + return "mac-os-arm64"; + if (arch.contains("64")) + return "mac-os-64"; + if (arch.contains("86")) + return "mac-os-86"; + // Unknown, maybe something new, appending arch + return "mac-os-" + arch; + } else if (sys == "linux") { + if (arch == "x86_64") + return "linux-x64"; + if (arch == "i386") + return "linux-x86"; + // will work for arm32 arm(64) + return "linux-" + arch; + } + return {}; +} +} // namespace SysInfo diff --git a/launcher/SysInfo.h b/launcher/SysInfo.h new file mode 100644 index 000000000..f3688d60d --- /dev/null +++ b/launcher/SysInfo.h @@ -0,0 +1,8 @@ +#include + +namespace SysInfo { +QString currentSystem(); +QString useQTForArch(); +QString getSupportedJavaArchitecture(); +int suitableMaxMem(); +} // namespace SysInfo diff --git a/launcher/Untar.cpp b/launcher/Untar.cpp new file mode 100644 index 000000000..f1963e7aa --- /dev/null +++ b/launcher/Untar.cpp @@ -0,0 +1,260 @@ +// 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 . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "Untar.h" +#include +#include +#include +#include +#include +#include "FileSystem.h" + +// adaptation of the: +// - https://github.com/madler/zlib/blob/develop/contrib/untgz/untgz.c +// - https://en.wikipedia.org/wiki/Tar_(computing) +// - https://github.com/euroelessar/cutereader/blob/master/karchive/src/ktar.cpp + +#define BLOCKSIZE 512 +#define SHORTNAMESIZE 100 + +enum class TypeFlag : char { + Regular = '0', // regular file + ARegular = 0, // regular file + Link = '1', // link + Symlink = '2', // reserved + Character = '3', // character special + Block = '4', // block special + Directory = '5', // directory + FIFO = '6', // FIFO special + Contiguous = '7', // reserved + // Posix stuff + GlobalPosixHeader = 'g', + ExtendedPosixHeader = 'x', + // 'A'– 'Z' Vendor specific extensions(POSIX .1 - 1988) + // GNU + GNULongLink = 'K', /* long link name */ + GNULongName = 'L', /* long file name */ +}; + +// struct Header { /* byte offset */ +// char name[100]; /* 0 */ +// char mode[8]; /* 100 */ +// char uid[8]; /* 108 */ +// char gid[8]; /* 116 */ +// char size[12]; /* 124 */ +// char mtime[12]; /* 136 */ +// char chksum[8]; /* 148 */ +// TypeFlag typeflag; /* 156 */ +// char linkname[100]; /* 157 */ +// char magic[6]; /* 257 */ +// char version[2]; /* 263 */ +// char uname[32]; /* 265 */ +// char gname[32]; /* 297 */ +// char devmajor[8]; /* 329 */ +// char devminor[8]; /* 337 */ +// char prefix[155]; /* 345 */ +// /* 500 */ +// }; + +bool readLonglink(QIODevice* in, qint64 size, QByteArray& longlink) +{ + qint64 n = 0; + size--; // ignore trailing null + if (size < 0) { + qCritical() << "The filename size is negative"; + return false; + } + longlink.resize(size + (BLOCKSIZE - size % BLOCKSIZE)); // make the size divisible by BLOCKSIZE + for (qint64 offset = 0; offset < longlink.size(); offset += BLOCKSIZE) { + n = in->read(longlink.data() + offset, BLOCKSIZE); + if (n != BLOCKSIZE) { + qCritical() << "The expected blocksize was not respected for the name"; + return false; + } + } + longlink.truncate(qstrlen(longlink.constData())); + return true; +} + +int getOctal(char* buffer, int maxlenght, bool* ok) +{ + return QByteArray(buffer, qstrnlen(buffer, maxlenght)).toInt(ok, 8); +} + +QString decodeName(char* name) +{ + return QFile::decodeName(QByteArray(name, qstrnlen(name, 100))); +} +bool Tar::extract(QIODevice* in, QString dst) +{ + char buffer[BLOCKSIZE]; + QString name, symlink, firstFolderName; + bool doNotReset = false, ok; + while (true) { + auto n = in->read(buffer, BLOCKSIZE); + if (n != BLOCKSIZE) { // allways expect complete blocks + qCritical() << "The expected blocksize was not respected"; + return false; + } + if (buffer[0] == 0) { // end of archive + return true; + } + int mode = getOctal(buffer + 100, 8, &ok) | QFile::ReadUser | QFile::WriteUser; // hack to ensure write and read permisions + if (!ok) { + qCritical() << "The file mode can't be read"; + return false; + } + // there are names that are exactly 100 bytes long + // and neither longlink nor \0 terminated (bug:101472) + + if (name.isEmpty()) { + name = decodeName(buffer); + if (!firstFolderName.isEmpty() && name.startsWith(firstFolderName)) { + name = name.mid(firstFolderName.size()); + } + } + if (symlink.isEmpty()) + symlink = decodeName(buffer); + qint64 size = getOctal(buffer + 124, 12, &ok); + if (!ok) { + qCritical() << "The file size can't be read"; + return false; + } + switch (TypeFlag(buffer[156])) { + case TypeFlag::Regular: + /* fallthrough */ + case TypeFlag::ARegular: { + auto fileName = FS::PathCombine(dst, name); + if (!FS::ensureFilePathExists(fileName)) { + qCritical() << "Can't ensure the file path to exist: " << fileName; + return false; + } + QFile out(fileName); + if (!out.open(QFile::WriteOnly)) { + qCritical() << "Can't open file:" << fileName; + return false; + } + out.setPermissions(QFile::Permissions(mode)); + while (size > 0) { + QByteArray tmp(BLOCKSIZE, 0); + n = in->read(tmp.data(), BLOCKSIZE); + if (n != BLOCKSIZE) { + qCritical() << "The expected blocksize was not respected when reading file"; + return false; + } + tmp.truncate(qMin(qint64(BLOCKSIZE), size)); + out.write(tmp); + size -= BLOCKSIZE; + } + break; + } + case TypeFlag::Directory: { + if (firstFolderName.isEmpty()) { + firstFolderName = name; + break; + } + auto folderPath = FS::PathCombine(dst, name); + if (!FS::ensureFolderPathExists(folderPath)) { + qCritical() << "Can't ensure that folder exists: " << folderPath; + return false; + } + break; + } + case TypeFlag::GNULongLink: { + doNotReset = true; + QByteArray longlink; + if (readLonglink(in, size, longlink)) { + symlink = QFile::decodeName(longlink.constData()); + } else { + qCritical() << "Failed to read long link"; + return false; + } + break; + } + case TypeFlag::GNULongName: { + doNotReset = true; + QByteArray longlink; + if (readLonglink(in, size, longlink)) { + name = QFile::decodeName(longlink.constData()); + } else { + qCritical() << "Failed to read long name"; + return false; + } + break; + } + case TypeFlag::Link: + /* fallthrough */ + case TypeFlag::Symlink: { + auto fileName = FS::PathCombine(dst, name); + if (!FS::create_link(FS::PathCombine(QFileInfo(fileName).path(), symlink), fileName)()) { // do not use symlinks + qCritical() << "Can't create link for:" << fileName << " to:" << FS::PathCombine(QFileInfo(fileName).path(), symlink); + return false; + } + FS::ensureFilePathExists(fileName); + QFile::setPermissions(fileName, QFile::Permissions(mode)); + break; + } + case TypeFlag::Character: + /* fallthrough */ + case TypeFlag::Block: + /* fallthrough */ + case TypeFlag::FIFO: + /* fallthrough */ + case TypeFlag::Contiguous: + /* fallthrough */ + case TypeFlag::GlobalPosixHeader: + /* fallthrough */ + case TypeFlag::ExtendedPosixHeader: + /* fallthrough */ + default: + break; + } + if (!doNotReset) { + name.truncate(0); + symlink.truncate(0); + } + doNotReset = false; + } + return true; +} + +bool GZTar::extract(QString src, QString dst) +{ + QuaGzipFile a(src); + if (!a.open(QIODevice::ReadOnly)) { + qCritical() << "Can't open tar file:" << src; + return false; + } + return Tar::extract(&a, dst); +} \ No newline at end of file diff --git a/launcher/Untar.h b/launcher/Untar.h new file mode 100644 index 000000000..50e3a16e3 --- /dev/null +++ b/launcher/Untar.h @@ -0,0 +1,46 @@ +// 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 . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once +#include + +// this is a hack used for the java downloader (feel free to remove it in favor of a library) +// both extract functions will extract the first folder inside dest(disregarding the prefix) +namespace Tar { +bool extract(QIODevice* in, QString dst); +} + +namespace GZTar { +bool extract(QString src, QString dst); +} \ No newline at end of file diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp index 0ab9ae2c3..552900d35 100644 --- a/launcher/VersionProxyModel.cpp +++ b/launcher/VersionProxyModel.cpp @@ -114,10 +114,14 @@ QVariant VersionProxyModel::headerData(int section, Qt::Orientation orientation, return tr("Branch"); case Type: return tr("Type"); - case Architecture: + case CPUArchitecture: return tr("Architecture"); case Path: return tr("Path"); + case JavaName: + return tr("Java Name"); + case JavaMajor: + return tr("Major Version"); case Time: return tr("Released"); } @@ -131,10 +135,14 @@ QVariant VersionProxyModel::headerData(int section, Qt::Orientation orientation, return tr("The version's branch"); case Type: return tr("The version's type"); - case Architecture: + case CPUArchitecture: return tr("CPU Architecture"); case Path: return tr("Filesystem path to this version"); + case JavaName: + return tr("The alternative name of the java version"); + case JavaMajor: + return tr("The java major version"); case Time: return tr("Release date of this version"); } @@ -165,10 +173,14 @@ QVariant VersionProxyModel::data(const QModelIndex& index, int role) const return sourceModel()->data(parentIndex, BaseVersionList::BranchRole); case Type: return sourceModel()->data(parentIndex, BaseVersionList::TypeRole); - case Architecture: - return sourceModel()->data(parentIndex, BaseVersionList::ArchitectureRole); + case CPUArchitecture: + return sourceModel()->data(parentIndex, BaseVersionList::CPUArchitectureRole); case Path: return sourceModel()->data(parentIndex, BaseVersionList::PathRole); + case JavaName: + return sourceModel()->data(parentIndex, BaseVersionList::JavaNameRole); + case JavaMajor: + return sourceModel()->data(parentIndex, BaseVersionList::JavaMajorRole); case Time: return sourceModel()->data(parentIndex, Meta::VersionList::TimeRole).toDate(); default: @@ -308,12 +320,18 @@ void VersionProxyModel::setSourceModel(QAbstractItemModel* replacingRaw) m_columns.push_back(ParentVersion); } */ - if (roles.contains(BaseVersionList::ArchitectureRole)) { - m_columns.push_back(Architecture); + if (roles.contains(BaseVersionList::CPUArchitectureRole)) { + m_columns.push_back(CPUArchitecture); } if (roles.contains(BaseVersionList::PathRole)) { m_columns.push_back(Path); } + if (roles.contains(BaseVersionList::JavaNameRole)) { + m_columns.push_back(JavaName); + } + if (roles.contains(BaseVersionList::JavaMajorRole)) { + m_columns.push_back(JavaMajor); + } if (roles.contains(Meta::VersionList::TimeRole)) { m_columns.push_back(Time); } diff --git a/launcher/VersionProxyModel.h b/launcher/VersionProxyModel.h index 0863a7c80..7965af0ad 100644 --- a/launcher/VersionProxyModel.h +++ b/launcher/VersionProxyModel.h @@ -9,12 +9,12 @@ class VersionFilterModel; class VersionProxyModel : public QAbstractProxyModel { Q_OBJECT public: - enum Column { Name, ParentVersion, Branch, Type, Architecture, Path, Time }; + enum Column { Name, ParentVersion, Branch, Type, CPUArchitecture, Path, Time, JavaName, JavaMajor }; using FilterMap = QHash>; public: VersionProxyModel(QObject* parent = 0); - virtual ~VersionProxyModel(){}; + virtual ~VersionProxyModel() {}; virtual int columnCount(const QModelIndex& parent = QModelIndex()) const override; virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp index 5576b9745..e4157ea2d 100644 --- a/launcher/icons/IconList.cpp +++ b/launcher/icons/IconList.cpp @@ -322,7 +322,7 @@ const MMCIcon* IconList::icon(const QString& key) const bool IconList::deleteIcon(const QString& key) { - return iconFileExists(key) && QFile::remove(icon(key)->getFilePath()); + return iconFileExists(key) && FS::deletePath(icon(key)->getFilePath()); } bool IconList::trashIcon(const QString& key) diff --git a/launcher/icons/IconList.h b/launcher/icons/IconList.h index c51826057..553946c42 100644 --- a/launcher/icons/IconList.h +++ b/launcher/icons/IconList.h @@ -52,7 +52,7 @@ class IconList : public QAbstractListModel { Q_OBJECT public: explicit IconList(const QStringList& builtinPaths, QString path, QObject* parent = 0); - virtual ~IconList(){}; + virtual ~IconList() {}; QIcon getIcon(const QString& key) const; int getIconIndex(const QString& key) const; diff --git a/launcher/icons/IconUtils.cpp b/launcher/icons/IconUtils.cpp index 99c38f47a..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 { @@ -52,8 +52,7 @@ QString findBestIconIn(const QString& folder, const QString& iconKey) while (it.hasNext()) { it.next(); auto fileInfo = it.fileInfo(); - - if (fileInfo.completeBaseName() == iconKey && isIconSuffix(fileInfo.suffix())) + if ((fileInfo.completeBaseName() == iconKey || fileInfo.fileName() == iconKey) && isIconSuffix(fileInfo.suffix())) return fileInfo.absoluteFilePath(); } return {}; diff --git a/launcher/install_prereqs.cmake.in b/launcher/install_prereqs.cmake.in index e4408d161..acbce9650 100644 --- a/launcher/install_prereqs.cmake.in +++ b/launcher/install_prereqs.cmake.in @@ -1,5 +1,4 @@ set(CMAKE_MODULE_PATH "@CMAKE_MODULE_PATH@") - file(GLOB_RECURSE QTPLUGINS "${CMAKE_INSTALL_PREFIX}/@PLUGIN_DEST_DIR@/*@CMAKE_SHARED_LIBRARY_SUFFIX@") function(gp_resolved_file_type_override resolved_file type_var) if(resolved_file MATCHES "^/(usr/)?lib/libQt") diff --git a/launcher/java/JavaChecker.cpp b/launcher/java/JavaChecker.cpp index fc8da55c2..c54a5b04b 100644 --- a/launcher/java/JavaChecker.cpp +++ b/launcher/java/JavaChecker.cpp @@ -40,14 +40,15 @@ #include #include -#include "Application.h" #include "Commandline.h" #include "FileSystem.h" -#include "JavaUtils.h" +#include "java/JavaUtils.h" -JavaChecker::JavaChecker(QObject* parent) : QObject(parent) {} +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) +{} -void JavaChecker::performCheck() +void JavaChecker::executeTask() { QString checkerJar = JavaUtils::getJavaCheckPath(); @@ -72,7 +73,7 @@ void JavaChecker::performCheck() if (m_maxMem != 0) { args << QString("-Xmx%1m").arg(m_maxMem); } - if (m_permGen != 64) { + if (m_permGen != 64 && m_permGen != 0) { args << QString("-XX:PermSize=%1m").arg(m_permGen); } @@ -115,11 +116,10 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) QProcessPtr _process = process; process.reset(); - JavaCheckResult result; - { - result.path = m_path; - result.id = m_id; - } + Result result = { + m_path, + m_id, + }; result.errorLog = m_stderr; result.outLog = m_stdout; qDebug() << "STDOUT" << m_stdout; @@ -127,8 +127,9 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) qDebug() << "Java checker finished with status" << status << "exit code" << exitcode; if (status == QProcess::CrashExit || exitcode == 1) { - result.validity = JavaCheckResult::Validity::Errored; + result.validity = Result::Validity::Errored; emit checkFinished(result); + emitSucceeded(); return; } @@ -161,8 +162,9 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) } if (!results.contains("os.arch") || !results.contains("java.version") || !results.contains("java.vendor") || !success) { - result.validity = JavaCheckResult::Validity::ReturnedInvalidData; + result.validity = Result::Validity::ReturnedInvalidData; emit checkFinished(result); + emitSucceeded(); return; } @@ -171,7 +173,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) auto java_vendor = results["java.vendor"]; bool is_64 = os_arch == "x86_64" || os_arch == "amd64" || os_arch == "aarch64" || os_arch == "arm64"; - result.validity = JavaCheckResult::Validity::Valid; + result.validity = Result::Validity::Valid; result.is_64bit = is_64; result.mojangPlatform = is_64 ? "64" : "32"; result.realPlatform = os_arch; @@ -179,6 +181,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) result.javaVendor = java_vendor; qDebug() << "Java checker succeeded."; emit checkFinished(result); + emitSucceeded(); } void JavaChecker::error(QProcess::ProcessError err) @@ -190,15 +193,9 @@ void JavaChecker::error(QProcess::ProcessError err) qDebug() << "Native environment:"; qDebug() << QProcessEnvironment::systemEnvironment().toStringList(); killTimer.stop(); - JavaCheckResult result; - { - result.path = m_path; - result.id = m_id; - } - - emit checkFinished(result); - return; + emit checkFinished({ m_path, m_id }); } + emitSucceeded(); } void JavaChecker::timeout() diff --git a/launcher/java/JavaChecker.h b/launcher/java/JavaChecker.h index 7111f8522..171a18b76 100644 --- a/launcher/java/JavaChecker.h +++ b/launcher/java/JavaChecker.h @@ -3,49 +3,51 @@ #include #include -#include "QObjectPtr.h" - #include "JavaVersion.h" +#include "QObjectPtr.h" +#include "tasks/Task.h" -class JavaChecker; - -struct JavaCheckResult { - QString path; - QString mojangPlatform; - QString realPlatform; - JavaVersion javaVersion; - QString javaVendor; - QString outLog; - QString errorLog; - bool is_64bit = false; - int id; - enum class Validity { Errored, ReturnedInvalidData, Valid } validity = Validity::Errored; -}; - -using QProcessPtr = shared_qobject_ptr; -using JavaCheckerPtr = shared_qobject_ptr; -class JavaChecker : public QObject { +class JavaChecker : public Task { Q_OBJECT public: - explicit JavaChecker(QObject* parent = 0); - void performCheck(); + using QProcessPtr = shared_qobject_ptr; + using Ptr = shared_qobject_ptr; - QString m_path; - QString m_args; - int m_id = 0; - int m_minMem = 0; - int m_maxMem = 0; - int m_permGen = 64; + struct Result { + QString path; + int id; + QString mojangPlatform; + QString realPlatform; + JavaVersion javaVersion; + QString javaVendor; + QString outLog; + QString errorLog; + bool is_64bit = false; + 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); signals: - void checkFinished(JavaCheckResult result); + void checkFinished(const Result& result); + + protected: + virtual void executeTask() override; private: QProcessPtr process; QTimer killTimer; QString m_stdout; QString m_stderr; - public slots: + + QString m_path; + QString m_args; + int m_minMem = 0; + int m_maxMem = 0; + int m_permGen = 64; + int m_id = 0; + + private slots: void timeout(); void finished(int exitcode, QProcess::ExitStatus); void error(QProcess::ProcessError); diff --git a/launcher/java/JavaCheckerJob.cpp b/launcher/java/JavaCheckerJob.cpp deleted file mode 100644 index 870e2a09a..000000000 --- a/launcher/java/JavaCheckerJob.cpp +++ /dev/null @@ -1,41 +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 "JavaCheckerJob.h" - -#include - -void JavaCheckerJob::partFinished(JavaCheckResult result) -{ - num_finished++; - qDebug() << m_job_name.toLocal8Bit() << "progress:" << num_finished << "/" << javacheckers.size(); - setProgress(num_finished, javacheckers.size()); - - javaresults.replace(result.id, result); - - if (num_finished == javacheckers.size()) { - emitSucceeded(); - } -} - -void JavaCheckerJob::executeTask() -{ - qDebug() << m_job_name.toLocal8Bit() << " started."; - for (auto iter : javacheckers) { - javaresults.append(JavaCheckResult()); - connect(iter.get(), &JavaChecker::checkFinished, this, &JavaCheckerJob::partFinished); - iter->performCheck(); - } -} diff --git a/launcher/java/JavaCheckerJob.h b/launcher/java/JavaCheckerJob.h deleted file mode 100644 index ddf827968..000000000 --- a/launcher/java/JavaCheckerJob.h +++ /dev/null @@ -1,56 +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 -#include "JavaChecker.h" -#include "tasks/Task.h" - -class JavaCheckerJob; -using JavaCheckerJobPtr = shared_qobject_ptr; - -// FIXME: this just seems horribly redundant -class JavaCheckerJob : public Task { - Q_OBJECT - public: - explicit JavaCheckerJob(QString job_name) : Task(), m_job_name(job_name){}; - virtual ~JavaCheckerJob(){}; - - bool addJavaCheckerAction(JavaCheckerPtr base) - { - javacheckers.append(base); - // if this is already running, the action needs to be started right away! - if (isRunning()) { - setProgress(num_finished, javacheckers.size()); - connect(base.get(), &JavaChecker::checkFinished, this, &JavaCheckerJob::partFinished); - base->performCheck(); - } - return true; - } - QList getResults() { return javaresults; } - - private slots: - void partFinished(JavaCheckResult result); - - protected: - virtual void executeTask() override; - - private: - QString m_job_name; - QList javacheckers; - QList javaresults; - int num_finished = 0; -}; diff --git a/launcher/java/JavaInstall.cpp b/launcher/java/JavaInstall.cpp index cfa471402..8e97e0e14 100644 --- a/launcher/java/JavaInstall.cpp +++ b/launcher/java/JavaInstall.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (c) 2023 Trial97 + * 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 diff --git a/launcher/java/JavaInstall.h b/launcher/java/JavaInstall.h index 8c2743a00..7d8d392fa 100644 --- a/launcher/java/JavaInstall.h +++ b/launcher/java/JavaInstall.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (c) 2023 Trial97 + * 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 @@ -40,6 +40,7 @@ struct JavaInstall : public BaseVersion { QString arch; QString path; bool recommended = false; + bool is_64bit = false; }; using JavaInstallPtr = std::shared_ptr; diff --git a/launcher/java/JavaInstallList.cpp b/launcher/java/JavaInstallList.cpp index d8be4963f..569fda306 100644 --- a/launcher/java/JavaInstallList.cpp +++ b/launcher/java/JavaInstallList.cpp @@ -38,13 +38,17 @@ #include #include +#include -#include "java/JavaCheckerJob.h" +#include "Application.h" +#include "java/JavaChecker.h" #include "java/JavaInstallList.h" #include "java/JavaUtils.h" -#include "minecraft/VersionFilterData.h" +#include "tasks/ConcurrentTask.h" -JavaInstallList::JavaInstallList(QObject* parent) : BaseVersionList(parent) {} +JavaInstallList::JavaInstallList(QObject* parent, bool onlyManagedVersions) + : BaseVersionList(parent), m_only_managed_versions(onlyManagedVersions) +{} Task::Ptr JavaInstallList::getLoadTask() { @@ -55,7 +59,7 @@ Task::Ptr JavaInstallList::getLoadTask() Task::Ptr JavaInstallList::getCurrentTask() { if (m_status == Status::InProgress) { - return m_loadTask; + return m_load_task; } return nullptr; } @@ -64,8 +68,8 @@ void JavaInstallList::load() { if (m_status != Status::InProgress) { m_status = Status::InProgress; - m_loadTask.reset(new JavaListLoadTask(this)); - m_loadTask->start(); + m_load_task.reset(new JavaListLoadTask(this, m_only_managed_versions)); + m_load_task->start(); } } @@ -106,7 +110,7 @@ QVariant JavaInstallList::data(const QModelIndex& index, int role) const return version->recommended; case PathRole: return version->path; - case ArchitectureRole: + case CPUArchitectureRole: return version->arch; default: return QVariant(); @@ -115,7 +119,7 @@ QVariant JavaInstallList::data(const QModelIndex& index, int role) const BaseVersionList::RoleList JavaInstallList::providesRoles() const { - return { VersionPointerRole, VersionIdRole, VersionRole, RecommendedRole, PathRole, ArchitectureRole }; + return { VersionPointerRole, VersionIdRole, VersionRole, RecommendedRole, PathRole, CPUArchitectureRole }; } void JavaInstallList::updateListData(QList versions) @@ -129,7 +133,7 @@ void JavaInstallList::updateListData(QList versions) } endResetModel(); m_status = Status::Done; - m_loadTask.reset(); + m_load_task.reset(); } bool sortJavas(BaseVersion::Ptr left, BaseVersion::Ptr right) @@ -146,35 +150,30 @@ void JavaInstallList::sortVersions() endResetModel(); } -JavaListLoadTask::JavaListLoadTask(JavaInstallList* vlist) : Task() +JavaListLoadTask::JavaListLoadTask(JavaInstallList* vlist, bool onlyManagedVersions) : Task(), m_only_managed_versions(onlyManagedVersions) { m_list = vlist; - m_currentRecommended = NULL; + m_current_recommended = NULL; } -JavaListLoadTask::~JavaListLoadTask() {} - void JavaListLoadTask::executeTask() { setStatus(tr("Detecting Java installations...")); JavaUtils ju; - QList candidate_paths = ju.FindJavaPaths(); + QList candidate_paths = m_only_managed_versions ? getPrismJavaBundle() : ju.FindJavaPaths(); - m_job.reset(new JavaCheckerJob("Java detection")); + ConcurrentTask::Ptr job(new ConcurrentTask(this, "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); qDebug() << "Probing the following Java paths: "; int id = 0; for (QString candidate : candidate_paths) { - qDebug() << " " << candidate; - - auto candidate_checker = new JavaChecker(); - candidate_checker->m_path = candidate; - candidate_checker->m_id = id; - m_job->addJavaCheckerAction(JavaCheckerPtr(candidate_checker)); - + auto checker = new JavaChecker(candidate, "", 0, 0, 0, id, this); + connect(checker, &JavaChecker::checkFinished, [this](const JavaChecker::Result& result) { m_results << result; }); + job->addTask(Task::Ptr(checker)); id++; } @@ -184,16 +183,17 @@ void JavaListLoadTask::executeTask() void JavaListLoadTask::javaCheckerFinished() { QList candidates; - auto results = m_job->getResults(); + std::sort(m_results.begin(), m_results.end(), [](const JavaChecker::Result& a, const JavaChecker::Result& b) { return a.id < b.id; }); qDebug() << "Found the following valid Java installations:"; - for (JavaCheckResult result : results) { - if (result.validity == JavaCheckResult::Validity::Valid) { + for (auto result : m_results) { + if (result.validity == JavaChecker::Result::Validity::Valid) { JavaInstallPtr javaVersion(new JavaInstall()); javaVersion->id = result.javaVersion; javaVersion->arch = result.realPlatform; javaVersion->path = result.path; + javaVersion->is_64bit = result.is_64bit; candidates.append(javaVersion); qDebug() << " " << javaVersion->id.toString() << javaVersion->arch << javaVersion->path; diff --git a/launcher/java/JavaInstallList.h b/launcher/java/JavaInstallList.h index 1eebadf23..b77f17b28 100644 --- a/launcher/java/JavaInstallList.h +++ b/launcher/java/JavaInstallList.h @@ -19,9 +19,9 @@ #include #include "BaseVersionList.h" +#include "java/JavaChecker.h" #include "tasks/Task.h" -#include "JavaCheckerJob.h" #include "JavaInstall.h" #include "QObjectPtr.h" @@ -33,9 +33,9 @@ class JavaInstallList : public BaseVersionList { enum class Status { NotDone, InProgress, Done }; public: - explicit JavaInstallList(QObject* parent = 0); + explicit JavaInstallList(QObject* parent = 0, bool onlyManagedVersions = false); - Task::Ptr getLoadTask() override; + [[nodiscard]] Task::Ptr getLoadTask() override; bool isLoaded() override; const BaseVersion::Ptr at(int i) const override; int count() const override; @@ -53,23 +53,27 @@ class JavaInstallList : public BaseVersionList { protected: Status m_status = Status::NotDone; - shared_qobject_ptr m_loadTask; + shared_qobject_ptr m_load_task; QList m_vlist; + bool m_only_managed_versions; }; class JavaListLoadTask : public Task { Q_OBJECT public: - explicit JavaListLoadTask(JavaInstallList* vlist); - virtual ~JavaListLoadTask(); + explicit JavaListLoadTask(JavaInstallList* vlist, bool onlyManagedVersions = false); + virtual ~JavaListLoadTask() = default; + protected: void executeTask() override; public slots: void javaCheckerFinished(); protected: - shared_qobject_ptr m_job; + Task::Ptr m_job; JavaInstallList* m_list; - JavaInstall* m_currentRecommended; + JavaInstall* m_current_recommended; + QList m_results; + bool m_only_managed_versions; }; diff --git a/launcher/java/JavaMetadata.cpp b/launcher/java/JavaMetadata.cpp new file mode 100644 index 000000000..2d68f55c8 --- /dev/null +++ b/launcher/java/JavaMetadata.cpp @@ -0,0 +1,128 @@ +// 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/JavaMetadata.h" + +#include + +#include "Json.h" +#include "StringUtils.h" +#include "java/JavaVersion.h" +#include "minecraft/ParseUtils.h" + +namespace Java { + +DownloadType parseDownloadType(QString javaDownload) +{ + if (javaDownload == "manifest") + return DownloadType::Manifest; + else if (javaDownload == "archive") + return DownloadType::Archive; + else + return DownloadType::Unknown; +} +QString downloadTypeToString(DownloadType javaDownload) +{ + switch (javaDownload) { + case DownloadType::Manifest: + return "manifest"; + case DownloadType::Archive: + return "archive"; + case DownloadType::Unknown: + break; + } + return "unknown"; +} +MetadataPtr parseJavaMeta(const QJsonObject& in) +{ + auto meta = std::make_shared(); + + meta->m_name = Json::ensureString(in, "name", ""); + meta->vendor = Json::ensureString(in, "vendor", ""); + meta->url = Json::ensureString(in, "url", ""); + meta->releaseTime = timeFromS3Time(Json::ensureString(in, "releaseTime", "")); + meta->downloadType = parseDownloadType(Json::ensureString(in, "downloadType", "")); + meta->packageType = Json::ensureString(in, "packageType", ""); + meta->runtimeOS = Json::ensureString(in, "runtimeOS", "unknown"); + + if (in.contains("checksum")) { + auto obj = Json::requireObject(in, "checksum"); + meta->checksumHash = Json::ensureString(obj, "hash", ""); + meta->checksumType = Json::ensureString(obj, "type", ""); + } + + if (in.contains("version")) { + auto obj = Json::requireObject(in, "version"); + auto name = Json::ensureString(obj, "name", ""); + auto major = Json::ensureInteger(obj, "major", 0); + auto minor = Json::ensureInteger(obj, "minor", 0); + auto security = Json::ensureInteger(obj, "security", 0); + auto build = Json::ensureInteger(obj, "build", 0); + meta->version = JavaVersion(major, minor, security, build, name); + } + return meta; +} + +bool Metadata::operator<(const Metadata& rhs) +{ + auto id = version; + if (id < rhs.version) { + return true; + } + if (id > rhs.version) { + return false; + } + auto date = releaseTime; + if (date < rhs.releaseTime) { + return true; + } + if (date > rhs.releaseTime) { + return false; + } + return StringUtils::naturalCompare(m_name, rhs.m_name, Qt::CaseInsensitive) < 0; +} + +bool Metadata::operator==(const Metadata& rhs) +{ + return version == rhs.version && m_name == rhs.m_name; +} + +bool Metadata::operator>(const Metadata& rhs) +{ + return (!operator<(rhs)) && (!operator==(rhs)); +} + +bool Metadata::operator<(BaseVersion& a) +{ + try { + return operator<(dynamic_cast(a)); + } catch (const std::bad_cast& e) { + return BaseVersion::operator<(a); + } +} + +bool Metadata::operator>(BaseVersion& a) +{ + try { + return operator>(dynamic_cast(a)); + } catch (const std::bad_cast& e) { + return BaseVersion::operator>(a); + } +} + +} // namespace Java diff --git a/launcher/java/JavaMetadata.h b/launcher/java/JavaMetadata.h new file mode 100644 index 000000000..77a42fd78 --- /dev/null +++ b/launcher/java/JavaMetadata.h @@ -0,0 +1,64 @@ +// 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 + +#include + +#include "BaseVersion.h" +#include "java/JavaVersion.h" + +namespace Java { + +enum class DownloadType { Manifest, Archive, Unknown }; + +class Metadata : public BaseVersion { + public: + virtual QString descriptor() override { return version.toString(); } + + virtual QString name() override { return m_name; } + + virtual QString typeString() const override { return vendor; } + + virtual bool operator<(BaseVersion& a) override; + virtual bool operator>(BaseVersion& a) override; + bool operator<(const Metadata& rhs); + bool operator==(const Metadata& rhs); + bool operator>(const Metadata& rhs); + + QString m_name; + QString vendor; + QString url; + QDateTime releaseTime; + QString checksumType; + QString checksumHash; + DownloadType downloadType; + QString packageType; + JavaVersion version; + QString runtimeOS; +}; +using MetadataPtr = std::shared_ptr; + +DownloadType parseDownloadType(QString javaDownload); +QString downloadTypeToString(DownloadType javaDownload); +MetadataPtr parseJavaMeta(const QJsonObject& libObj); + +} // namespace Java \ No newline at end of file diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp index 3627cec39..bc8026348 100644 --- a/launcher/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -79,11 +79,9 @@ QProcessEnvironment CleanEnviroment() QStringList stripped = { #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) - "LD_LIBRARY_PATH", - "LD_PRELOAD", + "LD_LIBRARY_PATH", "LD_PRELOAD", #endif - "QT_PLUGIN_PATH", - "QT_FONTPATH" + "QT_PLUGIN_PATH", "QT_FONTPATH" }; for (auto key : rawenv.keys()) { auto value = rawenv.value(key); @@ -184,56 +182,58 @@ QList JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString else if (keyType == KEY_WOW64_32KEY) archType = "32"; - HKEY jreKey; - if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, keyName.toStdWString().c_str(), 0, KEY_READ | keyType | KEY_ENUMERATE_SUB_KEYS, &jreKey) == - ERROR_SUCCESS) { - // Read the current type version from the registry. - // This will be used to find any key that contains the JavaHome value. + for (HKEY baseRegistry : { HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE }) { + HKEY jreKey; + if (RegOpenKeyExW(baseRegistry, keyName.toStdWString().c_str(), 0, KEY_READ | keyType | KEY_ENUMERATE_SUB_KEYS, &jreKey) == + ERROR_SUCCESS) { + // Read the current type version from the registry. + // This will be used to find any key that contains the JavaHome value. - WCHAR subKeyName[255]; - DWORD subKeyNameSize, numSubKeys, retCode; + WCHAR subKeyName[255]; + DWORD subKeyNameSize, numSubKeys, retCode; - // Get the number of subkeys - RegQueryInfoKeyW(jreKey, NULL, NULL, NULL, &numSubKeys, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + // Get the number of subkeys + RegQueryInfoKeyW(jreKey, NULL, NULL, NULL, &numSubKeys, NULL, NULL, NULL, NULL, NULL, NULL, NULL); - // Iterate until RegEnumKeyEx fails - if (numSubKeys > 0) { - for (DWORD i = 0; i < numSubKeys; i++) { - subKeyNameSize = 255; - retCode = RegEnumKeyExW(jreKey, i, subKeyName, &subKeyNameSize, NULL, NULL, NULL, NULL); - QString newSubkeyName = QString::fromWCharArray(subKeyName); - if (retCode == ERROR_SUCCESS) { - // Now open the registry key for the version that we just got. - QString newKeyName = keyName + "\\" + newSubkeyName + subkeySuffix; + // Iterate until RegEnumKeyEx fails + if (numSubKeys > 0) { + for (DWORD i = 0; i < numSubKeys; i++) { + subKeyNameSize = 255; + retCode = RegEnumKeyExW(jreKey, i, subKeyName, &subKeyNameSize, NULL, NULL, NULL, NULL); + QString newSubkeyName = QString::fromWCharArray(subKeyName); + if (retCode == ERROR_SUCCESS) { + // Now open the registry key for the version that we just got. + QString newKeyName = keyName + "\\" + newSubkeyName + subkeySuffix; - HKEY newKey; - if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, newKeyName.toStdWString().c_str(), 0, KEY_READ | KEY_WOW64_64KEY, &newKey) == - ERROR_SUCCESS) { - // Read the JavaHome value to find where Java is installed. - DWORD valueSz = 0; - if (RegQueryValueExW(newKey, keyJavaDir.toStdWString().c_str(), NULL, NULL, NULL, &valueSz) == ERROR_SUCCESS) { - WCHAR* value = new WCHAR[valueSz]; - RegQueryValueExW(newKey, keyJavaDir.toStdWString().c_str(), NULL, NULL, (BYTE*)value, &valueSz); + HKEY newKey; + if (RegOpenKeyExW(baseRegistry, newKeyName.toStdWString().c_str(), 0, KEY_READ | keyType, &newKey) == + ERROR_SUCCESS) { + // Read the JavaHome value to find where Java is installed. + DWORD valueSz = 0; + if (RegQueryValueExW(newKey, keyJavaDir.toStdWString().c_str(), NULL, NULL, NULL, &valueSz) == ERROR_SUCCESS) { + WCHAR* value = new WCHAR[valueSz]; + RegQueryValueExW(newKey, keyJavaDir.toStdWString().c_str(), NULL, NULL, (BYTE*)value, &valueSz); - QString newValue = QString::fromWCharArray(value); - delete[] value; + QString newValue = QString::fromWCharArray(value); + delete[] value; - // Now, we construct the version object and add it to the list. - JavaInstallPtr javaVersion(new JavaInstall()); + // Now, we construct the version object and add it to the list. + JavaInstallPtr javaVersion(new JavaInstall()); - javaVersion->id = newSubkeyName; - javaVersion->arch = archType; - javaVersion->path = QDir(FS::PathCombine(newValue, "bin")).absoluteFilePath("javaw.exe"); - javas.append(javaVersion); + javaVersion->id = newSubkeyName; + javaVersion->arch = archType; + javaVersion->path = QDir(FS::PathCombine(newValue, "bin")).absoluteFilePath("javaw.exe"); + javas.append(javaVersion); + } + + RegCloseKey(newKey); } - - RegCloseKey(newKey); } } } - } - RegCloseKey(jreKey); + RegCloseKey(jreKey); + } } return javas; @@ -283,6 +283,12 @@ QList JavaUtils::FindJavaPaths() QList ADOPTIUMJDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Adoptium\\JDK", "Path", "\\hotspot\\MSI"); + // IBM Semeru + QList SEMERUJRE32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Semeru\\JRE", "Path", "\\openj9\\MSI"); + QList SEMERUJRE64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Semeru\\JRE", "Path", "\\openj9\\MSI"); + QList SEMERUJDK32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Semeru\\JDK", "Path", "\\openj9\\MSI"); + QList SEMERUJDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Semeru\\JDK", "Path", "\\openj9\\MSI"); + // Microsoft QList MICROSOFTJDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Microsoft\\JDK", "Path", "\\hotspot\\MSI"); @@ -300,6 +306,7 @@ QList JavaUtils::FindJavaPaths() java_candidates.append(NEWJRE64s); java_candidates.append(ADOPTOPENJRE64s); java_candidates.append(ADOPTIUMJRE64s); + java_candidates.append(SEMERUJRE64s); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre8/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre7/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre6/bin/javaw.exe")); @@ -308,6 +315,7 @@ QList JavaUtils::FindJavaPaths() java_candidates.append(ADOPTOPENJDK64s); java_candidates.append(FOUNDATIONJDK64s); java_candidates.append(ADOPTIUMJDK64s); + java_candidates.append(SEMERUJDK64s); java_candidates.append(MICROSOFTJDK64s); java_candidates.append(ZULU64s); java_candidates.append(LIBERICA64s); @@ -316,6 +324,7 @@ QList JavaUtils::FindJavaPaths() java_candidates.append(NEWJRE32s); java_candidates.append(ADOPTOPENJRE32s); java_candidates.append(ADOPTIUMJRE32s); + java_candidates.append(SEMERUJRE32s); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre8/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre7/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre6/bin/javaw.exe")); @@ -324,6 +333,7 @@ QList JavaUtils::FindJavaPaths() java_candidates.append(ADOPTOPENJDK32s); java_candidates.append(FOUNDATIONJDK32s); java_candidates.append(ADOPTIUMJDK32s); + java_candidates.append(SEMERUJDK32s); java_candidates.append(ZULU32s); java_candidates.append(LIBERICA32s); @@ -337,6 +347,7 @@ QList JavaUtils::FindJavaPaths() } candidates.append(getMinecraftJavaBundle()); + candidates.append(getPrismJavaBundle()); candidates = addJavasFromEnv(candidates); candidates.removeDuplicates(); return candidates; @@ -362,23 +373,47 @@ QList JavaUtils::FindJavaPaths() javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java"); } + + auto home = qEnvironmentVariable("HOME"); + + // javas downloaded by sdkman + QDir sdkmanDir(FS::PathCombine(home, ".sdkman/candidates/java")); + QStringList sdkmanJavas = sdkmanDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + foreach (const QString& java, sdkmanJavas) { + javas.append(sdkmanDir.absolutePath() + "/" + java + "/bin/java"); + } + + // java in user library folder (like from intellij downloads) + QDir userLibraryJVMDir(FS::PathCombine(home, "Library/Java/JavaVirtualMachines/")); + QStringList userLibraryJVMJavas = userLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + foreach (const QString& java, userLibraryJVMJavas) { + javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); + javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java"); + } + javas.append(getMinecraftJavaBundle()); + javas.append(getPrismJavaBundle()); javas = addJavasFromEnv(javas); javas.removeDuplicates(); return javas; } -#elif defined(Q_OS_LINUX) +#elif defined(Q_OS_LINUX) || defined(Q_OS_OPENBSD) || defined(Q_OS_FREEBSD) QList JavaUtils::FindJavaPaths() { QList javas; javas.append(this->GetDefaultJava()->path); - auto scanJavaDir = [&](const QString& dirPath) { + auto scanJavaDir = [&]( + const QString& dirPath, + const std::function& filter = [](const QFileInfo&) { return true; }) { QDir dir(dirPath); if (!dir.exists()) return; auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); for (auto& entry : entries) { + if (!filter(entry)) + continue; + QString prefix; prefix = entry.canonicalFilePath(); javas.append(FS::PathCombine(prefix, "jre/bin/java")); @@ -393,20 +428,33 @@ QList JavaUtils::FindJavaPaths() scanJavaDir(snap + dirPath); } }; +#if defined(Q_OS_LINUX) // oracle RPMs scanJavaDirs("/usr/java"); // general locations used by distro packaging scanJavaDirs("/usr/lib/jvm"); scanJavaDirs("/usr/lib64/jvm"); scanJavaDirs("/usr/lib32/jvm"); + // Gentoo's locations for openjdk and openjdk-bin respectively + auto gentooFilter = [](const QFileInfo& info) { + QString fileName = info.fileName(); + return fileName.startsWith("openjdk-") || fileName.startsWith("openj9-"); + }; + scanJavaDir("/usr/lib64", gentooFilter); + scanJavaDir("/usr/lib", gentooFilter); + scanJavaDir("/opt", gentooFilter); // javas stored in Prism Launcher's folder scanJavaDirs("java"); // manually installed JDKs in /opt scanJavaDirs("/opt/jdk"); scanJavaDirs("/opt/jdks"); + scanJavaDirs("/opt/ibm"); // IBM Semeru Certified Edition // flatpak scanJavaDirs("/app/jdk"); - +#elif defined(Q_OS_OPENBSD) || defined(Q_OS_FREEBSD) + // ports install to /usr/local on OpenBSD & FreeBSD + scanJavaDirs("/usr/local"); +#endif auto home = qEnvironmentVariable("HOME"); // javas downloaded by IntelliJ @@ -417,6 +465,7 @@ QList JavaUtils::FindJavaPaths() scanJavaDirs(FS::PathCombine(home, ".gradle/jdks")); javas.append(getMinecraftJavaBundle()); + javas.append(getPrismJavaBundle()); javas = addJavasFromEnv(javas); javas.removeDuplicates(); return javas; @@ -430,6 +479,8 @@ QList JavaUtils::FindJavaPaths() javas.append(this->GetDefaultJava()->path); javas.append(getMinecraftJavaBundle()); + javas.append(getPrismJavaBundle()); + javas.removeDuplicates(); return addJavasFromEnv(javas); } #endif @@ -441,12 +492,10 @@ QString JavaUtils::getJavaCheckPath() QStringList getMinecraftJavaBundle() { - QString executable = "java"; QStringList processpaths; #if defined(Q_OS_OSX) processpaths << FS::PathCombine(QDir::homePath(), FS::PathCombine("Library", "Application Support", "minecraft", "runtime")); #elif defined(Q_OS_WIN32) - executable += "w.exe"; auto appDataPath = QProcessEnvironment::systemEnvironment().value("APPDATA", ""); processpaths << FS::PathCombine(QFileInfo(appDataPath).absoluteFilePath(), ".minecraft", "runtime"); @@ -471,7 +520,7 @@ QStringList getMinecraftJavaBundle() auto binFound = false; for (auto& entry : entries) { if (entry.baseName() == "bin") { - javas.append(FS::PathCombine(entry.canonicalFilePath(), executable)); + javas.append(FS::PathCombine(entry.canonicalFilePath(), JavaUtils::javaExecutable)); binFound = true; break; } @@ -484,3 +533,33 @@ QStringList getMinecraftJavaBundle() } return javas; } + +#if defined(Q_OS_WIN32) +const QString JavaUtils::javaExecutable = "javaw.exe"; +#else +const QString JavaUtils::javaExecutable = "java"; +#endif + +QStringList getPrismJavaBundle() +{ + QList javas; + + auto scanDir = [&](QString prefix) { + javas.append(FS::PathCombine(prefix, "jre", "bin", JavaUtils::javaExecutable)); + javas.append(FS::PathCombine(prefix, "bin", JavaUtils::javaExecutable)); + javas.append(FS::PathCombine(prefix, JavaUtils::javaExecutable)); + }; + auto scanJavaDir = [&](const QString& dirPath) { + QDir dir(dirPath); + if (!dir.exists()) + return; + auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); + for (auto& entry : entries) { + scanDir(entry.canonicalFilePath()); + } + }; + + scanJavaDir(APPLICATION->javaPath()); + + return javas; +} diff --git a/launcher/java/JavaUtils.h b/launcher/java/JavaUtils.h index 2fb03af7a..eb3a17316 100644 --- a/launcher/java/JavaUtils.h +++ b/launcher/java/JavaUtils.h @@ -15,10 +15,9 @@ #pragma once +#include #include - -#include "JavaChecker.h" -#include "JavaInstallList.h" +#include "java/JavaInstall.h" #ifdef Q_OS_WIN #include @@ -27,6 +26,7 @@ QString stripVariableEntries(QString name, QString target, QString remove); QProcessEnvironment CleanEnviroment(); QStringList getMinecraftJavaBundle(); +QStringList getPrismJavaBundle(); class JavaUtils : public QObject { Q_OBJECT @@ -42,4 +42,5 @@ class JavaUtils : public QObject { #endif static QString getJavaCheckPath(); + static const QString javaExecutable; }; diff --git a/launcher/java/JavaVersion.cpp b/launcher/java/JavaVersion.cpp index b77bf2adf..5e9700012 100644 --- a/launcher/java/JavaVersion.cpp +++ b/launcher/java/JavaVersion.cpp @@ -43,12 +43,12 @@ QString JavaVersion::toString() const return m_string; } -bool JavaVersion::requiresPermGen() +bool JavaVersion::requiresPermGen() const { return !m_parseable || m_major < 8; } -bool JavaVersion::isModular() +bool JavaVersion::isModular() const { return m_parseable && m_major >= 9; } @@ -59,12 +59,6 @@ bool JavaVersion::operator<(const JavaVersion& rhs) auto major = m_major; auto rmajor = rhs.m_major; - // HACK: discourage using java 9 - if (major > 8) - major = -major; - if (rmajor > 8) - rmajor = -rmajor; - if (major < rmajor) return true; if (major > rmajor) @@ -109,3 +103,24 @@ bool JavaVersion::operator>(const JavaVersion& rhs) { return (!operator<(rhs)) && (!operator==(rhs)); } + +JavaVersion::JavaVersion(int major, int minor, int security, int build, QString name) + : m_major(major), m_minor(minor), m_security(security), m_name(name), m_parseable(true) +{ + QStringList versions; + if (build != 0) { + m_prerelease = QString::number(build); + versions.push_front(m_prerelease); + } + if (m_security != 0) + versions.push_front(QString::number(m_security)); + else if (!versions.isEmpty()) + versions.push_front("0"); + + if (m_minor != 0) + versions.push_front(QString::number(m_minor)); + else if (!versions.isEmpty()) + versions.push_front("0"); + versions.push_front(QString::number(m_major)); + m_string = versions.join("."); +} diff --git a/launcher/java/JavaVersion.h b/launcher/java/JavaVersion.h index 421578ea1..dfb4770da 100644 --- a/launcher/java/JavaVersion.h +++ b/launcher/java/JavaVersion.h @@ -16,6 +16,7 @@ class JavaVersion { public: JavaVersion() {} JavaVersion(const QString& rhs); + JavaVersion(int major, int minor, int security, int build = 0, QString name = ""); JavaVersion& operator=(const QString& rhs); @@ -23,21 +24,24 @@ class JavaVersion { bool operator==(const JavaVersion& rhs); bool operator>(const JavaVersion& rhs); - bool requiresPermGen(); + bool requiresPermGen() const; - bool isModular(); + bool isModular() const; QString toString() const; - int major() { return m_major; } - int minor() { return m_minor; } - int security() { return m_security; } + int major() const { return m_major; } + int minor() const { return m_minor; } + int security() const { return m_security; } + QString build() const { return m_prerelease; } + QString name() const { return m_name; } private: QString m_string; int m_major = 0; int m_minor = 0; int m_security = 0; + QString m_name = ""; bool m_parseable = false; QString m_prerelease; }; diff --git a/launcher/java/download/ArchiveDownloadTask.cpp b/launcher/java/download/ArchiveDownloadTask.cpp new file mode 100644 index 000000000..6d6ab0cef --- /dev/null +++ b/launcher/java/download/ArchiveDownloadTask.cpp @@ -0,0 +1,141 @@ +// 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/ArchiveDownloadTask.h" +#include +#include +#include "MMCZip.h" + +#include "Application.h" +#include "Untar.h" +#include "net/ChecksumValidator.h" +#include "net/NetJob.h" +#include "tasks/Task.h" + +namespace Java { +ArchiveDownloadTask::ArchiveDownloadTask(QUrl url, QString final_path, QString checksumType, QString checksumHash) + : m_url(url), m_final_path(final_path), m_checksum_type(checksumType), m_checksum_hash(checksumHash) +{} + +void ArchiveDownloadTask::executeTask() +{ + // JRE found ! download the zip + setStatus(tr("Downloading Java")); + + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("java", m_url.fileName()); + + auto download = makeShared(QString("JRE::DownloadJava"), APPLICATION->network()); + auto action = Net::Download::makeCached(m_url, entry); + if (!m_checksum_hash.isEmpty() && !m_checksum_type.isEmpty()) { + auto hashType = QCryptographicHash::Algorithm::Sha1; + if (m_checksum_type == "sha256") { + hashType = QCryptographicHash::Algorithm::Sha256; + } + action->addValidator(new Net::ChecksumValidator(hashType, QByteArray::fromHex(m_checksum_hash.toUtf8()))); + } + download->addNetAction(action); + auto fullPath = entry->getFullPath(); + + connect(download.get(), &Task::failed, this, &ArchiveDownloadTask::emitFailed); + connect(download.get(), &Task::progress, this, &ArchiveDownloadTask::setProgress); + connect(download.get(), &Task::stepProgress, this, &ArchiveDownloadTask::propagateStepProgress); + connect(download.get(), &Task::status, this, &ArchiveDownloadTask::setStatus); + connect(download.get(), &Task::details, this, &ArchiveDownloadTask::setDetails); + connect(download.get(), &Task::succeeded, [this, fullPath] { + // This should do all of the extracting and creating folders + extractJava(fullPath); + }); + m_task = download; + m_task->start(); +} + +void ArchiveDownloadTask::extractJava(QString input) +{ + setStatus(tr("Extracting java")); + if (input.endsWith("tar")) { + setStatus(tr("Extracting Java (Progress is not reported for tar archives)")); + QFile in(input); + if (!in.open(QFile::ReadOnly)) { + emitFailed(tr("Unable to open supplied tar file.")); + return; + } + if (!Tar::extract(&in, QDir(m_final_path).absolutePath())) { + emitFailed(tr("Unable to extract supplied tar file.")); + return; + } + emitSucceeded(); + return; + } else if (input.endsWith("tar.gz") || input.endsWith("taz") || input.endsWith("tgz")) { + setStatus(tr("Extracting Java (Progress is not reported for tar archives)")); + if (!GZTar::extract(input, QDir(m_final_path).absolutePath())) { + emitFailed(tr("Unable to extract supplied tar file.")); + return; + } + emitSucceeded(); + return; + } else if (input.endsWith("zip")) { + auto zip = std::make_shared(input); + if (!zip->open(QuaZip::mdUnzip)) { + emitFailed(tr("Unable to open supplied zip file.")); + return; + } + auto files = zip->getFileNameList(); + if (files.isEmpty()) { + emitFailed(tr("No files were found in the supplied zip file.")); + return; + } + m_task = makeShared(zip, m_final_path, files[0]); + + auto progressStep = std::make_shared(); + connect(m_task.get(), &Task::finished, this, [this, progressStep] { + progressStep->state = TaskStepState::Succeeded; + stepProgress(*progressStep); + }); + + connect(m_task.get(), &Task::succeeded, this, &ArchiveDownloadTask::emitSucceeded); + connect(m_task.get(), &Task::aborted, this, &ArchiveDownloadTask::emitAborted); + connect(m_task.get(), &Task::failed, this, [this, progressStep](QString reason) { + progressStep->state = TaskStepState::Failed; + stepProgress(*progressStep); + emitFailed(reason); + }); + connect(m_task.get(), &Task::stepProgress, this, &ArchiveDownloadTask::propagateStepProgress); + + connect(m_task.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) { + progressStep->update(current, total); + stepProgress(*progressStep); + }); + connect(m_task.get(), &Task::status, this, [this, progressStep](QString status) { + progressStep->status = status; + stepProgress(*progressStep); + }); + m_task->start(); + return; + } + + emitFailed(tr("Could not determine archive type!")); +} + +bool ArchiveDownloadTask::abort() +{ + auto aborted = canAbort(); + if (m_task) + aborted = m_task->abort(); + emitAborted(); + return aborted; +}; +} // namespace Java \ No newline at end of file diff --git a/launcher/java/download/ArchiveDownloadTask.h b/launcher/java/download/ArchiveDownloadTask.h new file mode 100644 index 000000000..1db33763a --- /dev/null +++ b/launcher/java/download/ArchiveDownloadTask.h @@ -0,0 +1,45 @@ +// 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 "tasks/Task.h" + +namespace Java { +class ArchiveDownloadTask : public Task { + Q_OBJECT + public: + ArchiveDownloadTask(QUrl url, QString final_path, QString checksumType = "", QString checksumHash = ""); + virtual ~ArchiveDownloadTask() = default; + + [[nodiscard]] bool canAbort() const override { return true; } + void executeTask() override; + virtual bool abort() override; + + private slots: + void extractJava(QString input); + + protected: + QUrl m_url; + QString m_final_path; + QString m_checksum_type; + QString m_checksum_hash; + Task::Ptr m_task; +}; +} // namespace Java \ No newline at end of file diff --git a/launcher/java/download/ManifestDownloadTask.cpp b/launcher/java/download/ManifestDownloadTask.cpp new file mode 100644 index 000000000..836afeaac --- /dev/null +++ b/launcher/java/download/ManifestDownloadTask.cpp @@ -0,0 +1,138 @@ +// 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/ManifestDownloadTask.h" + +#include "Application.h" +#include "FileSystem.h" +#include "Json.h" +#include "net/ChecksumValidator.h" +#include "net/NetJob.h" + +struct File { + QString path; + QString url; + QByteArray hash; + bool isExec; +}; + +namespace Java { +ManifestDownloadTask::ManifestDownloadTask(QUrl url, QString final_path, QString checksumType, QString checksumHash) + : m_url(url), m_final_path(final_path), m_checksum_type(checksumType), m_checksum_hash(checksumHash) +{} + +void ManifestDownloadTask::executeTask() +{ + setStatus(tr("Downloading Java")); + auto download = makeShared(QString("JRE::DownloadJava"), APPLICATION->network()); + auto files = std::make_shared(); + + auto action = Net::Download::makeByteArray(m_url, files); + if (!m_checksum_hash.isEmpty() && !m_checksum_type.isEmpty()) { + auto hashType = QCryptographicHash::Algorithm::Sha1; + if (m_checksum_type == "sha256") { + hashType = QCryptographicHash::Algorithm::Sha256; + } + action->addValidator(new Net::ChecksumValidator(hashType, QByteArray::fromHex(m_checksum_hash.toUtf8()))); + } + download->addNetAction(action); + + connect(download.get(), &Task::failed, this, &ManifestDownloadTask::emitFailed); + connect(download.get(), &Task::progress, this, &ManifestDownloadTask::setProgress); + connect(download.get(), &Task::stepProgress, this, &ManifestDownloadTask::propagateStepProgress); + connect(download.get(), &Task::status, this, &ManifestDownloadTask::setStatus); + connect(download.get(), &Task::details, this, &ManifestDownloadTask::setDetails); + + connect(download.get(), &Task::succeeded, [files, this] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*files, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response at " << parse_error.offset << ". Reason: " << parse_error.errorString(); + qWarning() << *files; + emitFailed(parse_error.errorString()); + return; + } + downloadJava(doc); + }); + m_task = download; + m_task->start(); +} + +void ManifestDownloadTask::downloadJava(const QJsonDocument& doc) +{ + // valid json doc, begin making jre spot + FS::ensureFolderPathExists(m_final_path); + std::vector toDownload; + auto list = Json::ensureObject(Json::ensureObject(doc.object()), "files"); + for (const auto& paths : list.keys()) { + auto file = FS::PathCombine(m_final_path, paths); + + const QJsonObject& meta = Json::ensureObject(list, paths); + auto type = Json::ensureString(meta, "type"); + if (type == "directory") { + FS::ensureFolderPathExists(file); + } else if (type == "link") { + // this is linux only ! + auto path = Json::ensureString(meta, "target"); + if (!path.isEmpty()) { + auto target = FS::PathCombine(file, "../" + path); + QFile(target).link(file); + } + } else if (type == "file") { + // TODO download compressed version if it exists ? + auto raw = Json::ensureObject(Json::ensureObject(meta, "downloads"), "raw"); + auto isExec = Json::ensureBoolean(meta, "executable", false); + auto url = Json::ensureString(raw, "url"); + if (!url.isEmpty() && QUrl(url).isValid()) { + auto f = File{ file, url, QByteArray::fromHex(Json::ensureString(raw, "sha1").toLatin1()), isExec }; + toDownload.push_back(f); + } + } + } + auto elementDownload = makeShared("JRE::FileDownload", APPLICATION->network()); + for (const auto& file : toDownload) { + auto dl = Net::Download::makeFile(file.url, file.path); + if (!file.hash.isEmpty()) { + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, file.hash)); + } + if (file.isExec) { + connect(dl.get(), &Net::Download::succeeded, + [file] { QFile(file.path).setPermissions(QFile(file.path).permissions() | QFileDevice::Permissions(0x1111)); }); + } + elementDownload->addNetAction(dl); + } + + connect(elementDownload.get(), &Task::failed, this, &ManifestDownloadTask::emitFailed); + connect(elementDownload.get(), &Task::progress, this, &ManifestDownloadTask::setProgress); + connect(elementDownload.get(), &Task::stepProgress, this, &ManifestDownloadTask::propagateStepProgress); + connect(elementDownload.get(), &Task::status, this, &ManifestDownloadTask::setStatus); + connect(elementDownload.get(), &Task::details, this, &ManifestDownloadTask::setDetails); + + connect(elementDownload.get(), &Task::succeeded, this, &ManifestDownloadTask::emitSucceeded); + m_task = elementDownload; + m_task->start(); +} + +bool ManifestDownloadTask::abort() +{ + auto aborted = canAbort(); + if (m_task) + aborted = m_task->abort(); + emitAborted(); + return aborted; +}; +} // namespace Java \ No newline at end of file diff --git a/launcher/java/download/ManifestDownloadTask.h b/launcher/java/download/ManifestDownloadTask.h new file mode 100644 index 000000000..ae9e0d0ed --- /dev/null +++ b/launcher/java/download/ManifestDownloadTask.h @@ -0,0 +1,46 @@ +// 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 "tasks/Task.h" + +namespace Java { + +class ManifestDownloadTask : public Task { + Q_OBJECT + public: + ManifestDownloadTask(QUrl url, QString final_path, QString checksumType = "", QString checksumHash = ""); + virtual ~ManifestDownloadTask() = default; + + [[nodiscard]] bool canAbort() const override { return true; } + void executeTask() override; + virtual bool abort() override; + + private slots: + void downloadJava(const QJsonDocument& doc); + + protected: + QUrl m_url; + QString m_final_path; + QString m_checksum_type; + QString m_checksum_hash; + Task::Ptr m_task; +}; +} // namespace Java \ No newline at end of file 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..f3e9dfce0 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(parent), 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 b1bec2b4a..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); @@ -37,9 +34,9 @@ class LaunchStep : public Task { void progressReportingRequest(); public slots: - virtual void proceed(){}; + virtual void proceed() {}; // called in the opposite order than the Task launch(), used to clean up or otherwise undo things after the launch ends - virtual void finalize(){}; + virtual void finalize() {}; protected: /* data */ LaunchTask* m_parent; diff --git a/launcher/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp index 06a32bd28..4e4f5ead4 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() diff --git a/launcher/launch/LaunchTask.h b/launcher/launch/LaunchTask.h index e79c43557..2fd8c78c7 100644 --- a/launcher/launch/LaunchTask.h +++ b/launcher/launch/LaunchTask.h @@ -41,7 +41,6 @@ #include "BaseInstance.h" #include "LaunchStep.h" #include "LogModel.h" -#include "LoggedProcess.h" #include "MessageLevel.h" class LaunchTask : public Task { @@ -55,7 +54,7 @@ class LaunchTask : public Task { public: /* methods */ static shared_qobject_ptr create(InstancePtr inst); - virtual ~LaunchTask(){}; + virtual ~LaunchTask() = default; void appendStep(shared_qobject_ptr step); void prependStep(shared_qobject_ptr step); 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 9262cdbe4..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 81337a88e..55d13b58c 100644 --- a/launcher/launch/steps/CheckJava.cpp +++ b/launcher/launch/steps/CheckJava.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include "java/JavaUtils.h" @@ -45,20 +46,23 @@ void CheckJava::executeTask() { auto instance = m_parent->instance(); auto settings = instance->settings(); - m_javaPath = FS::ResolveExecutable(settings->get("JavaPath").toString()); + + QString javaPathSetting = settings->get("JavaPath").toString(); + m_javaPath = FS::ResolveExecutable(javaPathSetting); + bool perInstance = settings->get("OverrideJava").toBool() || settings->get("OverrideJavaLocation").toBool(); auto realJavaPath = QStandardPaths::findExecutable(m_javaPath); if (realJavaPath.isEmpty()) { if (perInstance) { - emit logLine(QString("The java binary \"%1\" couldn't be found. Please fix the java path " + emit logLine(QString("The Java binary \"%1\" couldn't be found. Please fix the Java path " "override in the instance's settings or disable it.") - .arg(m_javaPath), + .arg(javaPathSetting), MessageLevel::Warning); } else { - emit logLine(QString("The java binary \"%1\" couldn't be found. Please set up java in " + emit logLine(QString("The Java binary \"%1\" couldn't be found. Please set up Java in " "the settings.") - .arg(m_javaPath), + .arg(javaPathSetting), MessageLevel::Warning); } emitFailed(QString("Java path is not valid.")); @@ -90,11 +94,10 @@ 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); + m_JavaChecker.reset(new JavaChecker(realJavaPath, "", 0, 0, 0, 0, this)); emit logLine(QString("Checking Java version..."), MessageLevel::Launcher); connect(m_JavaChecker.get(), &JavaChecker::checkFinished, this, &CheckJava::checkJavaFinished); - m_JavaChecker->m_path = realJavaPath; - m_JavaChecker->performCheck(); + m_JavaChecker->start(); return; } else { auto verString = instance->settings()->get("JavaVersion").toString(); @@ -103,13 +106,14 @@ void CheckJava::executeTask() auto vendorString = instance->settings()->get("JavaVendor").toString(); printJavaInfo(verString, archString, realArchString, vendorString); } + m_parent->instance()->updateRuntimeContext(); emitSucceeded(); } -void CheckJava::checkJavaFinished(JavaCheckResult result) +void CheckJava::checkJavaFinished(const JavaChecker::Result& result) { switch (result.validity) { - case JavaCheckResult::Validity::Errored: { + case JavaChecker::Result::Validity::Errored: { // Error message displayed if java can't start emit logLine(QString("Could not start java:"), MessageLevel::Error); emit logLines(result.errorLog.split('\n'), MessageLevel::Error); @@ -117,14 +121,15 @@ void CheckJava::checkJavaFinished(JavaCheckResult result) emitFailed(QString("Could not start java!")); return; } - case JavaCheckResult::Validity::ReturnedInvalidData: { + case JavaChecker::Result::Validity::ReturnedInvalidData: { 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; } - case JavaCheckResult::Validity::Valid: { + case JavaChecker::Result::Validity::Valid: { auto instance = m_parent->instance(); printJavaInfo(result.javaVersion.toString(), result.mojangPlatform, result.realPlatform, result.javaVendor); instance->settings()->set("JavaVersion", result.javaVersion.toString()); @@ -132,6 +137,7 @@ void CheckJava::checkJavaFinished(JavaCheckResult 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 4436e2a55..1c59b0053 100644 --- a/launcher/launch/steps/CheckJava.h +++ b/launcher/launch/steps/CheckJava.h @@ -22,13 +22,13 @@ class CheckJava : public LaunchStep { Q_OBJECT public: - explicit CheckJava(LaunchTask* parent) : LaunchStep(parent){}; - virtual ~CheckJava(){}; + explicit CheckJava(LaunchTask* parent) : LaunchStep(parent) {}; + virtual ~CheckJava() = default; virtual void executeTask(); virtual bool canAbort() const { return false; } private slots: - void checkJavaFinished(JavaCheckResult result); + void checkJavaFinished(const JavaChecker::Result& result); private: void printJavaInfo(const QString& version, const QString& architecture, const QString& realArchitecture, const QString& vendor); @@ -37,5 +37,5 @@ class CheckJava : public LaunchStep { private: QString m_javaPath; QString m_javaSignature; - JavaCheckerPtr m_JavaChecker; + JavaChecker::Ptr m_JavaChecker; }; diff --git a/launcher/launch/steps/LookupServerAddress.cpp b/launcher/launch/steps/LookupServerAddress.cpp index 9bdac203b..4b67b3092 100644 --- a/launcher/launch/steps/LookupServerAddress.cpp +++ b/launcher/launch/steps/LookupServerAddress.cpp @@ -30,7 +30,7 @@ void LookupServerAddress::setLookupAddress(const QString& lookupAddress) m_dnsLookup->setName(QString("_minecraft._tcp.%1").arg(lookupAddress)); } -void LookupServerAddress::setOutputAddressPtr(MinecraftServerTargetPtr output) +void LookupServerAddress::setOutputAddressPtr(MinecraftTarget::Ptr output) { m_output = std::move(output); } diff --git a/launcher/launch/steps/LookupServerAddress.h b/launcher/launch/steps/LookupServerAddress.h index abd92a5e8..506314ee8 100644 --- a/launcher/launch/steps/LookupServerAddress.h +++ b/launcher/launch/steps/LookupServerAddress.h @@ -19,20 +19,20 @@ #include #include -#include "minecraft/launch/MinecraftServerTarget.h" +#include "minecraft/launch/MinecraftTarget.h" class LookupServerAddress : public LaunchStep { Q_OBJECT public: explicit LookupServerAddress(LaunchTask* parent); - virtual ~LookupServerAddress(){}; + virtual ~LookupServerAddress() = default; virtual void executeTask(); virtual bool abort(); virtual bool canAbort() const { return true; } void setLookupAddress(const QString& lookupAddress); - void setOutputAddressPtr(MinecraftServerTargetPtr output); + void setOutputAddressPtr(MinecraftTarget::Ptr output); private slots: void on_dnsLookupFinished(); @@ -42,5 +42,5 @@ class LookupServerAddress : public LaunchStep { QDnsLookup* m_dnsLookup; QString m_lookupAddress; - MinecraftServerTargetPtr m_output; + MinecraftTarget::Ptr m_output; }; diff --git a/launcher/launch/steps/PostLaunchCommand.h b/launcher/launch/steps/PostLaunchCommand.h index 578433b86..fd1443b29 100644 --- a/launcher/launch/steps/PostLaunchCommand.h +++ b/launcher/launch/steps/PostLaunchCommand.h @@ -22,7 +22,7 @@ class PostLaunchCommand : public LaunchStep { Q_OBJECT public: explicit PostLaunchCommand(LaunchTask* parent); - virtual ~PostLaunchCommand(){}; + virtual ~PostLaunchCommand() {}; virtual void executeTask(); virtual bool abort(); diff --git a/launcher/launch/steps/PreLaunchCommand.h b/launcher/launch/steps/PreLaunchCommand.h index 10568ea34..b6dc6cd8b 100644 --- a/launcher/launch/steps/PreLaunchCommand.h +++ b/launcher/launch/steps/PreLaunchCommand.h @@ -22,7 +22,7 @@ class PreLaunchCommand : public LaunchStep { Q_OBJECT public: explicit PreLaunchCommand(LaunchTask* parent); - virtual ~PreLaunchCommand(){}; + virtual ~PreLaunchCommand() {}; virtual void executeTask(); virtual bool abort(); diff --git a/launcher/launch/steps/PrintServers.cpp b/launcher/launch/steps/PrintServers.cpp new file mode 100644 index 000000000..ba96d37b9 --- /dev/null +++ b/launcher/launch/steps/PrintServers.cpp @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Leia uwu + * + * 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 "PrintServers.h" +#include "QHostInfo" + +PrintServers::PrintServers(LaunchTask* parent, const QStringList& servers) : LaunchStep(parent) +{ + m_servers = servers; +} + +void PrintServers::executeTask() +{ + for (QString server : m_servers) { + QHostInfo::lookupHost(server, this, &PrintServers::resolveServer); + } +} + +void PrintServers::resolveServer(const QHostInfo& host_info) +{ + QString server = host_info.hostName(); + QString addresses = server + " resolves to:\n ["; + + if (!host_info.addresses().isEmpty()) { + for (QHostAddress address : host_info.addresses()) { + addresses += address.toString(); + if (!host_info.addresses().endsWith(address)) { + addresses += ", "; + } + } + } else { + addresses += "N/A"; + } + addresses += "]\n\n"; + + m_server_to_address.insert(server, addresses); + + // print server info in order once all servers are resolved + if (m_server_to_address.size() >= m_servers.size()) { + for (QString serv : m_servers) { + emit logLine(m_server_to_address.value(serv), MessageLevel::Launcher); + } + emitSucceeded(); + } +} + +bool PrintServers::canAbort() const +{ + return true; +} diff --git a/launcher/launch/steps/PrintServers.h b/launcher/launch/steps/PrintServers.h new file mode 100644 index 000000000..7d2f1b194 --- /dev/null +++ b/launcher/launch/steps/PrintServers.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Leia uwu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include +#include +#include +#include + +class PrintServers : public LaunchStep { + Q_OBJECT + public: + PrintServers(LaunchTask* parent, const QStringList& servers); + + virtual void executeTask(); + virtual bool canAbort() const; + + private: + void resolveServer(const QHostInfo& host_info); + QMap m_server_to_address; + QStringList m_servers; +}; diff --git a/launcher/launch/steps/QuitAfterGameStop.h b/launcher/launch/steps/QuitAfterGameStop.h index 9326b2a8c..19ca59632 100644 --- a/launcher/launch/steps/QuitAfterGameStop.h +++ b/launcher/launch/steps/QuitAfterGameStop.h @@ -23,8 +23,8 @@ class QuitAfterGameStop : public LaunchStep { Q_OBJECT public: - explicit QuitAfterGameStop(LaunchTask* parent) : LaunchStep(parent){}; - virtual ~QuitAfterGameStop(){}; + explicit QuitAfterGameStop(LaunchTask* parent) : LaunchStep(parent) {}; + virtual ~QuitAfterGameStop() = default; virtual void executeTask(); virtual bool canAbort() const { return false; } diff --git a/launcher/launch/steps/TextPrint.h b/launcher/launch/steps/TextPrint.h index bd6c28567..a96c2f887 100644 --- a/launcher/launch/steps/TextPrint.h +++ b/launcher/launch/steps/TextPrint.h @@ -28,7 +28,7 @@ class TextPrint : public LaunchStep { public: explicit TextPrint(LaunchTask* parent, const QStringList& lines, MessageLevel::Enum level); explicit TextPrint(LaunchTask* parent, const QString& line, MessageLevel::Enum level); - virtual ~TextPrint(){}; + virtual ~TextPrint() {}; virtual void executeTask(); virtual bool canAbort() const; 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/BaseEntity.cpp b/launcher/meta/BaseEntity.cpp index 5f9804e48..b0e754ada 100644 --- a/launcher/meta/BaseEntity.cpp +++ b/launcher/meta/BaseEntity.cpp @@ -15,27 +15,43 @@ #include "BaseEntity.h" +#include "Exception.h" +#include "FileSystem.h" #include "Json.h" +#include "modplatform/helpers/HashUtils.h" #include "net/ApiDownload.h" +#include "net/ChecksumValidator.h" #include "net/HttpMetaCache.h" +#include "net/Mode.h" #include "net/NetJob.h" #include "Application.h" #include "BuildConfig.h" +#include "tasks/Task.h" + +namespace Meta { class ParsingValidator : public Net::Validator { public: /* con/des */ - ParsingValidator(Meta::BaseEntity* entity) : m_entity(entity){}; - virtual ~ParsingValidator(){}; + ParsingValidator(BaseEntity* entity) : m_entity(entity) {}; + virtual ~ParsingValidator() = default; public: /* methods */ - bool init(QNetworkRequest&) override { return true; } + bool init(QNetworkRequest&) override + { + m_data.clear(); + return true; + } bool write(QByteArray& data) override { this->m_data.append(data); return true; } - bool abort() override { return true; } + bool abort() override + { + m_data.clear(); + return true; + } bool validate(QNetworkReply&) override { auto fname = m_entity->localFilename(); @@ -52,93 +68,131 @@ class ParsingValidator : public Net::Validator { private: /* data */ QByteArray m_data; - Meta::BaseEntity* m_entity; + BaseEntity* m_entity; }; -Meta::BaseEntity::~BaseEntity() {} - -QUrl Meta::BaseEntity::url() const +QUrl BaseEntity::url() const { auto s = APPLICATION->settings(); QString metaOverride = s->get("MetaURLOverride").toString(); if (metaOverride.isEmpty()) { return QUrl(BuildConfig.META_URL).resolved(localFilename()); - } else { - return QUrl(metaOverride).resolved(localFilename()); } + return QUrl(metaOverride).resolved(localFilename()); } -bool Meta::BaseEntity::loadLocalFile() +Task::Ptr BaseEntity::loadTask(Net::Mode mode) { - const QString fname = QDir("meta").absoluteFilePath(localFilename()); - if (!QFile::exists(fname)) { - return false; - } - // TODO: check if the file has the expected checksum - try { - auto doc = Json::requireDocument(fname, fname); - auto obj = Json::requireObject(doc, fname); - parse(obj); - return true; - } catch (const Exception& e) { - qDebug() << QString("Unable to parse file %1: %2").arg(fname, e.cause()); - // just make sure it's gone and we never consider it again. - QFile::remove(fname); - return false; + if (m_task && m_task->isRunning()) { + return m_task; } + m_task.reset(new BaseEntityLoadTask(this, mode)); + return m_task; } -void Meta::BaseEntity::load(Net::Mode loadType) +bool BaseEntity::isLoaded() const { - // load local file if nothing is loaded yet - if (!isLoaded()) { - if (loadLocalFile()) { - m_loadStatus = LoadStatus::Local; + // consider it loaded only if the main hash is either empty and was remote loadded or the hashes match and was loaded + return m_sha256.isEmpty() ? m_load_status == LoadStatus::Remote : m_load_status != LoadStatus::NotLoaded && m_sha256 == m_file_sha256; +} + +void BaseEntity::setSha256(QString sha256) +{ + m_sha256 = sha256; +} + +BaseEntity::LoadStatus BaseEntity::status() const +{ + return m_load_status; +} + +BaseEntityLoadTask::BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode) : m_entity(parent), m_mode(mode) {} + +void BaseEntityLoadTask::executeTask() +{ + const QString fname = QDir("meta").absoluteFilePath(m_entity->localFilename()); + auto hashMatches = false; + // the file exists on disk try to load it + if (QFile::exists(fname)) { + try { + QByteArray fileData; + // read local file if nothing is loaded yet + if (m_entity->m_load_status == BaseEntity::LoadStatus::NotLoaded || m_entity->m_file_sha256.isEmpty()) { + setStatus(tr("Loading local file")); + fileData = FS::read(fname); + m_entity->m_file_sha256 = Hashing::hash(fileData, Hashing::Algorithm::Sha256); + } + + // on online the hash needs to match + hashMatches = m_entity->m_sha256 == m_entity->m_file_sha256; + if (m_mode == Net::Mode::Online && !m_entity->m_sha256.isEmpty() && !hashMatches) { + throw Exception("mismatched checksum"); + } + + // load local file + if (m_entity->m_load_status == BaseEntity::LoadStatus::NotLoaded) { + auto doc = Json::requireDocument(fileData, fname); + auto obj = Json::requireObject(doc, fname); + m_entity->parse(obj); + m_entity->m_load_status = BaseEntity::LoadStatus::Local; + } + + } catch (const Exception& e) { + qDebug() << QString("Unable to parse file %1: %2").arg(fname, e.cause()); + // just make sure it's gone and we never consider it again. + FS::deletePath(fname); + m_entity->m_load_status = BaseEntity::LoadStatus::NotLoaded; } } // if we need remote update, run the update task - if (loadType == Net::Mode::Offline || !shouldStartRemoteUpdate()) { + auto wasLoadedOffline = m_entity->m_load_status != BaseEntity::LoadStatus::NotLoaded && m_mode == Net::Mode::Offline; + // if has is not present allways fetch from remote(e.g. the main index file), else only fetch if hash doesn't match + auto wasLoadedRemote = m_entity->m_sha256.isEmpty() ? m_entity->m_load_status == BaseEntity::LoadStatus::Remote : hashMatches; + if (wasLoadedOffline || wasLoadedRemote) { + emitSucceeded(); return; } - m_updateTask.reset(new NetJob(QObject::tr("Download of meta file %1").arg(localFilename()), APPLICATION->network())); - auto url = this->url(); - auto entry = APPLICATION->metacache()->resolveEntry("meta", localFilename()); + m_task.reset(new NetJob(QObject::tr("Download of meta file %1").arg(m_entity->localFilename()), APPLICATION->network())); + auto url = m_entity->url(); + auto entry = APPLICATION->metacache()->resolveEntry("meta", m_entity->localFilename()); entry->setStale(true); auto dl = Net::ApiDownload::makeCached(url, entry); /* * The validator parses the file and loads it into the object. * If that fails, the file is not written to storage. */ - dl->addValidator(new ParsingValidator(this)); - m_updateTask->addNetAction(dl); - m_updateStatus = UpdateStatus::InProgress; - QObject::connect(m_updateTask.get(), &NetJob::succeeded, [&]() { - m_loadStatus = LoadStatus::Remote; - m_updateStatus = UpdateStatus::Succeeded; - m_updateTask.reset(); + if (!m_entity->m_sha256.isEmpty()) + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Algorithm::Sha256, m_entity->m_sha256)); + dl->addValidator(new ParsingValidator(m_entity)); + m_task->addNetAction(dl); + m_task->setAskRetry(false); + connect(m_task.get(), &Task::failed, this, &BaseEntityLoadTask::emitFailed); + connect(m_task.get(), &Task::succeeded, this, &BaseEntityLoadTask::emitSucceeded); + connect(m_task.get(), &Task::succeeded, this, [this]() { + m_entity->m_load_status = BaseEntity::LoadStatus::Remote; + m_entity->m_file_sha256 = m_entity->m_sha256; }); - QObject::connect(m_updateTask.get(), &NetJob::failed, [&]() { - m_updateStatus = UpdateStatus::Failed; - m_updateTask.reset(); - }); - m_updateTask->start(); + + connect(m_task.get(), &Task::progress, this, &Task::setProgress); + connect(m_task.get(), &Task::stepProgress, this, &BaseEntityLoadTask::propagateStepProgress); + connect(m_task.get(), &Task::status, this, &Task::setStatus); + connect(m_task.get(), &Task::details, this, &Task::setDetails); + + m_task->start(); } -bool Meta::BaseEntity::isLoaded() const +bool BaseEntityLoadTask::canAbort() const { - return m_loadStatus > LoadStatus::NotLoaded; + return m_task ? m_task->canAbort() : false; } -bool Meta::BaseEntity::shouldStartRemoteUpdate() const +bool BaseEntityLoadTask::abort() { - // TODO: version-locks and offline mode? - return m_updateStatus != UpdateStatus::InProgress; -} - -Task::Ptr Meta::BaseEntity::getCurrentTask() -{ - if (m_updateStatus == UpdateStatus::InProgress) { - return m_updateTask; + if (m_task) { + Task::abort(); + return m_task->abort(); } - return nullptr; + return Task::abort(); } + +} // namespace Meta diff --git a/launcher/meta/BaseEntity.h b/launcher/meta/BaseEntity.h index 1336a5217..17aa0cb87 100644 --- a/launcher/meta/BaseEntity.h +++ b/launcher/meta/BaseEntity.h @@ -17,38 +17,57 @@ #include #include -#include "QObjectPtr.h" #include "net/Mode.h" #include "net/NetJob.h" +#include "tasks/Task.h" namespace Meta { +class BaseEntityLoadTask; class BaseEntity { + friend BaseEntityLoadTask; + public: /* types */ using Ptr = std::shared_ptr; enum class LoadStatus { NotLoaded, Local, Remote }; - enum class UpdateStatus { NotDone, InProgress, Failed, Succeeded }; public: - virtual ~BaseEntity(); - - virtual void parse(const QJsonObject& obj) = 0; + virtual ~BaseEntity() = default; virtual QString localFilename() const = 0; virtual QUrl url() const; - bool isLoaded() const; - bool shouldStartRemoteUpdate() const; + LoadStatus status() const; - void load(Net::Mode loadType); - Task::Ptr getCurrentTask(); + /* for parsers */ + void setSha256(QString sha256); - protected: /* methods */ - bool loadLocalFile(); + virtual void parse(const QJsonObject& obj) = 0; + [[nodiscard]] Task::Ptr loadTask(Net::Mode loadType = Net::Mode::Online); + + protected: + QString m_sha256; // the expected sha256 + QString m_file_sha256; // the file sha256 private: - LoadStatus m_loadStatus = LoadStatus::NotLoaded; - UpdateStatus m_updateStatus = UpdateStatus::NotDone; - NetJob::Ptr m_updateTask; + LoadStatus m_load_status = LoadStatus::NotLoaded; + Task::Ptr m_task; +}; + +class BaseEntityLoadTask : public Task { + Q_OBJECT + + public: + explicit BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode); + ~BaseEntityLoadTask() override = default; + + virtual void executeTask() override; + virtual bool canAbort() const override; + virtual bool abort() override; + + private: + BaseEntity* m_entity; + Net::Mode m_mode; + NetJob::Ptr m_task; }; } // namespace Meta diff --git a/launcher/meta/Index.cpp b/launcher/meta/Index.cpp index 657019f8a..bd0745b6b 100644 --- a/launcher/meta/Index.cpp +++ b/launcher/meta/Index.cpp @@ -16,7 +16,10 @@ #include "Index.h" #include "JsonFormat.h" +#include "QObjectPtr.h" #include "VersionList.h" +#include "meta/BaseEntity.h" +#include "tasks/SequentialTask.h" namespace Meta { Index::Index(QObject* parent) : QAbstractListModel(parent) {} @@ -51,14 +54,17 @@ QVariant Index::data(const QModelIndex& index, int role) const } return QVariant(); } + int Index::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : m_lists.size(); } + int Index::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : 1; } + QVariant Index::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole && section == 0) { @@ -79,6 +85,7 @@ VersionList::Ptr Index::get(const QString& uid) if (!out) { out = std::make_shared(uid); m_uids[uid] = out; + m_lists.append(out); } return out; } @@ -96,7 +103,7 @@ void Index::parse(const QJsonObject& obj) void Index::merge(const std::shared_ptr& other) { - const QVector lists = std::dynamic_pointer_cast(other)->m_lists; + const QVector lists = other->m_lists; // initial load, no need to merge if (m_lists.isEmpty()) { beginResetModel(); @@ -123,7 +130,33 @@ void Index::merge(const std::shared_ptr& other) void Index::connectVersionList(const int row, const VersionList::Ptr& list) { - connect(list.get(), &VersionList::nameChanged, this, - [this, row]() { emit dataChanged(index(row), index(row), QVector() << Qt::DisplayRole); }); + connect(list.get(), &VersionList::nameChanged, this, [this, row] { emit dataChanged(index(row), index(row), { Qt::DisplayRole }); }); +} + +Task::Ptr Index::loadVersion(const QString& uid, const QString& version, Net::Mode mode, bool force) +{ + if (mode == Net::Mode::Offline) { + return get(uid, version)->loadTask(mode); + } + + 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)); + if (status() != BaseEntity::LoadStatus::Remote || force) { + loadTask->addTask(this->loadTask(mode)); + } + loadTask->addTask(versionList->loadTask(mode)); + loadTask->addTask(versionList->getVersion(version)->loadTask(mode)); + return loadTask; +} + +Version::Ptr Index::getLoadedVersion(const QString& uid, const QString& version) +{ + QEventLoop ev; + auto task = loadVersion(uid, version); + QObject::connect(task.get(), &Task::finished, &ev, &QEventLoop::quit); + task->start(); + ev.exec(); + return get(uid, version); } } // namespace Meta diff --git a/launcher/meta/Index.h b/launcher/meta/Index.h index 2c650ce2f..026a00c07 100644 --- a/launcher/meta/Index.h +++ b/launcher/meta/Index.h @@ -16,10 +16,10 @@ #pragma once #include -#include #include "BaseEntity.h" #include "meta/VersionList.h" +#include "net/Mode.h" class Task; @@ -30,6 +30,7 @@ class Index : public QAbstractListModel, public BaseEntity { public: explicit Index(QObject* parent = nullptr); explicit Index(const QVector& lists, QObject* parent = nullptr); + virtual ~Index() = default; enum { UidRole = Qt::UserRole, NameRole, ListPtrRole }; @@ -47,8 +48,15 @@ class Index : public QAbstractListModel, public BaseEntity { QVector lists() const { return m_lists; } + Task::Ptr loadVersion(const QString& uid, const QString& version = {}, Net::Mode mode = Net::Mode::Online, bool force = false); + + // this blocks until the version is loaded + Version::Ptr getLoadedVersion(const QString& uid, const QString& version); + public: // for usage by parsers only void merge(const std::shared_ptr& other); + + protected: void parse(const QJsonObject& obj) override; private: diff --git a/launcher/meta/JsonFormat.cpp b/launcher/meta/JsonFormat.cpp index 6c993f720..86af7277e 100644 --- a/launcher/meta/JsonFormat.cpp +++ b/launcher/meta/JsonFormat.cpp @@ -41,6 +41,7 @@ static std::shared_ptr parseIndexInternal(const QJsonObject& obj) std::transform(objects.begin(), objects.end(), std::back_inserter(lists), [](const QJsonObject& obj) { VersionList::Ptr list = std::make_shared(requireString(obj, "uid")); list->setName(ensureString(obj, "name", QString())); + list->setSha256(ensureString(obj, "sha256", QString())); return list; }); return std::make_shared(lists); @@ -58,6 +59,9 @@ static Version::Ptr parseCommonVersion(const QString& uid, const QJsonObject& ob parseRequires(obj, &reqs, "requires"); parseRequires(obj, &conflicts, "conflicts"); version->setRequires(reqs, conflicts); + if (auto sha256 = ensureString(obj, "sha256", QString()); !sha256.isEmpty()) { + version->setSha256(sha256); + } return version; } diff --git a/launcher/meta/JsonFormat.h b/launcher/meta/JsonFormat.h index d474bcc39..7fbf808a7 100644 --- a/launcher/meta/JsonFormat.h +++ b/launcher/meta/JsonFormat.h @@ -16,11 +16,9 @@ #pragma once #include -#include #include #include "Exception.h" -#include "meta/BaseEntity.h" namespace Meta { class Index; diff --git a/launcher/meta/Version.cpp b/launcher/meta/Version.cpp index 655a20b93..74e71e91c 100644 --- a/launcher/meta/Version.cpp +++ b/launcher/meta/Version.cpp @@ -18,12 +18,9 @@ #include #include "JsonFormat.h" -#include "minecraft/PackProfile.h" Meta::Version::Version(const QString& uid, const QString& version) : BaseVersion(), m_uid(uid), m_version(version) {} -Meta::Version::~Version() {} - QString Meta::Version::descriptor() { return m_version; @@ -71,6 +68,9 @@ void Meta::Version::mergeFromList(const Meta::Version::Ptr& other) if (m_volatile != other->m_volatile) { setVolatile(other->m_volatile); } + if (!other->m_sha256.isEmpty()) { + m_sha256 = other->m_sha256; + } } void Meta::Version::merge(const Version::Ptr& other) diff --git a/launcher/meta/Version.h b/launcher/meta/Version.h index 24da12d6d..46dc740da 100644 --- a/launcher/meta/Version.h +++ b/launcher/meta/Version.h @@ -38,7 +38,7 @@ class Version : public QObject, public BaseVersion, public BaseEntity { using Ptr = std::shared_ptr; explicit Version(const QString& uid, const QString& version); - virtual ~Version(); + virtual ~Version() = default; QString descriptor() override; QString name() override; @@ -52,7 +52,7 @@ class Version : public QObject, public BaseVersion, public BaseEntity { const Meta::RequireSet& requiredSet() const { return m_requires; } VersionFilePtr data() const { return m_data; } bool isRecommended() const { return m_recommended; } - bool isLoaded() const { return m_data != nullptr; } + bool isLoaded() const { return m_data != nullptr && BaseEntity::isLoaded(); } void merge(const Version::Ptr& other); void mergeFromList(const Version::Ptr& other); diff --git a/launcher/meta/VersionList.cpp b/launcher/meta/VersionList.cpp index 7b7ae1fa3..6856b5f8d 100644 --- a/launcher/meta/VersionList.cpp +++ b/launcher/meta/VersionList.cpp @@ -16,9 +16,15 @@ #include "VersionList.h" #include +#include +#include "Application.h" +#include "Index.h" #include "JsonFormat.h" #include "Version.h" +#include "meta/BaseEntity.h" +#include "net/Mode.h" +#include "tasks/SequentialTask.h" namespace Meta { VersionList::VersionList(const QString& uid, QObject* parent) : BaseVersionList(parent), m_uid(uid) @@ -28,8 +34,11 @@ VersionList::VersionList(const QString& uid, QObject* parent) : BaseVersionList( Task::Ptr VersionList::getLoadTask() { - load(Net::Mode::Online); - return getCurrentTask(); + auto loadTask = + makeShared(this, 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; } bool VersionList::isLoaded() @@ -91,7 +100,14 @@ 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")) { + major = "Java " + major.mid(4); + } + return major; + } // FIXME: this should be determined in whatever view/proxy is used... // case LatestRole: return version == getLatestStable(); default: @@ -101,10 +117,14 @@ QVariant VersionList::data(const QModelIndex& index, int role) const BaseVersionList::RoleList VersionList::providesRoles() const { - return { VersionPointerRole, VersionRole, VersionIdRole, ParentVersionRole, TypeRole, UidRole, - TimeRole, RequiresRole, SortRole, RecommendedRole, LatestRole, VersionPtrRole }; + return m_provided_roles; } +void VersionList::setProvidedRoles(RoleList roles) +{ + m_provided_roles = roles; +}; + QHash VersionList::roleNames() const { QHash roles = BaseVersionList::roleNames(); @@ -131,6 +151,8 @@ Version::Ptr VersionList::getVersion(const QString& version) if (!out) { out = std::make_shared(m_uid, version); m_lookup[version] = out; + setupAddedVersion(m_versions.size(), out); + m_versions.append(out); } return out; } @@ -171,6 +193,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) { @@ -191,6 +223,9 @@ void VersionList::mergeFromIndex(const VersionList::Ptr& other) if (m_name != other->m_name) { setName(other->m_name); } + if (!other->m_sha256.isEmpty()) { + m_sha256 = other->m_sha256; + } } void VersionList::merge(const VersionList::Ptr& other) @@ -198,23 +233,27 @@ void VersionList::merge(const VersionList::Ptr& other) if (m_name != other->m_name) { setName(other->m_name); } + if (!other->m_sha256.isEmpty()) { + m_sha256 = other->m_sha256; + } // TODO: do not reset the whole model. maybe? beginResetModel(); - m_versions.clear(); if (other->m_versions.isEmpty()) { qWarning() << "Empty list loaded ..."; } - for (const Version::Ptr& version : other->m_versions) { + for (auto version : other->m_versions) { // we already have the version. merge the contents if (m_lookup.contains(version->version())) { - m_lookup.value(version->version())->mergeFromList(version); + auto existing = m_lookup.value(version->version()); + existing->mergeFromList(version); + version = existing; } else { - m_lookup.insert(version->uid(), version); + m_lookup.insert(version->version(), version); + // connect it. + setupAddedVersion(m_versions.size(), version); + m_versions.append(version); } - // connect it. - setupAddedVersion(m_versions.size(), version); - m_versions.append(version); m_recommended = getBetterVersion(m_recommended, version); } endResetModel(); @@ -222,14 +261,15 @@ void VersionList::merge(const VersionList::Ptr& other) void VersionList::setupAddedVersion(const int row, const Version::Ptr& version) { - // FIXME: do not disconnect from everythin, disconnect only the lambdas here - version->disconnect(); + disconnect(version.get(), &Version::requiresChanged, this, nullptr); + disconnect(version.get(), &Version::timeChanged, this, nullptr); + disconnect(version.get(), &Version::typeChanged, this, nullptr); + connect(version.get(), &Version::requiresChanged, this, [this, row]() { emit dataChanged(index(row), index(row), QVector() << RequiresRole); }); connect(version.get(), &Version::timeChanged, this, - [this, row]() { emit dataChanged(index(row), index(row), QVector() << TimeRole << SortRole); }); - connect(version.get(), &Version::typeChanged, this, - [this, row]() { emit dataChanged(index(row), index(row), QVector() << TypeRole); }); + [this, row]() { emit dataChanged(index(row), index(row), { TimeRole, SortRole }); }); + connect(version.get(), &Version::typeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), { TypeRole }); }); } BaseVersion::Ptr VersionList::getRecommended() const @@ -237,4 +277,45 @@ BaseVersion::Ptr VersionList::getRecommended() const return m_recommended; } +void VersionList::waitToLoad() +{ + if (isLoaded()) + return; + QEventLoop ev; + auto task = getLoadTask(); + QObject::connect(task.get(), &Task::finished, &ev, &QEventLoop::quit); + 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 2c5624701..4215439db 100644 --- a/launcher/meta/VersionList.h +++ b/launcher/meta/VersionList.h @@ -30,23 +30,28 @@ class VersionList : public BaseVersionList, public BaseEntity { Q_PROPERTY(QString name READ name NOTIFY nameChanged) public: explicit VersionList(const QString& uid, QObject* parent = nullptr); + virtual ~VersionList() = default; using Ptr = std::shared_ptr; enum Roles { UidRole = Qt::UserRole + 100, TimeRole, RequiresRole, VersionPtrRole }; - Task::Ptr getLoadTask() override; bool isLoaded() override; + [[nodiscard]] Task::Ptr getLoadTask() override; const BaseVersion::Ptr at(int i) const override; int count() const override; 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; QHash roleNames() const override; + void setProvidedRoles(RoleList roles); + QString localFilename() const override; QString uid() const { return m_uid; } @@ -58,12 +63,17 @@ class VersionList : public BaseVersionList, public BaseEntity { QVector versions() const { return m_versions; } + // this blocks until the version list is loaded + void waitToLoad(); + public: // for usage only by parsers void setName(const QString& name); void setVersions(const QVector& versions); 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); @@ -73,12 +83,16 @@ class VersionList : public BaseVersionList, public BaseEntity { private: QVector m_versions; + QStringList m_externalRecommendsVersions; QHash m_lookup; QString m_uid; QString m_name; Version::Ptr m_recommended; + RoleList m_provided_roles = { VersionPointerRole, VersionRole, VersionIdRole, ParentVersionRole, TypeRole, UidRole, + TimeRole, RequiresRole, SortRole, RecommendedRole, LatestRole, VersionPtrRole }; + void setupAddedVersion(int row, const Version::Ptr& version); }; } // namespace Meta diff --git a/launcher/minecraft/AssetsUtils.cpp b/launcher/minecraft/AssetsUtils.cpp index 48e150d16..4406d9b34 100644 --- a/launcher/minecraft/AssetsUtils.cpp +++ b/launcher/minecraft/AssetsUtils.cpp @@ -51,6 +51,7 @@ #include "net/Download.h" #include "Application.h" +#include "net/NetRequest.h" namespace { QSet collectPathsFromDir(QString dirPath) @@ -276,14 +277,13 @@ bool reconstructAssets(QString assetsId, QString resourcesFolder) } // namespace AssetsUtils -NetAction::Ptr AssetObject::getDownloadAction() +Net::NetRequest::Ptr AssetObject::getDownloadAction() { QFileInfo objectFile(getLocalPath()); if ((!objectFile.isFile()) || (objectFile.size() != size)) { auto objectDL = Net::ApiDownload::makeFile(getUrl(), objectFile.filePath()); if (hash.size()) { - auto rawHash = QByteArray::fromHex(hash.toLatin1()); - objectDL->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawHash)); + objectDL->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, hash)); } objectDL->setProgress(objectDL->getProgress(), size); return objectDL; diff --git a/launcher/minecraft/AssetsUtils.h b/launcher/minecraft/AssetsUtils.h index 87956e57a..ea3613bd0 100644 --- a/launcher/minecraft/AssetsUtils.h +++ b/launcher/minecraft/AssetsUtils.h @@ -17,14 +17,14 @@ #include #include -#include "net/NetAction.h" #include "net/NetJob.h" +#include "net/NetRequest.h" struct AssetObject { QString getRelPath(); QUrl getUrl(); QString getLocalPath(); - NetAction::Ptr getDownloadAction(); + Net::NetRequest::Ptr getDownloadAction(); QString hash; qint64 size; diff --git a/launcher/minecraft/Component.cpp b/launcher/minecraft/Component.cpp index 79ea7a06d..1073ef324 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); @@ -56,18 +65,6 @@ Component::Component(PackProfile* parent, const QString& uid) m_uid = uid; } -Component::Component(PackProfile* parent, std::shared_ptr version) -{ - assert(parent); - m_parent = parent; - - m_metaVersion = version; - m_uid = version->uid(); - m_version = m_cachedVersion = version->version(); - m_cachedName = version->name(); - m_loaded = version->isLoaded(); -} - Component::Component(PackProfile* parent, const QString& uid, std::shared_ptr file) { assert(parent); @@ -102,9 +99,6 @@ void Component::applyTo(LaunchProfile* profile) std::shared_ptr Component::getVersionFile() const { if (m_metaVersion) { - if (!m_metaVersion->isLoaded()) { - m_metaVersion->load(Net::Mode::Online); - } return m_metaVersion->data(); } else { return m_file; @@ -131,29 +125,35 @@ int Component::getOrder() } return 0; } + void Component::setOrder(int order) { m_orderOverride = true; m_order = order; } + QString Component::getID() { return m_uid; } + QString Component::getName() { if (!m_cachedName.isEmpty()) return m_cachedName; return m_uid; } + QString Component::getVersion() { return m_cachedVersion; } + QString Component::getFilename() { return m_parent->patchFilePathForUid(m_uid); } + QDateTime Component::getReleaseDateTime() { if (m_metaVersion) { @@ -198,17 +198,14 @@ bool Component::isCustom() bool Component::isCustomizable() { - if (m_metaVersion) { - if (getVersionFile()) { - return true; - } - } - return false; + return m_metaVersion && getVersionFile(); } + bool Component::isRemovable() { return !m_important; } + bool Component::isRevertible() { if (isCustom()) { @@ -218,23 +215,39 @@ bool Component::isRevertible() } return false; } + bool Component::isMoveable() { // HACK, FIXME: this was too dumb and wouldn't follow dependency constraints anyway. For now hardcoded to 'true'. return true; } + bool Component::isVersionChangeable() { auto list = getVersionList(); if (list) { - if (!list->isLoaded()) { - list->load(Net::Mode::Online); - } + 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) { @@ -247,7 +260,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; } @@ -256,11 +270,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) { @@ -336,7 +370,7 @@ bool Component::revert() bool result = true; // just kill the file and reload if (QFile::exists(filename)) { - result = QFile::remove(filename); + result = FS::deletePath(filename); } if (result) { // file gone... @@ -414,3 +448,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 fdb61c45e..7ff30889f 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,17 +19,48 @@ 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: Component(PackProfile* parent, const QString& uid); // DEPRECATED: remove these constructors? - Component(PackProfile* parent, std::shared_ptr version); Component(PackProfile* parent, const QString& uid, std::shared_ptr file); virtual ~Component() {} + static const QMap KNOWN_MODLOADERS; + void applyTo(LaunchProfile* profile); bool isEnabled(); @@ -39,6 +73,8 @@ class Component : public QObject, public ProblemProvider { bool isRemovable(); bool isCustom(); bool isVersionChangeable(); + 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); @@ -59,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(); @@ -66,6 +104,12 @@ class Component : public QObject, public ProblemProvider { void updateCachedData(); + void waitLoadMeta(); + + void setUpdateAction(UpdateAction action); + void clearUpdateAction(); + UpdateAction getUpdateAction(); + signals: void dataChanged(); @@ -103,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 bb838043a..6656a84f8 100644 --- a/launcher/minecraft/ComponentUpdateTask.cpp +++ b/launcher/minecraft/ComponentUpdateTask.cpp @@ -1,18 +1,24 @@ #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" #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 @@ -35,7 +41,7 @@ ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list, QObject* parent) : Task(parent) { d.reset(new ComponentUpdateTaskData); - d->m_list = list; + d->m_profile = list; d->mode = mode; d->netmode = netmode; } @@ -44,7 +50,7 @@ ComponentUpdateTask::~ComponentUpdateTask() {} void ComponentUpdateTask::executeTask() { - qDebug() << "Loading components"; + qCDebug(instanceProfileResolveC) << "Loading components"; loadComponents(); } @@ -62,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; } @@ -93,9 +99,9 @@ static LoadResult loadComponent(ComponentPtr component, Task::Ptr& loadTask, Net component->m_loaded = true; result = LoadResult::LoadedLocal; } else { - metaVersion->load(netmode); - loadTask = metaVersion->getCurrentTask(); - if (loadTask) + loadTask = APPLICATION->metadataIndex()->loadVersion(component->m_uid, component->m_version, netmode); + loadTask->start(); + if (netmode == Net::Mode::Online) result = LoadResult::RequiresRemote; else if (metaVersion->isLoaded()) result = LoadResult::LoadedLocal; @@ -133,21 +139,6 @@ static LoadResult loadPackProfile(ComponentPtr component, Task::Ptr& loadTask, N } */ -static LoadResult loadIndex(Task::Ptr& loadTask, Net::Mode netmode) -{ - // FIXME: DECIDE. do we want to run the update task anyway? - if (APPLICATION->metadataIndex()->isLoaded()) { - qDebug() << "Index is already loaded"; - return LoadResult::LoadedLocal; - } - APPLICATION->metadataIndex()->load(netmode); - loadTask = APPLICATION->metadataIndex()->getCurrentTask(); - if (loadTask) { - return LoadResult::RequiresRemote; - } - // FIXME: this is assuming the load succeeded... did it really? - return LoadResult::LoadedLocal; -} } // namespace void ComponentUpdateTask::loadComponents() @@ -156,28 +147,13 @@ void ComponentUpdateTask::loadComponents() size_t taskIndex = 0; size_t componentIndex = 0; d->remoteLoadSuccessful = true; - // load the main index (it is needed to determine if components can revert) - { - // FIXME: tear out as a method? or lambda? - Task::Ptr indexLoadTask; - auto singleResult = loadIndex(indexLoadTask, d->netmode); - result = composeLoadResult(result, singleResult); - if (indexLoadTask) { - qDebug() << "Remote loading is being run for metadata index"; - RemoteLoadStatus status; - status.type = RemoteLoadStatus::Type::Index; - d->remoteLoadStatusList.append(status); - connect(indexLoadTask.get(), &Task::succeeded, [=]() { remoteLoadSucceeded(taskIndex); }); - connect(indexLoadTask.get(), &Task::failed, [=](const QString& error) { remoteLoadFailed(taskIndex, error); }); - connect(indexLoadTask.get(), &Task::aborted, [=]() { remoteLoadFailed(taskIndex, tr("Aborted")); }); - taskIndex++; - } - } + // 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 @@ -205,13 +181,15 @@ void ComponentUpdateTask::loadComponents() } result = composeLoadResult(result, singleResult); if (loadTask) { - qDebug() << "Remote loading is being run for" << component->getName(); - connect(loadTask.get(), &Task::succeeded, [=]() { remoteLoadSucceeded(taskIndex); }); - connect(loadTask.get(), &Task::failed, [=](const QString& error) { remoteLoadFailed(taskIndex, error); }); - connect(loadTask.get(), &Task::aborted, [=]() { remoteLoadFailed(taskIndex, tr("Aborted")); }); + 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")); }); RemoteLoadStatus status; status.type = loadType; status.PackProfileIndex = componentIndex; + status.task = loadTask; d->remoteLoadStatusList.append(status); taskIndex++; } @@ -221,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; } @@ -299,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 { @@ -382,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; } @@ -405,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 @@ -422,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; @@ -431,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()); @@ -447,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 { @@ -463,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()) { @@ -493,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; @@ -502,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); } @@ -512,24 +531,199 @@ 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) { - auto& taskSlot = d->remoteLoadStatusList[taskIndex]; - if (taskSlot.finished) { - qWarning() << "Got multiple results from remote load task" << taskIndex; + if (static_cast(d->remoteLoadStatusList.size()) < taskIndex) { + qCWarning(instanceProfileResolveC) << "Got task index outside of results" << taskIndex; return; } - qDebug() << "Remote task" << taskIndex << "succeeded"; + auto& taskSlot = d->remoteLoadStatusList[taskIndex]; + disconnect(taskSlot.task.get(), &Task::succeeded, this, nullptr); + disconnect(taskSlot.task.get(), &Task::failed, this, nullptr); + disconnect(taskSlot.task.get(), &Task::aborted, this, nullptr); + if (taskSlot.finished) { + qCWarning(instanceProfileResolveC) << "Got multiple results from remote load task" << taskIndex; + return; + } + 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(); } @@ -538,12 +732,19 @@ void ComponentUpdateTask::remoteLoadSucceeded(size_t taskIndex) void ComponentUpdateTask::remoteLoadFailed(size_t taskIndex, const QString& msg) { - auto& taskSlot = d->remoteLoadStatusList[taskIndex]; - if (taskSlot.finished) { - qWarning() << "Got multiple results from remote load task" << taskIndex; + if (static_cast(d->remoteLoadStatusList.size()) < taskIndex) { + qCWarning(instanceProfileResolveC) << "Got task index outside of results" << taskIndex; return; } - qDebug() << "Remote task" << taskIndex << "failed: " << msg; + auto& taskSlot = d->remoteLoadStatusList[taskIndex]; + disconnect(taskSlot.task.get(), &Task::succeeded, this, nullptr); + disconnect(taskSlot.task.get(), &Task::failed, this, nullptr); + disconnect(taskSlot.task.get(), &Task::aborted, this, nullptr); + if (taskSlot.finished) { + qCWarning(instanceProfileResolveC) << "Got multiple results from remote load task" << taskIndex; + return; + } + qCDebug(instanceProfileResolveC) << "Remote task" << taskIndex << "failed: " << msg; d->remoteLoadSuccessful = false; taskSlot.succeeded = false; taskSlot.finished = true; @@ -561,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..484c0bedd 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" @@ -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 00e8f2fbe..2fc0b6d9a 100644 --- a/launcher/minecraft/ComponentUpdateTask_p.h +++ b/launcher/minecraft/ComponentUpdateTask_p.h @@ -4,6 +4,9 @@ #include #include #include "net/Mode.h" +#include "tasks/Task.h" + +#include "minecraft/ComponentUpdateTask.h" class PackProfile; @@ -13,10 +16,11 @@ struct RemoteLoadStatus { bool finished = false; bool succeeded = false; QString error; + Task::Ptr task; }; 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 cf819b411..c11a0f915 100644 --- a/launcher/minecraft/LaunchProfile.cpp +++ b/launcher/minecraft/LaunchProfile.cpp @@ -165,6 +165,12 @@ void LaunchProfile::applyCompatibleJavaMajors(QList& javaMajor) m_compatibleJavaMajors.append(javaMajor); } +void LaunchProfile::applyCompatibleJavaName(QString javaName) +{ + if (!javaName.isEmpty()) + m_compatibleJavaName = javaName; +} + void LaunchProfile::applyLibrary(LibraryPtr library, const RuntimeContext& runtimeContext) { if (!library->isActive(runtimeContext)) { @@ -334,6 +340,11 @@ const QList& LaunchProfile::getCompatibleJavaMajors() const return m_compatibleJavaMajors; } +const QString LaunchProfile::getCompatibleJavaName() const +{ + return m_compatibleJavaName; +} + void LaunchProfile::getLibraryFiles(const RuntimeContext& runtimeContext, QStringList& jars, QStringList& nativeJars, diff --git a/launcher/minecraft/LaunchProfile.h b/launcher/minecraft/LaunchProfile.h index 12b312383..f1be6fee0 100644 --- a/launcher/minecraft/LaunchProfile.h +++ b/launcher/minecraft/LaunchProfile.h @@ -59,6 +59,7 @@ class LaunchProfile : public ProblemProvider { void applyMavenFile(LibraryPtr library, const RuntimeContext& runtimeContext); void applyAgent(AgentPtr agent, const RuntimeContext& runtimeContext); void applyCompatibleJavaMajors(QList& javaMajor); + void applyCompatibleJavaName(QString javaName); void applyMainJar(LibraryPtr jar); void applyProblemSeverity(ProblemSeverity severity); /// clear the profile @@ -80,6 +81,7 @@ class LaunchProfile : public ProblemProvider { const QList& getMavenFiles() const; const QList& getAgents() const; const QList& getCompatibleJavaMajors() const; + const QString getCompatibleJavaName() const; const LibraryPtr getMainJar() const; void getLibraryFiles(const RuntimeContext& runtimeContext, QStringList& jars, @@ -150,5 +152,7 @@ class LaunchProfile : public ProblemProvider { /// compatible java major versions QList m_compatibleJavaMajors; + QString m_compatibleJavaName; + ProblemSeverity m_problemSeverity = ProblemSeverity::None; }; diff --git a/launcher/minecraft/Library.cpp b/launcher/minecraft/Library.cpp index 0e8ddf03d..4f04f0eb9 100644 --- a/launcher/minecraft/Library.cpp +++ b/launcher/minecraft/Library.cpp @@ -35,12 +35,27 @@ #include "Library.h" #include "MinecraftInstance.h" +#include "net/NetRequest.h" #include #include #include #include +/** + * @brief Collect applicable files for the library. + * + * Depending on whether the library is native or not, it adds paths to the + * appropriate lists for jar files, native libraries for 32-bit, and native + * libraries for 64-bit. + * + * @param runtimeContext The current runtime context. + * @param jar List to store paths for jar files. + * @param native List to store paths for native libraries. + * @param native32 List to store paths for 32-bit native libraries. + * @param native64 List to store paths for 64-bit native libraries. + * @param overridePath Optional path to override the default storage path. + */ void Library::getApplicableFiles(const RuntimeContext& runtimeContext, QStringList& jar, QStringList& native, @@ -49,7 +64,9 @@ void Library::getApplicableFiles(const RuntimeContext& runtimeContext, const QString& overridePath) const { bool local = isLocal(); + // Lambda function to get the absolute file path auto actualPath = [&](QString relPath) { + relPath = FS::RemoveInvalidPathChars(relPath); QFileInfo out(FS::PathCombine(storagePrefix(), relPath)); if (local && !overridePath.isEmpty()) { QString fileName = out.fileName(); @@ -57,6 +74,7 @@ void Library::getApplicableFiles(const RuntimeContext& runtimeContext, } return out.absoluteFilePath(); }; + QString raw_storage = storageSuffix(runtimeContext); if (isNative()) { if (raw_storage.contains("${arch}")) { @@ -74,15 +92,29 @@ void Library::getApplicableFiles(const RuntimeContext& runtimeContext, } } -QList Library::getDownloads(const RuntimeContext& runtimeContext, - class HttpMetaCache* cache, - QStringList& failedLocalFiles, - const QString& overridePath) const +/** + * @brief Get download requests for the library files. + * + * Depending on whether the library is native or not, and the current runtime context, + * this function prepares download requests for the necessary files. It handles both local + * and remote files, checks for stale cache entries, and adds checksummed downloads. + * + * @param runtimeContext The current runtime context. + * @param cache Pointer to the HTTP meta cache. + * @param failedLocalFiles List to store paths for failed local files. + * @param overridePath Optional path to override the default storage path. + * @return QList List of download requests. + */ +QList Library::getDownloads(const RuntimeContext& runtimeContext, + class HttpMetaCache* cache, + QStringList& failedLocalFiles, + const QString& overridePath) const { - QList out; + QList out; bool stale = isAlwaysStale(); bool local = isLocal(); + // Lambda function to check if a local file exists auto check_local_file = [&](QString storage) { QFileInfo fileinfo(storage); QString fileName = fileinfo.fileName(); @@ -95,6 +127,7 @@ QList Library::getDownloads(const RuntimeContext& runtimeContext return true; }; + // Lambda function to add a download request auto add_download = [&](QString storage, QString url, QString sha1) { if (local) { return check_local_file(storage); @@ -114,9 +147,8 @@ QList Library::getDownloads(const RuntimeContext& runtimeContext options |= Net::Download::Option::MakeEternal; if (sha1.size()) { - auto rawSha1 = QByteArray::fromHex(sha1.toLatin1()); auto dl = Net::ApiDownload::makeCached(url, entry, options); - dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, sha1)); qDebug() << "Checksummed Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url; out.append(dl); } else { @@ -195,6 +227,15 @@ QList Library::getDownloads(const RuntimeContext& runtimeContext return out; } +/** + * @brief Check if the library is active in the given runtime context. + * + * This function evaluates rules to determine if the library should be active, + * considering both general rules and native compatibility. + * + * @param runtimeContext The current runtime context. + * @return bool True if the library is active, false otherwise. + */ bool Library::isActive(const RuntimeContext& runtimeContext) const { bool result = true; @@ -215,16 +256,35 @@ bool Library::isActive(const RuntimeContext& runtimeContext) const return result; } +/** + * @brief Check if the library is considered local. + * + * @return bool True if the library is local, false otherwise. + */ bool Library::isLocal() const { return m_hint == "local"; } +/** + * @brief Check if the library is always considered stale. + * + * @return bool True if the library is always stale, false otherwise. + */ bool Library::isAlwaysStale() const { return m_hint == "always-stale"; } +/** + * @brief Get the compatible native classifier for the current runtime context. + * + * This function attempts to match the current runtime context with the appropriate + * native classifier. + * + * @param runtimeContext The current runtime context. + * @return QString The compatible native classifier, or an empty string if none is found. + */ QString Library::getCompatibleNative(const RuntimeContext& runtimeContext) const { // try to match precise classifier "[os]-[arch]" @@ -239,16 +299,31 @@ QString Library::getCompatibleNative(const RuntimeContext& runtimeContext) const return entry.value(); } +/** + * @brief Set the storage prefix for the library. + * + * @param prefix The storage prefix to set. + */ void Library::setStoragePrefix(QString prefix) { m_storagePrefix = prefix; } +/** + * @brief Get the default storage prefix for libraries. + * + * @return QString The default storage prefix. + */ QString Library::defaultStoragePrefix() { return "libraries/"; } +/** + * @brief Get the current storage prefix for the library. + * + * @return QString The current storage prefix. + */ QString Library::storagePrefix() const { if (m_storagePrefix.isEmpty()) { @@ -257,6 +332,15 @@ QString Library::storagePrefix() const return m_storagePrefix; } +/** + * @brief Get the filename for the library in the current runtime context. + * + * This function determines the appropriate filename for the library, taking into + * account native classifiers if applicable. + * + * @param runtimeContext The current runtime context. + * @return QString The filename of the library. + */ QString Library::filename(const RuntimeContext& runtimeContext) const { if (!m_filename.isEmpty()) { @@ -278,6 +362,15 @@ QString Library::filename(const RuntimeContext& runtimeContext) const return nativeSpec.getFileName(); } +/** + * @brief Get the display name for the library in the current runtime context. + * + * This function returns the display name for the library, defaulting to the filename + * if no display name is set. + * + * @param runtimeContext The current runtime context. + * @return QString The display name of the library. + */ QString Library::displayName(const RuntimeContext& runtimeContext) const { if (!m_displayname.isEmpty()) @@ -285,6 +378,15 @@ QString Library::displayName(const RuntimeContext& runtimeContext) const return filename(runtimeContext); } +/** + * @brief Get the storage suffix for the library in the current runtime context. + * + * This function determines the appropriate storage suffix for the library, taking into + * account native classifiers if applicable. + * + * @param runtimeContext The current runtime context. + * @return QString The storage suffix of the library. + */ QString Library::storageSuffix(const RuntimeContext& runtimeContext) const { // non-native? use only the gradle specifier diff --git a/launcher/minecraft/Library.h b/launcher/minecraft/Library.h index adb89c4c6..d3019e814 100644 --- a/launcher/minecraft/Library.h +++ b/launcher/minecraft/Library.h @@ -34,7 +34,6 @@ */ #pragma once -#include #include #include #include @@ -48,6 +47,7 @@ #include "MojangDownloadInfo.h" #include "Rule.h" #include "RuntimeContext.h" +#include "net/NetRequest.h" class Library; class MinecraftInstance; @@ -144,10 +144,10 @@ class Library { bool isForge() const; // Get a list of downloads for this library - QList getDownloads(const RuntimeContext& runtimeContext, - class HttpMetaCache* cache, - QStringList& failedLocalFiles, - const QString& overridePath) const; + QList getDownloads(const RuntimeContext& runtimeContext, + class HttpMetaCache* cache, + QStringList& failedLocalFiles, + const QString& overridePath) const; QString getCompatibleNative(const RuntimeContext& runtimeContext) const; 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/net/StaticHeaderProxy.h b/launcher/minecraft/Logging.h similarity index 62% rename from launcher/net/StaticHeaderProxy.h rename to launcher/minecraft/Logging.h index 8af7d203d..00d43f419 100644 --- a/launcher/net/StaticHeaderProxy.h +++ b/launcher/minecraft/Logging.h @@ -1,39 +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 "net/HeaderProxy.h" - -namespace Net { - -class StaticHeaderProxy : public HeaderProxy { - public: - StaticHeaderProxy(QList hdrs = {}) : HeaderProxy(), m_hdrs(hdrs){}; - virtual ~StaticHeaderProxy() = default; - - public: - virtual QList headers(const QNetworkRequest&) const override { return m_hdrs; }; - void setHeaders(QList hdrs) { m_hdrs = hdrs; }; - - private: - QList m_hdrs; -}; - -} // namespace Net + +// 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 cb3166166..2b24ec090 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -38,9 +38,14 @@ #include "MinecraftInstance.h" #include "Application.h" #include "BuildConfig.h" +#include "QObjectPtr.h" +#include "minecraft/launch/AutoInstallJava.h" #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" @@ -51,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" @@ -68,9 +73,6 @@ #include "java/JavaUtils.h" -#include "meta/Index.h" -#include "meta/VersionList.h" - #include "icons/IconList.h" #include "mod/ModFolderModel.h" @@ -82,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" @@ -134,25 +135,21 @@ void MinecraftInstance::loadSpecificSettings() return; // Java Settings - auto javaOverride = m_settings->registerSetting("OverrideJava", false); auto locationOverride = m_settings->registerSetting("OverrideJavaLocation", false); auto argsOverride = m_settings->registerSetting("OverrideJavaArgs", false); - - // combinations - auto javaOrLocation = std::make_shared("JavaOrLocationOverride", javaOverride, locationOverride); - auto javaOrArgs = std::make_shared("JavaOrArgsOverride", javaOverride, argsOverride); + m_settings->registerSetting("AutomaticJava", false); if (auto global_settings = globalSettings()) { - m_settings->registerOverride(global_settings->getSetting("JavaPath"), javaOrLocation); - m_settings->registerOverride(global_settings->getSetting("JvmArgs"), javaOrArgs); - m_settings->registerOverride(global_settings->getSetting("IgnoreJavaCompatibility"), javaOrLocation); + m_settings->registerOverride(global_settings->getSetting("JavaPath"), locationOverride); + m_settings->registerOverride(global_settings->getSetting("JvmArgs"), argsOverride); + m_settings->registerOverride(global_settings->getSetting("IgnoreJavaCompatibility"), locationOverride); // special! - m_settings->registerPassthrough(global_settings->getSetting("JavaSignature"), javaOrLocation); - m_settings->registerPassthrough(global_settings->getSetting("JavaArchitecture"), javaOrLocation); - m_settings->registerPassthrough(global_settings->getSetting("JavaRealArchitecture"), javaOrLocation); - m_settings->registerPassthrough(global_settings->getSetting("JavaVersion"), javaOrLocation); - m_settings->registerPassthrough(global_settings->getSetting("JavaVendor"), javaOrLocation); + m_settings->registerPassthrough(global_settings->getSetting("JavaSignature"), locationOverride); + m_settings->registerPassthrough(global_settings->getSetting("JavaArchitecture"), locationOverride); + m_settings->registerPassthrough(global_settings->getSetting("JavaRealArchitecture"), locationOverride); + m_settings->registerPassthrough(global_settings->getSetting("JavaVersion"), locationOverride); + m_settings->registerPassthrough(global_settings->getSetting("JavaVendor"), locationOverride); // Window Size auto windowSetting = m_settings->registerSetting("OverrideWindow", false); @@ -198,6 +195,7 @@ void MinecraftInstance::loadSpecificSettings() // Join server on launch, this does not have a global override m_settings->registerSetting("JoinServerOnLaunch", false); m_settings->registerSetting("JoinServerOnLaunchAddress", ""); + m_settings->registerSetting("JoinWorldOnLaunch", ""); // Use account for instance, this does not have a global override m_settings->registerSetting("UseAccountForInstance", false); @@ -219,6 +217,7 @@ void MinecraftInstance::loadSpecificSettings() void MinecraftInstance::updateRuntimeContext() { m_runtimeContext.updateFromInstanceSettings(m_settings); + m_components->invalidateLaunchProfile(); } QString MinecraftInstance::typeName() const @@ -523,8 +522,7 @@ QStringList MinecraftInstance::javaArguments() if (javaVersion.isModular() && shouldApplyOnlineFixes()) // allow reflective access to java.net - required by the skin fix - args << "--add-opens" - << "java.base/java.net=ALL-UNNAMED"; + args << "--add-opens" << "java.base/java.net=ALL-UNNAMED"; return args; } @@ -608,7 +606,7 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment() // dlsym variant is only needed for OpenGL and not included in the vulkan layer appendLib("libMangoHud_dlsym.so"); appendLib("libMangoHud_opengl.so"); - appendLib(mangoHudLib.fileName()); + preloadList << mangoHudLibString; } env.insert("LD_PRELOAD", preloadList.join(QLatin1String(":"))); @@ -656,7 +654,7 @@ static QString replaceTokensIn(QString text, QMap with) return result; } -QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) const +QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) const { auto profile = m_components->getProfile(); QString args_pattern = profile->getMinecraftArguments(); @@ -664,12 +662,16 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, Mine args_pattern += " --tweakClass " + tweaker; } - if (serverToJoin && !serverToJoin->address.isEmpty()) { - if (profile->hasTrait("feature:is_quick_play_multiplayer")) { - args_pattern += " --quickPlayMultiplayer " + serverToJoin->address + ':' + QString::number(serverToJoin->port); - } else { - args_pattern += " --server " + serverToJoin->address; - args_pattern += " --port " + QString::number(serverToJoin->port); + if (targetToJoin) { + if (!targetToJoin->address.isEmpty()) { + if (profile->hasTrait("feature:is_quick_play_multiplayer")) { + args_pattern += " --quickPlayMultiplayer " + targetToJoin->address + ':' + QString::number(targetToJoin->port); + } else { + args_pattern += " --server " + targetToJoin->address; + args_pattern += " --port " + QString::number(targetToJoin->port); + } + } else if (!targetToJoin->world.isEmpty() && profile->hasTrait("feature:is_quick_play_singleplayer")) { + args_pattern += " --quickPlaySingleplayer " + targetToJoin->world; } } @@ -713,7 +715,7 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, Mine return parts; } -QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) +QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) { QString launchScript; @@ -732,9 +734,13 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftS launchScript += "appletClass " + appletClass + "\n"; } - if (serverToJoin && !serverToJoin->address.isEmpty()) { - launchScript += "serverAddress " + serverToJoin->address + "\n"; - launchScript += "serverPort " + QString::number(serverToJoin->port) + "\n"; + if (targetToJoin) { + if (!targetToJoin->address.isEmpty()) { + launchScript += "serverAddress " + targetToJoin->address + "\n"; + launchScript += "serverPort " + QString::number(targetToJoin->port) + "\n"; + } else if (!targetToJoin->world.isEmpty()) { + launchScript += "worldName " + targetToJoin->world + "\n"; + } } // generic minecraft params @@ -787,16 +793,15 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftS return launchScript; } -QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) +QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) { QStringList out; - out << "Main Class:" - << " " + getMainClass() << ""; - out << "Native path:" - << " " + getNativePath() << ""; + out << "Main Class:" << " " + getMainClass() << ""; + out << "Native path:" << " " + getNativePath() << ""; auto profile = m_components->getProfile(); + // traits auto alltraits = traits(); if (alltraits.size()) { out << "Traits:"; @@ -806,6 +811,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr out << ""; } + // native libraries auto settings = this->settings(); bool nativeOpenAL = settings->get("UseNativeOpenAL").toBool(); bool nativeGLFW = settings->get("UseNativeGLFW").toBool(); @@ -841,6 +847,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr out << ""; } + // mods and core mods auto printModList = [&](const QString& label, ModFolderModel& model) { if (model.size()) { out << QString("%1:").arg(label); @@ -869,6 +876,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr printModList("Mods", *(loaderModList().get())); printModList("Core Mods", *(coreModList().get())); + // jar mods auto& jarMods = profile->getJarMods(); if (jarMods.size()) { out << "Jar Mods:"; @@ -884,11 +892,13 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr out << ""; } - auto params = processMinecraftArgs(nullptr, serverToJoin); + // minecraft arguments + auto params = processMinecraftArgs(nullptr, targetToJoin); out << "Params:"; out << " " + params.join(' '); out << ""; + // window size QString windowParams; if (settings->get("LaunchMaximized").toBool()) { out << "Window size: max (if available)"; @@ -1000,7 +1010,7 @@ QString MinecraftInstance::getStatusbarDescription() QString description; description.append(tr("Minecraft %1").arg(mcVersion)); if (m_settings->get("ShowGameTime").toBool()) { - if (lastTimePlayed() > 0) { + if (lastTimePlayed() > 0 && lastLaunch() > 0) { QDateTime lastLaunchTime = QDateTime::fromMSecsSinceEpoch(lastLaunch()); description.append( tr(", last played on %1 for %2") @@ -1020,21 +1030,21 @@ 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, MinecraftServerTargetPtr serverToJoin) +shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) { updateRuntimeContext(); // FIXME: get rid of shared_from_this ... @@ -1048,26 +1058,28 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt process->appendStep(makeShared(pptr, "Minecraft folder is:\n" + gameRoot() + "\n\n", MessageLevel::Launcher)); } - // check java - { - process->appendStep(makeShared(pptr)); - } - // create the .minecraft folder and server-resource-packs (workaround for Minecraft bug MCL-3732) { process->appendStep(makeShared(pptr)); } - if (!serverToJoin && settings()->get("JoinServerOnLaunch").toBool()) { + if (!targetToJoin && settings()->get("JoinServerOnLaunch").toBool()) { QString fullAddress = settings()->get("JoinServerOnLaunchAddress").toString(); - serverToJoin.reset(new MinecraftServerTarget(MinecraftServerTarget::parse(fullAddress))); + if (!fullAddress.isEmpty()) { + targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(fullAddress, false))); + } else { + QString world = settings()->get("JoinWorldOnLaunch").toString(); + if (!world.isEmpty()) { + targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(world, true))); + } + } } - if (serverToJoin && serverToJoin->port == 25565) { + if (targetToJoin && targetToJoin->port == 25565) { // Resolve server address to join on launch auto step = makeShared(pptr); - step->setLookupAddress(serverToJoin->address); - step->setOutputAddressPtr(serverToJoin); + step->setLookupAddress(targetToJoin->address); + step->setOutputAddressPtr(targetToJoin); process->appendStep(step); } @@ -1078,14 +1090,26 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt process->appendStep(step); } + // load meta + { + auto mode = session->status != AuthSession::PlayableOffline ? Net::Mode::Online : Net::Mode::Offline; + process->appendStep(makeShared(pptr, makeShared(this, mode, pptr))); + } + + // check java + { + process->appendStep(makeShared(pptr)); + process->appendStep(makeShared(pptr)); + } + // 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)); + for (auto t : createUpdateTask()) { + process->appendStep(makeShared(pptr, t)); + } } // if there are any jar mods @@ -1100,7 +1124,7 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt // print some instance info here... { - process->appendStep(makeShared(pptr, session, serverToJoin)); + process->appendStep(makeShared(pptr, session, targetToJoin)); } // extract native jars if needed @@ -1123,7 +1147,7 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt auto step = makeShared(pptr); step->setWorkingDirectory(gameRoot()); step->setAuthSession(session); - step->setServerToJoin(serverToJoin); + step->setTargetToJoin(targetToJoin); process->appendStep(step); } diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index b1f305201..75e97ae45 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -39,7 +39,7 @@ #include #include #include "BaseInstance.h" -#include "minecraft/launch/MinecraftServerTarget.h" +#include "minecraft/launch/MinecraftTarget.h" #include "minecraft/mod/Mod.h" class ModFolderModel; @@ -56,7 +56,7 @@ class MinecraftInstance : public BaseInstance { Q_OBJECT public: MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir); - virtual ~MinecraftInstance(){}; + virtual ~MinecraftInstance() = default; virtual void saveNow() override; void loadSpecificSettings() override; @@ -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,12 +120,12 @@ class MinecraftInstance : public BaseInstance { std::shared_ptr gameOptionsModel(); ////// Launch stuff ////// - Task::Ptr createUpdateTask(Net::Mode mode) override; - shared_qobject_ptr createLaunchTask(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) override; + QList createUpdateTask() override; + shared_qobject_ptr createLaunchTask(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) override; QStringList extraArguments() override; - QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override; + QStringList verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) override; QList getJarMods() const; - QString createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin); + QString createLaunchScript(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin); /// get arguments passed to java QStringList javaArguments(); QString getLauncher(); @@ -155,7 +155,7 @@ class MinecraftInstance : public BaseInstance { virtual QString getMainClass() const; // FIXME: remove - virtual QStringList processMinecraftArgs(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) const; + virtual QStringList processMinecraftArgs(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) const; virtual JavaVersion getJavaVersion(); diff --git a/launcher/minecraft/MinecraftLoadAndCheck.cpp b/launcher/minecraft/MinecraftLoadAndCheck.cpp index 818e90cfc..a9dcdf067 100644 --- a/launcher/minecraft/MinecraftLoadAndCheck.cpp +++ b/launcher/minecraft/MinecraftLoadAndCheck.cpp @@ -2,41 +2,44 @@ #include "MinecraftInstance.h" #include "PackProfile.h" -MinecraftLoadAndCheck::MinecraftLoadAndCheck(MinecraftInstance* inst, QObject* parent) : Task(parent), m_inst(inst) {} +MinecraftLoadAndCheck::MinecraftLoadAndCheck(MinecraftInstance* inst, Net::Mode netmode, QObject* parent) + : Task(parent), 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 9556c1d6a..72e9e0caa 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, QObject* parent = nullptr); + 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 c009317a6..000000000 --- a/launcher/minecraft/MinecraftUpdate.cpp +++ /dev/null @@ -1,170 +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 -#include -#include -#include - -#include -#include "BaseInstance.h" -#include "minecraft/Library.h" -#include "minecraft/PackProfile.h" - -#include "update/AssetUpdateTask.h" -#include "update/FMLLibrariesTask.h" -#include "update/FoldersTask.h" -#include "update/LibrariesTask.h" - -#include -#include - -MinecraftUpdate::MinecraftUpdate(MinecraftInstance* inst, QObject* parent) : Task(parent), m_inst(inst) {} - -void MinecraftUpdate::executeTask() -{ - m_tasks.clear(); - // create folders - { - m_tasks.append(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) { - m_tasks.append(task); - } - } - - // libraries download - { - m_tasks.append(makeShared(m_inst)); - } - - // FML libraries download and copy into the instance - { - m_tasks.append(makeShared(m_inst)); - } - - // assets update - { - m_tasks.append(makeShared(m_inst)); - } - - if (!m_preFailure.isEmpty()) { - emitFailed(m_preFailure); - return; - } - next(); -} - -void MinecraftUpdate::next() -{ - if (m_abort) { - emitFailed(tr("Aborted by user.")); - return; - } - if (m_failed_out_of_order) { - emitFailed(m_fail_reason); - return; - } - m_currentTask++; - if (m_currentTask > 0) { - auto task = m_tasks[m_currentTask - 1]; - disconnect(task.get(), &Task::succeeded, this, &MinecraftUpdate::subtaskSucceeded); - disconnect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed); - disconnect(task.get(), &Task::aborted, this, &Task::abort); - disconnect(task.get(), &Task::progress, this, &MinecraftUpdate::progress); - disconnect(task.get(), &Task::stepProgress, this, &MinecraftUpdate::propagateStepProgress); - disconnect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus); - disconnect(task.get(), &Task::details, this, &MinecraftUpdate::setDetails); - } - if (m_currentTask == m_tasks.size()) { - emitSucceeded(); - return; - } - auto task = m_tasks[m_currentTask]; - // if the task is already finished by the time we look at it, skip it - if (task->isFinished()) { - qCritical() << "MinecraftUpdate: Skipping finished subtask" << m_currentTask << ":" << task.get(); - next(); - } - connect(task.get(), &Task::succeeded, this, &MinecraftUpdate::subtaskSucceeded); - connect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed); - connect(task.get(), &Task::aborted, this, &Task::abort); - connect(task.get(), &Task::progress, this, &MinecraftUpdate::progress); - connect(task.get(), &Task::stepProgress, this, &MinecraftUpdate::propagateStepProgress); - connect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus); - connect(task.get(), &Task::details, this, &MinecraftUpdate::setDetails); - // if the task is already running, do not start it again - if (!task->isRunning()) { - task->start(); - } -} - -void MinecraftUpdate::subtaskSucceeded() -{ - if (isFinished()) { - qCritical() << "MinecraftUpdate: Subtask" << sender() << "succeeded, but work was already done!"; - return; - } - auto senderTask = QObject::sender(); - auto currentTask = m_tasks[m_currentTask].get(); - if (senderTask != currentTask) { - qDebug() << "MinecraftUpdate: Subtask" << sender() << "succeeded out of order."; - return; - } - next(); -} - -void MinecraftUpdate::subtaskFailed(QString error) -{ - if (isFinished()) { - qCritical() << "MinecraftUpdate: Subtask" << sender() << "failed, but work was already done!"; - return; - } - auto senderTask = QObject::sender(); - auto currentTask = m_tasks[m_currentTask].get(); - if (senderTask != currentTask) { - qDebug() << "MinecraftUpdate: Subtask" << sender() << "failed out of order."; - m_failed_out_of_order = true; - m_fail_reason = error; - return; - } - emitFailed(error); -} - -bool MinecraftUpdate::abort() -{ - if (!m_abort) { - m_abort = true; - auto task = m_tasks[m_currentTask]; - if (task->canAbort()) { - return task->abort(); - } - } - return true; -} - -bool MinecraftUpdate::canAbort() const -{ - return true; -} diff --git a/launcher/minecraft/MinecraftUpdate.h b/launcher/minecraft/MinecraftUpdate.h deleted file mode 100644 index 9c41d7f56..000000000 --- a/launcher/minecraft/MinecraftUpdate.h +++ /dev/null @@ -1,57 +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 -#include -#include - -#include -#include "minecraft/VersionFilterData.h" -#include "net/NetJob.h" -#include "tasks/Task.h" - -class MinecraftVersion; -class MinecraftInstance; - -// FIXME: This looks very similar to a SequentialTask. Maybe we can reduce code duplications? :^) - -class MinecraftUpdate : public Task { - Q_OBJECT - public: - explicit MinecraftUpdate(MinecraftInstance* inst, QObject* parent = 0); - virtual ~MinecraftUpdate(){}; - - void executeTask() override; - bool canAbort() const override; - - private slots: - bool abort() override; - void subtaskSucceeded(); - void subtaskFailed(QString error); - - private: - void next(); - - private: - MinecraftInstance* m_inst = nullptr; - QList m_tasks; - QString m_preFailure; - int m_currentTask = -1; - bool m_abort = false; - bool m_failed_out_of_order = false; - QString m_fail_reason; -}; diff --git a/launcher/minecraft/MojangVersionFormat.cpp b/launcher/minecraft/MojangVersionFormat.cpp index bb782e47f..d17a3a21f 100644 --- a/launcher/minecraft/MojangVersionFormat.cpp +++ b/launcher/minecraft/MojangVersionFormat.cpp @@ -185,6 +185,9 @@ void MojangVersionFormat::readVersionProperties(const QJsonObject& in, VersionFi out->compatibleJavaMajors.append(requireInteger(compatible)); } } + if (in.contains("compatibleJavaName")) { + out->compatibleJavaName = requireString(in.value("compatibleJavaName")); + } if (in.contains("downloads")) { auto downloadsObj = requireObject(in, "downloads"); @@ -259,6 +262,9 @@ void MojangVersionFormat::writeVersionProperties(const VersionFile* in, QJsonObj } out.insert("compatibleJavaMajors", compatibleJavaMajorsOut); } + if (!in->compatibleJavaName.isEmpty()) { + writeString(out, "compatibleJavaName", in->compatibleJavaName); + } } QJsonDocument MojangVersionFormat::versionFileToJson(const VersionFilePtr& patch) diff --git a/launcher/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp index 306c95a6a..bd587beb2 100644 --- a/launcher/minecraft/OneSixVersionFormat.cpp +++ b/launcher/minecraft/OneSixVersionFormat.cpp @@ -36,6 +36,8 @@ #include "OneSixVersionFormat.h" #include #include +#include +#include "java/JavaMetadata.h" #include "minecraft/Agent.h" #include "minecraft/ParseUtils.h" @@ -255,6 +257,13 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument& doc out->m_volatile = requireBoolean(root, "volatile"); } + if (root.contains("runtimes")) { + out->runtimes = {}; + for (auto runtime : ensureArray(root, "runtimes")) { + out->runtimes.append(Java::parseJavaMeta(ensureObject(runtime))); + } + } + /* removed features that shouldn't be used */ if (root.contains("tweakers")) { out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element 'tweakers'")); diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index 180f8aa30..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" @@ -58,14 +65,11 @@ #include "ComponentUpdateTask.h" #include "PackProfile.h" #include "PackProfile_p.h" -#include "minecraft/mod/Mod.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() { @@ -154,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; } @@ -176,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 overriden order"; + qCCritical(instanceProfileC) << "Couldn't open" << componentsFile.fileName() << " for reading:" << componentsFile.errorString(); + qCWarning(instanceProfileC) << "Ignoring overridden order"; return false; } @@ -189,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 overriden order"; + qCCritical(instanceProfileC) << "Couldn't parse" << componentsFile.fileName() << ":" << error.errorString(); + qCWarning(instanceProfileC) << "Ignoring overridden order"; return false; } @@ -208,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; } @@ -241,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(); } @@ -273,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; @@ -286,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... @@ -299,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); @@ -347,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(); } @@ -370,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)); @@ -389,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") { @@ -405,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; } @@ -446,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(); @@ -462,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(); @@ -679,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; } @@ -712,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()); @@ -732,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; } } @@ -748,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; @@ -805,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()); @@ -839,7 +849,7 @@ bool PackProfile::installCustomJar_internal(QString filepath) QFileInfo jarInfo(finalPath); if (jarInfo.exists()) { - if (!QFile::remove(finalPath)) { + if (!FS::deletePath(finalPath)) { return false; } } @@ -859,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()); @@ -914,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; } @@ -936,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; @@ -954,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; @@ -994,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; } } @@ -1022,3 +1043,23 @@ std::optional PackProfile::getSupportedModLoaders() loaders |= ModPlatform::Forge; return loaders; } + +QList PackProfile::getModLoadersList() +{ + QList result; + for (auto c : d->components) { + if (c->isEnabled() && Component::KNOWN_MODLOADERS.contains(c->getID())) { + result.append(Component::KNOWN_MODLOADERS[c->getID()].type); + } + } + + // TODO: remove this or add version condition once Quilt drops official Fabric support + if (result.contains(ModPlatform::Quilt) && !result.contains(ModPlatform::Fabric)) { + result.append(ModPlatform::Fabric); + } + if (getComponentVersion("net.minecraft") == "1.20.1" && result.contains(ModPlatform::NeoForge) && + !result.contains(ModPlatform::Forge)) { + result.append(ModPlatform::Forge); + } + return result; +} diff --git a/launcher/minecraft/PackProfile.h b/launcher/minecraft/PackProfile.h index e58e9ae9a..b2de26ea0 100644 --- a/launcher/minecraft/PackProfile.h +++ b/launcher/minecraft/PackProfile.h @@ -146,14 +146,15 @@ class PackProfile : public QAbstractListModel { std::optional getModLoaders(); // this returns aditional loaders(Quilt supports fabric and NeoForge supports Forge) 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/ProfileUtils.cpp b/launcher/minecraft/ProfileUtils.cpp index f81d6cb7f..08ec0fac3 100644 --- a/launcher/minecraft/ProfileUtils.cpp +++ b/launcher/minecraft/ProfileUtils.cpp @@ -57,7 +57,7 @@ bool readOverrideOrders(QString path, PatchOrder& order) } if (!orderFile.open(QFile::ReadOnly)) { qCritical() << "Couldn't open" << orderFile.fileName() << " for reading:" << orderFile.errorString(); - qWarning() << "Ignoring overriden order"; + qWarning() << "Ignoring overridden order"; return false; } @@ -66,7 +66,7 @@ bool readOverrideOrders(QString path, PatchOrder& order) QJsonDocument doc = QJsonDocument::fromJson(orderFile.readAll(), &error); if (error.error != QJsonParseError::NoError) { qCritical() << "Couldn't parse" << orderFile.fileName() << ":" << error.errorString(); - qWarning() << "Ignoring overriden order"; + qWarning() << "Ignoring overridden order"; return false; } @@ -84,7 +84,7 @@ bool readOverrideOrders(QString path, PatchOrder& order) } } catch ([[maybe_unused]] const JSONValidationError& err) { qCritical() << "Couldn't parse" << orderFile.fileName() << ": bad file format"; - qWarning() << "Ignoring overriden order"; + qWarning() << "Ignoring overridden order"; order.clear(); return false; } diff --git a/launcher/minecraft/VersionFile.cpp b/launcher/minecraft/VersionFile.cpp index 6632bb8bf..8ee61128f 100644 --- a/launcher/minecraft/VersionFile.cpp +++ b/launcher/minecraft/VersionFile.cpp @@ -73,6 +73,7 @@ void VersionFile::applyTo(LaunchProfile* profile, const RuntimeContext& runtimeC profile->applyMods(mods); profile->applyTraits(traits); profile->applyCompatibleJavaMajors(compatibleJavaMajors); + profile->applyCompatibleJavaName(compatibleJavaName); for (auto library : libraries) { profile->applyLibrary(library, runtimeContext); diff --git a/launcher/minecraft/VersionFile.h b/launcher/minecraft/VersionFile.h index 280e35ee3..40f49aaa4 100644 --- a/launcher/minecraft/VersionFile.h +++ b/launcher/minecraft/VersionFile.h @@ -36,6 +36,8 @@ #pragma once #include +#include +#include #include #include #include @@ -45,6 +47,7 @@ #include "Agent.h" #include "Library.h" #include "ProblemProvider.h" +#include "java/JavaMetadata.h" #include "minecraft/Rule.h" class PackProfile; @@ -98,6 +101,9 @@ class VersionFile : public ProblemContainer { /// Mojang: list of compatible java majors QList compatibleJavaMajors; + /// Mojang: the name of recommended java version + QString compatibleJavaName; + /// Mojang: type of the Minecraft version QString type; @@ -149,6 +155,8 @@ class VersionFile : public ProblemContainer { /// is volatile -- may be removed as soon as it is no longer needed by something else bool m_volatile = false; + QList runtimes; + public: // Mojang: DEPRECATED list of 'downloads' - client jar, server jar, windows server exe, maybe more. QMap> mojangDownloads; diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp index 1a680ac56..1eba148a5 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -206,8 +206,8 @@ int64_t calculateWorldSize(const QFileInfo& file) QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories); int64_t total = 0; while (it.hasNext()) { - total += it.fileInfo().size(); it.next(); + total += it.fileInfo().size(); } return total; } diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index e1f1e9b1e..fd2082035 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -42,7 +42,7 @@ #include namespace { -void tokenToJSONV3(QJsonObject& parent, Katabasis::Token t, const char* tokenName) +void tokenToJSONV3(QJsonObject& parent, Token t, const char* tokenName) { if (!t.persistent) { return; @@ -74,9 +74,9 @@ void tokenToJSONV3(QJsonObject& parent, Katabasis::Token t, const char* tokenNam } } -Katabasis::Token tokenFromJSONV3(const QJsonObject& parent, const char* tokenName) +Token tokenFromJSONV3(const QJsonObject& parent, const char* tokenName) { - Katabasis::Token out; + Token out; auto tokenObject = parent.value(tokenName).toObject(); if (tokenObject.isEmpty()) { return out; @@ -94,7 +94,7 @@ Katabasis::Token tokenFromJSONV3(const QJsonObject& parent, const char* tokenNam auto token = tokenObject.value("token"); if (token.isString()) { out.token = token.toString(); - out.validity = Katabasis::Validity::Assumed; + out.validity = Validity::Assumed; } auto refresh_token = tokenObject.value("refresh_token"); @@ -241,13 +241,13 @@ MinecraftProfile profileFromJSONV3(const QJsonObject& parent, const char* tokenN } } } - out.validity = Katabasis::Validity::Assumed; + out.validity = Validity::Assumed; return out; } void entitlementToJSONV3(QJsonObject& parent, MinecraftEntitlement p) { - if (p.validity == Katabasis::Validity::None) { + if (p.validity == Validity::None) { return; } QJsonObject out; @@ -271,7 +271,7 @@ bool entitlementFromJSONV3(const QJsonObject& parent, MinecraftEntitlement& out) } out.canPlayMinecraft = canPlayMinecraftV.toBool(false); out.ownsMinecraft = ownsMinecraftV.toBool(false); - out.validity = Katabasis::Validity::Assumed; + out.validity = Validity::Assumed; } return true; } @@ -313,10 +313,10 @@ bool AccountData::resumeStateFromV3(QJsonObject data) minecraftProfile = profileFromJSONV3(data, "profile"); if (!entitlementFromJSONV3(data, minecraftEntitlement)) { - if (minecraftProfile.validity != Katabasis::Validity::None) { + if (minecraftProfile.validity != Validity::None) { minecraftEntitlement.canPlayMinecraft = true; minecraftEntitlement.ownsMinecraft = true; - minecraftEntitlement.validity = Katabasis::Validity::Assumed; + minecraftEntitlement.validity = Validity::Assumed; } } diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index bac77e17f..1ada4e38a 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -34,12 +34,29 @@ */ #pragma once -#include #include #include #include #include +#include +#include +#include +#include + +enum class Validity { None, Assumed, Certain }; + +struct Token { + QDateTime issueInstant; + QDateTime notAfter; + QString token; + QString refresh_token; + QVariantMap extra; + + Validity validity = Validity::None; + bool persistent = true; +}; + struct Skin { QString id; QString url; @@ -59,7 +76,7 @@ struct Cape { struct MinecraftEntitlement { bool ownsMinecraft = false; bool canPlayMinecraft = false; - Katabasis::Validity validity = Katabasis::Validity::None; + Validity validity = Validity::None; }; struct MinecraftProfile { @@ -68,7 +85,7 @@ struct MinecraftProfile { Skin skin; QString currentCape; QMap capes; - Katabasis::Validity validity = Katabasis::Validity::None; + Validity validity = Validity::None; }; enum class AccountType { MSA, Offline }; @@ -93,15 +110,15 @@ struct AccountData { AccountType type = AccountType::MSA; QString msaClientID; - Katabasis::Token msaToken; - Katabasis::Token userToken; - Katabasis::Token xboxApiToken; - Katabasis::Token mojangservicesToken; + Token msaToken; + Token userToken; + Token xboxApiToken; + Token mojangservicesToken; - Katabasis::Token yggdrasilToken; + Token yggdrasilToken; MinecraftProfile minecraftProfile; MinecraftEntitlement minecraftEntitlement; - Katabasis::Validity validity_ = Katabasis::Validity::None; + Validity validity_ = Validity::None; // runtime only information (not saved with the account) QString internalId; diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index 68ebe3626..d276d4c41 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -35,7 +35,7 @@ #include "AccountList.h" #include "AccountData.h" -#include "AccountTask.h" +#include "tasks/Task.h" #include #include @@ -639,8 +639,8 @@ void AccountList::tryNext() if (account->internalId() == accountId) { m_currentTask = account->refresh(); if (m_currentTask) { - connect(m_currentTask.get(), &AccountTask::succeeded, this, &AccountList::authSucceeded); - connect(m_currentTask.get(), &AccountTask::failed, this, &AccountList::authFailed); + connect(m_currentTask.get(), &Task::succeeded, this, &AccountList::authSucceeded); + connect(m_currentTask.get(), &Task::failed, this, &AccountList::authFailed); m_currentTask->start(); qDebug() << "RefreshSchedule: Processing account " << account->accountDisplayString() << " with internal ID " << accountId; diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h index 039730739..d3be6740e 100644 --- a/launcher/minecraft/auth/AccountList.h +++ b/launcher/minecraft/auth/AccountList.h @@ -36,6 +36,7 @@ #pragma once #include "MinecraftAccount.h" +#include "minecraft/auth/AuthFlow.h" #include #include @@ -144,7 +145,7 @@ class AccountList : public QAbstractListModel { QList m_refreshQueue; QTimer* m_refreshTimer; QTimer* m_nextTimer; - shared_qobject_ptr m_currentTask; + shared_qobject_ptr m_currentTask; /*! * Called whenever the list changes. diff --git a/launcher/minecraft/auth/AccountTask.cpp b/launcher/minecraft/auth/AccountTask.cpp deleted file mode 100644 index 4c3d6ee19..000000000 --- a/launcher/minecraft/auth/AccountTask.cpp +++ /dev/null @@ -1,134 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Sefa Eyeoglu - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "AccountTask.h" -#include "MinecraftAccount.h" - -#include -#include -#include -#include -#include -#include - -#include - -AccountTask::AccountTask(AccountData* data, QObject* parent) : Task(parent), m_data(data) -{ - changeState(AccountTaskState::STATE_CREATED); -} - -QString AccountTask::getStateMessage() const -{ - switch (m_taskState) { - case AccountTaskState::STATE_CREATED: - return "Waiting..."; - case AccountTaskState::STATE_WORKING: - return tr("Sending request to auth servers..."); - case AccountTaskState::STATE_SUCCEEDED: - return tr("Authentication task succeeded."); - case AccountTaskState::STATE_OFFLINE: - return tr("Failed to contact the authentication server."); - case AccountTaskState::STATE_DISABLED: - return tr("Client ID has changed. New session needs to be created."); - case AccountTaskState::STATE_FAILED_SOFT: - return tr("Encountered an error during authentication."); - case AccountTaskState::STATE_FAILED_HARD: - return tr("Failed to authenticate. The session has expired."); - case AccountTaskState::STATE_FAILED_GONE: - return tr("Failed to authenticate. The account no longer exists."); - default: - return tr("..."); - } -} - -bool AccountTask::changeState(AccountTaskState newState, QString reason) -{ - m_taskState = newState; - // FIXME: virtual method invoked in constructor. - // We want that behavior, but maybe make it less weird? - setStatus(getStateMessage()); - switch (newState) { - case AccountTaskState::STATE_CREATED: { - m_data->errorString.clear(); - return true; - } - case AccountTaskState::STATE_WORKING: { - m_data->accountState = AccountState::Working; - return true; - } - case AccountTaskState::STATE_SUCCEEDED: { - m_data->accountState = AccountState::Online; - emitSucceeded(); - return false; - } - case AccountTaskState::STATE_OFFLINE: { - m_data->errorString = reason; - m_data->accountState = AccountState::Offline; - emitFailed(reason); - return false; - } - case AccountTaskState::STATE_DISABLED: { - m_data->errorString = reason; - m_data->accountState = AccountState::Disabled; - emitFailed(reason); - return false; - } - case AccountTaskState::STATE_FAILED_SOFT: { - m_data->errorString = reason; - m_data->accountState = AccountState::Errored; - emitFailed(reason); - return false; - } - case AccountTaskState::STATE_FAILED_HARD: { - m_data->errorString = reason; - m_data->accountState = AccountState::Expired; - emitFailed(reason); - return false; - } - case AccountTaskState::STATE_FAILED_GONE: { - m_data->errorString = reason; - m_data->accountState = AccountState::Gone; - emitFailed(reason); - return false; - } - default: { - QString error = tr("Unknown account task state: %1").arg(int(newState)); - m_data->accountState = AccountState::Errored; - emitFailed(error); - return false; - } - } -} diff --git a/launcher/minecraft/auth/AccountTask.h b/launcher/minecraft/auth/AccountTask.h deleted file mode 100644 index 82332c0b9..000000000 --- a/launcher/minecraft/auth/AccountTask.h +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Sefa Eyeoglu - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include - -#include -#include -#include -#include - -#include "MinecraftAccount.h" - -class QNetworkReply; - -/** - * Enum for describing the state of the current task. - * Used by the getStateMessage function to determine what the status message should be. - */ -enum class AccountTaskState { - STATE_CREATED, - STATE_WORKING, - STATE_SUCCEEDED, - STATE_DISABLED, //!< MSA Client ID has changed. Tell user to reloginn - STATE_FAILED_SOFT, //!< soft failure. authentication went through partially - STATE_FAILED_HARD, //!< hard failure. main tokens are invalid - STATE_FAILED_GONE, //!< hard failure. main tokens are invalid, and the account no longer exists - STATE_OFFLINE //!< soft failure. authentication failed in the first step in a 'soft' way -}; - -class AccountTask : public Task { - Q_OBJECT - public: - explicit AccountTask(AccountData* data, QObject* parent = 0); - virtual ~AccountTask(){}; - - AccountTaskState m_taskState = AccountTaskState::STATE_CREATED; - - AccountTaskState taskState() { return m_taskState; } - - signals: - void showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn); - void hideVerificationUriAndCode(); - - protected: - /** - * Returns the state message for the given state. - * Used to set the status message for the task. - * Should be overridden by subclasses that want to change messages for a given state. - */ - virtual QString getStateMessage() const; - - protected slots: - // NOTE: true -> non-terminal state, false -> terminal state - bool changeState(AccountTaskState newState, QString reason = QString()); - - protected: - AccountData* m_data = nullptr; -}; diff --git a/launcher/minecraft/auth/AuthFlow.cpp b/launcher/minecraft/auth/AuthFlow.cpp new file mode 100644 index 000000000..45926206c --- /dev/null +++ b/launcher/minecraft/auth/AuthFlow.cpp @@ -0,0 +1,156 @@ +#include +#include +#include +#include + +#include "minecraft/auth/AccountData.h" +#include "minecraft/auth/steps/EntitlementsStep.h" +#include "minecraft/auth/steps/GetSkinStep.h" +#include "minecraft/auth/steps/LauncherLoginStep.h" +#include "minecraft/auth/steps/MSADeviceCodeStep.h" +#include "minecraft/auth/steps/MSAStep.h" +#include "minecraft/auth/steps/MinecraftProfileStep.h" +#include "minecraft/auth/steps/XboxAuthorizationStep.h" +#include "minecraft/auth/steps/XboxProfileStep.h" +#include "minecraft/auth/steps/XboxUserStep.h" +#include "tasks/Task.h" + +#include "AuthFlow.h" + +#include + +AuthFlow::AuthFlow(AccountData* data, Action action, QObject* parent) : Task(parent), m_data(data) +{ + if (data->type == AccountType::MSA) { + if (action == Action::DeviceCode) { + auto oauthStep = makeShared(m_data); + connect(oauthStep.get(), &MSADeviceCodeStep::authorizeWithBrowser, this, &AuthFlow::authorizeWithBrowserWithExtra); + connect(this, &Task::aborted, oauthStep.get(), &MSADeviceCodeStep::abort); + m_steps.append(oauthStep); + } else { + auto oauthStep = makeShared(m_data, action == Action::Refresh); + connect(oauthStep.get(), &MSAStep::authorizeWithBrowser, this, &AuthFlow::authorizeWithBrowser); + m_steps.append(oauthStep); + } + m_steps.append(makeShared(m_data)); + m_steps.append(makeShared(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox")); + m_steps.append( + makeShared(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang")); + m_steps.append(makeShared(m_data)); + m_steps.append(makeShared(m_data)); + m_steps.append(makeShared(m_data)); + m_steps.append(makeShared(m_data)); + m_steps.append(makeShared(m_data)); + } + changeState(AccountTaskState::STATE_CREATED); +} + +void AuthFlow::succeed() +{ + m_data->validity_ = Validity::Certain; + changeState(AccountTaskState::STATE_SUCCEEDED, tr("Finished all authentication steps")); +} + +void AuthFlow::executeTask() +{ + changeState(AccountTaskState::STATE_WORKING, tr("Initializing")); + nextStep(); +} + +void AuthFlow::nextStep() +{ + if (!Task::isRunning()) { + return; + } + if (m_steps.size() == 0) { + // we got to the end without an incident... assume this is all. + m_currentStep.reset(); + succeed(); + return; + } + m_currentStep = m_steps.front(); + qDebug() << "AuthFlow:" << m_currentStep->describe(); + m_steps.pop_front(); + connect(m_currentStep.get(), &AuthStep::finished, this, &AuthFlow::stepFinished); + + m_currentStep->perform(); +} + +void AuthFlow::stepFinished(AccountTaskState resultingState, QString message) +{ + if (changeState(resultingState, message)) + nextStep(); +} + +bool AuthFlow::changeState(AccountTaskState newState, QString reason) +{ + m_taskState = newState; + setDetails(reason); + switch (newState) { + case AccountTaskState::STATE_CREATED: { + setStatus(tr("Waiting...")); + m_data->errorString.clear(); + return true; + } + case AccountTaskState::STATE_WORKING: { + setStatus(m_currentStep ? m_currentStep->describe() : tr("Working...")); + m_data->accountState = AccountState::Working; + return true; + } + case AccountTaskState::STATE_SUCCEEDED: { + setStatus(tr("Authentication task succeeded.")); + m_data->accountState = AccountState::Online; + emitSucceeded(); + return false; + } + case AccountTaskState::STATE_OFFLINE: { + setStatus(tr("Failed to contact the authentication server.")); + m_data->errorString = reason; + m_data->accountState = AccountState::Offline; + emitFailed(reason); + return false; + } + case AccountTaskState::STATE_DISABLED: { + setStatus(tr("Client ID has changed. New session needs to be created.")); + m_data->errorString = reason; + m_data->accountState = AccountState::Disabled; + emitFailed(reason); + return false; + } + case AccountTaskState::STATE_FAILED_SOFT: { + setStatus(tr("Encountered an error during authentication.")); + m_data->errorString = reason; + m_data->accountState = AccountState::Errored; + emitFailed(reason); + return false; + } + case AccountTaskState::STATE_FAILED_HARD: { + setStatus(tr("Failed to authenticate. The session has expired.")); + m_data->errorString = reason; + m_data->accountState = AccountState::Expired; + emitFailed(reason); + return false; + } + case AccountTaskState::STATE_FAILED_GONE: { + setStatus(tr("Failed to authenticate. The account no longer exists.")); + m_data->errorString = reason; + m_data->accountState = AccountState::Gone; + emitFailed(reason); + return false; + } + default: { + setStatus(tr("...")); + QString error = tr("Unknown account task state: %1").arg(int(newState)); + m_data->accountState = AccountState::Errored; + emitFailed(error); + return false; + } + } +} +bool AuthFlow::abort() +{ + emitAborted(); + if (m_currentStep) + m_currentStep->abort(); + return true; +} \ No newline at end of file diff --git a/launcher/minecraft/auth/AuthFlow.h b/launcher/minecraft/auth/AuthFlow.h new file mode 100644 index 000000000..4d18ac845 --- /dev/null +++ b/launcher/minecraft/auth/AuthFlow.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "minecraft/auth/AccountData.h" +#include "minecraft/auth/AuthStep.h" +#include "tasks/Task.h" + +class AuthFlow : public Task { + Q_OBJECT + + public: + enum class Action { Refresh, Login, DeviceCode }; + + explicit AuthFlow(AccountData* data, Action action = Action::Refresh, QObject* parent = 0); + virtual ~AuthFlow() = default; + + void executeTask() override; + + AccountTaskState taskState() { return m_taskState; } + + public slots: + bool abort() override; + + signals: + void authorizeWithBrowser(const QUrl& url); + void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn); + + protected: + void succeed(); + void nextStep(); + + private slots: + // NOTE: true -> non-terminal state, false -> terminal state + bool changeState(AccountTaskState newState, QString reason = QString()); + void stepFinished(AccountTaskState resultingState, QString message); + + private: + AccountTaskState m_taskState = AccountTaskState::STATE_CREATED; + QList m_steps; + AuthStep::Ptr m_currentStep; + AccountData* m_data = nullptr; +}; diff --git a/launcher/minecraft/auth/AuthRequest.cpp b/launcher/minecraft/auth/AuthRequest.cpp deleted file mode 100644 index 189978cc0..000000000 --- a/launcher/minecraft/auth/AuthRequest.cpp +++ /dev/null @@ -1,175 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Sefa Eyeoglu - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include - -#include -#include -#include -#include - -#include "Application.h" -#include "AuthRequest.h" -#include "katabasis/Globals.h" - -AuthRequest::AuthRequest(QObject* parent) : QObject(parent) {} - -AuthRequest::~AuthRequest() {} - -void AuthRequest::get(const QNetworkRequest& req, int timeout /* = 60*1000*/) -{ - setup(req, QNetworkAccessManager::GetOperation); - reply_ = APPLICATION->network()->get(request_); - status_ = Requesting; - timedReplies_.add(new Katabasis::Reply(reply_, timeout)); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 - connect(reply_, &QNetworkReply::errorOccurred, this, &AuthRequest::onRequestError); -#else // &QNetworkReply::error SIGNAL depricated - connect(reply_, QOverload::of(&QNetworkReply::error), this, &AuthRequest::onRequestError); -#endif - connect(reply_, &QNetworkReply::finished, this, &AuthRequest::onRequestFinished); - connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors); -} - -void AuthRequest::post(const QNetworkRequest& req, const QByteArray& data, int timeout /* = 60*1000*/) -{ - setup(req, QNetworkAccessManager::PostOperation); - data_ = data; - status_ = Requesting; - reply_ = APPLICATION->network()->post(request_, data_); - timedReplies_.add(new Katabasis::Reply(reply_, timeout)); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 - connect(reply_, &QNetworkReply::errorOccurred, this, &AuthRequest::onRequestError); -#else // &QNetworkReply::error SIGNAL depricated - connect(reply_, QOverload::of(&QNetworkReply::error), this, &AuthRequest::onRequestError); -#endif - connect(reply_, &QNetworkReply::finished, this, &AuthRequest::onRequestFinished); - connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors); - connect(reply_, &QNetworkReply::uploadProgress, this, &AuthRequest::onUploadProgress); -} - -void AuthRequest::onRequestFinished() -{ - if (status_ == Idle) { - return; - } - if (reply_ != qobject_cast(sender())) { - return; - } - httpStatus_ = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - finish(); -} - -void AuthRequest::onRequestError(QNetworkReply::NetworkError error) -{ - qWarning() << "AuthRequest::onRequestError: Error" << (int)error; - if (status_ == Idle) { - return; - } - if (reply_ != qobject_cast(sender())) { - return; - } - errorString_ = reply_->errorString(); - httpStatus_ = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - error_ = error; - qWarning() << "AuthRequest::onRequestError: Error string: " << errorString_; - qWarning() << "AuthRequest::onRequestError: HTTP status" << httpStatus_ - << reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); - - // QTimer::singleShot(10, this, SLOT(finish())); -} - -void AuthRequest::onSslErrors(QList errors) -{ - int i = 1; - for (auto error : errors) { - qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString(); - auto cert = error.certificate(); - qCritical() << "Certificate in question:\n" << cert.toText(); - i++; - } -} - -void AuthRequest::onUploadProgress(qint64 uploaded, qint64 total) -{ - if (status_ == Idle) { - qWarning() << "AuthRequest::onUploadProgress: No pending request"; - return; - } - if (reply_ != qobject_cast(sender())) { - return; - } - // Restart timeout because request in progress - Katabasis::Reply* o2Reply = timedReplies_.find(reply_); - if (o2Reply) { - o2Reply->start(); - } - emit uploadProgress(uploaded, total); -} - -void AuthRequest::setup(const QNetworkRequest& req, QNetworkAccessManager::Operation operation, const QByteArray& verb) -{ - request_ = req; - operation_ = operation; - url_ = req.url(); - - QUrl url = url_; - request_.setUrl(url); - - if (!verb.isEmpty()) { - request_.setRawHeader(Katabasis::HTTP_HTTP_HEADER, verb); - } - - status_ = Requesting; - error_ = QNetworkReply::NoError; - errorString_.clear(); - httpStatus_ = 0; -} - -void AuthRequest::finish() -{ - QByteArray data; - if (status_ == Idle) { - qWarning() << "AuthRequest::finish: No pending request"; - return; - } - data = reply_->readAll(); - status_ = Idle; - timedReplies_.remove(reply_); - reply_->disconnect(this); - reply_->deleteLater(); - QList headers = reply_->rawHeaderPairs(); - emit finished(error_, data, headers); -} diff --git a/launcher/minecraft/auth/AuthRequest.h b/launcher/minecraft/auth/AuthRequest.h deleted file mode 100644 index 84d2a7d68..000000000 --- a/launcher/minecraft/auth/AuthRequest.h +++ /dev/null @@ -1,67 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include - -#include "katabasis/Reply.h" - -/// Makes authentication requests. -class AuthRequest : public QObject { - Q_OBJECT - - public: - explicit AuthRequest(QObject* parent = 0); - ~AuthRequest(); - - public slots: - void get(const QNetworkRequest& req, int timeout = 60 * 1000); - void post(const QNetworkRequest& req, const QByteArray& data, int timeout = 60 * 1000); - - signals: - - /// Emitted when a request has been completed or failed. - void finished(QNetworkReply::NetworkError error, QByteArray data, QList headers); - - /// Emitted when an upload has progressed. - void uploadProgress(qint64 bytesSent, qint64 bytesTotal); - - protected slots: - - /// Handle request finished. - void onRequestFinished(); - - /// Handle request error. - void onRequestError(QNetworkReply::NetworkError error); - - /// Handle ssl errors. - void onSslErrors(QList errors); - - /// Finish the request, emit finished() signal. - void finish(); - - /// Handle upload progress. - void onUploadProgress(qint64 uploaded, qint64 total); - - public: - QNetworkReply::NetworkError error_; - int httpStatus_ = 0; - QString errorString_; - - protected: - void setup(const QNetworkRequest& request, QNetworkAccessManager::Operation operation, const QByteArray& verb = QByteArray()); - - enum Status { Idle, Requesting, ReRequesting }; - - QNetworkRequest request_; - QByteArray data_; - QNetworkReply* reply_; - Status status_; - QNetworkAccessManager::Operation operation_; - QUrl url_; - Katabasis::ReplyList timedReplies_; - - QTimer* timer_; -}; diff --git a/launcher/minecraft/auth/AuthSession.cpp b/launcher/minecraft/auth/AuthSession.cpp index 37534f983..3657befec 100644 --- a/launcher/minecraft/auth/AuthSession.cpp +++ b/launcher/minecraft/auth/AuthSession.cpp @@ -30,8 +30,13 @@ bool AuthSession::MakeOffline(QString offline_playername) return true; } -void AuthSession::MakeDemo() +void AuthSession::MakeDemo(QString name, QString u) { - player_name = "Player"; + wants_online = false; demo = true; -} + uuid = u; + session = "-"; + access_token = "0"; + player_name = name; + status = PlayableOnline; // needs online to download the assets +}; \ No newline at end of file diff --git a/launcher/minecraft/auth/AuthSession.h b/launcher/minecraft/auth/AuthSession.h index cec238033..54e7d69e0 100644 --- a/launcher/minecraft/auth/AuthSession.h +++ b/launcher/minecraft/auth/AuthSession.h @@ -10,7 +10,7 @@ class QNetworkAccessManager; struct AuthSession { bool MakeOffline(QString offline_playername); - void MakeDemo(); + void MakeDemo(QString name, QString uuid); QString serializeUserProperties(); diff --git a/launcher/minecraft/auth/AuthStep.cpp b/launcher/minecraft/auth/AuthStep.cpp deleted file mode 100644 index 6240cc549..000000000 --- a/launcher/minecraft/auth/AuthStep.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "AuthStep.h" - -AuthStep::AuthStep(AccountData* data) : QObject(nullptr), m_data(data) {} - -AuthStep::~AuthStep() noexcept = default; diff --git a/launcher/minecraft/auth/AuthStep.h b/launcher/minecraft/auth/AuthStep.h index becd9b0c5..aaaec6e7f 100644 --- a/launcher/minecraft/auth/AuthStep.h +++ b/launcher/minecraft/auth/AuthStep.h @@ -3,30 +3,41 @@ #include #include -#include "AccountTask.h" #include "QObjectPtr.h" #include "minecraft/auth/AccountData.h" +/** + * Enum for describing the state of the current task. + * Used by the getStateMessage function to determine what the status message should be. + */ +enum class AccountTaskState { + STATE_CREATED, + STATE_WORKING, + STATE_SUCCEEDED, + STATE_DISABLED, //!< MSA Client ID has changed. Tell user to reloginn + STATE_FAILED_SOFT, //!< soft failure. authentication went through partially + STATE_FAILED_HARD, //!< hard failure. main tokens are invalid + STATE_FAILED_GONE, //!< hard failure. main tokens are invalid, and the account no longer exists + STATE_OFFLINE //!< soft failure. authentication failed in the first step in a 'soft' way +}; + class AuthStep : public QObject { Q_OBJECT public: using Ptr = shared_qobject_ptr; - public: - explicit AuthStep(AccountData* data); - virtual ~AuthStep() noexcept; + explicit AuthStep(AccountData* data) : QObject(nullptr), m_data(data) {}; + virtual ~AuthStep() noexcept = default; virtual QString describe() = 0; public slots: virtual void perform() = 0; - virtual void rehydrate() = 0; + virtual void abort() {} signals: void finished(AccountTaskState resultingState, QString message); - void showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn); - void hideVerificationUriAndCode(); protected: AccountData* m_data; diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index ecee93d98..5b063604c 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -50,9 +50,8 @@ #include -#include "flows/MSA.h" -#include "flows/Offline.h" #include "minecraft/auth/AccountData.h" +#include "minecraft/auth/AuthFlow.h" MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) { @@ -80,15 +79,13 @@ MinecraftAccountPtr MinecraftAccount::createOffline(const QString& username) auto account = makeShared(); account->data.type = AccountType::Offline; account->data.yggdrasilToken.token = "0"; - account->data.yggdrasilToken.validity = Katabasis::Validity::Certain; + account->data.yggdrasilToken.validity = Validity::Certain; account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); account->data.yggdrasilToken.extra["userName"] = username; account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); - account->data.minecraftEntitlement.ownsMinecraft = true; - account->data.minecraftEntitlement.canPlayMinecraft = true; account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(QRegularExpression("[{}-]")); account->data.minecraftProfile.name = username; - account->data.minecraftProfile.validity = Katabasis::Validity::Certain; + account->data.minecraftProfile.validity = Validity::Certain; return account; } @@ -120,11 +117,11 @@ QPixmap MinecraftAccount::getFace() const return skin.scaled(64, 64, Qt::KeepAspectRatio); } -shared_qobject_ptr MinecraftAccount::loginMSA() +shared_qobject_ptr MinecraftAccount::login(bool useDeviceCode) { Q_ASSERT(m_currentTask.get() == nullptr); - m_currentTask.reset(new MSAInteractive(&data)); + m_currentTask.reset(new AuthFlow(&data, useDeviceCode ? AuthFlow::Action::DeviceCode : AuthFlow::Action::Login, this)); 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")); }); @@ -132,29 +129,13 @@ shared_qobject_ptr MinecraftAccount::loginMSA() return m_currentTask; } -shared_qobject_ptr MinecraftAccount::loginOffline() -{ - Q_ASSERT(m_currentTask.get() == nullptr); - - m_currentTask.reset(new OfflineLogin(&data)); - 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")); }); - emit activityChanged(true); - return m_currentTask; -} - -shared_qobject_ptr MinecraftAccount::refresh() +shared_qobject_ptr MinecraftAccount::refresh() { if (m_currentTask) { return m_currentTask; } - if (data.type == AccountType::MSA) { - m_currentTask.reset(new MSASilent(&data)); - } else { - m_currentTask.reset(new OfflineRefresh(&data)); - } + m_currentTask.reset(new AuthFlow(&data, AuthFlow::Action::Refresh, this)); connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed); @@ -163,7 +144,7 @@ shared_qobject_ptr MinecraftAccount::refresh() return m_currentTask; } -shared_qobject_ptr MinecraftAccount::currentTask() +shared_qobject_ptr MinecraftAccount::currentTask() { return m_currentTask; } @@ -189,17 +170,17 @@ void MinecraftAccount::authFailed(QString reason) if (accountType() == AccountType::MSA) { data.msaToken.token = QString(); data.msaToken.refresh_token = QString(); - data.msaToken.validity = Katabasis::Validity::None; - data.validity_ = Katabasis::Validity::None; + data.msaToken.validity = Validity::None; + data.validity_ = Validity::None; } else { data.yggdrasilToken.token = QString(); - data.yggdrasilToken.validity = Katabasis::Validity::None; - data.validity_ = Katabasis::Validity::None; + data.yggdrasilToken.validity = Validity::None; + data.validity_ = Validity::None; } emit changed(); } break; case AccountTaskState::STATE_FAILED_GONE: { - data.validity_ = Katabasis::Validity::None; + data.validity_ = Validity::None; emit changed(); } break; case AccountTaskState::STATE_CREATED: @@ -229,13 +210,13 @@ bool MinecraftAccount::shouldRefresh() const return false; } switch (data.validity_) { - case Katabasis::Validity::Certain: { + case Validity::Certain: { break; } - case Katabasis::Validity::None: { + case Validity::None: { return false; } - case Katabasis::Validity::Assumed: { + case Validity::Assumed: { return true; } } @@ -270,6 +251,8 @@ void MinecraftAccount::fillSession(AuthSessionPtr session) session->player_name = data.profileName(); // profile ID session->uuid = data.profileId(); + if (session->uuid.isEmpty()) + session->uuid = uuidFromUsername(session->player_name).toString().remove(QRegularExpression("[{}-]")); // 'legacy' or 'mojang', depending on account type session->user_type = typeString(); if (!session->access_token.isEmpty()) { diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h index f773b3bc9..f6fcfada2 100644 --- a/launcher/minecraft/auth/MinecraftAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -43,15 +43,13 @@ #include #include -#include - #include "AccountData.h" #include "AuthSession.h" #include "QObjectPtr.h" #include "Usable.h" +#include "minecraft/auth/AuthFlow.h" class Task; -class AccountTask; class MinecraftAccount; using MinecraftAccountPtr = shared_qobject_ptr; @@ -97,13 +95,11 @@ class MinecraftAccount : public QObject, public Usable { QJsonObject saveToJson() const; public: /* manipulation */ - shared_qobject_ptr loginMSA(); + shared_qobject_ptr login(bool useDeviceCode = false); - shared_qobject_ptr loginOffline(); + shared_qobject_ptr refresh(); - shared_qobject_ptr refresh(); - - shared_qobject_ptr currentTask(); + shared_qobject_ptr currentTask(); public: /* queries */ QString internalId() const { return data.internalId; } @@ -120,7 +116,7 @@ class MinecraftAccount : public QObject, public Usable { [[nodiscard]] AccountType accountType() const noexcept { return data.type; } - bool ownsMinecraft() const { return data.minecraftEntitlement.ownsMinecraft; } + bool ownsMinecraft() const { return data.type != AccountType::Offline && data.minecraftEntitlement.ownsMinecraft; } bool hasProfile() const { return data.profileId().size() != 0; } @@ -166,7 +162,7 @@ class MinecraftAccount : public QObject, public Usable { AccountData data; // current task we are executing here - shared_qobject_ptr m_currentTask; + shared_qobject_ptr m_currentTask; protected: /* methods */ void incrementUses() override; diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index f6179a93e..3aa458ace 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -79,7 +79,7 @@ bool getBool(QJsonValue value, bool& out) // 2148916238 = child account not linked to a family */ -bool parseXTokenResponse(QByteArray& data, Katabasis::Token& output, QString name) +bool parseXTokenResponse(QByteArray& data, Token& output, QString name) { qDebug() << "Parsing" << name << ":"; qCDebug(authCredentials()) << data; @@ -135,7 +135,7 @@ bool parseXTokenResponse(QByteArray& data, Katabasis::Token& output, QString nam qWarning() << "Missing uhs"; return false; } - output.validity = Katabasis::Validity::Certain; + output.validity = Validity::Certain; qDebug() << name << "is valid."; return true; } @@ -213,7 +213,7 @@ bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output) output.capes[capeOut.id] = capeOut; } output.currentCape = currentCape; - output.validity = Katabasis::Validity::Certain; + output.validity = Validity::Certain; return true; } @@ -347,7 +347,7 @@ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output) Skin skinOut; // fill in default skin info ourselves, as this endpoint doesn't provide it bool steve = isDefaultModelSteve(output.id); - skinOut.variant = steve ? "classic" : "slim"; + skinOut.variant = steve ? "CLASSIC" : "SLIM"; skinOut.url = steve ? SKIN_URL_STEVE : SKIN_URL_ALEX; // sadly we can't figure this out, but I don't think it really matters... skinOut.id = "00000000-0000-0000-0000-000000000000"; @@ -388,7 +388,7 @@ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output) output.currentCape = capeOut.alias; } - output.validity = Katabasis::Validity::Certain; + output.validity = Validity::Certain; return true; } @@ -422,7 +422,7 @@ bool parseMinecraftEntitlements(QByteArray& data, MinecraftEntitlement& output) output.ownsMinecraft = true; } } - output.validity = Katabasis::Validity::Certain; + output.validity = Validity::Certain; return true; } @@ -456,7 +456,7 @@ bool parseRolloutResponse(QByteArray& data, bool& result) return true; } -bool parseMojangResponse(QByteArray& data, Katabasis::Token& output) +bool parseMojangResponse(QByteArray& data, Token& output) { QJsonParseError jsonError; qDebug() << "Parsing Mojang response..."; @@ -488,7 +488,7 @@ bool parseMojangResponse(QByteArray& data, Katabasis::Token& output) qWarning() << "access_token is not valid"; return false; } - output.validity = Katabasis::Validity::Certain; + output.validity = Validity::Certain; qDebug() << "Mojang response is valid."; return true; } diff --git a/launcher/minecraft/auth/Parsers.h b/launcher/minecraft/auth/Parsers.h index d073f9994..4a235e4c2 100644 --- a/launcher/minecraft/auth/Parsers.h +++ b/launcher/minecraft/auth/Parsers.h @@ -9,8 +9,8 @@ bool getNumber(QJsonValue value, double& out); bool getNumber(QJsonValue value, int64_t& out); bool getBool(QJsonValue value, bool& out); -bool parseXTokenResponse(QByteArray& data, Katabasis::Token& output, QString name); -bool parseMojangResponse(QByteArray& data, Katabasis::Token& output); +bool parseXTokenResponse(QByteArray& data, Token& output, QString name); +bool parseMojangResponse(QByteArray& data, Token& output); bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output); bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output); diff --git a/launcher/minecraft/auth/flows/AuthFlow.cpp b/launcher/minecraft/auth/flows/AuthFlow.cpp deleted file mode 100644 index c51839a8c..000000000 --- a/launcher/minecraft/auth/flows/AuthFlow.cpp +++ /dev/null @@ -1,67 +0,0 @@ -#include -#include -#include -#include - -#include "AuthFlow.h" -#include "katabasis/Globals.h" - -#include - -AuthFlow::AuthFlow(AccountData* data, QObject* parent) : AccountTask(data, parent) {} - -void AuthFlow::succeed() -{ - m_data->validity_ = Katabasis::Validity::Certain; - changeState(AccountTaskState::STATE_SUCCEEDED, tr("Finished all authentication steps")); -} - -void AuthFlow::executeTask() -{ - if (m_currentStep) { - return; - } - changeState(AccountTaskState::STATE_WORKING, tr("Initializing")); - nextStep(); -} - -void AuthFlow::nextStep() -{ - if (m_steps.size() == 0) { - // we got to the end without an incident... assume this is all. - m_currentStep.reset(); - succeed(); - return; - } - m_currentStep = m_steps.front(); - qDebug() << "AuthFlow:" << m_currentStep->describe(); - m_steps.pop_front(); - connect(m_currentStep.get(), &AuthStep::finished, this, &AuthFlow::stepFinished); - connect(m_currentStep.get(), &AuthStep::showVerificationUriAndCode, this, &AuthFlow::showVerificationUriAndCode); - connect(m_currentStep.get(), &AuthStep::hideVerificationUriAndCode, this, &AuthFlow::hideVerificationUriAndCode); - - m_currentStep->perform(); -} - -QString AuthFlow::getStateMessage() const -{ - switch (m_taskState) { - case AccountTaskState::STATE_WORKING: { - if (m_currentStep) { - return m_currentStep->describe(); - } else { - return tr("Working..."); - } - } - default: { - return AccountTask::getStateMessage(); - } - } -} - -void AuthFlow::stepFinished(AccountTaskState resultingState, QString message) -{ - if (changeState(resultingState, message)) { - nextStep(); - } -} diff --git a/launcher/minecraft/auth/flows/AuthFlow.h b/launcher/minecraft/auth/flows/AuthFlow.h deleted file mode 100644 index e39e926dd..000000000 --- a/launcher/minecraft/auth/flows/AuthFlow.h +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include - -#include "minecraft/auth/AccountData.h" -#include "minecraft/auth/AccountTask.h" -#include "minecraft/auth/AuthStep.h" - -class AuthFlow : public AccountTask { - Q_OBJECT - - public: - explicit AuthFlow(AccountData* data, QObject* parent = 0); - - Katabasis::Validity validity() { return m_data->validity_; }; - - QString getStateMessage() const override; - - void executeTask() override; - - signals: - void activityChanged(Katabasis::Activity activity); - - private slots: - void stepFinished(AccountTaskState resultingState, QString message); - - protected: - void succeed(); - void nextStep(); - - protected: - QList m_steps; - AuthStep::Ptr m_currentStep; -}; diff --git a/launcher/minecraft/auth/flows/MSA.cpp b/launcher/minecraft/auth/flows/MSA.cpp deleted file mode 100644 index f0399342e..000000000 --- a/launcher/minecraft/auth/flows/MSA.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include "MSA.h" - -#include "minecraft/auth/steps/EntitlementsStep.h" -#include "minecraft/auth/steps/GetSkinStep.h" -#include "minecraft/auth/steps/LauncherLoginStep.h" -#include "minecraft/auth/steps/MSAStep.h" -#include "minecraft/auth/steps/MinecraftProfileStep.h" -#include "minecraft/auth/steps/XboxAuthorizationStep.h" -#include "minecraft/auth/steps/XboxProfileStep.h" -#include "minecraft/auth/steps/XboxUserStep.h" - -MSASilent::MSASilent(AccountData* data, QObject* parent) : AuthFlow(data, parent) -{ - m_steps.append(makeShared(m_data, MSAStep::Action::Refresh)); - m_steps.append(makeShared(m_data)); - m_steps.append(makeShared(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox")); - m_steps.append(makeShared(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang")); - m_steps.append(makeShared(m_data)); - m_steps.append(makeShared(m_data)); - m_steps.append(makeShared(m_data)); - m_steps.append(makeShared(m_data)); - m_steps.append(makeShared(m_data)); -} - -MSAInteractive::MSAInteractive(AccountData* data, QObject* parent) : AuthFlow(data, parent) -{ - m_steps.append(makeShared(m_data, MSAStep::Action::Login)); - m_steps.append(makeShared(m_data)); - m_steps.append(makeShared(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox")); - m_steps.append(makeShared(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang")); - m_steps.append(makeShared(m_data)); - m_steps.append(makeShared(m_data)); - m_steps.append(makeShared(m_data)); - m_steps.append(makeShared(m_data)); - m_steps.append(makeShared(m_data)); -} diff --git a/launcher/minecraft/auth/flows/MSA.h b/launcher/minecraft/auth/flows/MSA.h deleted file mode 100644 index e403d530f..000000000 --- a/launcher/minecraft/auth/flows/MSA.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once -#include "AuthFlow.h" - -class MSAInteractive : public AuthFlow { - Q_OBJECT - public: - explicit MSAInteractive(AccountData* data, QObject* parent = 0); -}; - -class MSASilent : public AuthFlow { - Q_OBJECT - public: - explicit MSASilent(AccountData* data, QObject* parent = 0); -}; diff --git a/launcher/minecraft/auth/flows/Offline.cpp b/launcher/minecraft/auth/flows/Offline.cpp deleted file mode 100644 index 3770b869a..000000000 --- a/launcher/minecraft/auth/flows/Offline.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include "Offline.h" - -#include "minecraft/auth/steps/OfflineStep.h" - -OfflineRefresh::OfflineRefresh(AccountData* data, QObject* parent) : AuthFlow(data, parent) -{ - m_steps.append(makeShared(m_data)); -} - -OfflineLogin::OfflineLogin(AccountData* data, QObject* parent) : AuthFlow(data, parent) -{ - m_steps.append(makeShared(m_data)); -} diff --git a/launcher/minecraft/auth/flows/Offline.h b/launcher/minecraft/auth/flows/Offline.h deleted file mode 100644 index 2bc9c7612..000000000 --- a/launcher/minecraft/auth/flows/Offline.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once -#include "AuthFlow.h" - -class OfflineRefresh : public AuthFlow { - Q_OBJECT - public: - explicit OfflineRefresh(AccountData* data, QObject* parent = 0); -}; - -class OfflineLogin : public AuthFlow { - Q_OBJECT - public: - explicit OfflineLogin(AccountData* data, QObject* parent = 0); -}; diff --git a/launcher/minecraft/auth/steps/EntitlementsStep.cpp b/launcher/minecraft/auth/steps/EntitlementsStep.cpp index 0573dcb6e..5b9809c52 100644 --- a/launcher/minecraft/auth/steps/EntitlementsStep.cpp +++ b/launcher/minecraft/auth/steps/EntitlementsStep.cpp @@ -1,16 +1,21 @@ #include "EntitlementsStep.h" +#include #include +#include #include +#include +#include "Application.h" #include "Logging.h" -#include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/Parsers.h" +#include "net/Download.h" +#include "net/NetJob.h" +#include "net/RawHeaderProxy.h" +#include "tasks/Task.h" EntitlementsStep::EntitlementsStep(AccountData* data) : AuthStep(data) {} -EntitlementsStep::~EntitlementsStep() noexcept = default; - QString EntitlementsStep::describe() { return tr("Determining game ownership."); @@ -19,35 +24,34 @@ QString EntitlementsStep::describe() void EntitlementsStep::perform() { auto uuid = QUuid::createUuid(); - m_entitlementsRequestId = uuid.toString().remove('{').remove('}'); - auto url = "https://api.minecraftservices.com/entitlements/license?requestId=" + m_entitlementsRequestId; - QNetworkRequest request = QNetworkRequest(url); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Accept", "application/json"); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); - AuthRequest* requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &EntitlementsStep::onRequestDone); - requestor->get(request); + m_entitlements_request_id = uuid.toString().remove('{').remove('}'); + + QUrl url("https://api.minecraftservices.com/entitlements/license?requestId=" + m_entitlements_request_id); + auto headers = QList{ { "Content-Type", "application/json" }, + { "Accept", "application/json" }, + { "Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8() } }; + + m_response.reset(new QByteArray()); + m_request = Net::Download::makeByteArray(url, m_response); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + m_task.reset(new NetJob("EntitlementsStep", APPLICATION->network())); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &EntitlementsStep::onRequestDone); + + m_task->start(); qDebug() << "Getting entitlements..."; } -void EntitlementsStep::rehydrate() +void EntitlementsStep::onRequestDone() { - // NOOP, for now. We only save bools and there's nothing to check. -} - -void EntitlementsStep::onRequestDone([[maybe_unused]] QNetworkReply::NetworkError error, - QByteArray data, - [[maybe_unused]] QList headers) -{ - auto requestor = qobject_cast(QObject::sender()); - requestor->deleteLater(); - - qCDebug(authCredentials()) << data; + qCDebug(authCredentials()) << *m_response; // TODO: check presence of same entitlementsRequestId? // TODO: validate JWTs? - Parsers::parseMinecraftEntitlements(data, m_data->minecraftEntitlement); + Parsers::parseMinecraftEntitlements(*m_response, m_data->minecraftEntitlement); emit finished(AccountTaskState::STATE_WORKING, tr("Got entitlements")); } diff --git a/launcher/minecraft/auth/steps/EntitlementsStep.h b/launcher/minecraft/auth/steps/EntitlementsStep.h index be16bda13..f20fcac08 100644 --- a/launcher/minecraft/auth/steps/EntitlementsStep.h +++ b/launcher/minecraft/auth/steps/EntitlementsStep.h @@ -1,24 +1,28 @@ #pragma once #include +#include -#include "QObjectPtr.h" #include "minecraft/auth/AuthStep.h" +#include "net/Download.h" +#include "net/NetJob.h" class EntitlementsStep : public AuthStep { Q_OBJECT public: explicit EntitlementsStep(AccountData* data); - virtual ~EntitlementsStep() noexcept; + virtual ~EntitlementsStep() noexcept = default; void perform() override; - void rehydrate() override; QString describe() override; private slots: - void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); + void onRequestDone(); private: - QString m_entitlementsRequestId; + QString m_entitlements_request_id; + std::shared_ptr m_response; + Net::Download::Ptr m_request; + NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/auth/steps/GetSkinStep.cpp b/launcher/minecraft/auth/steps/GetSkinStep.cpp index 520877020..e067bc34c 100644 --- a/launcher/minecraft/auth/steps/GetSkinStep.cpp +++ b/launcher/minecraft/auth/steps/GetSkinStep.cpp @@ -3,13 +3,10 @@ #include -#include "minecraft/auth/AuthRequest.h" -#include "minecraft/auth/Parsers.h" +#include "Application.h" GetSkinStep::GetSkinStep(AccountData* data) : AuthStep(data) {} -GetSkinStep::~GetSkinStep() noexcept = default; - QString GetSkinStep::describe() { return tr("Getting skin."); @@ -17,25 +14,23 @@ QString GetSkinStep::describe() void GetSkinStep::perform() { - auto url = QUrl(m_data->minecraftProfile.skin.url); - QNetworkRequest request = QNetworkRequest(url); - AuthRequest* requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &GetSkinStep::onRequestDone); - requestor->get(request); + QUrl url(m_data->minecraftProfile.skin.url); + + m_response.reset(new QByteArray()); + m_request = Net::Download::makeByteArray(url, m_response); + + m_task.reset(new NetJob("GetSkinStep", APPLICATION->network())); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &GetSkinStep::onRequestDone); + + m_task->start(); } -void GetSkinStep::rehydrate() +void GetSkinStep::onRequestDone() { - // NOOP, for now. -} - -void GetSkinStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList headers) -{ - auto requestor = qobject_cast(QObject::sender()); - requestor->deleteLater(); - - if (error == QNetworkReply::NoError) { - m_data->minecraftProfile.skin.data = data; - } - emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Got skin")); + if (m_request->error() == QNetworkReply::NoError) + m_data->minecraftProfile.skin.data = *m_response; + emit finished(AccountTaskState::STATE_WORKING, tr("Got skin")); } diff --git a/launcher/minecraft/auth/steps/GetSkinStep.h b/launcher/minecraft/auth/steps/GetSkinStep.h index 105e497d1..c598f05d9 100644 --- a/launcher/minecraft/auth/steps/GetSkinStep.h +++ b/launcher/minecraft/auth/steps/GetSkinStep.h @@ -1,21 +1,27 @@ #pragma once #include +#include -#include "QObjectPtr.h" #include "minecraft/auth/AuthStep.h" +#include "net/Download.h" +#include "net/NetJob.h" class GetSkinStep : public AuthStep { Q_OBJECT public: explicit GetSkinStep(AccountData* data); - virtual ~GetSkinStep() noexcept; + virtual ~GetSkinStep() noexcept = default; void perform() override; - void rehydrate() override; QString describe() override; private slots: - void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); + void onRequestDone(); + + private: + std::shared_ptr m_response; + Net::Download::Ptr m_request; + NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp index c57f51113..954f013af 100644 --- a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp +++ b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp @@ -1,17 +1,17 @@ #include "LauncherLoginStep.h" #include +#include +#include "Application.h" #include "Logging.h" -#include "minecraft/auth/AccountTask.h" -#include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/Parsers.h" #include "net/NetUtils.h" +#include "net/RawHeaderProxy.h" +#include "net/Upload.h" LauncherLoginStep::LauncherLoginStep(AccountData* data) : AuthStep(data) {} -LauncherLoginStep::~LauncherLoginStep() noexcept = default; - QString LauncherLoginStep::describe() { return tr("Accessing Mojang services."); @@ -19,7 +19,7 @@ QString LauncherLoginStep::describe() void LauncherLoginStep::perform() { - auto requestURL = "https://api.minecraftservices.com/launcher/login"; + QUrl url("https://api.minecraftservices.com/launcher/login"); auto uhs = m_data->mojangservicesToken.extra["uhs"].toString(); auto xToken = m_data->mojangservicesToken.token; @@ -31,40 +31,41 @@ void LauncherLoginStep::perform() )XXX"; auto requestBody = mc_auth_template.arg(uhs, xToken); - QNetworkRequest request = QNetworkRequest(QUrl(requestURL)); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Accept", "application/json"); - AuthRequest* requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &LauncherLoginStep::onRequestDone); - requestor->post(request, requestBody.toUtf8()); + auto headers = QList{ + { "Content-Type", "application/json" }, + { "Accept", "application/json" }, + }; + + m_response.reset(new QByteArray()); + m_request = Net::Upload::makeByteArray(url, m_response, requestBody.toUtf8()); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + m_task.reset(new NetJob("LauncherLoginStep", APPLICATION->network())); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &LauncherLoginStep::onRequestDone); + + m_task->start(); qDebug() << "Getting Minecraft access token..."; } -void LauncherLoginStep::rehydrate() +void LauncherLoginStep::onRequestDone() { - // TODO: check the token validity -} - -void LauncherLoginStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList headers) -{ - auto requestor = qobject_cast(QObject::sender()); - requestor->deleteLater(); - - qCDebug(authCredentials()) << data; - if (error != QNetworkReply::NoError) { - qWarning() << "Reply error:" << error; - qCDebug(authCredentials()) << data; - if (Net::isApplicationError(error)) { - emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to get Minecraft access token: %1").arg(requestor->errorString_)); + qCDebug(authCredentials()) << *m_response; + if (m_request->error() != QNetworkReply::NoError) { + qWarning() << "Reply error:" << m_request->error(); + if (Net::isApplicationError(m_request->error())) { + emit finished(AccountTaskState::STATE_FAILED_SOFT, + tr("Failed to get Minecraft access token: %1").arg(m_request->errorString())); } else { - emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to get Minecraft access token: %1").arg(requestor->errorString_)); + emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to get Minecraft access token: %1").arg(m_request->errorString())); } return; } - if (!Parsers::parseMojangResponse(data, m_data->yggdrasilToken)) { + if (!Parsers::parseMojangResponse(*m_response, m_data->yggdrasilToken)) { qWarning() << "Could not parse login_with_xbox response..."; - qCDebug(authCredentials()) << data; emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to parse the Minecraft access token response.")); return; } diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.h b/launcher/minecraft/auth/steps/LauncherLoginStep.h index 30c18e675..0b5969f2b 100644 --- a/launcher/minecraft/auth/steps/LauncherLoginStep.h +++ b/launcher/minecraft/auth/steps/LauncherLoginStep.h @@ -1,21 +1,27 @@ #pragma once #include +#include -#include "QObjectPtr.h" #include "minecraft/auth/AuthStep.h" +#include "net/NetJob.h" +#include "net/Upload.h" class LauncherLoginStep : public AuthStep { Q_OBJECT public: explicit LauncherLoginStep(AccountData* data); - virtual ~LauncherLoginStep() noexcept; + virtual ~LauncherLoginStep() noexcept = default; void perform() override; - void rehydrate() override; QString describe() override; private slots: - void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); + void onRequestDone(); + + private: + std::shared_ptr m_response; + Net::Upload::Ptr m_request; + NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp b/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp new file mode 100644 index 000000000..c283b153e --- /dev/null +++ b/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp @@ -0,0 +1,277 @@ +// 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 . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MSADeviceCodeStep.h" + +#include +#include + +#include "Application.h" +#include "Json.h" +#include "net/RawHeaderProxy.h" + +// https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-device-code +MSADeviceCodeStep::MSADeviceCodeStep(AccountData* data) : AuthStep(data) +{ + m_clientId = APPLICATION->getMSAClientID(); + connect(&m_expiration_timer, &QTimer::timeout, this, &MSADeviceCodeStep::abort); + connect(&m_pool_timer, &QTimer::timeout, this, &MSADeviceCodeStep::authenticateUser); +} + +QString MSADeviceCodeStep::describe() +{ + return tr("Logging in with Microsoft account(device code)."); +} + +void MSADeviceCodeStep::perform() +{ + QUrlQuery data; + data.addQueryItem("client_id", m_clientId); + data.addQueryItem("scope", "XboxLive.SignIn XboxLive.offline_access"); + auto payload = data.query(QUrl::FullyEncoded).toUtf8(); + QUrl url("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"); + auto headers = QList{ + { "Content-Type", "application/x-www-form-urlencoded" }, + { "Accept", "application/json" }, + }; + m_response.reset(new QByteArray()); + m_request = Net::Upload::makeByteArray(url, m_response, payload); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + m_task.reset(new NetJob("MSADeviceCodeStep", APPLICATION->network())); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &MSADeviceCodeStep::deviceAutorizationFinished); + + m_task->start(); +} + +struct DeviceAutorizationResponse { + QString device_code; + QString user_code; + QString verification_uri; + int expires_in; + int interval; + + QString error; + QString error_description; +}; + +DeviceAutorizationResponse parseDeviceAutorizationResponse(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(); + return {}; + } + + if (!doc.isObject()) { + qWarning() << "Device autorization response is not an object"; + return {}; + } + auto obj = doc.object(); + return { + Json::ensureString(obj, "device_code"), Json::ensureString(obj, "user_code"), Json::ensureString(obj, "verification_uri"), + Json::ensureInteger(obj, "expires_in"), Json::ensureInteger(obj, "interval"), Json::ensureString(obj, "error"), + Json::ensureString(obj, "error_description"), + }; +} + +void MSADeviceCodeStep::deviceAutorizationFinished() +{ + auto rsp = parseDeviceAutorizationResponse(*m_response); + if (!rsp.error.isEmpty() || !rsp.error_description.isEmpty()) { + qWarning() << "Device authorization failed:" << rsp.error; + emit finished(AccountTaskState::STATE_FAILED_HARD, + tr("Device authorization failed: %1").arg(rsp.error_description.isEmpty() ? rsp.error : rsp.error_description)); + return; + } + if (!m_request->wasSuccessful() || m_request->error() != QNetworkReply::NoError) { + emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Failed to retrieve device authorization")); + qDebug() << *m_response; + return; + } + + if (rsp.device_code.isEmpty() || rsp.user_code.isEmpty() || rsp.verification_uri.isEmpty() || rsp.expires_in == 0) { + emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Device authorization failed: required fields missing")); + return; + } + if (rsp.interval != 0) { + interval = rsp.interval; + } + m_device_code = rsp.device_code; + emit authorizeWithBrowser(rsp.verification_uri, rsp.user_code, rsp.expires_in); + m_expiration_timer.setTimerType(Qt::VeryCoarseTimer); + m_expiration_timer.setInterval(rsp.expires_in * 1000); + m_expiration_timer.setSingleShot(true); + m_expiration_timer.start(); + m_pool_timer.setTimerType(Qt::VeryCoarseTimer); + m_pool_timer.setSingleShot(true); + startPoolTimer(); +} + +void MSADeviceCodeStep::abort() +{ + m_expiration_timer.stop(); + m_pool_timer.stop(); + if (m_request) { + m_request->abort(); + } + m_is_aborted = true; + emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Task aborted")); +} + +void MSADeviceCodeStep::startPoolTimer() +{ + if (m_is_aborted) { + return; + } + if (m_expiration_timer.remainingTime() < interval * 1000) { + perform(); + return; + } + + m_pool_timer.setInterval(interval * 1000); + m_pool_timer.start(); +} + +void MSADeviceCodeStep::authenticateUser() +{ + QUrlQuery data; + data.addQueryItem("client_id", m_clientId); + data.addQueryItem("grant_type", "urn:ietf:params:oauth:grant-type:device_code"); + data.addQueryItem("device_code", m_device_code); + auto payload = data.query(QUrl::FullyEncoded).toUtf8(); + QUrl url("https://login.microsoftonline.com/consumers/oauth2/v2.0/token"); + auto headers = QList{ + { "Content-Type", "application/x-www-form-urlencoded" }, + { "Accept", "application/json" }, + }; + m_response.reset(new QByteArray()); + m_request = Net::Upload::makeByteArray(url, m_response, payload); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + connect(m_request.get(), &Task::finished, this, &MSADeviceCodeStep::authenticationFinished); + + m_request->setNetwork(APPLICATION->network()); + m_request->start(); +} + +struct AuthenticationResponse { + QString access_token; + QString token_type; + QString refresh_token; + int expires_in; + + QString error; + QString error_description; + + QVariantMap extra; +}; + +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(); + return {}; + } + + if (!doc.isObject()) { + qWarning() << "Device autorization response is not an object"; + return {}; + } + auto obj = doc.object(); + return { Json::ensureString(obj, "access_token"), + Json::ensureString(obj, "token_type"), + Json::ensureString(obj, "refresh_token"), + Json::ensureInteger(obj, "expires_in"), + Json::ensureString(obj, "error"), + Json::ensureString(obj, "error_description"), + obj.toVariantMap() }; +} + +void MSADeviceCodeStep::authenticationFinished() +{ + if (m_request->error() == QNetworkReply::TimeoutError) { + // rfc8628#section-3.5 + // "On encountering a connection timeout, clients MUST unilaterally + // reduce their polling frequency before retrying. The use of an + // exponential backoff algorithm to achieve this, such as doubling the + // polling interval on each such connection timeout, is RECOMMENDED." + interval *= 2; + startPoolTimer(); + return; + } + auto rsp = parseAuthenticationResponse(*m_response); + if (rsp.error == "slow_down") { + // rfc8628#section-3.5 + // "A variant of 'authorization_pending', the authorization request is + // still pending and polling should continue, but the interval MUST + // be increased by 5 seconds for this and all subsequent requests." + interval += 5; + startPoolTimer(); + return; + } + if (rsp.error == "authorization_pending") { + // keep trying - rfc8628#section-3.5 + // "The authorization request is still pending as the end user hasn't + // yet completed the user-interaction steps (Section 3.3)." + startPoolTimer(); + return; + } + if (!rsp.error.isEmpty() || !rsp.error_description.isEmpty()) { + qWarning() << "Device Access failed:" << rsp.error; + emit finished(AccountTaskState::STATE_FAILED_HARD, + tr("Device Access failed: %1").arg(rsp.error_description.isEmpty() ? rsp.error : rsp.error_description)); + return; + } + if (!m_request->wasSuccessful() || m_request->error() != QNetworkReply::NoError) { + startPoolTimer(); // it failed so just try again without increasing the interval + return; + } + + m_expiration_timer.stop(); + m_data->msaClientID = m_clientId; + m_data->msaToken.issueInstant = QDateTime::currentDateTimeUtc(); + m_data->msaToken.notAfter = QDateTime::currentDateTime().addSecs(rsp.expires_in); + m_data->msaToken.extra = rsp.extra; + 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 new file mode 100644 index 000000000..616008def --- /dev/null +++ b/launcher/minecraft/auth/steps/MSADeviceCodeStep.h @@ -0,0 +1,78 @@ +// 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 . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include + +#include "minecraft/auth/AuthStep.h" +#include "net/NetJob.h" +#include "net/Upload.h" + +class MSADeviceCodeStep : public AuthStep { + Q_OBJECT + public: + explicit MSADeviceCodeStep(AccountData* data); + virtual ~MSADeviceCodeStep() noexcept = default; + + void perform() override; + + QString describe() override; + + public slots: + void abort() override; + + signals: + void authorizeWithBrowser(QString url, QString code, int expiresIn); + + private slots: + void deviceAutorizationFinished(); + void startPoolTimer(); + void authenticateUser(); + void authenticationFinished(); + + private: + QString m_clientId; + QString m_device_code; + bool m_is_aborted = false; + int interval = 5; + + QTimer m_pool_timer; + QTimer m_expiration_timer; + + std::shared_ptr m_response; + Net::Upload::Ptr m_request; + NetJob::Ptr m_task; +}; diff --git a/launcher/minecraft/auth/steps/MSAStep.cpp b/launcher/minecraft/auth/steps/MSAStep.cpp index 1aa22765d..74999414c 100644 --- a/launcher/minecraft/auth/steps/MSAStep.cpp +++ b/launcher/minecraft/auth/steps/MSAStep.cpp @@ -35,123 +35,152 @@ #include "MSAStep.h" +#include #include - -#include "BuildConfig.h" -#include "minecraft/auth/AuthRequest.h" -#include "minecraft/auth/Parsers.h" +#include +#include #include "Application.h" -#include "Logging.h" +#include "BuildConfig.h" +#include "FileSystem.h" -using OAuth2 = Katabasis::DeviceFlow; -using Activity = Katabasis::Activity; +#include +#include +#include -MSAStep::MSAStep(AccountData* data, Action action) : AuthStep(data), m_action(action) +bool isSchemeHandlerRegistered() { - m_clientId = APPLICATION->getMSAClientID(); - OAuth2::Options opts; - opts.scope = "XboxLive.signin offline_access"; - opts.clientIdentifier = m_clientId; - opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"; - opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; +#ifdef Q_OS_LINUX + QProcess process; + process.start("xdg-mime", { "query", "default", "x-scheme-handler/" + BuildConfig.LAUNCHER_APP_BINARY_NAME }); + process.waitForFinished(); + QString output = process.readAllStandardOutput().trimmed(); - // FIXME: OAuth2 is not aware of our fancy shared pointers - m_oauth2 = new OAuth2(opts, m_data->msaToken, this, APPLICATION->network().get()); + return output.contains(BuildConfig.LAUNCHER_APP_BINARY_NAME); - connect(m_oauth2, &OAuth2::activityChanged, this, &MSAStep::onOAuthActivityChanged); - connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &MSAStep::showVerificationUriAndCode); +#elif defined(Q_OS_WIN) + QString regPath = QString("HKEY_CURRENT_USER\\Software\\Classes\\%1").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); + QSettings settings(regPath, QSettings::NativeFormat); + + return settings.contains("shell/open/command/."); +#endif + return true; } -MSAStep::~MSAStep() noexcept = default; +class CustomOAuthOobReplyHandler : public QOAuthOobReplyHandler { + Q_OBJECT + + public: + explicit CustomOAuthOobReplyHandler(QObject* parent = nullptr) : QOAuthOobReplyHandler(parent) + { + connect(APPLICATION, &Application::oauthReplyRecieved, this, &QOAuthOobReplyHandler::callbackReceived); + } + ~CustomOAuthOobReplyHandler() override + { + disconnect(APPLICATION, &Application::oauthReplyRecieved, this, &QOAuthOobReplyHandler::callbackReceived); + } + QString callback() const override { return BuildConfig.LAUNCHER_APP_BINARY_NAME + "://oauth/microsoft"; } +}; + +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()) + + { + auto replyHandler = new QOAuthHttpServerReplyHandler(this); + replyHandler->setCallbackText(QString(R"XXX( + + Login Successful, redirecting... + + )XXX") + .arg(BuildConfig.LOGIN_CALLBACK_URL)); + oauth2.setReplyHandler(replyHandler); + } else { + oauth2.setReplyHandler(new CustomOAuthOobReplyHandler(this)); + } + oauth2.setAuthorizationUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize")); + oauth2.setAccessTokenUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")); + oauth2.setScope("XboxLive.SignIn XboxLive.offline_access"); + oauth2.setClientIdentifier(m_clientId); + oauth2.setNetworkAccessManager(APPLICATION->network().get()); + + connect(&oauth2, &QOAuth2AuthorizationCodeFlow::granted, this, [this] { + m_data->msaClientID = oauth2.clientIdentifier(); + m_data->msaToken.issueInstant = QDateTime::currentDateTimeUtc(); + m_data->msaToken.notAfter = oauth2.expirationAt(); + m_data->msaToken.extra = oauth2.extraTokens(); + m_data->msaToken.refresh_token = oauth2.refreshToken(); + m_data->msaToken.token = oauth2.token(); + emit finished(AccountTaskState::STATE_WORKING, tr("Got ")); + }); + connect(&oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, &MSAStep::authorizeWithBrowser); + connect(&oauth2, &QOAuth2AuthorizationCodeFlow::requestFailed, this, [this, silent](const QAbstractOAuth2::Error err) { + auto state = AccountTaskState::STATE_FAILED_HARD; + if (oauth2.status() == QAbstractOAuth::Status::Granted || silent) { + if (err == QAbstractOAuth2::Error::NetworkError) { + state = AccountTaskState::STATE_OFFLINE; + } else { + state = AccountTaskState::STATE_FAILED_SOFT; + } + } + auto message = tr("Microsoft user authentication failed."); + if (silent) { + message = tr("Failed to refresh token."); + } + qWarning() << message; + emit finished(state, message); + }); + connect(&oauth2, &QOAuth2AuthorizationCodeFlow::error, this, + [this](const QString& error, const QString& errorDescription, const QUrl& uri) { + qWarning() << "Failed to login because" << error << errorDescription; + emit finished(AccountTaskState::STATE_FAILED_HARD, errorDescription); + }); + + connect(&oauth2, &QOAuth2AuthorizationCodeFlow::extraTokensChanged, this, + [this](const QVariantMap& tokens) { m_data->msaToken.extra = tokens; }); + + connect(&oauth2, &QOAuth2AuthorizationCodeFlow::clientIdentifierChanged, this, + [this](const QString& clientIdentifier) { m_data->msaClientID = clientIdentifier; }); +} QString MSAStep::describe() { return tr("Logging in with Microsoft account."); } -void MSAStep::rehydrate() -{ - switch (m_action) { - case Refresh: { - // TODO: check the tokens and see if they are old (older than a day) - return; - } - case Login: { - // NOOP - return; - } - } -} - void MSAStep::perform() { - switch (m_action) { - case Refresh: { - if (m_data->msaClientID != m_clientId) { - emit hideVerificationUriAndCode(); - emit finished(AccountTaskState::STATE_DISABLED, - tr("Microsoft user authentication failed - client identification has changed.")); - } - m_oauth2->refresh(); + if (m_silent) { + if (m_data->msaClientID != m_clientId) { + emit finished(AccountTaskState::STATE_DISABLED, + tr("Microsoft user authentication failed - client identification has changed.")); return; } - case Login: { - QVariantMap extraOpts; - extraOpts["prompt"] = "select_account"; - m_oauth2->setExtraRequestParams(extraOpts); + if (m_data->msaToken.refresh_token.isEmpty()) { + emit finished(AccountTaskState::STATE_DISABLED, tr("Microsoft user authentication failed - refresh token is empty.")); + return; + } + oauth2.setRefreshToken(m_data->msaToken.refresh_token); + oauth2.refreshAccessToken(); + } else { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) // QMultiMap param changed in 6.0 + oauth2.setModifyParametersFunction( + [](QAbstractOAuth::Stage stage, QMultiMap* map) { map->insert("prompt", "select_account"); }); +#else + oauth2.setModifyParametersFunction( + [](QAbstractOAuth::Stage stage, QMap* map) { map->insert("prompt", "select_account"); }); +#endif - *m_data = AccountData(); - m_data->msaClientID = m_clientId; - m_oauth2->login(); - return; - } + *m_data = AccountData(); + m_data->msaClientID = m_clientId; + oauth2.grant(); } } -void MSAStep::onOAuthActivityChanged(Katabasis::Activity activity) -{ - switch (activity) { - case Katabasis::Activity::Idle: - case Katabasis::Activity::LoggingIn: - case Katabasis::Activity::Refreshing: - case Katabasis::Activity::LoggingOut: { - // We asked it to do something, it's doing it. Nothing to act upon. - return; - } - case Katabasis::Activity::Succeeded: { - // Succeeded or did not invalidate tokens - emit hideVerificationUriAndCode(); - QVariantMap extraTokens = m_oauth2->extraTokens(); - if (!extraTokens.isEmpty()) { - qCDebug(authCredentials()) << "Extra tokens in response:"; - foreach (QString key, extraTokens.keys()) { - qCDebug(authCredentials()) << "\t" << key << ":" << extraTokens.value(key); - } - } - emit finished(AccountTaskState::STATE_WORKING, tr("Got ")); - return; - } - case Katabasis::Activity::FailedSoft: { - // NOTE: soft error in the first step means 'offline' - emit hideVerificationUriAndCode(); - emit finished(AccountTaskState::STATE_OFFLINE, tr("Microsoft user authentication ended with a network error.")); - return; - } - case Katabasis::Activity::FailedGone: { - emit hideVerificationUriAndCode(); - emit finished(AccountTaskState::STATE_FAILED_GONE, tr("Microsoft user authentication failed - user no longer exists.")); - return; - } - case Katabasis::Activity::FailedHard: { - emit hideVerificationUriAndCode(); - emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication failed.")); - return; - } - default: { - emit hideVerificationUriAndCode(); - emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication completed with an unrecognized result.")); - return; - } - } -} +#include "MSAStep.moc" \ No newline at end of file diff --git a/launcher/minecraft/auth/steps/MSAStep.h b/launcher/minecraft/auth/steps/MSAStep.h index b6635d4a5..675cfb2ca 100644 --- a/launcher/minecraft/auth/steps/MSAStep.h +++ b/launcher/minecraft/auth/steps/MSAStep.h @@ -36,30 +36,24 @@ #pragma once #include -#include "QObjectPtr.h" #include "minecraft/auth/AuthStep.h" -#include - +#include class MSAStep : public AuthStep { Q_OBJECT public: - enum Action { Refresh, Login }; - - public: - explicit MSAStep(AccountData* data, Action action); - virtual ~MSAStep() noexcept; + explicit MSAStep(AccountData* data, bool silent = false); + virtual ~MSAStep() noexcept = default; void perform() override; - void rehydrate() override; QString describe() override; - private slots: - void onOAuthActivityChanged(Katabasis::Activity activity); + signals: + void authorizeWithBrowser(const QUrl& url); private: - Katabasis::DeviceFlow* m_oauth2 = nullptr; - Action m_action; + bool m_silent; QString m_clientId; + QOAuth2AuthorizationCodeFlow oauth2; }; diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp index a854342bc..c1529b086 100644 --- a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp +++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp @@ -2,15 +2,13 @@ #include -#include "Logging.h" -#include "minecraft/auth/AuthRequest.h" +#include "Application.h" #include "minecraft/auth/Parsers.h" #include "net/NetUtils.h" +#include "net/RawHeaderProxy.h" MinecraftProfileStep::MinecraftProfileStep(AccountData* data) : AuthStep(data) {} -MinecraftProfileStep::~MinecraftProfileStep() noexcept = default; - QString MinecraftProfileStep::describe() { return tr("Fetching the Minecraft profile."); @@ -18,52 +16,51 @@ QString MinecraftProfileStep::describe() void MinecraftProfileStep::perform() { - auto url = QUrl("https://api.minecraftservices.com/minecraft/profile"); - QNetworkRequest request = QNetworkRequest(url); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); + QUrl url("https://api.minecraftservices.com/minecraft/profile"); + auto headers = QList{ { "Content-Type", "application/json" }, + { "Accept", "application/json" }, + { "Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8() } }; - AuthRequest* requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &MinecraftProfileStep::onRequestDone); - requestor->get(request); + m_response.reset(new QByteArray()); + m_request = Net::Download::makeByteArray(url, m_response); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + m_task.reset(new NetJob("MinecraftProfileStep", APPLICATION->network())); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &MinecraftProfileStep::onRequestDone); + + m_task->start(); } -void MinecraftProfileStep::rehydrate() +void MinecraftProfileStep::onRequestDone() { - // NOOP, for now. We only save bools and there's nothing to check. -} - -void MinecraftProfileStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList headers) -{ - auto requestor = qobject_cast(QObject::sender()); - requestor->deleteLater(); - - qCDebug(authCredentials()) << data; - if (error == QNetworkReply::ContentNotFoundError) { + if (m_request->error() == QNetworkReply::ContentNotFoundError) { // NOTE: Succeed even if we do not have a profile. This is a valid account state. m_data->minecraftProfile = MinecraftProfile(); - emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Account has no Minecraft profile.")); + emit finished(AccountTaskState::STATE_WORKING, tr("Account has no Minecraft profile.")); return; } - if (error != QNetworkReply::NoError) { + if (m_request->error() != QNetworkReply::NoError) { qWarning() << "Error getting profile:"; - qWarning() << " HTTP Status: " << requestor->httpStatus_; - qWarning() << " Internal error no.: " << error; - qWarning() << " Error string: " << requestor->errorString_; + qWarning() << " HTTP Status: " << m_request->replyStatusCode(); + qWarning() << " Internal error no.: " << m_request->error(); + qWarning() << " Error string: " << m_request->errorString(); qWarning() << " Response:"; - qWarning() << QString::fromUtf8(data); + qWarning() << QString::fromUtf8(*m_response); - if (Net::isApplicationError(error)) { + if (Net::isApplicationError(m_request->error())) { emit finished(AccountTaskState::STATE_FAILED_SOFT, - tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_)); + tr("Minecraft Java profile acquisition failed: %1").arg(m_request->errorString())); } else { emit finished(AccountTaskState::STATE_OFFLINE, - tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_)); + tr("Minecraft Java profile acquisition failed: %1").arg(m_request->errorString())); } return; } - if (!Parsers::parseMinecraftProfile(data, m_data->minecraftProfile)) { + if (!Parsers::parseMinecraftProfile(*m_response, m_data->minecraftProfile)) { m_data->minecraftProfile = MinecraftProfile(); emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Minecraft Java profile response could not be parsed")); return; diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.h b/launcher/minecraft/auth/steps/MinecraftProfileStep.h index cb30dab21..e8b35b875 100644 --- a/launcher/minecraft/auth/steps/MinecraftProfileStep.h +++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.h @@ -1,21 +1,27 @@ #pragma once #include +#include -#include "QObjectPtr.h" #include "minecraft/auth/AuthStep.h" +#include "net/Download.h" +#include "net/NetJob.h" class MinecraftProfileStep : public AuthStep { Q_OBJECT public: explicit MinecraftProfileStep(AccountData* data); - virtual ~MinecraftProfileStep() noexcept; + virtual ~MinecraftProfileStep() noexcept = default; void perform() override; - void rehydrate() override; QString describe() override; private slots: - void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); + void onRequestDone(); + + private: + std::shared_ptr m_response; + Net::Download::Ptr m_request; + NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/auth/steps/OfflineStep.cpp b/launcher/minecraft/auth/steps/OfflineStep.cpp deleted file mode 100644 index bf111abe8..000000000 --- a/launcher/minecraft/auth/steps/OfflineStep.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include "OfflineStep.h" - -#include "Application.h" - -OfflineStep::OfflineStep(AccountData* data) : AuthStep(data) {} -OfflineStep::~OfflineStep() noexcept = default; - -QString OfflineStep::describe() -{ - return tr("Creating offline account."); -} - -void OfflineStep::rehydrate() -{ - // NOOP -} - -void OfflineStep::perform() -{ - emit finished(AccountTaskState::STATE_WORKING, tr("Created offline account.")); -} diff --git a/launcher/minecraft/auth/steps/OfflineStep.h b/launcher/minecraft/auth/steps/OfflineStep.h deleted file mode 100644 index 3bf123d6a..000000000 --- a/launcher/minecraft/auth/steps/OfflineStep.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once -#include - -#include "QObjectPtr.h" -#include "minecraft/auth/AuthStep.h" - -#include - -class OfflineStep : public AuthStep { - Q_OBJECT - public: - explicit OfflineStep(AccountData* data); - virtual ~OfflineStep() noexcept; - - void perform() override; - void rehydrate() override; - - QString describe() override; -}; diff --git a/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp index 84c52c386..f9de9210b 100644 --- a/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp +++ b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp @@ -4,27 +4,22 @@ #include #include +#include "Application.h" #include "Logging.h" -#include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/Parsers.h" #include "net/NetUtils.h" +#include "net/RawHeaderProxy.h" +#include "net/Upload.h" -XboxAuthorizationStep::XboxAuthorizationStep(AccountData* data, Katabasis::Token* token, QString relyingParty, QString authorizationKind) +XboxAuthorizationStep::XboxAuthorizationStep(AccountData* data, Token* token, QString relyingParty, QString authorizationKind) : AuthStep(data), m_token(token), m_relyingParty(relyingParty), m_authorizationKind(authorizationKind) {} -XboxAuthorizationStep::~XboxAuthorizationStep() noexcept = default; - QString XboxAuthorizationStep::describe() { return tr("Getting authorization to access %1 services.").arg(m_authorizationKind); } -void XboxAuthorizationStep::rehydrate() -{ - // FIXME: check if the tokens are good? -} - void XboxAuthorizationStep::perform() { QString xbox_auth_template = R"XXX( @@ -41,40 +36,47 @@ void XboxAuthorizationStep::perform() )XXX"; auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token, m_relyingParty); // http://xboxlive.com - QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize")); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Accept", "application/json"); - AuthRequest* requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &XboxAuthorizationStep::onRequestDone); - requestor->post(request, xbox_auth_data.toUtf8()); + QUrl url("https://xsts.auth.xboxlive.com/xsts/authorize"); + auto headers = QList{ + { "Content-Type", "application/json" }, + { "Accept", "application/json" }, + }; + m_response.reset(new QByteArray()); + m_request = Net::Upload::makeByteArray(url, m_response, xbox_auth_data.toUtf8()); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + m_task.reset(new NetJob("XboxAuthorizationStep", APPLICATION->network())); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &XboxAuthorizationStep::onRequestDone); + + m_task->start(); qDebug() << "Getting authorization token for " << m_relyingParty; } -void XboxAuthorizationStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList headers) +void XboxAuthorizationStep::onRequestDone() { - auto requestor = qobject_cast(QObject::sender()); - requestor->deleteLater(); - - qCDebug(authCredentials()) << data; - if (error != QNetworkReply::NoError) { - qWarning() << "Reply error:" << error; - if (Net::isApplicationError(error)) { - if (!processSTSError(error, data, headers)) { + qCDebug(authCredentials()) << *m_response; + if (m_request->error() != QNetworkReply::NoError) { + qWarning() << "Reply error:" << m_request->error(); + if (Net::isApplicationError(m_request->error())) { + if (!processSTSError()) { emit finished(AccountTaskState::STATE_FAILED_SOFT, - tr("Failed to get authorization for %1 services. Error %2.").arg(m_authorizationKind, error)); + tr("Failed to get authorization for %1 services. Error %2.").arg(m_authorizationKind, m_request->error())); } else { emit finished(AccountTaskState::STATE_FAILED_SOFT, - tr("Unknown STS error for %1 services: %2").arg(m_authorizationKind, requestor->errorString_)); + tr("Unknown STS error for %1 services: %2").arg(m_authorizationKind, m_request->errorString())); } } else { emit finished(AccountTaskState::STATE_OFFLINE, - tr("Failed to get authorization for %1 services: %2").arg(m_authorizationKind, requestor->errorString_)); + tr("Failed to get authorization for %1 services: %2").arg(m_authorizationKind, m_request->errorString())); } return; } - Katabasis::Token temp; - if (!Parsers::parseXTokenResponse(data, temp, m_authorizationKind)) { + Token temp; + if (!Parsers::parseXTokenResponse(*m_response, temp, m_authorizationKind)) { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Could not parse authorization response for access to %1 services.").arg(m_authorizationKind)); return; @@ -91,11 +93,11 @@ void XboxAuthorizationStep::onRequestDone(QNetworkReply::NetworkError error, QBy emit finished(AccountTaskState::STATE_WORKING, tr("Got authorization to access %1").arg(m_relyingParty)); } -bool XboxAuthorizationStep::processSTSError(QNetworkReply::NetworkError error, QByteArray data, QList headers) +bool XboxAuthorizationStep::processSTSError() { - if (error == QNetworkReply::AuthenticationRequiredError) { + if (m_request->error() == QNetworkReply::AuthenticationRequiredError) { QJsonParseError jsonError; - QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + QJsonDocument doc = QJsonDocument::fromJson(*m_response, &jsonError); if (jsonError.error) { qWarning() << "Cannot parse error XSTS response as JSON: " << jsonError.errorString(); emit finished(AccountTaskState::STATE_FAILED_SOFT, diff --git a/launcher/minecraft/auth/steps/XboxAuthorizationStep.h b/launcher/minecraft/auth/steps/XboxAuthorizationStep.h index dee24c954..8418727c4 100644 --- a/launcher/minecraft/auth/steps/XboxAuthorizationStep.h +++ b/launcher/minecraft/auth/steps/XboxAuthorizationStep.h @@ -1,29 +1,34 @@ #pragma once #include +#include -#include "QObjectPtr.h" #include "minecraft/auth/AuthStep.h" +#include "net/NetJob.h" +#include "net/Upload.h" class XboxAuthorizationStep : public AuthStep { Q_OBJECT public: - explicit XboxAuthorizationStep(AccountData* data, Katabasis::Token* token, QString relyingParty, QString authorizationKind); - virtual ~XboxAuthorizationStep() noexcept; + explicit XboxAuthorizationStep(AccountData* data, Token* token, QString relyingParty, QString authorizationKind); + virtual ~XboxAuthorizationStep() noexcept = default; void perform() override; - void rehydrate() override; QString describe() override; private: - bool processSTSError(QNetworkReply::NetworkError error, QByteArray data, QList headers); + bool processSTSError(); private slots: - void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); + void onRequestDone(); private: - Katabasis::Token* m_token; + Token* m_token; QString m_relyingParty; QString m_authorizationKind; + + std::shared_ptr m_response; + Net::Upload::Ptr m_request; + NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/auth/steps/XboxProfileStep.cpp b/launcher/minecraft/auth/steps/XboxProfileStep.cpp index fd2b32cce..b896357c0 100644 --- a/launcher/minecraft/auth/steps/XboxProfileStep.cpp +++ b/launcher/minecraft/auth/steps/XboxProfileStep.cpp @@ -3,28 +3,21 @@ #include #include +#include "Application.h" #include "Logging.h" -#include "minecraft/auth/AuthRequest.h" -#include "minecraft/auth/Parsers.h" #include "net/NetUtils.h" +#include "net/RawHeaderProxy.h" XboxProfileStep::XboxProfileStep(AccountData* data) : AuthStep(data) {} -XboxProfileStep::~XboxProfileStep() noexcept = default; - QString XboxProfileStep::describe() { return tr("Fetching Xbox profile."); } -void XboxProfileStep::rehydrate() -{ - // NOOP, for now. We only save bools and there's nothing to check. -} - void XboxProfileStep::perform() { - auto url = QUrl("https://profile.xboxlive.com/users/me/profile/settings"); + QUrl url("https://profile.xboxlive.com/users/me/profile/settings"); QUrlQuery q; q.addQueryItem("settings", "GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw," @@ -33,36 +26,41 @@ void XboxProfileStep::perform() "PreferredColor,Location,Bio,Watermarks," "RealName,RealNameOverride,IsQuarantined"); url.setQuery(q); + auto headers = QList{ + { "Content-Type", "application/json" }, + { "Accept", "application/json" }, + { "x-xbl-contract-version", "3" }, + { "Authorization", QString("XBL3.0 x=%1;%2").arg(m_data->userToken.extra["uhs"].toString(), m_data->xboxApiToken.token).toUtf8() } + }; - QNetworkRequest request = QNetworkRequest(url); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Accept", "application/json"); - request.setRawHeader("x-xbl-contract-version", "3"); - request.setRawHeader("Authorization", - QString("XBL3.0 x=%1;%2").arg(m_data->userToken.extra["uhs"].toString(), m_data->xboxApiToken.token).toUtf8()); - AuthRequest* requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &XboxProfileStep::onRequestDone); - requestor->get(request); + m_response.reset(new QByteArray()); + m_request = Net::Download::makeByteArray(url, m_response); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + m_task.reset(new NetJob("XboxProfileStep", APPLICATION->network())); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &XboxProfileStep::onRequestDone); + + m_task->start(); qDebug() << "Getting Xbox profile..."; } -void XboxProfileStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList headers) +void XboxProfileStep::onRequestDone() { - auto requestor = qobject_cast(QObject::sender()); - requestor->deleteLater(); - - if (error != QNetworkReply::NoError) { - qWarning() << "Reply error:" << error; - qCDebug(authCredentials()) << data; - if (Net::isApplicationError(error)) { - emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to retrieve the Xbox profile: %1").arg(requestor->errorString_)); + if (m_request->error() != QNetworkReply::NoError) { + qWarning() << "Reply error:" << m_request->error(); + qCDebug(authCredentials()) << *m_response; + if (Net::isApplicationError(m_request->error())) { + emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to retrieve the Xbox profile: %1").arg(m_request->errorString())); } else { - emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to retrieve the Xbox profile: %1").arg(requestor->errorString_)); + emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to retrieve the Xbox profile: %1").arg(m_request->errorString())); } return; } - qCDebug(authCredentials()) << "XBox profile: " << data; + qCDebug(authCredentials()) << "XBox profile: " << *m_response; emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox profile")); } diff --git a/launcher/minecraft/auth/steps/XboxProfileStep.h b/launcher/minecraft/auth/steps/XboxProfileStep.h index b8494b6e5..f2ab874f2 100644 --- a/launcher/minecraft/auth/steps/XboxProfileStep.h +++ b/launcher/minecraft/auth/steps/XboxProfileStep.h @@ -1,21 +1,27 @@ #pragma once #include +#include -#include "QObjectPtr.h" #include "minecraft/auth/AuthStep.h" +#include "net/Download.h" +#include "net/NetJob.h" class XboxProfileStep : public AuthStep { Q_OBJECT public: explicit XboxProfileStep(AccountData* data); - virtual ~XboxProfileStep() noexcept; + virtual ~XboxProfileStep() noexcept = default; void perform() override; - void rehydrate() override; QString describe() override; private slots: - void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); + void onRequestDone(); + + private: + std::shared_ptr m_response; + Net::Download::Ptr m_request; + NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/auth/steps/XboxUserStep.cpp b/launcher/minecraft/auth/steps/XboxUserStep.cpp index 856036d23..4e5abb62b 100644 --- a/launcher/minecraft/auth/steps/XboxUserStep.cpp +++ b/launcher/minecraft/auth/steps/XboxUserStep.cpp @@ -2,24 +2,18 @@ #include -#include "minecraft/auth/AuthRequest.h" +#include "Application.h" #include "minecraft/auth/Parsers.h" #include "net/NetUtils.h" +#include "net/RawHeaderProxy.h" XboxUserStep::XboxUserStep(AccountData* data) : AuthStep(data) {} -XboxUserStep::~XboxUserStep() noexcept = default; - QString XboxUserStep::describe() { return tr("Logging in as an Xbox user."); } -void XboxUserStep::rehydrate() -{ - // NOOP, for now. We only save bools and there's nothing to check. -} - void XboxUserStep::perform() { QString xbox_auth_template = R"XXX( @@ -35,36 +29,42 @@ void XboxUserStep::perform() )XXX"; auto xbox_auth_data = xbox_auth_template.arg(m_data->msaToken.token); - QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate")); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Accept", "application/json"); - // set contract-version header (prevent err 400 bad-request?) - // https://learn.microsoft.com/en-us/gaming/gdk/_content/gc/reference/live/rest/additional/httpstandardheaders - request.setRawHeader("x-xbl-contract-version", "1"); + QUrl url("https://user.auth.xboxlive.com/user/authenticate"); + auto headers = QList{ + { "Content-Type", "application/json" }, + { "Accept", "application/json" }, + // set contract-version header (prevent err 400 bad-request?) + // https://learn.microsoft.com/en-us/gaming/gdk/_content/gc/reference/live/rest/additional/httpstandardheaders + { "x-xbl-contract-version", "1" } + }; + m_response.reset(new QByteArray()); + m_request = Net::Upload::makeByteArray(url, m_response, xbox_auth_data.toUtf8()); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); - auto* requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &XboxUserStep::onRequestDone); - requestor->post(request, xbox_auth_data.toUtf8()); + m_task.reset(new NetJob("XboxUserStep", APPLICATION->network())); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &XboxUserStep::onRequestDone); + + m_task->start(); qDebug() << "First layer of XBox auth ... commencing."; } -void XboxUserStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList headers) +void XboxUserStep::onRequestDone() { - auto requestor = qobject_cast(QObject::sender()); - requestor->deleteLater(); - - if (error != QNetworkReply::NoError) { - qWarning() << "Reply error:" << error; - if (Net::isApplicationError(error)) { - emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication failed: %1").arg(requestor->errorString_)); + if (m_request->error() != QNetworkReply::NoError) { + qWarning() << "Reply error:" << m_request->error(); + if (Net::isApplicationError(m_request->error())) { + emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication failed: %1").arg(m_request->errorString())); } else { - emit finished(AccountTaskState::STATE_OFFLINE, tr("XBox user authentication failed: %1").arg(requestor->errorString_)); + emit finished(AccountTaskState::STATE_OFFLINE, tr("XBox user authentication failed: %1").arg(m_request->errorString())); } return; } - Katabasis::Token temp; - if (!Parsers::parseXTokenResponse(data, temp, "UToken")) { + Token temp; + if (!Parsers::parseXTokenResponse(*m_response, temp, "UToken")) { qWarning() << "Could not parse user authentication response..."; emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication response could not be understood.")); return; diff --git a/launcher/minecraft/auth/steps/XboxUserStep.h b/launcher/minecraft/auth/steps/XboxUserStep.h index e92727a4d..f6cc822f2 100644 --- a/launcher/minecraft/auth/steps/XboxUserStep.h +++ b/launcher/minecraft/auth/steps/XboxUserStep.h @@ -1,21 +1,27 @@ #pragma once #include +#include -#include "QObjectPtr.h" #include "minecraft/auth/AuthStep.h" +#include "net/NetJob.h" +#include "net/Upload.h" class XboxUserStep : public AuthStep { Q_OBJECT public: explicit XboxUserStep(AccountData* data); - virtual ~XboxUserStep() noexcept; + virtual ~XboxUserStep() noexcept = default; void perform() override; - void rehydrate() override; QString describe() override; private slots: - void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); + void onRequestDone(); + + private: + std::shared_ptr m_response; + Net::Upload::Ptr m_request; + NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/launch/AutoInstallJava.cpp b/launcher/minecraft/launch/AutoInstallJava.cpp new file mode 100644 index 000000000..4fad6f15f --- /dev/null +++ b/launcher/minecraft/launch/AutoInstallJava.cpp @@ -0,0 +1,251 @@ +// 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 . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "AutoInstallJava.h" +#include +#include +#include + +#include "Application.h" +#include "FileSystem.h" +#include "MessageLevel.h" +#include "QObjectPtr.h" +#include "SysInfo.h" +#include "java/JavaInstall.h" +#include "java/JavaInstallList.h" +#include "java/JavaUtils.h" +#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()) {}; + +void AutoInstallJava::executeTask() +{ + auto settings = m_instance->settings(); + if (!APPLICATION->settings()->get("AutomaticJavaSwitch").toBool() || + (settings->get("OverrideJavaLocation").toBool() && QFileInfo::exists(settings->get("JavaPath").toString()))) { + emitSucceeded(); + return; + } + auto packProfile = m_instance->getPackProfile(); + if (!APPLICATION->settings()->get("AutomaticJavaDownload").toBool()) { + auto javas = APPLICATION->javalist(); + m_current_task = javas->getLoadTask(); + connect(m_current_task.get(), &Task::finished, this, [this, javas, packProfile] { + for (auto i = 0; i < javas->count(); i++) { + 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); + } + setJavaPath(java->path); + return; + } + } + emit logLine(tr("No compatible Java version was found. Using the default one."), MessageLevel::Warning); + emitSucceeded(); + }); + connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); + connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); + connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); + connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); + emit progressReportingRequest(); + return; + } + if (m_supported_arch.isEmpty()) { + emit logLine(tr("Your system (%1-%2) is not compatible with automatic Java installation. Using the default Java path.") + .arg(SysInfo::currentSystem(), SysInfo::useQTForArch()), + MessageLevel::Warning); + emitSucceeded(); + return; + } + auto wantedJavaName = packProfile->getProfile()->getCompatibleJavaName(); + if (wantedJavaName.isEmpty()) { + emit logLine(tr("Your meta information is out of date or doesn't have the information necessary to determine what installation of " + "Java should be used. " + "Using the default Java path."), + MessageLevel::Warning); + emitSucceeded(); + return; + } + QDir javaDir(APPLICATION->javaPath()); + auto wantedJavaPath = javaDir.absoluteFilePath(wantedJavaName); + if (QFileInfo::exists(wantedJavaPath)) { + setJavaPathFromPartial(); + return; + } + auto versionList = APPLICATION->metadataIndex()->get("net.minecraft.java"); + m_current_task = versionList->getLoadTask(); + connect(m_current_task.get(), &Task::succeeded, this, &AutoInstallJava::tryNextMajorJava); + connect(m_current_task.get(), &Task::failed, this, &AutoInstallJava::emitFailed); + connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); + connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); + connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); + connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); + if (!m_current_task->isRunning()) { + m_current_task->start(); + } + emit progressReportingRequest(); +} + +void AutoInstallJava::setJavaPath(QString path) +{ + auto settings = m_instance->settings(); + settings->set("OverrideJavaLocation", true); + settings->set("JavaPath", path); + settings->set("AutomaticJava", true); + emit logLine(tr("Compatible Java found at: %1.").arg(path), MessageLevel::Info); + emitSucceeded(); +} + +void AutoInstallJava::setJavaPathFromPartial() +{ + auto packProfile = m_instance->getPackProfile(); + auto javaName = packProfile->getProfile()->getCompatibleJavaName(); + QDir javaDir(APPLICATION->javaPath()); + // just checking if the executable is there should suffice + // but if needed this can be achieved through refreshing the javalist + // and retrieving the path that contains the java name + auto relativeBinary = FS::PathCombine(javaName, "bin", JavaUtils::javaExecutable); + auto finalPath = javaDir.absoluteFilePath(relativeBinary); + if (QFileInfo::exists(finalPath)) { + setJavaPath(finalPath); + } else { + emit logLine(tr("No compatible Java version was found (the binary file does not exist). Using the default one."), + MessageLevel::Warning); + emitSucceeded(); + } + return; +} + +void AutoInstallJava::downloadJava(Meta::Version::Ptr version, QString javaName) +{ + auto runtimes = version->data()->runtimes; + for (auto java : runtimes) { + if (java->runtimeOS == m_supported_arch && java->name() == javaName) { + QDir javaDir(APPLICATION->javaPath()); + auto final_path = javaDir.absoluteFilePath(java->m_name); + switch (java->downloadType) { + case Java::DownloadType::Manifest: + m_current_task = makeShared(java->url, final_path, java->checksumType, java->checksumHash); + break; + case Java::DownloadType::Archive: + m_current_task = makeShared(java->url, final_path, java->checksumType, java->checksumHash); + break; + case Java::DownloadType::Unknown: + emitFailed(tr("Could not determine Java download type!")); + return; + } +#if defined(Q_OS_MACOS) + auto seq = makeShared(this, 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(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); + connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); + connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); + connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); + m_current_task->start(); + return; + } + } + tryNextMajorJava(); +} + +void AutoInstallJava::tryNextMajorJava() +{ + if (!isRunning()) + return; + auto versionList = APPLICATION->metadataIndex()->get("net.minecraft.java"); + auto packProfile = m_instance->getPackProfile(); + auto wantedJavaName = packProfile->getProfile()->getCompatibleJavaName(); + auto majorJavaVersions = packProfile->getProfile()->getCompatibleJavaMajors(); + if (m_majorJavaVersionIndex >= majorJavaVersions.length()) { + emit logLine( + tr("No versions of Java were found for your operating system: %1-%2").arg(SysInfo::currentSystem(), SysInfo::useQTForArch()), + MessageLevel::Warning); + emit logLine(tr("No compatible version of Java was found. Using the default one."), MessageLevel::Warning); + emitSucceeded(); + return; + } + auto majorJavaVersion = majorJavaVersions[m_majorJavaVersionIndex]; + m_majorJavaVersionIndex++; + + auto javaMajor = versionList->getVersion(QString("java%1").arg(majorJavaVersion)); + + if (javaMajor->isLoaded()) { + downloadJava(javaMajor, wantedJavaName); + } else { + m_current_task = APPLICATION->metadataIndex()->loadVersion("net.minecraft.java", javaMajor->version(), Net::Mode::Online); + connect(m_current_task.get(), &Task::succeeded, this, + [this, javaMajor, wantedJavaName] { downloadJava(javaMajor, wantedJavaName); }); + connect(m_current_task.get(), &Task::failed, this, &AutoInstallJava::tryNextMajorJava); + connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); + connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); + connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); + connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); + if (!m_current_task->isRunning()) { + m_current_task->start(); + } + } +} +bool AutoInstallJava::abort() +{ + 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 new file mode 100644 index 000000000..cbfcf5ee7 --- /dev/null +++ b/launcher/minecraft/launch/AutoInstallJava.h @@ -0,0 +1,67 @@ +// 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 . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include "meta/Version.h" +#include "minecraft/MinecraftInstance.h" +#include "tasks/Task.h" + +class AutoInstallJava : public LaunchStep { + Q_OBJECT + + public: + explicit AutoInstallJava(LaunchTask* parent); + ~AutoInstallJava() override = default; + + void executeTask() override; + bool canAbort() const override { return m_current_task ? m_current_task->canAbort() : false; } + bool abort() override; + + protected: + void setJavaPath(QString path); + void setJavaPathFromPartial(); + void downloadJava(Meta::Version::Ptr version, QString javaName); + void tryNextMajorJava(); + + private: + MinecraftInstancePtr m_instance; + Task::Ptr m_current_task; + + qsizetype m_majorJavaVersionIndex = 0; + const QString m_supported_arch; +}; diff --git a/launcher/minecraft/launch/ClaimAccount.h b/launcher/minecraft/launch/ClaimAccount.h index 3d47539ac..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.h b/launcher/minecraft/launch/CreateGameFolders.h index 44524ded5..b44762d62 100644 --- a/launcher/minecraft/launch/CreateGameFolders.h +++ b/launcher/minecraft/launch/CreateGameFolders.h @@ -24,7 +24,7 @@ class CreateGameFolders : public LaunchStep { Q_OBJECT public: explicit CreateGameFolders(LaunchTask* parent); - virtual ~CreateGameFolders(){}; + virtual ~CreateGameFolders() {}; virtual void executeTask(); virtual bool canAbort() const { return false; } diff --git a/launcher/minecraft/launch/ExtractNatives.h b/launcher/minecraft/launch/ExtractNatives.h index 4837a9dbb..1ad9a416e 100644 --- a/launcher/minecraft/launch/ExtractNatives.h +++ b/launcher/minecraft/launch/ExtractNatives.h @@ -21,8 +21,8 @@ class ExtractNatives : public LaunchStep { Q_OBJECT public: - explicit ExtractNatives(LaunchTask* parent) : LaunchStep(parent){}; - virtual ~ExtractNatives(){}; + explicit ExtractNatives(LaunchTask* parent) : LaunchStep(parent) {}; + virtual ~ExtractNatives() {}; void executeTask() override; bool canAbort() const override { return false; } diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index 4e021c4a8..2b932ae47 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -90,7 +90,7 @@ void LauncherPartLaunch::executeTask() } } - m_launchScript = minecraftInstance->createLaunchScript(m_session, m_serverToJoin); + m_launchScript = minecraftInstance->createLaunchScript(m_session, m_targetToJoin); QStringList args = minecraftInstance->javaArguments(); QString allArgs = args.join(", "); emit logLine("Java Arguments:\n[" + m_parent->censorPrivateInfo(allArgs) + "]\n\n", MessageLevel::Launcher); @@ -178,6 +178,7 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state) APPLICATION->showMainWindow(); m_parent->setPid(-1); + m_parent->instance()->setMinecraftRunning(false); // if the exit code wasn't 0, report this as a crash auto exitCode = m_process.exitCode(); if (exitCode != 0) { @@ -193,7 +194,6 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state) case LoggedProcess::Running: emit logLine(QString("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::Launcher); m_parent->setPid(m_process.processId()); - m_parent->instance()->setLastLaunch(); // send the launch script to the launcher part m_process.write(m_launchScript.toUtf8()); @@ -213,6 +213,7 @@ void LauncherPartLaunch::setWorkingDirectory(const QString& wd) void LauncherPartLaunch::proceed() { if (mayProceed) { + m_parent->instance()->setMinecraftRunning(true); QString launchString("launch\n"); m_process.write(launchString.toUtf8()); mayProceed = false; diff --git a/launcher/minecraft/launch/LauncherPartLaunch.h b/launcher/minecraft/launch/LauncherPartLaunch.h index 9f6ca1e7b..ea125aa9e 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.h +++ b/launcher/minecraft/launch/LauncherPartLaunch.h @@ -19,13 +19,13 @@ #include #include -#include "MinecraftServerTarget.h" +#include "MinecraftTarget.h" class LauncherPartLaunch : public LaunchStep { Q_OBJECT public: explicit LauncherPartLaunch(LaunchTask* parent); - virtual ~LauncherPartLaunch(){}; + virtual ~LauncherPartLaunch() = default; virtual void executeTask(); virtual bool abort(); @@ -34,7 +34,7 @@ class LauncherPartLaunch : public LaunchStep { void setWorkingDirectory(const QString& wd); void setAuthSession(AuthSessionPtr session) { m_session = session; } - void setServerToJoin(MinecraftServerTargetPtr serverToJoin) { m_serverToJoin = std::move(serverToJoin); } + void setTargetToJoin(MinecraftTarget::Ptr targetToJoin) { m_targetToJoin = std::move(targetToJoin); } private slots: void on_state(LoggedProcess::State state); @@ -44,7 +44,7 @@ class LauncherPartLaunch : public LaunchStep { QString m_command; AuthSessionPtr m_session; QString m_launchScript; - MinecraftServerTargetPtr m_serverToJoin; + MinecraftTarget::Ptr m_targetToJoin; bool mayProceed = false; }; diff --git a/launcher/minecraft/launch/MinecraftServerTarget.cpp b/launcher/minecraft/launch/MinecraftTarget.cpp similarity index 86% rename from launcher/minecraft/launch/MinecraftServerTarget.cpp rename to launcher/minecraft/launch/MinecraftTarget.cpp index e201efab1..ba9f87511 100644 --- a/launcher/minecraft/launch/MinecraftServerTarget.cpp +++ b/launcher/minecraft/launch/MinecraftTarget.cpp @@ -13,13 +13,18 @@ * limitations under the License. */ -#include "MinecraftServerTarget.h" +#include "MinecraftTarget.h" #include // FIXME: the way this is written, it can't ever do any sort of validation and can accept total junk -MinecraftServerTarget MinecraftServerTarget::parse(const QString& fullAddress) +MinecraftTarget MinecraftTarget::parse(const QString& fullAddress, bool useWorld) { + if (useWorld) { + MinecraftTarget target; + target.world = fullAddress; + return target; + } QStringList split = fullAddress.split(":"); // The logic below replicates the exact logic minecraft uses for parsing server addresses. @@ -56,5 +61,5 @@ MinecraftServerTarget MinecraftServerTarget::parse(const QString& fullAddress) } } - return MinecraftServerTarget{ realAddress, realPort }; + return MinecraftTarget{ realAddress, realPort }; } diff --git a/launcher/minecraft/launch/MinecraftServerTarget.h b/launcher/minecraft/launch/MinecraftTarget.h similarity index 80% rename from launcher/minecraft/launch/MinecraftServerTarget.h rename to launcher/minecraft/launch/MinecraftTarget.h index 2edd8a30d..7f8b268d9 100644 --- a/launcher/minecraft/launch/MinecraftServerTarget.h +++ b/launcher/minecraft/launch/MinecraftTarget.h @@ -19,11 +19,11 @@ #include -struct MinecraftServerTarget { +struct MinecraftTarget { QString address; quint16 port; - static MinecraftServerTarget parse(const QString& fullAddress); + QString world; + static MinecraftTarget parse(const QString& fullAddress, bool useWorld); + using Ptr = std::shared_ptr; }; - -using MinecraftServerTargetPtr = std::shared_ptr; diff --git a/launcher/minecraft/launch/ModMinecraftJar.h b/launcher/minecraft/launch/ModMinecraftJar.h index 12e73b5f8..6fc2a8a26 100644 --- a/launcher/minecraft/launch/ModMinecraftJar.h +++ b/launcher/minecraft/launch/ModMinecraftJar.h @@ -21,8 +21,8 @@ class ModMinecraftJar : public LaunchStep { Q_OBJECT public: - explicit ModMinecraftJar(LaunchTask* parent) : LaunchStep(parent){}; - virtual ~ModMinecraftJar(){}; + explicit ModMinecraftJar(LaunchTask* parent) : LaunchStep(parent) {}; + virtual ~ModMinecraftJar() {}; virtual void executeTask() override; virtual bool canAbort() const override { return false; } diff --git a/launcher/minecraft/launch/PrintInstanceInfo.cpp b/launcher/minecraft/launch/PrintInstanceInfo.cpp index e3a45b030..e44d09839 100644 --- a/launcher/minecraft/launch/PrintInstanceInfo.cpp +++ b/launcher/minecraft/launch/PrintInstanceInfo.cpp @@ -129,6 +129,6 @@ void PrintInstanceInfo::executeTask() #endif logLines(log, MessageLevel::Launcher); - logLines(instance->verboseDescription(m_session, m_serverToJoin), MessageLevel::Launcher); + logLines(instance->verboseDescription(m_session, m_targetToJoin), MessageLevel::Launcher); emitSucceeded(); } diff --git a/launcher/minecraft/launch/PrintInstanceInfo.h b/launcher/minecraft/launch/PrintInstanceInfo.h index 8e1c41b62..4138c0cd2 100644 --- a/launcher/minecraft/launch/PrintInstanceInfo.h +++ b/launcher/minecraft/launch/PrintInstanceInfo.h @@ -16,22 +16,21 @@ #pragma once #include -#include #include "minecraft/auth/AuthSession.h" -#include "minecraft/launch/MinecraftServerTarget.h" +#include "minecraft/launch/MinecraftTarget.h" // FIXME: temporary wrapper for existing task. class PrintInstanceInfo : public LaunchStep { Q_OBJECT public: - explicit PrintInstanceInfo(LaunchTask* parent, AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) - : LaunchStep(parent), m_session(session), m_serverToJoin(serverToJoin){}; - virtual ~PrintInstanceInfo(){}; + explicit PrintInstanceInfo(LaunchTask* parent, AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) + : LaunchStep(parent), m_session(session), m_targetToJoin(targetToJoin) {}; + virtual ~PrintInstanceInfo() = default; virtual void executeTask(); virtual bool canAbort() const { return false; } private: AuthSessionPtr m_session; - MinecraftServerTargetPtr m_serverToJoin; + MinecraftTarget::Ptr m_targetToJoin; }; diff --git a/launcher/minecraft/launch/ReconstructAssets.h b/launcher/minecraft/launch/ReconstructAssets.h index bd867c8d4..2c910c595 100644 --- a/launcher/minecraft/launch/ReconstructAssets.h +++ b/launcher/minecraft/launch/ReconstructAssets.h @@ -21,8 +21,8 @@ class ReconstructAssets : public LaunchStep { Q_OBJECT public: - explicit ReconstructAssets(LaunchTask* parent) : LaunchStep(parent){}; - virtual ~ReconstructAssets(){}; + explicit ReconstructAssets(LaunchTask* parent) : LaunchStep(parent) {}; + virtual ~ReconstructAssets() {}; void executeTask() override; bool canAbort() const override { return false; } diff --git a/launcher/minecraft/launch/ScanModFolders.h b/launcher/minecraft/launch/ScanModFolders.h index a5b75825b..5d9350952 100644 --- a/launcher/minecraft/launch/ScanModFolders.h +++ b/launcher/minecraft/launch/ScanModFolders.h @@ -21,8 +21,8 @@ class ScanModFolders : public LaunchStep { Q_OBJECT public: - explicit ScanModFolders(LaunchTask* parent) : LaunchStep(parent){}; - virtual ~ScanModFolders(){}; + explicit ScanModFolders(LaunchTask* parent) : LaunchStep(parent) {}; + virtual ~ScanModFolders() {}; virtual void executeTask() override; virtual bool canAbort() const override { return false; } diff --git a/launcher/minecraft/launch/VerifyJavaInstall.cpp b/launcher/minecraft/launch/VerifyJavaInstall.cpp index cdd1f7fd1..1e7448089 100644 --- a/launcher/minecraft/launch/VerifyJavaInstall.cpp +++ b/launcher/minecraft/launch/VerifyJavaInstall.cpp @@ -34,7 +34,12 @@ */ #include "VerifyJavaInstall.h" +#include +#include "Application.h" +#include "MessageLevel.h" +#include "java/JavaInstall.h" +#include "java/JavaInstallList.h" #include "java/JavaVersion.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" @@ -46,6 +51,15 @@ void VerifyJavaInstall::executeTask() auto settings = instance->settings(); auto storedVersion = settings->get("JavaVersion").toString(); auto ignoreCompatibility = settings->get("IgnoreJavaCompatibility").toBool(); + auto javaArchitecture = settings->get("JavaArchitecture").toString(); + auto maxMemAlloc = settings->get("MaxMemAlloc").toInt(); + + if (javaArchitecture == "32" && maxMemAlloc > 2048) { + emit logLine(tr("Max memory allocation exceeds the supported value.\n" + "The selected installation of Java is 32-bit and doesn't support more than 2048MiB of RAM.\n" + "The instance may not start due to this."), + MessageLevel::Error); + } auto compatibleMajors = packProfile->getProfile()->getCompatibleJavaMajors(); diff --git a/launcher/minecraft/launch/VerifyJavaInstall.h b/launcher/minecraft/launch/VerifyJavaInstall.h index dabbf3b25..3591ce665 100644 --- a/launcher/minecraft/launch/VerifyJavaInstall.h +++ b/launcher/minecraft/launch/VerifyJavaInstall.h @@ -42,7 +42,7 @@ class VerifyJavaInstall : public LaunchStep { Q_OBJECT public: - explicit VerifyJavaInstall(LaunchTask* parent) : LaunchStep(parent){}; + explicit VerifyJavaInstall(LaunchTask* parent) : LaunchStep(parent) {}; ~VerifyJavaInstall() override = default; void executeTask() override; diff --git a/launcher/minecraft/mod/DataPack.cpp b/launcher/minecraft/mod/DataPack.cpp index fc2d3f68b..4a9e77a70 100644 --- a/launcher/minecraft/mod/DataPack.cpp +++ b/launcher/minecraft/mod/DataPack.cpp @@ -28,15 +28,52 @@ #include "Version.h" // Values taken from: -// https://minecraft.wiki/w/Tutorials/Creating_a_data_pack#%22pack_format%22 -static const QMap> s_pack_format_versions = { - { 4, { Version("1.13"), Version("1.14.4") } }, { 5, { Version("1.15"), Version("1.16.1") } }, - { 6, { Version("1.16.2"), Version("1.16.5") } }, { 7, { Version("1.17"), Version("1.17.1") } }, - { 8, { Version("1.18"), Version("1.18.1") } }, { 9, { Version("1.18.2"), Version("1.18.2") } }, - { 10, { Version("1.19"), Version("1.19.3") } }, { 11, { Version("23w03a"), Version("23w05a") } }, - { 12, { Version("1.19.4"), Version("1.19.4") } }, { 13, { Version("23w12a"), Version("23w14a") } }, - { 14, { Version("23w16a"), Version("23w17a") } }, { 15, { Version("1.20"), Version("1.20") } }, -}; +// https://minecraft.wiki/w/Pack_format#List_of_data_pack_formats +static const QMap> s_pack_format_versions = { { 4, { Version("1.13"), Version("1.14.4") } }, + { 5, { Version("1.15"), Version("1.16.1") } }, + { 6, { Version("1.16.2"), Version("1.16.5") } }, + { 7, { Version("1.17"), Version("1.17.1") } }, + { 8, { Version("1.18"), Version("1.18.1") } }, + { 9, { Version("1.18.2"), Version("1.18.2") } }, + { 10, { Version("1.19"), Version("1.19.3") } }, + { 11, { Version("23w03a"), Version("23w05a") } }, + { 12, { Version("1.19.4"), Version("1.19.4") } }, + { 13, { Version("23w12a"), Version("23w14a") } }, + { 14, { Version("23w16a"), Version("23w17a") } }, + { 15, { Version("1.20"), Version("1.20.1") } }, + { 16, { Version("23w31a"), Version("23w31a") } }, + { 17, { Version("23w32a"), Version("23w35a") } }, + { 18, { Version("1.20.2"), Version("1.20.2") } }, + { 19, { Version("23w40a"), Version("23w40a") } }, + { 20, { Version("23w41a"), Version("23w41a") } }, + { 21, { Version("23w42a"), Version("23w42a") } }, + { 22, { Version("23w43a"), Version("23w43b") } }, + { 23, { Version("23w44a"), Version("23w44a") } }, + { 24, { Version("23w45a"), Version("23w45a") } }, + { 25, { Version("23w46a"), Version("23w46a") } }, + { 26, { Version("1.20.3"), Version("1.20.4") } }, + { 27, { Version("23w51a"), Version("23w51b") } }, + { 28, { Version("24w05a"), Version("24w05b") } }, + { 29, { Version("24w04a"), Version("24w04a") } }, + { 30, { Version("24w05a"), Version("24w05b") } }, + { 31, { Version("24w06a"), Version("24w06a") } }, + { 32, { Version("24w07a"), Version("24w07a") } }, + { 33, { Version("24w09a"), Version("24w09a") } }, + { 34, { Version("24w10a"), Version("24w10a") } }, + { 35, { Version("24w11a"), Version("24w11a") } }, + { 36, { Version("24w12a"), Version("24w12a") } }, + { 37, { Version("24w13a"), Version("24w13a") } }, + { 38, { Version("24w14a"), Version("24w14a") } }, + { 39, { Version("1.20.5-pre1"), Version("1.20.5-pre1") } }, + { 40, { Version("1.20.5-pre2"), Version("1.20.5-pre2") } }, + { 41, { Version("1.20.5"), Version("1.20.6") } }, + { 42, { Version("24w18a"), Version("24w18a") } }, + { 43, { Version("24w19a"), Version("24w19b") } }, + { 44, { Version("24w20a"), Version("24w20a") } }, + { 45, { Version("21w21a"), Version("21w21b") } }, + { 46, { Version("1.21-pre1"), Version("1.21-pre1") } }, + { 47, { Version("1.21-pre2"), Version("1.21-pre2") } }, + { 48, { Version("1.21"), Version("1.21") } } }; void DataPack::setPackFormat(int new_format_id) { @@ -65,29 +102,24 @@ std::pair DataPack::compatibleVersions() const return s_pack_format_versions.constFind(m_pack_format).value(); } -std::pair DataPack::compare(const Resource& other, SortType type) const +int DataPack::compare(const Resource& other, SortType type) const { auto const& cast_other = static_cast(other); - switch (type) { - default: { - auto res = Resource::compare(other, type); - if (res.first != 0) - return res; - break; - } + default: + return Resource::compare(other, type); case SortType::PACK_FORMAT: { auto this_ver = packFormat(); auto other_ver = cast_other.packFormat(); if (this_ver > other_ver) - return { 1, type == SortType::PACK_FORMAT }; + return 1; if (this_ver < other_ver) - return { -1, type == SortType::PACK_FORMAT }; + return -1; break; } } - return { 0, false }; + return 0; } bool DataPack::applyFilter(QRegularExpression filter) const diff --git a/launcher/minecraft/mod/DataPack.h b/launcher/minecraft/mod/DataPack.h index b3787b238..4855b0203 100644 --- a/launcher/minecraft/mod/DataPack.h +++ b/launcher/minecraft/mod/DataPack.h @@ -56,7 +56,7 @@ class DataPack : public Resource { bool valid() const override; - [[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair override; + [[nodiscard]] int compare(Resource const& other, SortType type) const override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; protected: diff --git a/launcher/minecraft/mod/MetadataHandler.h b/launcher/minecraft/mod/MetadataHandler.h index c325763f9..653c3c1ee 100644 --- a/launcher/minecraft/mod/MetadataHandler.h +++ b/launcher/minecraft/mod/MetadataHandler.h @@ -63,4 +63,10 @@ inline auto get(const QDir& index_dir, QVariant& mod_id) -> ModStruct { return Packwiz::V1::getIndexForMod(index_dir, mod_id); } + +inline auto modSideToString(ModSide side) -> QString +{ + return Packwiz::V1::sideToString(side); +} + }; // namespace Metadata diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 465687005..8a4c19dab 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -57,7 +57,7 @@ void Mod::setDetails(const ModDetails& details) m_local_details = details; } -std::pair Mod::compare(const Resource& other, SortType type) const +int Mod::compare(const Resource& other, SortType type) const { auto cast_other = dynamic_cast(&other); if (!cast_other) @@ -67,29 +67,44 @@ std::pair Mod::compare(const Resource& other, SortType type) const default: case SortType::ENABLED: case SortType::NAME: - case SortType::DATE: { - auto res = Resource::compare(other, type); - if (res.first != 0) - return res; - break; - } + case SortType::DATE: + case SortType::SIZE: + return Resource::compare(other, type); case SortType::VERSION: { auto this_ver = Version(version()); auto other_ver = Version(cast_other->version()); if (this_ver > other_ver) - return { 1, type == SortType::VERSION }; + return 1; if (this_ver < other_ver) - return { -1, type == SortType::VERSION }; + return -1; break; } - case SortType::PROVIDER: { - auto compare_result = QString::compare(provider(), cast_other->provider(), Qt::CaseInsensitive); + case SortType::SIDE: { + auto compare_result = QString::compare(side(), cast_other->side(), Qt::CaseInsensitive); if (compare_result != 0) - return { compare_result, type == SortType::PROVIDER }; + return compare_result; + break; + } + case SortType::MC_VERSIONS: { + auto compare_result = QString::compare(mcVersions(), cast_other->mcVersions(), Qt::CaseInsensitive); + if (compare_result != 0) + return compare_result; + break; + } + case SortType::LOADERS: { + auto compare_result = QString::compare(loaders(), cast_other->loaders(), Qt::CaseInsensitive); + if (compare_result != 0) + return compare_result; + break; + } + case SortType::RELEASE_TYPE: { + auto compare_result = QString::compare(releaseType(), cast_other->releaseType(), Qt::CaseInsensitive); + if (compare_result != 0) + return compare_result; break; } } - return { 0, false }; + return 0; } bool Mod::applyFilter(QRegularExpression filter) const @@ -137,6 +152,47 @@ auto Mod::metaurl() const -> QString return ModPlatform::getMetaURL(metadata()->provider, metadata()->project_id); } +auto Mod::loaders() const -> QString +{ + if (metadata()) { + QStringList loaders; + auto modLoaders = metadata()->loaders; + for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Cauldron, ModPlatform::LiteLoader, ModPlatform::Fabric, + ModPlatform::Quilt }) { + if (modLoaders & loader) { + loaders << getModLoaderAsString(loader); + } + } + return loaders.join(", "); + } + + return {}; +} + +auto Mod::side() const -> QString +{ + if (metadata()) + return Metadata::modSideToString(metadata()->side); + + return Metadata::modSideToString(Metadata::ModSide::UniversalSide); +} + +auto Mod::mcVersions() const -> QString +{ + if (metadata()) + return metadata()->mcVersions.join(", "); + + return {}; +} + +auto Mod::releaseType() const -> QString +{ + if (metadata()) + return metadata()->releaseType.toString(); + + return ModPlatform::IndexedVersionType().toString(); +} + auto Mod::description() const -> QString { return details().description; @@ -200,7 +256,7 @@ QPixmap Mod::icon(QSize size, Qt::AspectRatioMode mode) const return {}; if (m_pack_image_cache_key.was_ever_used) { - qDebug() << "Mod" << name() << "Had it's icon evicted form the cache. reloading..."; + 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. diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 5f82572a2..01fa9c99f 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -67,7 +67,10 @@ class Mod : public Resource { auto licenses() const -> const QList&; auto issueTracker() const -> QString; auto metaurl() const -> QString; - void setDetails(const ModDetails& details); + auto side() const -> QString; + auto loaders() const -> QString; + auto mcVersions() const -> QString; + auto releaseType() const -> QString; /** Get the intneral path to the mod's icon file*/ QString iconPath() const { return m_local_details.icon_file; } @@ -76,11 +79,18 @@ class Mod : public Resource { /** Thread-safe. */ void setIcon(QImage new_image) const; + void setDetails(const ModDetails& details); + bool valid() const override; - [[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair override; + [[nodiscard]] int compare(const Resource & other, SortType type) const override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; + // Delete all the files of this mod + auto destroy(QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool; + // Delete the metadata only + void destroyMetadata(QDir& index_dir); + void finishResolvingWithDetails(ModDetails&& details); protected: diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index d5735dcb8..11c784b96 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -51,20 +51,28 @@ #include "Application.h" -#include "Resource.h" +#include "Json.h" #include "minecraft/mod/tasks/LocalModParseTask.h" #include "minecraft/mod/tasks/LocalResourceUpdateTask.h" +#include "modplatform/ModIndex.h" +#include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameModIndex.h" ModFolderModel::ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) { - m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider" }); - m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider") }); - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION, SortType::DATE, SortType::PROVIDER }; - m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, + m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider", "Size", "Side", "Loaders", + "Minecraft Versions", "Release Type" }); + m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider"), + tr("Size"), tr("Side"), tr("Loaders"), tr("Minecraft Versions"), tr("Release Type") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION, + SortType::DATE, SortType::PROVIDER, SortType::SIZE, SortType::SIDE, + SortType::LOADERS, SortType::MC_VERSIONS, SortType::RELEASE_TYPE }; + m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, + QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; - m_columnsHideable = { false, true, false, true, true, true }; + m_columnsHideable = { false, true, false, true, true, true, true, true, true, true, true }; + m_columnsHiddenByDefault = { false, false, false, false, false, false, false, true, true, true, true }; } QVariant ModFolderModel::data(const QModelIndex& index, int role) const @@ -81,7 +89,7 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const case NameColumn: return m_resources[row]->name(); case VersionColumn: { - switch (m_resources[row]->type()) { + switch (at(row).type()) { case ResourceType::FOLDER: return tr("Folder"); case ResourceType::SINGLEFILE: @@ -92,15 +100,30 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const return at(row).version(); } case DateColumn: - return m_resources[row]->dateTimeChanged(); - case ProviderColumn: + return at(row).dateTimeChanged(); + case ProviderColumn: { return at(row).provider(); + } + case SideColumn: { + return at(row).side(); + } + case LoadersColumn: { + return at(row).loaders(); + } + case McVersionsColumn: { + return at(row).mcVersions(); + } + case ReleaseTypeColumn: { + return at(row).releaseType(); + } + case SizeColumn: + return at(row).sizeStr(); default: return QVariant(); } case Qt::ToolTipRole: - if (column == NAME_COLUMN) { + if (column == NameColumn) { if (at(row).isSymLinkUnder(instDirPath())) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." @@ -114,7 +137,7 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const } return m_resources[row]->internal_id(); case Qt::DecorationRole: { - if (column == NAME_COLUMN && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) + if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) return APPLICATION->getThemedIcon("status-yellow"); if (column == ImageColumn) { return at(row).icon({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); @@ -149,6 +172,11 @@ QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientatio case DateColumn: case ProviderColumn: case ImageColumn: + case SideColumn: + case LoadersColumn: + case McVersionsColumn: + case ReleaseTypeColumn: + case SizeColumn: return columnNames().at(section); default: return QVariant(); @@ -166,6 +194,16 @@ QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientatio return tr("The date and time this mod was last changed (or added)."); case ProviderColumn: return tr("The source provider of the mod."); + case SideColumn: + return tr("On what environment the mod is running."); + case LoadersColumn: + return tr("The mod loader."); + case McVersionsColumn: + return tr("The supported minecraft versions."); + case ReleaseTypeColumn: + return tr("The release type."); + case SizeColumn: + return tr("The size of the mod."); default: return QVariant(); } diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index cd5e99b11..629a2621c 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -3,6 +3,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -60,7 +61,20 @@ class QFileSystemWatcher; class ModFolderModel : public ResourceFolderModel { Q_OBJECT public: - enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, VersionColumn, DateColumn, ProviderColumn, NUM_COLUMNS }; + enum Columns { + ActiveColumn = 0, + ImageColumn, + NameColumn, + VersionColumn, + DateColumn, + ProviderColumn, + SizeColumn, + SideColumn, + LoadersColumn, + McVersionsColumn, + ReleaseTypeColumn, + NUM_COLUMNS + }; enum ModStatusAction { Disable, Enable, Toggle }; ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); diff --git a/launcher/minecraft/mod/Resource.cpp b/launcher/minecraft/mod/Resource.cpp index 7c1fb9108..c7ef145b6 100644 --- a/launcher/minecraft/mod/Resource.cpp +++ b/launcher/minecraft/mod/Resource.cpp @@ -1,9 +1,12 @@ #include "Resource.h" +#include #include #include +#include #include "FileSystem.h" +#include "StringUtils.h" Resource::Resource(QObject* parent) : QObject(parent) {} @@ -18,6 +21,20 @@ void Resource::setFile(QFileInfo file_info) parseFile(); } +static std::tuple calculateFileSize(const QFileInfo& file) +{ + if (file.isDir()) { + auto dir = QDir(file.absoluteFilePath()); + dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); + auto count = dir.count(); + auto str = QObject::tr("item"); + if (count != 1) + str = QObject::tr("items"); + return { QString("%1 %2").arg(QString::number(count), str), count }; + } + return { StringUtils::humanReadableFileSize(file.size(), true), file.size() }; +} + void Resource::parseFile() { QString file_name{ m_file_info.fileName() }; @@ -26,6 +43,7 @@ void Resource::parseFile() m_internal_id = file_name; + std::tie(m_size_str, m_size_info) = calculateFileSize(m_file_info); if (m_file_info.isDir()) { m_type = ResourceType::FOLDER; m_name = file_name; @@ -85,37 +103,55 @@ void Resource::setMetadata(std::shared_ptr&& metadata) m_metadata = metadata; } -std::pair Resource::compare(const Resource& other, SortType type) const +int Resource::compare(const Resource& other, SortType type) const { switch (type) { default: case SortType::ENABLED: if (enabled() && !other.enabled()) - return { 1, type == SortType::ENABLED }; + return 1; if (!enabled() && other.enabled()) - return { -1, type == SortType::ENABLED }; + return -1; break; case SortType::NAME: { QString this_name{ name() }; QString other_name{ other.name() }; + // TODO do we need this? it could result in 0 being returned removeThePrefix(this_name); removeThePrefix(other_name); - auto compare_result = QString::compare(this_name, other_name, Qt::CaseInsensitive); - if (compare_result != 0) - return { compare_result, type == SortType::NAME }; - break; + return QString::compare(this_name, other_name, Qt::CaseInsensitive); } case SortType::DATE: if (dateTimeChanged() > other.dateTimeChanged()) - return { 1, type == SortType::DATE }; + return 1; if (dateTimeChanged() < other.dateTimeChanged()) - return { -1, type == SortType::DATE }; + return -1; break; + case SortType::SIZE: { + if (this->type() != other.type()) { + if (this->type() == ResourceType::FOLDER) + return -1; + if (other.type() == ResourceType::FOLDER) + return 1; + } + + if (sizeInfo() > other.sizeInfo()) + return 1; + if (sizeInfo() < other.sizeInfo()) + return -1; + break; + } + case SortType::PROVIDER: { + auto compare_result = QString::compare(provider(), other.provider(), Qt::CaseInsensitive); + if (compare_result != 0) + return compare_result; + break; + } } - return { 0, false }; + return 0; } bool Resource::applyFilter(QRegularExpression filter) const @@ -154,15 +190,14 @@ bool Resource::enable(EnableAction action) if (!path.endsWith(".disabled")) return false; path.chop(9); - - if (!file.rename(path)) - return false; } else { path += ".disabled"; - - if (!file.rename(path)) - return false; + if (QFile::exists(path)) { + path = FS::getUniqueResourceName(path); + } } + if (!file.rename(path)) + return false; setFile(QFileInfo(path)); @@ -209,4 +244,12 @@ bool Resource::isSymLinkUnder(const QString& instPath) const bool Resource::isMoreThanOneHardLink() const { return FS::hardLinkCount(m_file_info.absoluteFilePath()) > 1; +} + +auto Resource::getOriginalFileName() const -> QString +{ + auto fileName = m_file_info.fileName(); + if (!m_enabled) + fileName.chop(9); + return fileName; } \ No newline at end of file diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h index 772f456f1..f6667871a 100644 --- a/launcher/minecraft/mod/Resource.h +++ b/launcher/minecraft/mod/Resource.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include @@ -24,7 +59,7 @@ enum class ResourceStatus { UNKNOWN, // Default status }; -enum class SortType { NAME, DATE, VERSION, ENABLED, PACK_FORMAT, PROVIDER }; +enum class SortType { NAME, DATE, VERSION, ENABLED, PACK_FORMAT, PROVIDER, SIZE, SIDE, MC_VERSIONS, LOADERS, RELEASE_TYPE }; enum class EnableAction { ENABLE, DISABLE, TOGGLE }; @@ -54,6 +89,9 @@ class Resource : public QObject { [[nodiscard]] auto internal_id() const -> QString { return m_internal_id; } [[nodiscard]] auto type() const -> ResourceType { return m_type; } [[nodiscard]] bool enabled() const { return m_enabled; } + [[nodiscard]] auto getOriginalFileName() const -> QString; + [[nodiscard]] QString sizeStr() const { return m_size_str; } + [[nodiscard]] qint64 sizeInfo() const { return m_size_info; } [[nodiscard]] virtual auto name() const -> QString; [[nodiscard]] virtual bool valid() const { return m_type != ResourceType::UNKNOWN; } @@ -71,10 +109,8 @@ class Resource : public QObject { * > 0: 'this' comes after 'other' * = 0: 'this' is equal to 'other' * < 0: 'this' comes before 'other' - * - * The second argument in the pair is true if the sorting type that decided which one is greater was 'type'. */ - [[nodiscard]] virtual auto compare(Resource const& other, SortType type = SortType::NAME) const -> std::pair; + [[nodiscard]] virtual int compare(Resource const& other, SortType type = SortType::NAME) const; /** Returns whether the given filter should filter out 'this' (false), * or if such filter includes the Resource (true). @@ -142,4 +178,6 @@ class Resource : public QObject { bool m_is_resolving = false; bool m_is_resolved = false; int m_resolution_ticket = 0; + QString m_size_str; + qint64 m_size_info; }; diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index a7a80db05..12f13ec12 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -17,6 +17,7 @@ #include "FileSystem.h" #include "QVariantUtils.h" +#include "StringUtils.h" #include "minecraft/mod/tasks/ResourceFolderLoadTask.h" #include "Json.h" @@ -119,7 +120,7 @@ bool ResourceFolderModel::installResource(QString original_path) case ResourceType::ZIPFILE: case ResourceType::LITEMOD: { if (QFile::exists(new_path) || QFile::exists(new_path + QString(".disabled"))) { - if (!QFile::remove(new_path)) { + if (!FS::deletePath(new_path)) { qCritical() << "Cleaning up new location (" << new_path << ") was unsuccessful!"; return false; } @@ -280,9 +281,6 @@ bool ResourceFolderModel::setResourceEnabled(const QModelIndexList& indexes, Ena } auto new_id = resource->internal_id(); - if (m_resources_index.contains(new_id)) { - // FIXME: https://github.com/PolyMC/PolyMC/issues/550 - } m_resources_index.remove(old_id); m_resources_index[new_id] = row; @@ -350,7 +348,12 @@ void ResourceFolderModel::resolveResource(Resource* res) connect( task.get(), &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); connect( - task.get(), &Task::finished, this, [=] { m_active_parse_tasks.remove(ticket); }, Qt::ConnectionType::QueuedConnection); + task.get(), &Task::finished, this, + [=] { + m_active_parse_tasks.remove(ticket); + emit parseFinished(); + }, + Qt::ConnectionType::QueuedConnection); m_helper_thread_task.addTask(task); @@ -487,6 +490,8 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const return m_resources[row]->dateTimeChanged(); case PROVIDER_COLUMN: return m_resources[row]->provider(); + case SIZE_COLUMN: + return m_resources[row]->sizeStr(); default: return {}; } @@ -557,6 +562,7 @@ QVariant ResourceFolderModel::headerData(int section, [[maybe_unused]] Qt::Orien case NAME_COLUMN: case DATE_COLUMN: case PROVIDER_COLUMN: + case SIZE_COLUMN: return columnNames().at(section); default: return {}; @@ -572,6 +578,8 @@ QVariant ResourceFolderModel::headerData(int section, [[maybe_unused]] Qt::Orien return tr("The date and time this resource was last changed (or added)."); case PROVIDER_COLUMN: return tr("The source provider of the resource."); + case SIZE_COLUMN: + return tr("The size of the resource."); default: return {}; } @@ -601,6 +609,10 @@ void ResourceFolderModel::saveColumns(QTreeView* tree) void ResourceFolderModel::loadColumns(QTreeView* tree) { + for (auto i = 0; i < m_columnsHiddenByDefault.size(); ++i) { + tree->setColumnHidden(i, m_columnsHiddenByDefault[i]); + } + auto const setting_name = QString("UI/%1_Page/Columns").arg(id()); auto setting = (m_instance->settings()->contains(setting_name)) ? m_instance->settings()->getSetting(setting_name) : m_instance->settings()->registerSetting(setting_name); @@ -678,15 +690,36 @@ SortType ResourceFolderModel::columnToSortKey(size_t column) const auto const& resource_right = model->at(source_right.row()); auto compare_result = resource_left.compare(resource_right, column_sort_key); - if (compare_result.first == 0) + if (compare_result == 0) return QSortFilterProxyModel::lessThan(source_left, source_right); - if (compare_result.second || sortOrder() != Qt::DescendingOrder) - return (compare_result.first < 0); - return (compare_result.first > 0); + return compare_result < 0; } QString ResourceFolderModel::instDirPath() const { return QFileInfo(m_instance->instanceRoot()).absoluteFilePath(); } + +void ResourceFolderModel::onParseFailed(int ticket, QString resource_id) +{ + auto iter = m_active_parse_tasks.constFind(ticket); + if (iter == m_active_parse_tasks.constEnd()) + return; + + auto removed_index = m_resources_index[resource_id]; + auto removed_it = m_resources.begin() + removed_index; + Q_ASSERT(removed_it != m_resources.end()); + + beginRemoveRows(QModelIndex(), removed_index, removed_index); + m_resources.erase(removed_it); + + // update index + m_resources_index.clear(); + int idx = 0; + for (auto const& mod : qAsConst(m_resources)) { + m_resources_index[mod->internal_id()] = idx; + idx++; + } + endRemoveRows(); +} diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index e65c69b25..c3ce822c8 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -49,7 +49,7 @@ class QSortFilterProxyModel; return nullptr; \ return static_cast((*iter).get()); \ } \ - QList selected##T##s(const QModelIndexList& indexes) \ + QList selected##T##s(const QModelIndexList& indexes) \ { \ QList result; \ for (const QModelIndex& index : indexes) { \ @@ -60,7 +60,7 @@ class QSortFilterProxyModel; } \ return result; \ } \ - QList all##T##s() \ + QList all##T##s() \ { \ QList result; \ result.reserve(m_resources.size()); \ @@ -151,7 +151,8 @@ class ResourceFolderModel : public QAbstractListModel { /* Qt behavior */ /* Basic columns */ - enum Columns { ACTIVE_COLUMN = 0, NAME_COLUMN, DATE_COLUMN, PROVIDER_COLUMN, NUM_COLUMNS }; + enum Columns { ACTIVE_COLUMN = 0, NAME_COLUMN, DATE_COLUMN, PROVIDER_COLUMN, SIZE_COLUMN, NUM_COLUMNS }; + QStringList columnNames(bool translated = true) const { return translated ? m_column_names_translated : m_column_names; } [[nodiscard]] int rowCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : static_cast(size()); } @@ -198,6 +199,7 @@ class ResourceFolderModel : public QAbstractListModel { signals: void updateFinished(); + void parseFinished(); protected: /** This creates a new update task to be executed by update(). @@ -246,20 +248,18 @@ class ResourceFolderModel : public QAbstractListModel { * if the resource is complex and has more stuff to parse. */ virtual void onParseSucceeded(int ticket, QString resource_id); - virtual void onParseFailed(int ticket, QString resource_id) - { - Q_UNUSED(ticket); - Q_UNUSED(resource_id); - } + virtual void onParseFailed(int ticket, QString resource_id); protected: // Represents the relationship between a column's index (represented by the list index), and it's sorting key. // As such, the order in with they appear is very important! - QList m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE, SortType::PROVIDER }; - QStringList m_column_names = { "Enable", "Name", "Last Modified", "Provider" }; - QStringList m_column_names_translated = { tr("Enable"), tr("Name"), tr("Last Modified"), tr("Provider") }; - QList m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive }; - QList m_columnsHideable = { false, false, true, true }; + QList m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE, SortType::PROVIDER, SortType::SIZE }; + QStringList m_column_names = { "Enable", "Name", "Last Modified", "Provider", "Size" }; + QStringList m_column_names_translated = { tr("Enable"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size") }; + QList m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, + QHeaderView::Interactive }; + QList m_columnsHideable = { false, false, true, true, true }; + QList m_columnsHiddenByDefault = { false, false, false, false, true }; QDir m_dir; BaseInstance* m_instance; diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp index 074534405..81fb91485 100644 --- a/launcher/minecraft/mod/ResourcePack.cpp +++ b/launcher/minecraft/mod/ResourcePack.cpp @@ -11,15 +11,24 @@ #include "minecraft/mod/tasks/LocalResourcePackParseTask.h" // Values taken from: -// https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta +// https://minecraft.wiki/w/Pack_format#List_of_resource_pack_formats static const QMap> s_pack_format_versions = { - { 1, { Version("1.6.1"), Version("1.8.9") } }, { 2, { Version("1.9"), Version("1.10.2") } }, - { 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } }, - { 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } }, - { 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } }, - { 9, { Version("1.19"), Version("1.19.2") } }, { 11, { Version("22w42a"), Version("22w44a") } }, - { 12, { Version("1.19.3"), Version("1.19.3") } }, { 13, { Version("1.19.4"), Version("1.19.4") } }, - { 14, { Version("1.20"), Version("1.20") } } + { 1, { Version("1.6.1"), Version("1.8.9") } }, { 2, { Version("1.9"), Version("1.10.2") } }, + { 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } }, + { 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } }, + { 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } }, + { 9, { Version("1.19"), Version("1.19.2") } }, { 11, { Version("22w42a"), Version("22w44a") } }, + { 12, { Version("1.19.3"), Version("1.19.3") } }, { 13, { Version("1.19.4"), Version("1.19.4") } }, + { 14, { Version("23w14a"), Version("23w16a") } }, { 15, { Version("1.20"), Version("1.20.1") } }, + { 16, { Version("23w31a"), Version("23w31a") } }, { 17, { Version("23w32a"), Version("23w35a") } }, + { 18, { Version("1.20.2"), Version("23w16a") } }, { 19, { Version("23w42a"), Version("23w42a") } }, + { 20, { Version("23w43a"), Version("23w44a") } }, { 21, { Version("23w45a"), Version("23w46a") } }, + { 22, { Version("1.20.3-pre1"), Version("23w51b") } }, { 24, { Version("24w03a"), Version("24w04a") } }, + { 25, { Version("24w05a"), Version("24w05b") } }, { 26, { Version("24w06a"), Version("24w07a") } }, + { 28, { Version("24w09a"), Version("24w10a") } }, { 29, { Version("24w11a"), Version("24w11a") } }, + { 30, { Version("24w12a"), Version("23w12a") } }, { 31, { Version("24w13a"), Version("1.20.5-pre3") } }, + { 32, { Version("1.20.5-pre4"), Version("1.20.6") } }, { 33, { Version("24w18a"), Version("24w20a") } }, + { 34, { Version("24w21a"), Version("1.21") } } }; void ResourcePack::setPackFormat(int new_format_id) @@ -94,29 +103,24 @@ std::pair ResourcePack::compatibleVersions() const return s_pack_format_versions.constFind(m_pack_format).value(); } -std::pair ResourcePack::compare(const Resource& other, SortType type) const +int ResourcePack::compare(const Resource& other, SortType type) const { auto const& cast_other = static_cast(other); - switch (type) { - default: { - auto res = Resource::compare(other, type); - if (res.first != 0) - return res; - break; - } + default: + return Resource::compare(other, type); case SortType::PACK_FORMAT: { auto this_ver = packFormat(); auto other_ver = cast_other.packFormat(); if (this_ver > other_ver) - return { 1, type == SortType::PACK_FORMAT }; + return 1; if (this_ver < other_ver) - return { -1, type == SortType::PACK_FORMAT }; + return -1; break; } } - return { 0, false }; + return 0; } bool ResourcePack::applyFilter(QRegularExpression filter) const diff --git a/launcher/minecraft/mod/ResourcePack.h b/launcher/minecraft/mod/ResourcePack.h index c06f3793d..2254fc5c4 100644 --- a/launcher/minecraft/mod/ResourcePack.h +++ b/launcher/minecraft/mod/ResourcePack.h @@ -44,7 +44,7 @@ class ResourcePack : public Resource { bool valid() const override; - [[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair override; + [[nodiscard]] int compare(Resource const& other, SortType type) const override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; protected: diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index ccbf80367..d41420c5f 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -50,13 +50,14 @@ ResourcePackFolderModel::ResourcePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) : ResourceFolderModel(dir, instance, is_indexed, create_dir, parent) { - m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified", "Provider" }); + m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified", "Provider", "Size" }); m_column_names_translated = - QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified"), tr("Provider") }); - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE, SortType::PROVIDER }; - m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, - QHeaderView::Interactive }; - m_columnsHideable = { false, true, false, true, true, true }; + QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified"), tr("Provider"), tr("Size") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, + SortType::DATE, SortType::PROVIDER, SortType::SIZE }; + m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, + QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; + m_columnsHideable = { false, true, false, true, true, true, true }; } QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const @@ -89,6 +90,8 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const return m_resources[row]->dateTimeChanged(); case ProviderColumn: return m_resources[row]->provider(); + case SizeColumn: + return m_resources[row]->sizeStr(); default: return {}; } @@ -148,6 +151,7 @@ QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::O case DateColumn: case ImageColumn: case ProviderColumn: + case SizeColumn: return columnNames().at(section); default: return {}; @@ -166,6 +170,8 @@ QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::O return tr("The date and time this resource pack was last changed (or added)."); case ProviderColumn: return tr("The source provider of the resource pack."); + case SizeColumn: + return tr("The size of the resource pack."); default: return {}; } diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.h b/launcher/minecraft/mod/ResourcePackFolderModel.h index f00acee29..9dbf41b85 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.h +++ b/launcher/minecraft/mod/ResourcePackFolderModel.h @@ -7,7 +7,7 @@ class ResourcePackFolderModel : public ResourceFolderModel { Q_OBJECT public: - enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, PackFormatColumn, DateColumn, ProviderColumn, NUM_COLUMNS }; + enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, PackFormatColumn, DateColumn, ProviderColumn, SizeColumn, NUM_COLUMNS }; explicit ResourcePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); diff --git a/launcher/minecraft/mod/ShaderPack.cpp b/launcher/minecraft/mod/ShaderPack.cpp index 2c094f26a..ccb344cb5 100644 --- a/launcher/minecraft/mod/ShaderPack.cpp +++ b/launcher/minecraft/mod/ShaderPack.cpp @@ -35,8 +35,3 @@ bool ShaderPack::valid() const { return m_pack_format != ShaderPackFormat::INVALID; } - -bool ShaderPack::applyFilter(QRegularExpression filter) const -{ - return valid() && Resource::applyFilter(filter); -} diff --git a/launcher/minecraft/mod/ShaderPack.h b/launcher/minecraft/mod/ShaderPack.h index d07c124be..ec0f9404e 100644 --- a/launcher/minecraft/mod/ShaderPack.h +++ b/launcher/minecraft/mod/ShaderPack.h @@ -54,7 +54,6 @@ class ShaderPack : public Resource { void setPackFormat(ShaderPackFormat new_format); bool valid() const override; - [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; protected: mutable QMutex m_data_lock; diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index 3795795a2..293567cc8 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -45,11 +45,12 @@ TexturePackFolderModel::TexturePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) { - m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified", "Provider" }); - m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified"), tr("Provider") }); - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE }; - m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive }; - m_columnsHideable = { false, true, false, true, true }; + m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified", "Provider", "Size" }); + m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE, SortType::SIZE }; + m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, QHeaderView::Interactive }; + m_columnsHideable = { false, true, false, true, true, true }; + m_columnsHiddenByDefault = { false, false, false, false, false, true }; } Task* TexturePackFolderModel::createParseTask(Resource& resource) @@ -74,6 +75,8 @@ QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const return m_resources[row]->dateTimeChanged(); case ProviderColumn: return m_resources[row]->provider(); + case SizeColumn: + return m_resources[row]->sizeStr(); default: return {}; } @@ -126,6 +129,7 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or case DateColumn: case ImageColumn: case ProviderColumn: + case SizeColumn: return columnNames().at(section); default: return {}; @@ -140,6 +144,8 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or return tr("The date and time this texture pack was last changed (or added)."); case ProviderColumn: return tr("The source provider of the texture pack."); + case SizeColumn: + return tr("The size of the texture pack."); default: return {}; } diff --git a/launcher/minecraft/mod/TexturePackFolderModel.h b/launcher/minecraft/mod/TexturePackFolderModel.h index 649842e23..7a9264e8f 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.h +++ b/launcher/minecraft/mod/TexturePackFolderModel.h @@ -44,7 +44,7 @@ class TexturePackFolderModel : public ResourceFolderModel { Q_OBJECT public: - enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, DateColumn, ProviderColumn, NUM_COLUMNS }; + enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, DateColumn, ProviderColumn, SizeColumn, NUM_COLUMNS }; explicit TexturePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp index 238032532..b9288d2b3 100644 --- a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp @@ -23,12 +23,12 @@ #include #include "Json.h" #include "QObjectPtr.h" +#include "minecraft/PackProfile.h" #include "minecraft/mod/MetadataHandler.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" #include "modplatform/flame/FlameAPI.h" #include "modplatform/modrinth/ModrinthAPI.h" -#include "tasks/ConcurrentTask.h" #include "tasks/SequentialTask.h" #include "ui/pages/modplatform/ModModel.h" #include "ui/pages/modplatform/flame/FlameResourceModels.h" @@ -44,6 +44,14 @@ static ModPlatform::ModLoaderTypes mcLoaders(BaseInstance* inst) return static_cast(inst)->getPackProfile()->getSupportedModLoaders().value(); } +static bool checkDependencies(std::shared_ptr sel, + Version mcVersion, + ModPlatform::ModLoaderTypes loaders) +{ + return (sel->pack->versions.isEmpty() || sel->version.mcVersion.contains(mcVersion.toString())) && + (!loaders || !sel->version.loaders || sel->version.loaders & loaders); +} + GetModDependenciesTask::GetModDependenciesTask(QObject* parent, BaseInstance* instance, ModFolderModel* folder, @@ -68,9 +76,10 @@ GetModDependenciesTask::GetModDependenciesTask(QObject* parent, void GetModDependenciesTask::prepare() { for (auto sel : m_selected) { - for (auto dep : getDependenciesForVersion(sel->version, sel->pack->provider)) { - addTask(prepareDependencyTask(dep, sel->pack->provider, 20)); - } + if (checkDependencies(sel, m_version, m_loaderType)) + for (auto dep : getDependenciesForVersion(sel->version, sel->pack->provider)) { + addTask(prepareDependencyTask(dep, sel->pack->provider, 20)); + } } } diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index 3982f3338..60257ce0c 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -746,7 +746,7 @@ void LocalModParseTask::executeTask() m_result->details = mod.details(); if (m_aborted) - emit finished(); + emitAborted(); else emitSucceeded(); } diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp index 26bc07637..27fbf3c6d 100644 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp @@ -178,6 +178,88 @@ bool processZIP(ResourcePack& pack, ProcessingLevel level) return true; } +QString buildStyle(const QJsonObject& obj) +{ + QStringList styles; + if (auto color = Json::ensureString(obj, "color"); !color.isEmpty()) { + styles << QString("color: %1;").arg(color); + } + if (obj.contains("bold")) { + QString weight = "normal"; + if (Json::ensureBoolean(obj, "bold", false)) { + weight = "bold"; + } + styles << QString("font-weight: %1;").arg(weight); + } + if (obj.contains("italic")) { + QString style = "normal"; + if (Json::ensureBoolean(obj, "italic", false)) { + style = "italic"; + } + styles << QString("font-style: %1;").arg(style); + } + + return styles.isEmpty() ? "" : QString("style=\"%1\"").arg(styles.join(" ")); +} + +QString processComponent(const QJsonArray& value, bool strikethrough, bool underline) +{ + QString result; + for (auto current : value) + result += processComponent(current, strikethrough, underline); + return result; +} + +QString processComponent(const QJsonObject& obj, bool strikethrough, bool underline) +{ + underline = Json::ensureBoolean(obj, "underlined", underline); + strikethrough = Json::ensureBoolean(obj, "strikethrough", strikethrough); + + QString result = Json::ensureString(obj, "text"); + if (underline) { + result = QString("%1").arg(result); + } + if (strikethrough) { + result = QString("%1").arg(result); + } + // the extra needs to be a array + result += processComponent(Json::ensureArray(obj, "extra"), strikethrough, underline); + if (auto style = buildStyle(obj); !style.isEmpty()) { + result = QString("%2").arg(style, result); + } + if (obj.contains("clickEvent")) { + auto click_event = Json::ensureObject(obj, "clickEvent"); + auto action = Json::ensureString(click_event, "action"); + auto value = Json::ensureString(click_event, "value"); + if (action == "open_url" && !value.isEmpty()) { + result = QString("%2").arg(value, result); + } + } + return result; +} + +QString processComponent(const QJsonValue& value, bool strikethrough, bool underline) +{ + if (value.isString()) { + return value.toString(); + } + if (value.isBool()) { + return value.toBool() ? "true" : "false"; + } + if (value.isDouble()) { + return QString::number(value.toDouble()); + } + if (value.isArray()) { + return processComponent(value.toArray(), strikethrough, underline); + } + if (value.isObject()) { + return processComponent(value.toObject(), strikethrough, underline); + } + qWarning() << "Invalid component type!"; + return {}; +} + +// https://minecraft.wiki/w/Raw_JSON_text_format // https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data) { @@ -186,7 +268,9 @@ bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data) auto pack_obj = Json::requireObject(json_doc.object(), "pack", {}); pack.setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0)); - pack.setDescription(Json::ensureString(pack_obj, "description", "")); + + pack.setDescription(processComponent(pack_obj.value("description"))); + } catch (Json::JsonException& e) { qWarning() << "JsonException: " << e.what() << e.cause(); return false; @@ -286,8 +370,10 @@ bool LocalResourcePackParseTask::abort() void LocalResourcePackParseTask::executeTask() { - if (!ResourcePackUtils::process(m_resource_pack)) + if (!ResourcePackUtils::process(m_resource_pack)) { + emitFailed("this is not a resource pack"); return; + } if (m_aborted) emitAborted(); diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h index 5199bf3f0..97bf7b2ba 100644 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h @@ -34,6 +34,7 @@ bool process(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processZIP(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processFolder(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); +QString processComponent(const QJsonValue& value, bool strikethrough = false, bool underline = false); bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data); bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data); diff --git a/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.h b/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.h index 6e2efbd6a..f8869258e 100644 --- a/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.h +++ b/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.h @@ -42,6 +42,6 @@ class LocalResourceUpdateTask : public Task { private: QDir m_index_dir; - ModPlatform::IndexedPack& m_project; - ModPlatform::IndexedVersion& m_version; + ModPlatform::IndexedPack m_project; + ModPlatform::IndexedVersion m_version; }; diff --git a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp index a9949735b..4deebcd1d 100644 --- a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp @@ -103,8 +103,10 @@ bool LocalShaderPackParseTask::abort() void LocalShaderPackParseTask::executeTask() { - if (!ShaderPackUtils::process(m_shader_pack)) + if (!ShaderPackUtils::process(m_shader_pack)) { + emitFailed("this is not a shader pack"); return; + } if (m_aborted) emitAborted(); diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp index d7e61ca90..00cc2def2 100644 --- a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp @@ -241,8 +241,10 @@ bool LocalTexturePackParseTask::abort() void LocalTexturePackParseTask::executeTask() { - if (!TexturePackUtils::process(m_texture_pack)) + if (!TexturePackUtils::process(m_texture_pack)) { + emitFailed("this is not a texture pack"); return; + } if (m_aborted) emitAborted(); diff --git a/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp index 117cffa60..9aa931d1c 100644 --- a/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp @@ -36,6 +36,7 @@ #include "ResourceFolderLoadTask.h" +#include "FileSystem.h" #include "minecraft/mod/MetadataHandler.h" #include @@ -68,6 +69,13 @@ void ResourceFolderLoadTask::executeTask() // Read JAR files that don't have metadata m_resource_dir.refresh(); for (auto entry : m_resource_dir.entryInfoList()) { + auto filePath = entry.absoluteFilePath(); + auto newFilePath = FS::getUniqueResourceName(filePath); + if (newFilePath != filePath) { + FS::move(filePath, newFilePath); + entry = QFileInfo(newFilePath); + } + Resource* resource = m_create_func(entry); if (resource->enabled()) { diff --git a/launcher/minecraft/services/CapeChange.cpp b/launcher/minecraft/services/CapeChange.cpp deleted file mode 100644 index 2ba38a6af..000000000 --- a/launcher/minecraft/services/CapeChange.cpp +++ /dev/null @@ -1,121 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Sefa Eyeoglu - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "CapeChange.h" - -#include -#include - -#include "Application.h" - -CapeChange::CapeChange(QObject* parent, QString token, QString cape) : Task(parent), m_capeId(cape), m_token(token) {} - -void CapeChange::setCape([[maybe_unused]] QString& cape) -{ - QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active")); - auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); - QNetworkReply* rep = APPLICATION->network()->put(request, requestString.toUtf8()); - - setStatus(tr("Equipping cape")); - - m_reply = shared_qobject_ptr(rep); - connect(rep, &QNetworkReply::uploadProgress, this, &CapeChange::setProgress); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 - connect(rep, &QNetworkReply::errorOccurred, this, &CapeChange::downloadError); -#else - connect(rep, QOverload::of(&QNetworkReply::error), this, &CapeChange::downloadError); -#endif - connect(rep, &QNetworkReply::sslErrors, this, &CapeChange::sslErrors); - connect(rep, &QNetworkReply::finished, this, &CapeChange::downloadFinished); -} - -void CapeChange::clearCape() -{ - QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active")); - auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); - QNetworkReply* rep = APPLICATION->network()->deleteResource(request); - - setStatus(tr("Removing cape")); - - m_reply = shared_qobject_ptr(rep); - connect(rep, &QNetworkReply::uploadProgress, this, &CapeChange::setProgress); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 - connect(rep, &QNetworkReply::errorOccurred, this, &CapeChange::downloadError); -#else - connect(rep, QOverload::of(&QNetworkReply::error), this, &CapeChange::downloadError); -#endif - connect(rep, &QNetworkReply::sslErrors, this, &CapeChange::sslErrors); - connect(rep, &QNetworkReply::finished, this, &CapeChange::downloadFinished); -} - -void CapeChange::executeTask() -{ - if (m_capeId.isEmpty()) { - clearCape(); - } else { - setCape(m_capeId); - } -} - -void CapeChange::downloadError(QNetworkReply::NetworkError error) -{ - // error happened during download. - qCritical() << "Network error: " << error; - emitFailed(m_reply->errorString()); -} - -void CapeChange::sslErrors(const QList& errors) -{ - int i = 1; - for (auto error : errors) { - qCritical() << "Cape change SSL Error #" << i << " : " << error.errorString(); - auto cert = error.certificate(); - qCritical() << "Certificate in question:\n" << cert.toText(); - i++; - } -} - -void CapeChange::downloadFinished() -{ - // if the download failed - if (m_reply->error() != QNetworkReply::NetworkError::NoError) { - emitFailed(QString("Network error: %1").arg(m_reply->errorString())); - m_reply.reset(); - return; - } - emitSucceeded(); -} diff --git a/launcher/minecraft/services/CapeChange.h b/launcher/minecraft/services/CapeChange.h deleted file mode 100644 index d0c893c44..000000000 --- a/launcher/minecraft/services/CapeChange.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include -#include -#include -#include "QObjectPtr.h" -#include "tasks/Task.h" - -class CapeChange : public Task { - Q_OBJECT - public: - CapeChange(QObject* parent, QString token, QString capeId); - virtual ~CapeChange() {} - - private: - void setCape(QString& cape); - void clearCape(); - - private: - QString m_capeId; - QString m_token; - shared_qobject_ptr m_reply; - - protected: - virtual void executeTask(); - - public slots: - void downloadError(QNetworkReply::NetworkError); - void sslErrors(const QList& errors); - void downloadFinished(); -}; diff --git a/launcher/minecraft/services/SkinDelete.cpp b/launcher/minecraft/services/SkinDelete.cpp deleted file mode 100644 index 9e9020692..000000000 --- a/launcher/minecraft/services/SkinDelete.cpp +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Sefa Eyeoglu - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "SkinDelete.h" - -#include -#include - -#include "Application.h" - -SkinDelete::SkinDelete(QObject* parent, QString token) : Task(parent), m_token(token) {} - -void SkinDelete::executeTask() -{ - QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active")); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); - QNetworkReply* rep = APPLICATION->network()->deleteResource(request); - m_reply = shared_qobject_ptr(rep); - - setStatus(tr("Deleting skin")); - connect(rep, &QNetworkReply::uploadProgress, this, &SkinDelete::setProgress); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 - connect(rep, &QNetworkReply::errorOccurred, this, &SkinDelete::downloadError); -#else - connect(rep, QOverload::of(&QNetworkReply::error), this, &SkinDelete::downloadError); -#endif - connect(rep, &QNetworkReply::sslErrors, this, &SkinDelete::sslErrors); - connect(rep, &QNetworkReply::finished, this, &SkinDelete::downloadFinished); -} - -void SkinDelete::downloadError(QNetworkReply::NetworkError error) -{ - // error happened during download. - qCritical() << "Network error: " << error; - emitFailed(m_reply->errorString()); -} - -void SkinDelete::sslErrors(const QList& errors) -{ - int i = 1; - for (auto error : errors) { - qCritical() << "Skin Delete SSL Error #" << i << " : " << error.errorString(); - auto cert = error.certificate(); - qCritical() << "Certificate in question:\n" << cert.toText(); - i++; - } -} - -void SkinDelete::downloadFinished() -{ - // if the download failed - if (m_reply->error() != QNetworkReply::NetworkError::NoError) { - emitFailed(QString("Network error: %1").arg(m_reply->errorString())); - m_reply.reset(); - return; - } - emitSucceeded(); -} diff --git a/launcher/minecraft/services/SkinDelete.h b/launcher/minecraft/services/SkinDelete.h deleted file mode 100644 index 44e30453f..000000000 --- a/launcher/minecraft/services/SkinDelete.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include -#include -#include "tasks/Task.h" - -using SkinDeletePtr = shared_qobject_ptr; - -class SkinDelete : public Task { - Q_OBJECT - public: - SkinDelete(QObject* parent, QString token); - virtual ~SkinDelete() = default; - - private: - QString m_token; - shared_qobject_ptr m_reply; - - protected: - virtual void executeTask(); - - public slots: - void downloadError(QNetworkReply::NetworkError); - void sslErrors(const QList& errors); - void downloadFinished(); -}; diff --git a/launcher/minecraft/services/SkinUpload.cpp b/launcher/minecraft/services/SkinUpload.cpp deleted file mode 100644 index 163b481b1..000000000 --- a/launcher/minecraft/services/SkinUpload.cpp +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Sefa Eyeoglu - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "SkinUpload.h" - -#include -#include - -#include "Application.h" - -QByteArray getVariant(SkinUpload::Model model) -{ - switch (model) { - default: - qDebug() << "Unknown skin type!"; - case SkinUpload::STEVE: - return "CLASSIC"; - case SkinUpload::ALEX: - return "SLIM"; - } -} - -SkinUpload::SkinUpload(QObject* parent, QString token, QByteArray skin, SkinUpload::Model model) - : Task(parent), m_model(model), m_skin(skin), m_token(token) -{} - -void SkinUpload::executeTask() -{ - QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins")); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); - QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); - - QHttpPart skin; - skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png")); - skin.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\"; filename=\"skin.png\"")); - skin.setBody(m_skin); - - QHttpPart model; - model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"variant\"")); - model.setBody(getVariant(m_model)); - - multiPart->append(skin); - multiPart->append(model); - - QNetworkReply* rep = APPLICATION->network()->post(request, multiPart); - m_reply = shared_qobject_ptr(rep); - - setStatus(tr("Uploading skin")); - connect(rep, &QNetworkReply::uploadProgress, this, &SkinUpload::setProgress); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 - connect(rep, &QNetworkReply::errorOccurred, this, &SkinUpload::downloadError); -#else - connect(rep, QOverload::of(&QNetworkReply::error), this, &SkinUpload::downloadError); -#endif - connect(rep, &QNetworkReply::sslErrors, this, &SkinUpload::sslErrors); - connect(rep, &QNetworkReply::finished, this, &SkinUpload::downloadFinished); -} - -void SkinUpload::downloadError(QNetworkReply::NetworkError error) -{ - // error happened during download. - qCritical() << "Network error: " << error; - emitFailed(m_reply->errorString()); -} - -void SkinUpload::sslErrors(const QList& errors) -{ - int i = 1; - for (auto error : errors) { - qCritical() << "Skin Upload SSL Error #" << i << " : " << error.errorString(); - auto cert = error.certificate(); - qCritical() << "Certificate in question:\n" << cert.toText(); - i++; - } -} - -void SkinUpload::downloadFinished() -{ - // if the download failed - if (m_reply->error() != QNetworkReply::NetworkError::NoError) { - emitFailed(QString("Network error: %1").arg(m_reply->errorString())); - m_reply.reset(); - return; - } - emitSucceeded(); -} diff --git a/launcher/minecraft/services/SkinUpload.h b/launcher/minecraft/services/SkinUpload.h deleted file mode 100644 index 016367ff8..000000000 --- a/launcher/minecraft/services/SkinUpload.h +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include -#include -#include -#include "tasks/Task.h" - -using SkinUploadPtr = shared_qobject_ptr; - -class SkinUpload : public Task { - Q_OBJECT - public: - enum Model { STEVE, ALEX }; - - // Note this class takes ownership of the file. - SkinUpload(QObject* parent, QString token, QByteArray skin, Model model = STEVE); - virtual ~SkinUpload() {} - - private: - Model m_model; - QByteArray m_skin; - QString m_token; - shared_qobject_ptr m_reply; - - protected: - virtual void executeTask(); - - public slots: - - void downloadError(QNetworkReply::NetworkError); - void sslErrors(const QList& errors); - - void downloadFinished(); -}; diff --git a/launcher/minecraft/skins/CapeChange.cpp b/launcher/minecraft/skins/CapeChange.cpp new file mode 100644 index 000000000..abbaa0b67 --- /dev/null +++ b/launcher/minecraft/skins/CapeChange.cpp @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "CapeChange.h" + +#include + +#include "net/ByteArraySink.h" +#include "net/RawHeaderProxy.h" + +CapeChange::CapeChange(QString cape) : NetRequest(), m_capeId(cape) +{ + logCat = taskMCSkinsLogC; +} + +QNetworkReply* CapeChange::getReply(QNetworkRequest& request) +{ + if (m_capeId.isEmpty()) { + setStatus(tr("Removing cape")); + return m_network->deleteResource(request); + } else { + setStatus(tr("Equipping cape")); + return m_network->put(request, QString("{\"capeId\":\"%1\"}").arg(m_capeId).toUtf8()); + } +} + +CapeChange::Ptr CapeChange::make(QString token, QString capeId) +{ + auto up = makeShared(capeId); + up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active"); + up->setObjectName(QString("BYTES:") + up->m_url.toString()); + up->m_sink.reset(new Net::ByteArraySink(std::make_shared())); + up->addHeaderProxy(new Net::RawHeaderProxy(QList{ + { "Authorization", QString("Bearer %1").arg(token).toLocal8Bit() }, + })); + return up; +} diff --git a/launcher/minecraft/skins/CapeChange.h b/launcher/minecraft/skins/CapeChange.h new file mode 100644 index 000000000..2be904f4d --- /dev/null +++ b/launcher/minecraft/skins/CapeChange.h @@ -0,0 +1,37 @@ +// 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 "net/NetRequest.h" + +class CapeChange : public Net::NetRequest { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + CapeChange(QString capeId); + virtual ~CapeChange() = default; + + static CapeChange::Ptr make(QString token, QString capeId); + + protected: + virtual QNetworkReply* getReply(QNetworkRequest&) override; + + private: + QString m_capeId; +}; diff --git a/launcher/minecraft/skins/SkinDelete.cpp b/launcher/minecraft/skins/SkinDelete.cpp new file mode 100644 index 000000000..94aca62ca --- /dev/null +++ b/launcher/minecraft/skins/SkinDelete.cpp @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "SkinDelete.h" + +#include "net/ByteArraySink.h" +#include "net/RawHeaderProxy.h" + +SkinDelete::SkinDelete() : NetRequest() +{ + logCat = taskMCSkinsLogC; +} + +QNetworkReply* SkinDelete::getReply(QNetworkRequest& request) +{ + setStatus(tr("Deleting skin")); + return m_network->deleteResource(request); +} + +SkinDelete::Ptr SkinDelete::make(QString token) +{ + auto up = makeShared(); + up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active"); + up->m_sink.reset(new Net::ByteArraySink(std::make_shared())); + up->addHeaderProxy(new Net::RawHeaderProxy(QList{ + { "Authorization", QString("Bearer %1").arg(token).toLocal8Bit() }, + })); + return up; +} diff --git a/launcher/minecraft/skins/SkinDelete.h b/launcher/minecraft/skins/SkinDelete.h new file mode 100644 index 000000000..d6a68d22c --- /dev/null +++ b/launcher/minecraft/skins/SkinDelete.h @@ -0,0 +1,34 @@ +// 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 "net/NetRequest.h" + +class SkinDelete : public Net::NetRequest { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + SkinDelete(); + virtual ~SkinDelete() = default; + + static SkinDelete::Ptr make(QString token); + + protected: + virtual QNetworkReply* getReply(QNetworkRequest&) override; +}; diff --git a/launcher/minecraft/skins/SkinList.cpp b/launcher/minecraft/skins/SkinList.cpp new file mode 100644 index 000000000..017cb8dc2 --- /dev/null +++ b/launcher/minecraft/skins/SkinList.cpp @@ -0,0 +1,393 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "SkinList.h" + +#include +#include + +#include "FileSystem.h" +#include "Json.h" +#include "minecraft/skins/SkinModel.h" + +SkinList::SkinList(QObject* parent, QString path, MinecraftAccountPtr acct) : QAbstractListModel(parent), m_acct(acct) +{ + FS::ensureFolderPathExists(m_dir.absolutePath()); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); + m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); + m_watcher.reset(new QFileSystemWatcher(this)); + is_watching = false; + connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &SkinList::directoryChanged); + connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &SkinList::fileChanged); + directoryChanged(path); +} + +void SkinList::startWatching() +{ + if (is_watching) { + return; + } + update(); + is_watching = m_watcher->addPath(m_dir.absolutePath()); + if (is_watching) { + qDebug() << "Started watching " << m_dir.absolutePath(); + } else { + qDebug() << "Failed to start watching " << m_dir.absolutePath(); + } +} + +void SkinList::stopWatching() +{ + save(); + if (!is_watching) { + return; + } + is_watching = !m_watcher->removePath(m_dir.absolutePath()); + if (!is_watching) { + qDebug() << "Stopped watching " << m_dir.absolutePath(); + } else { + qDebug() << "Failed to stop watching " << m_dir.absolutePath(); + } +} + +bool SkinList::update() +{ + QVector newSkins; + m_dir.refresh(); + + auto manifestInfo = QFileInfo(m_dir.absoluteFilePath("index.json")); + if (manifestInfo.exists()) { + try { + auto doc = Json::requireDocument(manifestInfo.absoluteFilePath(), "SkinList JSON file"); + const auto root = doc.object(); + auto skins = Json::ensureArray(root, "skins"); + for (auto jSkin : skins) { + SkinModel s(m_dir, Json::ensureObject(jSkin)); + if (s.isValid()) { + newSkins << s; + } + } + } catch (const Exception& e) { + qCritical() << "Couldn't load skins json:" << e.cause(); + } + } + + bool needsSave = false; + const auto& skin = m_acct->accountData()->minecraftProfile.skin; + if (!skin.url.isEmpty() && !skin.data.isEmpty()) { + QPixmap skinTexture; + SkinModel* nskin = nullptr; + for (auto i = 0; i < newSkins.size(); i++) { + if (newSkins[i].getURL() == skin.url) { + nskin = &newSkins[i]; + break; + } + } + if (!nskin) { + auto name = m_acct->profileName() + ".png"; + if (QFileInfo(m_dir.absoluteFilePath(name)).exists()) { + name = QUrl(skin.url).fileName() + ".png"; + } + auto path = m_dir.absoluteFilePath(name); + if (skinTexture.loadFromData(skin.data, "PNG") && skinTexture.save(path)) { + SkinModel s(path); + s.setModel(skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); + s.setCapeId(m_acct->accountData()->minecraftProfile.currentCape); + s.setURL(skin.url); + newSkins << s; + needsSave = true; + } + } else { + nskin->setCapeId(m_acct->accountData()->minecraftProfile.currentCape); + nskin->setModel(skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); + } + } + + auto folderContents = m_dir.entryInfoList(); + // if there are any untracked files... + for (QFileInfo entry : folderContents) { + if (!entry.isFile() && entry.suffix() != "png") + continue; + + SkinModel w(entry.absoluteFilePath()); + if (w.isValid()) { + auto add = true; + for (auto s : newSkins) { + if (s.name() == w.name()) { + add = false; + break; + } + } + if (add) { + newSkins.append(w); + needsSave = true; + } + } + } + std::sort(newSkins.begin(), newSkins.end(), + [](const SkinModel& a, const SkinModel& b) { return a.getPath().localeAwareCompare(b.getPath()) < 0; }); + beginResetModel(); + m_skin_list.swap(newSkins); + endResetModel(); + if (needsSave) + save(); + return true; +} + +void SkinList::directoryChanged(const QString& path) +{ + QDir new_dir(path); + if (!new_dir.exists()) + if (!FS::ensureFolderPathExists(new_dir.absolutePath())) + return; + if (m_dir.absolutePath() != new_dir.absolutePath()) { + m_dir.setPath(path); + m_dir.refresh(); + if (is_watching) + stopWatching(); + startWatching(); + } + update(); +} + +void SkinList::fileChanged(const QString& path) +{ + qDebug() << "Checking " << path; + QFileInfo checkfile(path); + if (!checkfile.exists()) + return; + + for (int i = 0; i < m_skin_list.count(); i++) { + if (m_skin_list[i].getPath() == checkfile.absoluteFilePath()) { + m_skin_list[i].refresh(); + dataChanged(index(i), index(i)); + break; + } + } +} + +QStringList SkinList::mimeTypes() const +{ + return { "text/uri-list" }; +} + +Qt::DropActions SkinList::supportedDropActions() const +{ + return Qt::CopyAction; +} + +bool SkinList::dropMimeData(const QMimeData* data, + Qt::DropAction action, + [[maybe_unused]] int row, + [[maybe_unused]] int column, + [[maybe_unused]] const QModelIndex& parent) +{ + if (action == Qt::IgnoreAction) + return true; + // check if the action is supported + if (!data || !(action & supportedDropActions())) + return false; + + // files dropped from outside? + if (data->hasUrls()) { + auto urls = data->urls(); + QStringList skinFiles; + for (auto url : urls) { + // only local files may be dropped... + if (!url.isLocalFile()) + continue; + skinFiles << url.toLocalFile(); + } + installSkins(skinFiles); + return true; + } + return false; +} + +Qt::ItemFlags SkinList::flags(const QModelIndex& index) const +{ + Qt::ItemFlags f = Qt::ItemIsDropEnabled | QAbstractListModel::flags(index); + if (index.isValid()) { + f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); + } + return f; +} + +QVariant SkinList::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + + if (row < 0 || row >= m_skin_list.size()) + return QVariant(); + auto skin = m_skin_list[row]; + switch (role) { + case Qt::DecorationRole: + return skin.getTexture(); + case Qt::DisplayRole: + return skin.name(); + case Qt::UserRole: + return skin.name(); + case Qt::EditRole: + return skin.name(); + default: + return QVariant(); + } +} + +int SkinList::rowCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : m_skin_list.size(); +} + +void SkinList::installSkins(const QStringList& iconFiles) +{ + for (QString file : iconFiles) + installSkin(file); +} + +QString SkinList::installSkin(const QString& file, const QString& name) +{ + if (file.isEmpty()) + return tr("Path is empty."); + QFileInfo fileinfo(file); + if (!fileinfo.exists()) + return tr("File doesn't exist."); + if (!fileinfo.isFile()) + return tr("Not a file."); + if (!fileinfo.isReadable()) + return tr("File is not readable."); + if (fileinfo.suffix() != "png" && !SkinModel(fileinfo.absoluteFilePath()).isValid()) + return tr("Skin images must be 64x64 or 64x32 pixel PNG files."); + + QString target = FS::PathCombine(m_dir.absolutePath(), name.isEmpty() ? fileinfo.fileName() : name); + + return QFile::copy(file, target) ? "" : tr("Unable to copy file"); +} + +int SkinList::getSkinIndex(const QString& key) const +{ + for (int i = 0; i < m_skin_list.count(); i++) { + if (m_skin_list[i].name() == key) { + return i; + } + } + return -1; +} + +const SkinModel* SkinList::skin(const QString& key) const +{ + int idx = getSkinIndex(key); + if (idx == -1) + return nullptr; + return &m_skin_list[idx]; +} + +SkinModel* SkinList::skin(const QString& key) +{ + int idx = getSkinIndex(key); + if (idx == -1) + return nullptr; + return &m_skin_list[idx]; +} + +bool SkinList::deleteSkin(const QString& key, const bool trash) +{ + int idx = getSkinIndex(key); + if (idx != -1) { + auto s = m_skin_list[idx]; + if (trash) { + if (FS::trash(s.getPath(), nullptr)) { + m_skin_list.remove(idx); + save(); + return true; + } + } else if (QFile::remove(s.getPath())) { + m_skin_list.remove(idx); + save(); + return true; + } + } + return false; +} + +void SkinList::save() +{ + QJsonObject doc; + QJsonArray arr; + for (auto s : m_skin_list) { + arr << s.toJSON(); + } + doc["skins"] = arr; + 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() +{ + const auto& skin = m_acct->accountData()->minecraftProfile.skin; + for (int i = 0; i < m_skin_list.count(); i++) { + if (m_skin_list[i].getURL() == skin.url) { + return i; + } + } + return -1; +} + +bool SkinList::setData(const QModelIndex& idx, const QVariant& value, int role) +{ + if (!idx.isValid() || role != Qt::EditRole) { + return false; + } + + int row = idx.row(); + if (row < 0 || row >= m_skin_list.size()) + return false; + auto& skin = m_skin_list[row]; + auto newName = value.toString(); + if (skin.name() != newName) { + skin.rename(newName); + save(); + } + return true; +} + +void SkinList::updateSkin(SkinModel* s) +{ + auto done = false; + for (auto i = 0; i < m_skin_list.size(); i++) { + if (m_skin_list[i].getPath() == s->getPath()) { + m_skin_list[i].setCapeId(s->getCapeId()); + m_skin_list[i].setModel(s->getModel()); + m_skin_list[i].setURL(s->getURL()); + done = true; + break; + } + } + if (!done) { + beginInsertRows(QModelIndex(), m_skin_list.count(), m_skin_list.count() + 1); + m_skin_list.append(*s); + endInsertRows(); + } + save(); +} diff --git a/launcher/minecraft/skins/SkinList.h b/launcher/minecraft/skins/SkinList.h new file mode 100644 index 000000000..66af6a17b --- /dev/null +++ b/launcher/minecraft/skins/SkinList.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include + +#include "QObjectPtr.h" +#include "SkinModel.h" +#include "minecraft/auth/MinecraftAccount.h" + +class SkinList : public QAbstractListModel { + Q_OBJECT + public: + explicit SkinList(QObject* parent, QString path, MinecraftAccountPtr acct); + virtual ~SkinList() { save(); }; + + int getSkinIndex(const QString& key) const; + + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex& idx, const QVariant& value, int role) override; + virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; + + virtual QStringList mimeTypes() const override; + virtual Qt::DropActions supportedDropActions() const override; + virtual bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; + virtual Qt::ItemFlags flags(const QModelIndex& index) const override; + + bool deleteSkin(const QString& key, const bool trash); + + void installSkins(const QStringList& iconFiles); + QString installSkin(const QString& file, const QString& name = {}); + + const SkinModel* skin(const QString& key) const; + SkinModel* skin(const QString& key); + + void startWatching(); + void stopWatching(); + + QString getDir() const { return m_dir.absolutePath(); } + void save(); + int getSelectedAccountSkin(); + + void updateSkin(SkinModel* s); + + private: + // hide copy constructor + SkinList(const SkinList&) = delete; + // hide assign op + SkinList& operator=(const SkinList&) = delete; + + protected slots: + void directoryChanged(const QString& path); + void fileChanged(const QString& path); + bool update(); + + private: + shared_qobject_ptr m_watcher; + bool is_watching; + QVector m_skin_list; + QDir m_dir; + MinecraftAccountPtr m_acct; +}; \ No newline at end of file diff --git a/launcher/minecraft/skins/SkinModel.cpp b/launcher/minecraft/skins/SkinModel.cpp new file mode 100644 index 000000000..937864e2c --- /dev/null +++ b/launcher/minecraft/skins/SkinModel.cpp @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "SkinModel.h" +#include +#include +#include +#include + +#include "FileSystem.h" +#include "Json.h" + +SkinModel::SkinModel(QString path) : m_path(path), m_texture(path), m_model(Model::CLASSIC) {} + +SkinModel::SkinModel(QDir skinDir, QJsonObject obj) + : m_cape_id(Json::ensureString(obj, "capeId")), m_model(Model::CLASSIC), m_url(Json::ensureString(obj, "url")) +{ + auto name = Json::ensureString(obj, "name"); + + if (auto model = Json::ensureString(obj, "model"); model == "SLIM") { + m_model = Model::SLIM; + } + m_path = skinDir.absoluteFilePath(name) + ".png"; + m_texture = QPixmap(m_path); +} + +QString SkinModel::name() const +{ + return QFileInfo(m_path).completeBaseName(); +} + +bool SkinModel::rename(QString newName) +{ + auto info = QFileInfo(m_path); + m_path = FS::PathCombine(info.absolutePath(), newName + ".png"); + return FS::move(info.absoluteFilePath(), m_path); +} + +QJsonObject SkinModel::toJSON() const +{ + QJsonObject obj; + obj["name"] = name(); + obj["capeId"] = m_cape_id; + obj["url"] = m_url; + obj["model"] = getModelString(); + return obj; +} + +QString SkinModel::getModelString() const +{ + switch (m_model) { + case CLASSIC: + return "CLASSIC"; + case SLIM: + return "SLIM"; + } + return {}; +} + +bool SkinModel::isValid() const +{ + return !m_texture.isNull() && (m_texture.size().height() == 32 || m_texture.size().height() == 64) && m_texture.size().width() == 64; +} diff --git a/launcher/minecraft/skins/SkinModel.h b/launcher/minecraft/skins/SkinModel.h new file mode 100644 index 000000000..46e9d6cf1 --- /dev/null +++ b/launcher/minecraft/skins/SkinModel.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include + +class SkinModel { + public: + enum Model { CLASSIC, SLIM }; + + SkinModel() = default; + SkinModel(QString path); + SkinModel(QDir skinDir, QJsonObject obj); + virtual ~SkinModel() = default; + + QString name() const; + QString getModelString() const; + bool isValid() const; + QString getPath() const { return m_path; } + QPixmap getTexture() const { return m_texture; } + QString getCapeId() const { return m_cape_id; } + Model getModel() const { return m_model; } + QString getURL() const { return m_url; } + + bool rename(QString newName); + void setCapeId(QString capeID) { m_cape_id = capeID; } + void setModel(Model model) { m_model = model; } + void setURL(QString url) { m_url = url; } + void refresh() { m_texture = QPixmap(m_path); } + + QJsonObject toJSON() const; + + private: + QString m_path; + QPixmap m_texture; + QString m_cape_id; + Model m_model; + QString m_url; +}; \ No newline at end of file diff --git a/launcher/minecraft/skins/SkinUpload.cpp b/launcher/minecraft/skins/SkinUpload.cpp new file mode 100644 index 000000000..ccc29d281 --- /dev/null +++ b/launcher/minecraft/skins/SkinUpload.cpp @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "SkinUpload.h" + +#include + +#include "FileSystem.h" +#include "net/ByteArraySink.h" +#include "net/RawHeaderProxy.h" + +SkinUpload::SkinUpload(QString path, QString variant) : NetRequest(), m_path(path), m_variant(variant) +{ + logCat = taskMCSkinsLogC; +} + +QNetworkReply* SkinUpload::getReply(QNetworkRequest& request) +{ + QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType, this); + + QHttpPart skin; + skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png")); + skin.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\"; filename=\"skin.png\"")); + + skin.setBody(FS::read(m_path)); + + QHttpPart model; + model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"variant\"")); + model.setBody(m_variant.toUtf8()); + + multiPart->append(skin); + multiPart->append(model); + setStatus(tr("Uploading skin")); + return m_network->post(request, multiPart); +} + +SkinUpload::Ptr SkinUpload::make(QString token, QString path, QString variant) +{ + auto up = makeShared(path, variant); + up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/skins"); + up->setObjectName(QString("BYTES:") + up->m_url.toString()); + up->m_sink.reset(new Net::ByteArraySink(std::make_shared())); + up->addHeaderProxy(new Net::RawHeaderProxy(QList{ + { "Authorization", QString("Bearer %1").arg(token).toLocal8Bit() }, + })); + return up; +} diff --git a/launcher/minecraft/skins/SkinUpload.h b/launcher/minecraft/skins/SkinUpload.h new file mode 100644 index 000000000..c1a4930b7 --- /dev/null +++ b/launcher/minecraft/skins/SkinUpload.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "net/NetRequest.h" + +class SkinUpload : public Net::NetRequest { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + + // Note this class takes ownership of the file. + SkinUpload(QString path, QString variant); + virtual ~SkinUpload() = default; + + static SkinUpload::Ptr make(QString token, QString path, QString variant); + + protected: + virtual QNetworkReply* getReply(QNetworkRequest&) override; + + private: + QString m_path; + QString m_variant; +}; diff --git a/launcher/minecraft/update/AssetUpdateTask.cpp b/launcher/minecraft/update/AssetUpdateTask.cpp index 8af014996..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...")); @@ -32,8 +31,7 @@ void AssetUpdateTask::executeTask() auto hexSha1 = assets->sha1.toLatin1(); qDebug() << "Asset index SHA1:" << hexSha1; auto dl = Net::ApiDownload::makeCached(indexUrl, entry); - auto rawSha1 = QByteArray::fromHex(assets->sha1.toLatin1()); - dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, assets->sha1)); job->addNetAction(dl); downloadJob.reset(job); 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 9d0102be7..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 2d2954b2a..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 c969e74df..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 04481d0e3..1ffcc97ce 100644 --- a/launcher/modplatform/CheckUpdateTask.h +++ b/launcher/modplatform/CheckUpdateTask.h @@ -15,9 +15,13 @@ class CheckUpdateTask : public Task { public: CheckUpdateTask(QList& resources, std::list& mcVersions, - std::optional loaders, - std::shared_ptr resource_model) - : Task(nullptr), m_resources(resources), m_game_versions(mcVersions), m_loaders(loaders), m_resource_model(resource_model){}; + QList loadersList, + std::shared_ptr resourceModel) + : Task(nullptr) + , m_resources(resources) + , m_game_versions(mcVersions) + , m_loaders_list(std::move(loadersList)) + , m_resource_model(resourceModel){}; struct Update { QString name; @@ -67,7 +71,7 @@ class CheckUpdateTask : public Task { protected: QList& m_resources; std::list& m_game_versions; - std::optional m_loaders; + QList m_loaders_list; std::shared_ptr m_resource_model; std::vector m_updates; diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp index 277cd0764..ec3538010 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -43,6 +43,10 @@ EnsureMetadataTask::EnsureMetadataTask(QList& resources, QDir dir, Mo } } +EnsureMetadataTask::EnsureMetadataTask(QHash& resources, QDir dir, ModPlatform::ResourceProvider prov) + : Task(nullptr), m_resources(resources), m_index_dir(dir), m_provider(prov), m_current_task(nullptr) +{} + Hashing::Hasher::Ptr EnsureMetadataTask::createNewHash(Resource* resource) { if (!resource || !resource->valid() || resource->type() == ResourceType::FOLDER) diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h index d82d9c26e..a78ffc0c5 100644 --- a/launcher/modplatform/EnsureMetadataTask.h +++ b/launcher/modplatform/EnsureMetadataTask.h @@ -17,6 +17,7 @@ class EnsureMetadataTask : public Task { public: EnsureMetadataTask(Resource*, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); EnsureMetadataTask(QList&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); + EnsureMetadataTask(QHash&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); ~EnsureMetadataTask() = default; diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index fc79dff15..8c85ae122 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln + * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -58,7 +59,7 @@ IndexedVersionType::VersionType IndexedVersionType::enumFromString(const QString return s_indexed_version_type_names.value(type, IndexedVersionType::VersionType::Unknown); } -auto ProviderCapabilities::name(ResourceProvider p) -> const char* +const char* ProviderCapabilities::name(ResourceProvider p) { switch (p) { case ResourceProvider::MODRINTH: @@ -68,7 +69,8 @@ auto ProviderCapabilities::name(ResourceProvider p) -> const char* } return {}; } -auto ProviderCapabilities::readableName(ResourceProvider p) -> QString + +QString ProviderCapabilities::readableName(ResourceProvider p) { switch (p) { case ResourceProvider::MODRINTH: @@ -78,7 +80,8 @@ auto ProviderCapabilities::readableName(ResourceProvider p) -> QString } return {}; } -auto ProviderCapabilities::hashType(ResourceProvider p) -> QStringList + +QStringList ProviderCapabilities::hashType(ResourceProvider p) { switch (p) { case ResourceProvider::MODRINTH: @@ -90,34 +93,13 @@ auto ProviderCapabilities::hashType(ResourceProvider p) -> QStringList return {}; } -auto ProviderCapabilities::hash(ResourceProvider p, QIODevice* device, QString type) -> QString -{ - QCryptographicHash::Algorithm algo = QCryptographicHash::Sha1; - switch (p) { - case ResourceProvider::MODRINTH: { - algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Sha512; - break; - } - case ResourceProvider::FLAME: - algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Md5; - break; - } - - QCryptographicHash hash(algo); - if (!hash.addData(device)) - qCritical() << "Failed to read JAR to create hash!"; - - Q_ASSERT(hash.result().length() == hash.hashLength(algo)); - return { hash.result().toHex() }; -} - QString getMetaURL(ResourceProvider provider, QVariant projectID) { return ((provider == ModPlatform::ResourceProvider::FLAME) ? "https://www.curseforge.com/projects/" : "https://modrinth.com/mod/") + projectID.toString(); } -auto getModLoaderString(ModLoaderType type) -> const QString +auto getModLoaderAsString(ModLoaderType type) -> const QString { switch (type) { case NeoForge: @@ -138,4 +120,21 @@ auto getModLoaderString(ModLoaderType type) -> const QString return ""; } +auto getModLoaderFromString(QString type) -> ModLoaderType +{ + if (type == "neoforge") + return NeoForge; + if (type == "forge") + return Forge; + if (type == "cauldron") + return Cauldron; + if (type == "liteloader") + return LiteLoader; + if (type == "fabric") + return Fabric; + if (type == "quilt") + return Quilt; + return {}; +} + } // namespace ModPlatform diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index aeae87235..b3f86933f 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -41,11 +41,10 @@ enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK }; enum class DependencyType { REQUIRED, OPTIONAL, INCOMPATIBLE, EMBEDDED, TOOL, INCLUDE, UNKNOWN }; namespace ProviderCapabilities { - auto name(ResourceProvider) -> const char*; - auto readableName(ResourceProvider) -> QString; - auto hashType(ResourceProvider) -> QStringList; - auto hash(ResourceProvider, QIODevice*, QString type = "") -> QString; -}; +const char* name(ResourceProvider); +QString readableName(ResourceProvider); +QStringList hashType(ResourceProvider); +} // namespace ProviderCapabilities struct ModpackAuthor { QString name; @@ -108,6 +107,7 @@ struct IndexedVersion { bool is_preferred = true; QString changelog; QList dependencies; + QString side; // this is for flame API // For internal use, not provided by APIs bool is_currently_selected = false; @@ -182,7 +182,8 @@ inline auto getOverrideDeps() -> QList QString getMetaURL(ResourceProvider provider, QVariant projectID); -auto getModLoaderString(ModLoaderType type) -> const QString; +auto getModLoaderAsString(ModLoaderType type) -> const QString; +auto getModLoaderFromString(QString type) -> ModLoaderType; constexpr bool hasSingleModLoaderSelected(ModLoaderTypes l) noexcept { @@ -190,6 +191,11 @@ constexpr bool hasSingleModLoaderSelected(ModLoaderTypes l) noexcept return x && !(x & (x - 1)); } +struct Category { + QString name; + QString id; +}; + } // namespace ModPlatform Q_DECLARE_METATYPE(ModPlatform::IndexedPack) diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h index 7b787c6a0..8feb3fd82 100644 --- a/launcher/modplatform/ResourceAPI.h +++ b/launcher/modplatform/ResourceAPI.h @@ -4,6 +4,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -73,6 +74,8 @@ class ResourceAPI { std::optional sorting; std::optional loaders; std::optional > versions; + std::optional side; + std::optional categoryIds; }; struct SearchCallbacks { std::function on_succeed; diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 8ae8145de..abe7d0177 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -282,7 +282,7 @@ void PackInstallTask::deleteExistingFiles() // Delete the files for (const auto& item : filesToDelete) { - QFile::remove(item); + FS::deletePath(item); } } @@ -343,9 +343,7 @@ QString PackInstallTask::getVersionForLoader(QString uid) return Q_NULLPTR; } - if (!vlist->isLoaded()) { - vlist->load(Net::Mode::Online); - } + vlist->waitToLoad(); if (m_version.loader.recommended || m_version.loader.latest) { for (int i = 0; i < vlist->versions().size(); i++) { @@ -638,8 +636,7 @@ void PackInstallTask::installConfigs() auto dl = Net::ApiDownload::makeCached(url, entry); if (!m_version.configs.sha1.isEmpty()) { - auto rawSha1 = QByteArray::fromHex(m_version.configs.sha1.toLatin1()); - dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, m_version.configs.sha1)); } jobPtr->addNetAction(dl); archivePath = entry->getFullPath(); @@ -758,8 +755,7 @@ void PackInstallTask::downloadMods() auto dl = Net::ApiDownload::makeCached(url, entry); if (!mod.md5.isEmpty()) { - auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); - dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5)); } jobPtr->addNetAction(dl); } else if (mod.type == ModType::Decomp) { @@ -769,8 +765,7 @@ void PackInstallTask::downloadMods() auto dl = Net::ApiDownload::makeCached(url, entry); if (!mod.md5.isEmpty()) { - auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); - dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5)); } jobPtr->addNetAction(dl); } else { @@ -783,8 +778,7 @@ void PackInstallTask::downloadMods() auto dl = Net::ApiDownload::makeCached(url, entry); if (!mod.md5.isEmpty()) { - auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); - dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5)); } jobPtr->addNetAction(dl); @@ -987,7 +981,7 @@ bool PackInstallTask::extractMods(const QMap& toExtract, // the copy from the Configs.zip QFileInfo fileInfo(to); if (fileInfo.exists()) { - if (!QFile::remove(to)) { + if (!FS::deletePath(to)) { qWarning() << "Failed to delete" << to; return false; } @@ -1031,6 +1025,12 @@ void PackInstallTask::install() return; components->setComponentVersion("net.minecraftforge", version); + } else if (m_version.loader.type == QString("neoforge")) { + auto version = getVersionForLoader("net.neoforged"); + if (version == Q_NULLPTR) + return; + + components->setComponentVersion("net.neoforged", version); } else if (m_version.loader.type == QString("fabric")) { auto version = getVersionForLoader("net.fabricmc.fabric-loader"); if (version == Q_NULLPTR) @@ -1069,36 +1069,7 @@ void PackInstallTask::install() static Meta::Version::Ptr getComponentVersion(const QString& uid, const QString& version) { - auto vlist = APPLICATION->metadataIndex()->get(uid); - if (!vlist) - return {}; - - if (!vlist->isLoaded()) { - QEventLoop loadVersionLoop; - auto task = vlist->getLoadTask(); - QObject::connect(task.get(), &Task::finished, &loadVersionLoop, &QEventLoop::quit); - if (!task->isRunning()) - task->start(); - - loadVersionLoop.exec(); - } - - auto ver = vlist->getVersion(version); - if (!ver) - return {}; - - if (!ver->isLoaded()) { - QEventLoop loadVersionLoop; - ver->load(Net::Mode::Online); - auto task = ver->getCurrentTask(); - QObject::connect(task.get(), &Task::finished, &loadVersionLoop, &QEventLoop::quit); - if (!task->isRunning()) - task->start(); - - loadVersionLoop.exec(); - } - - return ver; + return APPLICATION->metadataIndex()->getLoadedVersion(uid, version); } } // namespace ATLauncher diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index 8d23896d9..4c2f3d69e 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -1,86 +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; + m_result.reset(new QByteArray()); - 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); + 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)); - 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"; @@ -91,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(), &Net::ApiDownload::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; + + failed(parse_error.errorString()); + 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(); - - for (auto it = blockedProjects.keyBegin(); it != blockedProjects.keyEnd(); it++) { - auto& out = *it; - auto bytes = blockedProjects[out]; - if (!out->resolved) { - continue; - } - - 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; - } + m_result.reset(new QByteArray()); + QStringList addonIds; + for (auto file : m_manifest.files) { + addonIds.push_back(QString::number(file.projectId)); } - // 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(), &Net::ApiDownload::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++; - } - 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 { + m_task = flameAPI.getProjects(addonIds, m_result); + + 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; + } + + 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; + } + 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 c280827af..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 bb4f18983..72437976d 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -3,14 +3,16 @@ // SPDX-License-Identifier: GPL-3.0-only #include "FlameAPI.h" +#include +#include #include "FlameModIndex.h" #include "Application.h" #include "Json.h" +#include "modplatform/ModIndex.h" #include "net/ApiDownload.h" #include "net/ApiUpload.h" #include "net/NetJob.h" -#include "net/Upload.h" Task::Ptr FlameAPI::matchFingerprints(const QList& fingerprints, std::shared_ptr response) { @@ -32,7 +34,7 @@ Task::Ptr FlameAPI::matchFingerprints(const QList& fingerprints, std::shar return netJob; } -auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString +QString FlameAPI::getModFileChangelog(int modId, int fileId) { QEventLoop lock; QString changelog; @@ -67,7 +69,7 @@ auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString return changelog; } -auto FlameAPI::getModDescription(int modId) -> QString +QString FlameAPI::getModDescription(int modId) { QEventLoop lock; QString description; @@ -100,7 +102,7 @@ auto FlameAPI::getModDescription(int modId) -> QString return description; } -auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion +QList FlameAPI::getLatestVersions(VersionSearchArgs&& args) { auto versions_url_optional = getVersionsURL(args); if (!versions_url_optional.has_value()) @@ -112,7 +114,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe auto netJob = makeShared(QString("Flame::GetLatestVersion(%1)").arg(args.pack.name), APPLICATION->network()); auto response = std::make_shared(); - ModPlatform::IndexedVersion ver; + QList ver; netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response)); @@ -132,9 +134,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe for (auto file : arr) { auto file_obj = Json::requireObject(file); - auto file_tmp = FlameMod::loadIndexedPackVersion(file_obj); - if (file_tmp.date > ver.date && (!args.loaders.has_value() || !file_tmp.loaders || args.loaders.value() & file_tmp.loaders)) - ver = file_tmp; + ver.append(FlameMod::loadIndexedPackVersion(file_obj)); } } catch (Json::JsonException& e) { @@ -144,7 +144,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe } }); - QObject::connect(netJob.get(), &NetJob::finished, [&loop] { loop.quit(); }); + QObject::connect(netJob.get(), &NetJob::finished, &loop, &QEventLoop::quit); netJob->start(); @@ -220,3 +220,65 @@ QList FlameAPI::getSortingMethods() const { 7, "Category", QObject::tr("Sort by Category") }, { 8, "GameVersion", QObject::tr("Sort by Game Version") } }; } + +Task::Ptr FlameAPI::getModCategories(std::shared_ptr response) +{ + 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)); + QObject::connect(netJob.get(), &Task::failed, [](QString msg) { qDebug() << "Flame failed to get categories:" << msg; }); + return netJob; +} + +QList FlameAPI::loadModCategories(std::shared_ptr response) +{ + QList categories; + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from categories at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return categories; + } + + try { + auto obj = Json::requireObject(doc); + auto arr = Json::requireArray(obj, "data"); + + for (auto val : arr) { + auto cat = Json::requireObject(val); + auto id = Json::requireInteger(cat, "id"); + auto name = Json::requireString(cat, "name"); + categories.push_back({ name, QString::number(id) }); + } + + } catch (Json::JsonException& e) { + qCritical() << "Failed to parse response from a version request."; + qCritical() << e.what(); + qDebug() << doc; + } + return categories; +}; + +std::optional FlameAPI::getLatestVersion(QList versions, + 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; + } + } + return ver; + }; + for (auto l : instanceLoaders) { + auto ver = bestVersion(l); + if (ver.has_value()) { + return ver; + } + } + return bestVersion(modLoaders); +} diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index e22d8f0d8..1160151c5 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -4,7 +4,7 @@ #pragma once -#include +#include #include #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" @@ -12,19 +12,25 @@ class FlameAPI : public NetworkResourceAPI { public: - auto getModFileChangelog(int modId, int fileId) -> QString; - auto getModDescription(int modId) -> QString; + QString getModFileChangelog(int modId, int fileId); + QString getModDescription(int modId); - auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion; + QList getLatestVersions(VersionSearchArgs&& args); + std::optional getLatestVersion(QList versions, + QList instanceLoaders, + ModPlatform::ModLoaderTypes fallback); Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const override; Task::Ptr matchFingerprints(const QList& fingerprints, std::shared_ptr response); Task::Ptr getFiles(const QStringList& fileIds, std::shared_ptr response) const; Task::Ptr getFile(const QString& addonId, const QString& fileId, std::shared_ptr response) const; - [[nodiscard]] auto getSortingMethods() const -> QList override; + static Task::Ptr getModCategories(std::shared_ptr response); + static QList loadModCategories(std::shared_ptr response); - static inline auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool + [[nodiscard]] QList getSortingMethods() const override; + + static inline bool validateModLoaders(ModPlatform::ModLoaderTypes loaders) { return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt); } @@ -63,7 +69,7 @@ class FlameAPI : public NetworkResourceAPI { return 0; } - static auto getModLoaderStrings(const ModPlatform::ModLoaderTypes types) -> const QStringList + static const QStringList getModLoaderStrings(const ModPlatform::ModLoaderTypes types) { QStringList l; for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt }) { @@ -74,10 +80,7 @@ class FlameAPI : public NetworkResourceAPI { return l; } - static auto getModLoaderFilters(ModPlatform::ModLoaderTypes types) -> const QString - { - return "[" + getModLoaderStrings(types).join(',') + "]"; - } + static const QString getModLoaderFilters(ModPlatform::ModLoaderTypes types) { return "[" + getModLoaderStrings(types).join(',') + "]"; } private: [[nodiscard]] std::optional getSearchURL(SearchArgs const& args) const override @@ -96,6 +99,9 @@ class FlameAPI : public NetworkResourceAPI { get_arguments.append("sortOrder=desc"); if (args.loaders.has_value()) 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); return "https://api.curseforge.com/v1/mods/search?gameId=432&" + get_arguments.join('&'); diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp index 9a3249bc7..2b469276d 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.cpp +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -1,4 +1,5 @@ #include "FlameCheckUpdate.h" +#include "Application.h" #include "FlameAPI.h" #include "FlameModIndex.h" @@ -124,25 +125,21 @@ void FlameCheckUpdate::executeTask() int i = 0; for (auto* resource : m_resources) { - if (!resource->enabled()) { - emit checkFailed(resource, tr("Disabled resources won't be updated, to prevent resource duplication issues!")); - continue; - } - setStatus(tr("Getting API response from CurseForge for '%1'...").arg(resource->name())); setProgress(i++, m_resources.size()); - auto latest_ver = api.getLatestVersion({ { resource->metadata()->project_id.toString() }, m_game_versions, m_loaders }); + auto latest_vers = api.getLatestVersions({ { resource->metadata()->project_id.toString() }, m_game_versions }); // Check if we were aborted while getting the latest version if (m_was_aborted) { aborted(); return; } + auto latest_ver = api.getLatestVersion(latest_vers, m_loaders_list, resource->metadata()->loaders); setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(resource->name())); - if (!latest_ver.addonId.isValid()) { + if (!latest_ver.has_value() || !latest_ver->addonId.isValid()) { QString reason; if (dynamic_cast(resource) != nullptr) reason = @@ -155,9 +152,9 @@ void FlameCheckUpdate::executeTask() continue; } - if (latest_ver.downloadUrl.isEmpty() && latest_ver.fileId != resource->metadata()->file_id) { - auto pack = getProjectInfo(latest_ver); - auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, latest_ver.fileId.toString()); + if (latest_ver->downloadUrl.isEmpty() && latest_ver->fileId != resource->metadata()->file_id) { + auto pack = getProjectInfo(latest_ver.value()); + auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, latest_ver->fileId.toString()); emit checkFailed(resource, tr("Resource has a new update available, but is not downloadable using CurseForge."), recover_url); continue; @@ -169,11 +166,9 @@ void FlameCheckUpdate::executeTask() pack->slug = resource->metadata()->slug; pack->addonId = resource->metadata()->project_id; pack->provider = ModPlatform::ResourceProvider::FLAME; - if (!latest_ver.hash.isEmpty() && - (resource->metadata()->hash != latest_ver.hash || resource->status() == ResourceStatus::NOT_INSTALLED)) { - auto download_task = makeShared(pack, latest_ver, m_resource_model); - - QString old_version = resource->metadata()->version_number; + if (!latest_ver->hash.isEmpty() && + (resource->metadata()->hash != latest_ver->hash || resource->status() == ResourceStatus::NOT_INSTALLED)) { + auto old_version = resource->metadata()->version_number; if (old_version.isEmpty()) { if (resource->status() == ResourceStatus::NOT_INSTALLED) old_version = tr("Not installed"); @@ -181,11 +176,12 @@ void FlameCheckUpdate::executeTask() old_version = tr("Unknown"); } - m_updates.emplace_back(pack->name, resource->metadata()->hash, old_version, latest_ver.version, latest_ver.version_type, - api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()), - ModPlatform::ResourceProvider::FLAME, download_task); + auto download_task = makeShared(pack, latest_ver.value(), m_resource_model); + m_updates.emplace_back(pack->name, resource->metadata()->hash, old_version, latest_ver->version, latest_ver->version_type, + api.getModFileChangelog(latest_ver->addonId.toInt(), latest_ver->fileId.toInt()), + ModPlatform::ResourceProvider::FLAME, download_task, resource->enabled()); } - m_deps.append(std::make_shared(pack, latest_ver)); + m_deps.append(std::make_shared(pack, latest_ver.value())); } emitSucceeded(); diff --git a/launcher/modplatform/flame/FlameCheckUpdate.h b/launcher/modplatform/flame/FlameCheckUpdate.h index 9ae944153..0094bb13a 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.h +++ b/launcher/modplatform/flame/FlameCheckUpdate.h @@ -10,9 +10,9 @@ class FlameCheckUpdate : public CheckUpdateTask { public: FlameCheckUpdate(QList& resources, std::list& mcVersions, - std::optional loaders, - std::shared_ptr resource_model) - : CheckUpdateTask(resources, mcVersions, loaders, resource_model) + QList loadersList, + std::shared_ptr resourceModel) + : CheckUpdateTask(resources, mcVersions, loadersList, resourceModel) {} public slots: diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index ef552c3c2..7007a8c84 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/LocalResourceUpdateTask.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)); } @@ -322,7 +324,7 @@ bool FlameCreationTask::createInstance() // Keep index file in case we need it some other time (like when changing versions) QString new_index_place(FS::PathCombine(parent_folder, "manifest.json")); FS::ensureFilePathExists(new_index_place); - QFile::rename(index_path, new_index_place); + FS::move(index_path, new_index_place); } catch (const JSONValidationError& e) { setError(tr("Could not understand pack manifest:\n") + e.cause()); @@ -336,7 +338,7 @@ bool FlameCreationTask::createInstance() Override::createOverrides("overrides", parent_folder, overridePath); QString mcPath = FS::PathCombine(m_stagingPath, "minecraft"); - if (!QFile::rename(overridePath, mcPath)) { + if (!FS::move(overridePath, mcPath)) { setError(tr("Could not rename the overrides folder:\n") + m_pack.overrides); return false; } @@ -471,15 +473,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 +523,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 +539,10 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop) selectedOptionalMods = optionalModDialog.getResult(); } for (const auto& result : results) { - auto relpath = FS::PathCombine(result.targetFolder, result.fileName); + auto fileName = result.version.fileName; + fileName = FS::RemoveInvalidPathChars(fileName); + auto relpath = FS::PathCombine(result.targetFolder, fileName); + if (!result.required && !selectedOptionalMods.contains(relpath)) { relpath += ".disabled"; } @@ -545,36 +550,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(); @@ -585,7 +570,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(); @@ -623,9 +607,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); @@ -665,6 +650,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"); @@ -690,4 +676,17 @@ void FlameCreationTask::validateZIPResources() break; } } + // TODO make this work with other sorts of resource + auto task = makeShared(this, "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/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 16cbbade4..7de05f177 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -1,5 +1,6 @@ #include "FlameModIndex.h" +#include "FileSystem.h" #include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" @@ -19,6 +20,9 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) QJsonObject logo = Json::ensureObject(obj, "logo"); pack.logoName = Json::ensureString(logo, "title"); pack.logoUrl = Json::ensureString(logo, "thumbnailUrl"); + if (pack.logoUrl.isEmpty()) { + pack.logoUrl = Json::ensureString(logo, "url"); + } auto authors = Json::ensureArray(obj, "authors"); for (auto authorIter : authors) { @@ -78,10 +82,6 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, const BaseInstance* inst) { QVector unsortedVersions; - auto profile = (dynamic_cast(inst))->getPackProfile(); - QString mcVersion = profile->getComponentVersion("net.minecraft"); - auto loaders = profile->getSupportedModLoaders(); - for (auto versionIter : arr) { auto obj = versionIter.toObject(); @@ -89,8 +89,7 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, if (!file.addonId.isValid()) file.addonId = pack.addonId; - if (file.fileId.isValid() && - (!loaders.has_value() || !file.loaders || loaders.value() & file.loaders)) // Heuristic to check if the returned value is valid + if (file.fileId.isValid()) // Heuristic to check if the returned value is valid unsortedVersions.append(file); } @@ -116,19 +115,25 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> if (str.contains('.')) file.mcVersion.append(str); - auto loader = str.toLower(); - if (loader == "neoforge") + + if (auto loader = str.toLower(); loader == "neoforge") file.loaders |= ModPlatform::NeoForge; - if (loader == "forge") + else if (loader == "forge") file.loaders |= ModPlatform::Forge; - if (loader == "cauldron") + else if (loader == "cauldron") file.loaders |= ModPlatform::Cauldron; - if (loader == "liteloader") + else if (loader == "liteloader") file.loaders |= ModPlatform::LiteLoader; - if (loader == "fabric") + else if (loader == "fabric") file.loaders |= ModPlatform::Fabric; - if (loader == "quilt") + else if (loader == "quilt") file.loaders |= ModPlatform::Quilt; + else if (loader == "server" || loader == "client") { + if (file.side.isEmpty()) + file.side = loader; + else if (file.side != loader) + file.side = "both"; + } } file.addonId = Json::requireInteger(obj, "modId"); @@ -137,6 +142,7 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> file.version = Json::requireString(obj, "displayName"); file.downloadUrl = Json::ensureString(obj, "downloadUrl"); file.fileName = Json::requireString(obj, "fileName"); + file.fileName = FS::RemoveInvalidPathChars(file.fileName); ModPlatform::IndexedVersionType::VersionType ver_type; switch (Json::requireInteger(obj, "releaseType")) { diff --git a/launcher/modplatform/flame/FlamePackExportTask.cpp b/launcher/modplatform/flame/FlamePackExportTask.cpp index 3a2028fd1..d661f1f05 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.cpp +++ b/launcher/modplatform/flame/FlamePackExportTask.cpp @@ -116,7 +116,7 @@ void FlamePackExportTask::collectHashes() if (relative.startsWith("resourcepacks/") && (relative.endsWith(".zip") || relative.endsWith(".zip.disabled"))) { // is resourcepack - auto hashTask = Hashing::createFlameHasher(file.absoluteFilePath()); + auto hashTask = Hashing::createHasher(file.absoluteFilePath(), ModPlatform::ResourceProvider::FLAME); connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, relative, file](QString hash) { if (m_state == Task::State::Running) { pendingHashes.insert(hash, { relative, file.absoluteFilePath(), relative.endsWith(".zip") }); @@ -140,7 +140,7 @@ void FlamePackExportTask::collectHashes() continue; } - auto hashTask = Hashing::createFlameHasher(mod->fileinfo().absoluteFilePath()); + auto hashTask = Hashing::createHasher(mod->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::FLAME); connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) { if (m_state == Task::State::Running) { pendingHashes.insert(hash, { mod->name(), mod->fileinfo().absoluteFilePath(), mod->enabled(), true }); @@ -201,7 +201,7 @@ void FlamePackExportTask::makeApiRequest() << " reason: " << parseError.errorString(); qWarning() << *response; - failed(parseError.errorString()); + emitFailed(parseError.errorString()); return; } @@ -213,6 +213,7 @@ void FlamePackExportTask::makeApiRequest() if (dataArr.isEmpty()) { qWarning() << "No matches found for fingerprint search!"; + getProjectsInfo(); return; } for (auto match : dataArr) { @@ -243,9 +244,9 @@ void FlamePackExportTask::makeApiRequest() qDebug() << doc; } pendingHashes.clear(); + getProjectsInfo(); }); - connect(task.get(), &Task::finished, this, &FlamePackExportTask::getProjectsInfo); - connect(task.get(), &NetJob::failed, this, &FlamePackExportTask::emitFailed); + connect(task.get(), &Task::failed, this, &FlamePackExportTask::getProjectsInfo); task->start(); } @@ -279,7 +280,7 @@ void FlamePackExportTask::getProjectsInfo() qWarning() << "Error while parsing JSON response from CurseForge projects task at " << parseError.offset << " reason: " << parseError.errorString(); qWarning() << *response; - failed(parseError.errorString()); + emitFailed(parseError.errorString()); return; } @@ -333,7 +334,7 @@ void FlamePackExportTask::buildZip() setStatus(tr("Adding files...")); setProgress(4, 5); - auto zipTask = makeShared(output, gameRoot, files, "overrides/", true); + auto zipTask = makeShared(output, gameRoot, files, "overrides/", true, false); zipTask->addExtraFile("manifest.json", generateIndex()); zipTask->addExtraFile("modlist.html", generateHTML()); 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/ExportToModList.cpp b/launcher/modplatform/helpers/ExportToModList.cpp index 1f01c4a89..aea16ab50 100644 --- a/launcher/modplatform/helpers/ExportToModList.cpp +++ b/launcher/modplatform/helpers/ExportToModList.cpp @@ -42,17 +42,28 @@ QString toHTML(QList mods, OptionalData extraData) } if (extraData & Authors && !mod->authors().isEmpty()) line += " by " + mod->authors().join(", ").toHtmlEscaped(); + if (extraData & FileName) + line += QString(" (%1)").arg(mod->fileinfo().fileName().toHtmlEscaped()); + lines.append(QString("
  • %1
  • ").arg(line)); } return QString("
      \n\t%1\n
    ").arg(lines.join("\n\t")); } +QString toMarkdownEscaped(QString src) +{ + for (auto ch : "\\`*_{}[]<>()#+-.!|") + src.replace(ch, QString("\\%1").arg(ch)); + return src; +} + QString toMarkdown(QList mods, OptionalData extraData) { QStringList lines; + for (auto mod : mods) { auto meta = mod->metadata(); - auto modName = mod->name(); + auto modName = toMarkdownEscaped(mod->name()); if (extraData & Url) { auto url = mod->metaurl(); if (!url.isEmpty()) @@ -60,14 +71,16 @@ QString toMarkdown(QList mods, OptionalData extraData) } auto line = modName; if (extraData & Version) { - auto ver = mod->version(); + auto ver = toMarkdownEscaped(mod->version()); if (ver.isEmpty() && meta != nullptr) - ver = meta->version().toString(); + ver = toMarkdownEscaped(meta->version().toString()); if (!ver.isEmpty()) line += QString(" [%1]").arg(ver); } if (extraData & Authors && !mod->authors().isEmpty()) - line += " by " + mod->authors().join(", "); + line += " by " + toMarkdownEscaped(mod->authors().join(", ")); + if (extraData & FileName) + line += QString(" (%1)").arg(toMarkdownEscaped(mod->fileinfo().fileName())); lines << "- " + line; } return lines.join("\n"); @@ -95,6 +108,8 @@ QString toPlainTXT(QList mods, OptionalData extraData) } if (extraData & Authors && !mod->authors().isEmpty()) line += " by " + mod->authors().join(", "); + if (extraData & FileName) + line += QString(" (%1)").arg(mod->fileinfo().fileName()); lines << line; } return lines.join("\n"); @@ -122,6 +137,8 @@ QString toJSON(QList mods, OptionalData extraData) } if (extraData & Authors && !mod->authors().isEmpty()) line["authors"] = QJsonArray::fromStringList(mod->authors()); + if (extraData & FileName) + line["filename"] = mod->fileinfo().fileName(); lines << line; } QJsonDocument doc; @@ -154,6 +171,8 @@ QString toCSV(QList mods, OptionalData extraData) authors = QString("\"%1\"").arg(mod->authors().join(",")); data << authors; } + if (extraData & FileName) + data << mod->fileinfo().fileName(); lines << data.join(","); } return lines.join("\n"); @@ -189,11 +208,13 @@ QString exportToModList(QList mods, QString lineTemplate) if (ver.isEmpty() && meta != nullptr) ver = meta->version().toString(); auto authors = mod->authors().join(", "); + auto filename = mod->fileinfo().fileName(); lines << QString(lineTemplate) .replace("{name}", modName) .replace("{url}", url) .replace("{version}", ver) - .replace("{authors}", authors); + .replace("{authors}", authors) + .replace("{filename}", filename); } return lines.join("\n"); } diff --git a/launcher/modplatform/helpers/ExportToModList.h b/launcher/modplatform/helpers/ExportToModList.h index 7ea4ba9c2..ab7797fe6 100644 --- a/launcher/modplatform/helpers/ExportToModList.h +++ b/launcher/modplatform/helpers/ExportToModList.h @@ -23,11 +23,7 @@ namespace ExportToModList { enum Formats { HTML, MARKDOWN, PLAINTXT, JSON, CSV, CUSTOM }; -enum OptionalData { - Authors = 1 << 0, - Url = 1 << 1, - Version = 1 << 2, -}; +enum OptionalData { Authors = 1 << 0, Url = 1 << 1, Version = 1 << 2, FileName = 1 << 3 }; QString exportToModList(QList mods, Formats format, OptionalData extraData); QString exportToModList(QList mods, QString lineTemplate); } // namespace ExportToModList diff --git a/launcher/modplatform/helpers/HashUtils.cpp b/launcher/modplatform/helpers/HashUtils.cpp index 19e5b447a..a3b8d904c 100644 --- a/launcher/modplatform/helpers/HashUtils.cpp +++ b/launcher/modplatform/helpers/HashUtils.cpp @@ -1,10 +1,9 @@ #include "HashUtils.h" +#include #include #include - -#include "FileSystem.h" -#include "StringUtils.h" +#include #include @@ -14,129 +13,153 @@ Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provid { switch (provider) { case ModPlatform::ResourceProvider::MODRINTH: - return createModrinthHasher(file_path); + return makeShared(file_path, + ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first()); case ModPlatform::ResourceProvider::FLAME: - return createFlameHasher(file_path); + return makeShared(file_path, Algorithm::Murmur2); default: - qCritical() << "[Hashing]" - << "Unrecognized mod platform!"; + qCritical() << "[Hashing]" << "Unrecognized mod platform!"; return nullptr; } } -Hasher::Ptr createModrinthHasher(QString file_path) +Hasher::Ptr createHasher(QString file_path, QString type) { - return makeShared(file_path); + return makeShared(file_path, type); } -Hasher::Ptr createFlameHasher(QString file_path) +class QIODeviceReader : public Murmur2::Reader { + public: + QIODeviceReader(QIODevice* device) : m_device(device) {} + virtual ~QIODeviceReader() = default; + virtual int read(char* s, int n) { return m_device->read(s, n); } + virtual bool eof() { return m_device->atEnd(); } + virtual void goToBeginning() { m_device->seek(0); } + virtual void close() { m_device->close(); } + + private: + QIODevice* m_device; +}; + +QString algorithmToString(Algorithm type) { - return makeShared(file_path); + switch (type) { + case Algorithm::Md4: + return "md4"; + case Algorithm::Md5: + return "md5"; + case Algorithm::Sha1: + return "sha1"; + case Algorithm::Sha256: + return "sha256"; + case Algorithm::Sha512: + return "sha512"; + case Algorithm::Murmur2: + return "murmur2"; + // case Algorithm::Unknown: + default: + break; + } + return "unknown"; } -Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider) +Algorithm algorithmFromString(QString type) { - return makeShared(file_path, provider); + if (type == "md4") + return Algorithm::Md4; + if (type == "md5") + return Algorithm::Md5; + if (type == "sha1") + return Algorithm::Sha1; + if (type == "sha256") + return Algorithm::Sha256; + if (type == "sha512") + return Algorithm::Sha512; + if (type == "murmur2") + return Algorithm::Murmur2; + return Algorithm::Unknown; } -Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider, QString type) +QString hash(QIODevice* device, Algorithm type) { - auto hasher = makeShared(file_path, provider); - hasher->useHashType(type); - return hasher; -} - -void ModrinthHasher::executeTask() -{ - QFile file(m_path); - - try { - file.open(QFile::ReadOnly); - } catch (FS::FileSystemException& e) { - qCritical() << QString("Failed to open JAR file in %1").arg(m_path); - qCritical() << QString("Reason: ") << e.cause(); - - emitFailed("Failed to open file for hashing."); - return; + if (!device->isOpen() && !device->open(QFile::ReadOnly)) + return ""; + QCryptographicHash::Algorithm alg = QCryptographicHash::Sha1; + switch (type) { + case Algorithm::Md4: + alg = QCryptographicHash::Algorithm::Md4; + break; + case Algorithm::Md5: + alg = QCryptographicHash::Algorithm::Md5; + break; + case Algorithm::Sha1: + alg = QCryptographicHash::Algorithm::Sha1; + break; + case Algorithm::Sha256: + alg = QCryptographicHash::Algorithm::Sha256; + break; + case Algorithm::Sha512: + alg = QCryptographicHash::Algorithm::Sha512; + break; + case Algorithm::Murmur2: { // CF-specific + auto should_filter_out = [](char c) { return (c == 9 || c == 10 || c == 13 || c == 32); }; + auto reader = std::make_unique(device); + auto result = QString::number(Murmur2::hash(reader.get(), 4 * MiB, should_filter_out)); + device->close(); + return result; + } + case Algorithm::Unknown: + device->close(); + return ""; } - auto hash_type = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first(); - m_hash = ModPlatform::ProviderCapabilities::hash(ModPlatform::ResourceProvider::MODRINTH, &file, hash_type); + QCryptographicHash hash(alg); + if (!hash.addData(device)) + qCritical() << "Failed to read JAR to create hash!"; - file.close(); - - if (m_hash.isEmpty()) { - emitFailed("Empty hash!"); - } else { - emitSucceeded(); - emit resultsReady(m_hash); - } + Q_ASSERT(hash.result().length() == hash.hashLength(alg)); + auto result = hash.result().toHex(); + device->close(); + return result; } -void FlameHasher::executeTask() +QString hash(QString fileName, Algorithm type) { - // CF-specific - auto should_filter_out = [](char c) { return (c == 9 || c == 10 || c == 13 || c == 32); }; - - std::ifstream file_stream(StringUtils::toStdString(m_path).c_str(), std::ifstream::binary); - // TODO: This is very heavy work, but apparently QtConcurrent can't use move semantics, so we can't boop this to another thread. - // How do we make this non-blocking then? - m_hash = QString::number(MurmurHash2(std::move(file_stream), 4 * MiB, should_filter_out)); - - if (m_hash.isEmpty()) { - emitFailed("Empty hash!"); - } else { - emitSucceeded(); - emit resultsReady(m_hash); - } + QFile file(fileName); + return hash(&file, type); } -BlockedModHasher::BlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider) : Hasher(file_path), provider(provider) +QString hash(QByteArray data, Algorithm type) { - setObjectName(QString("BlockedModHasher: %1").arg(file_path)); - hash_type = ModPlatform::ProviderCapabilities::hashType(provider).first(); + QBuffer buff(&data); + return hash(&buff, type); } -void BlockedModHasher::executeTask() +void Hasher::executeTask() { - QFile file(m_path); - - try { - file.open(QFile::ReadOnly); - } catch (FS::FileSystemException& e) { - qCritical() << QString("Failed to open JAR file in %1").arg(m_path); - qCritical() << QString("Reason: ") << e.cause(); - - emitFailed("Failed to open file for hashing."); - return; - } - - m_hash = ModPlatform::ProviderCapabilities::hash(provider, &file, hash_type); - - file.close(); - - if (m_hash.isEmpty()) { - emitFailed("Empty hash!"); - } else { - emitSucceeded(); - emit resultsReady(m_hash); - } + m_future = QtConcurrent::run( + QThreadPool::globalInstance(), [](QString fileName, Algorithm type) { return hash(fileName, type); }, m_path, m_alg); + connect(&m_watcher, &QFutureWatcher::finished, this, [this] { + if (m_future.isCanceled()) { + emitAborted(); + } else if (m_result = m_future.result(); m_result.isEmpty()) { + emitFailed("Empty hash!"); + } else { + emitSucceeded(); + emit resultsReady(m_result); + } + }); + m_watcher.setFuture(m_future); } -QStringList BlockedModHasher::getHashTypes() +bool Hasher::abort() { - return ModPlatform::ProviderCapabilities::hashType(provider); -} - -bool BlockedModHasher::useHashType(QString type) -{ - auto types = ModPlatform::ProviderCapabilities::hashType(provider); - if (types.contains(type)) { - hash_type = type; + if (m_future.isRunning()) { + m_future.cancel(); + // NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not + // occur immediately. return true; } - qDebug() << "Bad hash type " << type << " for provider"; return false; } - } // namespace Hashing diff --git a/launcher/modplatform/helpers/HashUtils.h b/launcher/modplatform/helpers/HashUtils.h index 73a2435a2..5d8b7d132 100644 --- a/launcher/modplatform/helpers/HashUtils.h +++ b/launcher/modplatform/helpers/HashUtils.h @@ -1,5 +1,8 @@ #pragma once +#include +#include +#include #include #include "modplatform/ModIndex.h" @@ -7,61 +10,42 @@ namespace Hashing { +enum class Algorithm { Md4, Md5, Sha1, Sha256, Sha512, Murmur2, Unknown }; + +QString algorithmToString(Algorithm type); +Algorithm algorithmFromString(QString type); +QString hash(QIODevice* device, Algorithm type); +QString hash(QString fileName, Algorithm type); +QString hash(QByteArray data, Algorithm type); + class Hasher : public Task { Q_OBJECT public: using Ptr = shared_qobject_ptr; - Hasher(QString file_path) : m_path(std::move(file_path)) {} + Hasher(QString file_path, Algorithm alg) : m_path(file_path), m_alg(alg) {} + Hasher(QString file_path, QString alg) : Hasher(file_path, algorithmFromString(alg)) {} - /* We can't really abort this task, but we can say we aborted and finish our thing quickly :) */ - bool abort() override { return true; } + bool abort() override; - void executeTask() override = 0; + void executeTask() override; - QString getResult() const { return m_hash; }; + QString getResult() const { return m_result; }; QString getPath() const { return m_path; }; signals: void resultsReady(QString hash); - protected: - QString m_hash; - QString m_path; -}; - -class FlameHasher : public Hasher { - public: - FlameHasher(QString file_path) : Hasher(file_path) { setObjectName(QString("FlameHasher: %1").arg(file_path)); } - - void executeTask() override; -}; - -class ModrinthHasher : public Hasher { - public: - ModrinthHasher(QString file_path) : Hasher(file_path) { setObjectName(QString("ModrinthHasher: %1").arg(file_path)); } - - void executeTask() override; -}; - -class BlockedModHasher : public Hasher { - public: - BlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider); - - void executeTask() override; - - QStringList getHashTypes(); - bool useHashType(QString type); - private: - ModPlatform::ResourceProvider provider; - QString hash_type; + QString m_result; + QString m_path; + Algorithm m_alg; + + QFuture m_future; + QFutureWatcher m_watcher; }; Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provider); -Hasher::Ptr createFlameHasher(QString file_path); -Hasher::Ptr createModrinthHasher(QString file_path); -Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider); -Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider, QString type); +Hasher::Ptr createHasher(QString file_path, QString type); } // namespace Hashing diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.cpp b/launcher/modplatform/helpers/NetworkResourceAPI.cpp index 225583764..974e732a7 100644 --- a/launcher/modplatform/helpers/NetworkResourceAPI.cpp +++ b/launcher/modplatform/helpers/NetworkResourceAPI.cpp @@ -45,8 +45,8 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks& 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 && failed_action->m_reply) - network_error_code = failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) + network_error_code = failed_action->replyStatusCode(); callbacks.on_fail(reason, network_error_code); }); @@ -104,8 +104,8 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi }); 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 && failed_action->m_reply) - network_error_code = failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) + network_error_code = failed_action->replyStatusCode(); callbacks.on_fail(reason, network_error_code); }); @@ -155,8 +155,8 @@ Task::Ptr NetworkResourceAPI::getDependencyVersion(DependencySearchArgs&& args, }); 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 && failed_action->m_reply) - network_error_code = failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) + network_error_code = failed_action->replyStatusCode(); callbacks.on_fail(reason, network_error_code); }); diff --git a/launcher/modplatform/helpers/OverrideUtils.cpp b/launcher/modplatform/helpers/OverrideUtils.cpp index 65b5f7603..60983a5cf 100644 --- a/launcher/modplatform/helpers/OverrideUtils.cpp +++ b/launcher/modplatform/helpers/OverrideUtils.cpp @@ -10,7 +10,7 @@ void createOverrides(const QString& name, const QString& parent_folder, const QS { QString file_path(FS::PathCombine(parent_folder, name + ".txt")); if (QFile::exists(file_path)) - QFile::remove(file_path); + FS::deletePath(file_path); FS::ensureFilePathExists(file_path); diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.h b/launcher/modplatform/legacy_ftb/PackFetchTask.h index f2116ce99..e37d949d5 100644 --- a/launcher/modplatform/legacy_ftb/PackFetchTask.h +++ b/launcher/modplatform/legacy_ftb/PackFetchTask.h @@ -13,7 +13,7 @@ class PackFetchTask : public QObject { Q_OBJECT public: - PackFetchTask(shared_qobject_ptr network) : QObject(nullptr), m_network(network){}; + PackFetchTask(shared_qobject_ptr network) : QObject(nullptr), m_network(network) {}; virtual ~PackFetchTask() = default; void fetch(); diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp index 0048c7fac..7157f7f2d 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -137,7 +137,7 @@ void PackInstallTask::install() QDir unzipMcDir(m_stagingPath + "/unzip/minecraft"); if (unzipMcDir.exists()) { // ok, found minecraft dir, move contents to instance dir - if (!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/minecraft")) { + if (!FS::move(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/minecraft")) { emitFailed(tr("Failed to move unzipped Minecraft!")); return; } diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp index 9777c2cfd..4798ace84 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.cpp +++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp @@ -120,3 +120,41 @@ QList ModrinthAPI::getSortingMethods() const { 4, "newest", QObject::tr("Sort by Newest") }, { 5, "updated", QObject::tr("Sort by Last Updated") } }; } + +Task::Ptr ModrinthAPI::getModCategories(std::shared_ptr response) +{ + auto netJob = makeShared(QString("Modrinth::GetCategories"), APPLICATION->network()); + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(BuildConfig.MODRINTH_PROD_URL + "/tag/category"), response)); + QObject::connect(netJob.get(), &Task::failed, [](QString msg) { qDebug() << "Modrinth failed to get categories:" << msg; }); + return netJob; +} + +QList ModrinthAPI::loadModCategories(std::shared_ptr response) +{ + QList categories; + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from categories at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return categories; + } + + try { + auto arr = Json::requireArray(doc); + + for (auto val : arr) { + auto cat = Json::requireObject(val); + auto name = Json::requireString(cat, "name"); + if (Json::ensureString(cat, "project_type", "") == "mod") + categories.push_back({ name, name }); + } + + } catch (Json::JsonException& e) { + qCritical() << "Failed to parse response from a version request."; + qCritical() << e.what(); + qDebug() << doc; + } + return categories; +}; \ No newline at end of file diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index d0f0811b2..d1f8f712a 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -30,6 +30,9 @@ 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 loadModCategories(std::shared_ptr response); + public: [[nodiscard]] auto getSortingMethods() const -> QList override; @@ -41,7 +44,7 @@ class ModrinthAPI : public NetworkResourceAPI { for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt, ModPlatform::LiteLoader }) { if (types & loader) { - l << getModLoaderString(loader); + l << getModLoaderAsString(loader); } } return l; @@ -56,6 +59,27 @@ class ModrinthAPI : public NetworkResourceAPI { return l.join(','); } + static auto getCategoriesFilters(QStringList categories) -> const QString + { + QStringList l; + for (auto cat : categories) { + l << QString("\"categories:%1\"").arg(cat); + } + return l.join(','); + } + + static auto getSideFilters(QString side) -> const QString + { + if (side.isEmpty() || side == "both") { + return {}; + } + if (side == "client") + return QString("\"client_side:required\",\"client_side:optional\""); + if (side == "server") + return QString("\"server_side:required\",\"server_side:optional\""); + return {}; + } + private: [[nodiscard]] static QString resourceTypeParameter(ModPlatform::ResourceType type) { @@ -73,6 +97,7 @@ class ModrinthAPI : public NetworkResourceAPI { return ""; } + [[nodiscard]] QString createFacets(SearchArgs const& args) const { QStringList facets_list; @@ -81,6 +106,14 @@ class ModrinthAPI : public NetworkResourceAPI { facets_list.append(QString("[%1]").arg(getModLoaderFilters(args.loaders.value()))); if (args.versions.has_value()) facets_list.append(QString("[%1]").arg(getGameVersionsArray(args.versions.value()))); + if (args.side.has_value()) { + auto side = getSideFilters(args.side.value()); + if (!side.isEmpty()) + facets_list.append(QString("[%1]").arg(side)); + } + if (args.categoryIds.has_value() && !args.categoryIds->empty()) + facets_list.append(QString("[%1]").arg(getCategoriesFilters(args.categoryIds.value()))); + facets_list.append(QString("[\"project_type:%1\"]").arg(resourceTypeParameter(args.type))); return QString("[%1]").arg(facets_list.join(',')); diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index 881f5499c..c004bd8f2 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -1,23 +1,23 @@ #include "ModrinthCheckUpdate.h" +#include "Application.h" #include "ModrinthAPI.h" #include "ModrinthPackIndex.h" #include "Json.h" +#include "QObjectPtr.h" #include "ResourceDownloadTask.h" #include "modplatform/helpers/HashUtils.h" #include "tasks/ConcurrentTask.h" -#include "minecraft/mod/ModFolderModel.h" - static ModrinthAPI api; bool ModrinthCheckUpdate::abort() { - if (m_net_job) - return m_net_job->abort(); + if (m_job) + return m_job->abort(); return true; } @@ -29,158 +29,187 @@ bool ModrinthCheckUpdate::abort() void ModrinthCheckUpdate::executeTask() { setStatus(tr("Preparing resources for Modrinth...")); - setProgress(0, 3); + setProgress(0, 9); - QHash mappings; - - // Create all hashes - QStringList hashes; - auto best_hash_type = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first(); - - ConcurrentTask hashing_task(this, "MakeModrinthHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); + auto hashing_task = + makeShared(this, "MakeModrinthHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); for (auto* resource : m_resources) { - if (!resource->enabled()) { - emit checkFailed(resource, tr("Disabled resources won't be updated, to prevent resource duplication issues!")); - continue; - } - auto hash = resource->metadata()->hash; // Sadly the API can only handle one hash type per call, se we // need to generate a new hash if the current one is innadequate // (though it will rarely happen, if at all) - if (resource->metadata()->hash_format != best_hash_type) { - auto hash_task = Hashing::createModrinthHasher(resource->fileinfo().absoluteFilePath()); - connect(hash_task.get(), &Hashing::Hasher::resultsReady, [&hashes, &mappings, resource](QString hash) { - hashes.append(hash); - mappings.insert(hash, resource); - }); + if (resource->metadata()->hash_format != m_hash_type) { + auto hash_task = Hashing::createHasher(resource->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::MODRINTH); + connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_mappings.insert(hash, resource); }); connect(hash_task.get(), &Task::failed, [this] { failed("Failed to generate hash"); }); - hashing_task.addTask(hash_task); + hashing_task->addTask(hash_task); } else { - hashes.append(hash); - mappings.insert(hash, resource); + m_mappings.insert(hash, resource); } } - QEventLoop loop; - connect(&hashing_task, &Task::finished, [&loop] { loop.quit(); }); - hashing_task.start(); - loop.exec(); + connect(hashing_task.get(), &Task::finished, this, &ModrinthCheckUpdate::checkNextLoader); + m_job = hashing_task; + hashing_task->start(); +} - auto response = std::make_shared(); - auto job = api.latestVersions(hashes, best_hash_type, m_game_versions, m_loaders, response); +void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr response, + ModPlatform::ModLoaderTypes loader, + bool forceModLoaderCheck) +{ + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from ModrinthCheckUpdate at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; - connect(job.get(), &Task::succeeded, this, [this, response, mappings, best_hash_type, job] { - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from ModrinthCheckUpdate at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; + emitFailed(parse_error.errorString()); + return; + } - emitFailed(parse_error.errorString()); - return; - } + setStatus(tr("Parsing the API response from Modrinth...")); + setProgress(m_next_loader_idx * 2, 9); - setStatus(tr("Parsing the API response from Modrinth...")); - setProgress(2, 3); + try { + for (auto iter = m_mappings.begin(); iter != m_mappings.end(); iter++) { + const QString& hash = iter.key(); + Resource* resource = iter.value(); - try { - for (auto iter = mappings.begin(); iter != mappings.end(); iter++) { - const QString& hash = iter.key(); - Resource* resource = iter.value(); - auto project_obj = doc[hash].toObject(); + if (forceModLoaderCheck && !(resource->metadata()->loaders & loader)) + continue; - // If the returned project is empty, but we have Modrinth metadata, - // it means this specific version is not available - if (project_obj.isEmpty()) { - qDebug() << "Resource " << resource->name() << " got an empty response."; - qDebug() << "Hash: " << hash; + auto project_obj = doc[hash].toObject(); - QString reason; - if (dynamic_cast(resource) != nullptr) - reason = - tr("No valid version found for this resource. It's probably unavailable for the current game " - "version / mod loader."); - else - reason = tr("No valid version found for this resource. It's probably unavailable for the current game version."); + // If the returned project is empty, but we have Modrinth metadata, + // 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; - emit checkFailed(resource, reason); - 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; - if (m_loaders.has_value()) { - static auto flags = { ModPlatform::ModLoaderType::NeoForge, ModPlatform::ModLoaderType::Forge, - ModPlatform::ModLoaderType::Fabric, ModPlatform::ModLoaderType::Quilt, - ModPlatform::ModLoaderType::LiteLoader }; - for (auto flag : flags) { - if (m_loaders.value().testFlag(flag)) { - loader_filter = ModPlatform::getModLoaderString(flag); - break; - } - } - } - - // Currently, we rely on a couple heuristics to determine whether an update is actually available or not: - // - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the - // loader_filter - // - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case) - // Such is the pain of having arbitrary files for a given version .-. - - auto project_ver = Modrinth::loadIndexedPackVersion(project_obj, best_hash_type, loader_filter); - if (project_ver.downloadUrl.isEmpty()) { - qCritical() << "Modrinth resource without download url!"; - qCritical() << project_ver.fileName; - - emit checkFailed(mappings.find(hash).value(), tr("Resource has an empty download URL")); - - continue; - } - - auto resource_iter = mappings.find(hash); - if (resource_iter == mappings.end()) { - qCritical() << "Failed to remap resource from Modrinth!"; - continue; - } - - // Fake pack with the necessary info to pass to the download task :) - auto pack = std::make_shared(); - pack->name = resource->name(); - pack->slug = resource->metadata()->slug; - pack->addonId = resource->metadata()->project_id; - pack->provider = ModPlatform::ResourceProvider::MODRINTH; - if ((project_ver.hash != hash && project_ver.is_preferred) || (resource->status() == ResourceStatus::NOT_INSTALLED)) { - auto download_task = makeShared(pack, project_ver, m_resource_model); - - QString old_version = resource->metadata()->version_number; - if (old_version.isEmpty()) { - if (resource->status() == ResourceStatus::NOT_INSTALLED) - old_version = tr("Not installed"); - else - old_version = tr("Unknown"); - } - - m_updates.emplace_back(pack->name, hash, old_version, project_ver.version_number, project_ver.version_type, - project_ver.changelog, ModPlatform::ResourceProvider::MODRINTH, download_task); - } - m_deps.append(std::make_shared(pack, project_ver)); + continue; } - } catch (Json::JsonException& e) { - emitFailed(e.cause() + ": " + e.what()); - return; - } - emitSucceeded(); - }); - connect(job.get(), &Task::failed, this, &ModrinthCheckUpdate::emitFailed); + // 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; + } + } + + // Currently, we rely on a couple heuristics to determine whether an update is actually available or not: + // - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the + // loader_filter + // - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case) + // Such is the pain of having arbitrary files for a given version .-. + + auto project_ver = Modrinth::loadIndexedPackVersion(project_obj, m_hash_type, loader_filter); + if (project_ver.downloadUrl.isEmpty()) { + qCritical() << "Modrinth mod without download url!" << project_ver.fileName; + + continue; + } + + auto mod_iter = m_mappings.find(hash); + if (mod_iter == m_mappings.end()) { + qCritical() << "Failed to remap mod from Modrinth!"; + continue; + } + auto mod = *mod_iter; + m_mappings.remove(hash); + + // Fake pack with the necessary info to pass to the download task :) + auto pack = std::make_shared(); + pack->name = mod->name(); + pack->slug = mod->metadata()->slug; + pack->addonId = mod->metadata()->project_id; + pack->provider = ModPlatform::ResourceProvider::MODRINTH; + if ((project_ver.hash != hash && project_ver.is_preferred) || (resource->status() == ResourceStatus::NOT_INSTALLED)) { + auto download_task = makeShared(pack, project_ver, m_resource_model); + + QString old_version = resource->metadata()->version_number; + if (old_version.isEmpty()) { + if (resource->status() == ResourceStatus::NOT_INSTALLED) + old_version = tr("Not installed"); + else + old_version = tr("Unknown"); + } + + m_updates.emplace_back(pack->name, hash, old_version, project_ver.version_number, project_ver.version_type, + project_ver.changelog, ModPlatform::ResourceProvider::MODRINTH, download_task); + } + m_deps.append(std::make_shared(pack, project_ver)); + } + } catch (Json::JsonException& e) { + emitFailed(e.cause() + ": " + e.what()); + return; + } + checkNextLoader(); +} + +void ModrinthCheckUpdate::getUpdateModsForLoader(ModPlatform::ModLoaderTypes loader, bool forceModLoaderCheck) +{ + auto response = std::make_shared(); + QStringList hashes; + if (forceModLoaderCheck) { + for (auto hash : m_mappings.keys()) { + if (m_mappings[hash]->metadata()->loaders & loader) { + hashes.append(hash); + } + } + } else { + hashes = m_mappings.keys(); + } + auto job = api.latestVersions(hashes, m_hash_type, m_game_versions, loader, response); + + connect(job.get(), &Task::succeeded, this, + [this, response, loader, forceModLoaderCheck] { checkVersionsResponse(response, loader, forceModLoaderCheck); }); + + connect(job.get(), &Task::failed, this, &ModrinthCheckUpdate::checkNextLoader); setStatus(tr("Waiting for the API response from Modrinth...")); - setProgress(1, 3); + setProgress(m_next_loader_idx * 2 - 1, 9); - m_net_job = qSharedPointerObjectCast(job); + m_job = job; job->start(); } + +void ModrinthCheckUpdate::checkNextLoader() +{ + if (m_mappings.isEmpty()) { + emitSucceeded(); + return; + } + if (m_next_loader_idx < m_loaders_list.size()) { + getUpdateModsForLoader(m_loaders_list.at(m_next_loader_idx)); + m_next_loader_idx++; + return; + } + static auto flags = { ModPlatform::ModLoaderType::NeoForge, ModPlatform::ModLoaderType::Forge, ModPlatform::ModLoaderType::Quilt, + ModPlatform::ModLoaderType::Fabric }; + for (auto flag : flags) { + if (!m_loaders_list.contains(flag)) { + m_loaders_list.append(flag); + m_next_loader_idx++; + setProgress(m_next_loader_idx * 2 - 1, 9); + for (auto resource : m_mappings) { + if (resource->metadata()->loaders & flag) { + getUpdateModsForLoader(flag, true); + return; + } + } + setProgress(m_next_loader_idx * 2, 9); + } + } + for (auto m : m_mappings) { + emit checkFailed(m, + tr("No valid version found for this mod. It's probably unavailable for the current game version / mod loader.")); + } + emitSucceeded(); +} diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h index 5aa3c8cb6..0bbd21321 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h @@ -1,18 +1,17 @@ #pragma once -#include "Application.h" #include "modplatform/CheckUpdateTask.h" -#include "net/NetJob.h" class ModrinthCheckUpdate : public CheckUpdateTask { Q_OBJECT public: ModrinthCheckUpdate(QList& resources, - std::list& mcVersions, - std::optional loaders, - std::shared_ptr resource_model) - : CheckUpdateTask(resources, mcVersions, loaders, resource_model) + std::list& mcVersions, + QList loadersList, + std::shared_ptr resourceModel) + : CheckUpdateTask(resources, mcVersions, loadersList, resourceModel) + , m_hash_type(ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first()) {} public slots: @@ -20,7 +19,13 @@ class ModrinthCheckUpdate : public CheckUpdateTask { protected slots: void executeTask() override; + void getUpdateModsForLoader(ModPlatform::ModLoaderTypes loader, bool forceModLoaderCheck = false); + void checkVersionsResponse(std::shared_ptr response, ModPlatform::ModLoaderTypes loader, bool forceModLoaderCheck = false); + void checkNextLoader(); private: - NetJob::Ptr m_net_job = nullptr; + Task::Ptr m_job = nullptr; + QHash m_mappings; + QString m_hash_type; + int m_next_loader_idx = 0; }; diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index a688cea87..f4e8dbb5a 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(); } @@ -173,7 +178,7 @@ bool ModrinthCreationTask::createInstance() // Keep index file in case we need it some other time (like when changing versions) QString new_index_place(FS::PathCombine(parent_folder, "modrinth.index.json")); FS::ensureFilePathExists(new_index_place); - QFile::rename(index_path, new_index_place); + FS::move(index_path, new_index_place); auto mcPath = FS::PathCombine(m_stagingPath, m_root_path); @@ -183,7 +188,7 @@ bool ModrinthCreationTask::createInstance() Override::createOverrides("overrides", parent_folder, override_path); // Apply the overrides - if (!QFile::rename(override_path, mcPath)) { + if (!FS::move(override_path, mcPath)) { setError(tr("Could not rename the overrides folder:\n") + "overrides"); return false; } @@ -234,33 +239,43 @@ 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); - + // TODO make this work with other sorts of resource + QHash resources; for (auto file : m_files) { - auto file_path = FS::PathCombine(root_modpack_path, file.path); + auto fileName = file.path; + fileName = FS::RemoveInvalidPathChars(fileName); + auto file_path = FS::PathCombine(root_modpack_path, fileName); if (!root_modpack_url.isParentOf(QUrl::fromLocalFile(file_path))) { // This means we somehow got out of the root folder, so abort here to prevent exploits setError(tr("One of the files has a path that leads to an arbitrary location (%1). This is a security risk and isn't allowed.") - .arg(file.path)); + .arg(fileName)); return false; } + if (fileName.startsWith("mods/")) { + auto mod = new Mod(file_path); + ModDetails d; + d.mod_id = file_path; + mod->setDetails(d); + resources[file.hash.toHex()] = mod; + } 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(), &NetAction::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(); }); @@ -269,23 +284,44 @@ 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(); + QEventLoop ensureMetaLoop; + QDir folder = FS::PathCombine(instance.modsRoot(), ".index"); + auto ensureMetadataTask = makeShared(resources, 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 : resources) { + delete m; + } + resources.clear(); + // Update information of the already installed instance, if any. if (m_instance && ended_well) { setAbortable(false); @@ -344,23 +380,8 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, } QJsonObject hashes = Json::requireObject(modInfo, "hashes"); - QString hash; - QCryptographicHash::Algorithm hashAlgorithm; - hash = Json::ensureString(hashes, "sha1"); - hashAlgorithm = QCryptographicHash::Sha1; - if (hash.isEmpty()) { - hash = Json::ensureString(hashes, "sha512"); - hashAlgorithm = QCryptographicHash::Sha512; - if (hash.isEmpty()) { - hash = Json::ensureString(hashes, "sha256"); - hashAlgorithm = QCryptographicHash::Sha256; - if (hash.isEmpty()) { - throw JSONValidationError("No hash found for: " + file.path); - } - } - } - file.hash = QByteArray::fromHex(hash.toLatin1()); - file.hashAlgorithm = hashAlgorithm; + file.hash = QByteArray::fromHex(Json::requireString(hashes, "sha512").toLatin1()); + file.hashAlgorithm = QCryptographicHash::Sha512; // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode // (as Modrinth seems to incorrectly handle spaces) 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 +#include "BaseInstance.h" #include "InstanceCreationTask.h" -#include - -#include "minecraft/MinecraftInstance.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.cpp b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp index a07b0d5b4..d103170af 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp @@ -18,6 +18,7 @@ #include "ModrinthPackExportTask.h" +#include #include #include #include @@ -27,6 +28,8 @@ #include "minecraft/PackProfile.h" #include "minecraft/mod/MetadataHandler.h" #include "minecraft/mod/ModFolderModel.h" +#include "modplatform/helpers/HashUtils.h" +#include "tasks/Task.h" const QStringList ModrinthPackExportTask::PREFIXES({ "mods/", "coremods/", "resourcepacks/", "texturepacks/", "shaderpacks/" }); const QStringList ModrinthPackExportTask::FILE_EXTENSIONS({ "jar", "litemod", "zip" }); @@ -102,8 +105,6 @@ void ModrinthPackExportTask::collectHashes() })) continue; - QCryptographicHash sha512(QCryptographicHash::Algorithm::Sha512); - QFile openFile(file.absoluteFilePath()); if (!openFile.open(QFile::ReadOnly)) { qWarning() << "Could not open" << file << "for hashing"; @@ -115,7 +116,7 @@ void ModrinthPackExportTask::collectHashes() qWarning() << "Could not read" << file; continue; } - sha512.addData(data); + auto sha512 = Hashing::hash(data, Hashing::Algorithm::Sha512); auto allMods = mcInstance->loaderModList()->allMods(); if (auto modIter = std::find_if(allMods.begin(), allMods.end(), [&file](Mod* mod) { return mod->fileinfo() == file; }); @@ -127,11 +128,9 @@ void ModrinthPackExportTask::collectHashes() if (!url.isEmpty() && BuildConfig.MODRINTH_MRPACK_HOSTS.contains(url.host())) { qDebug() << "Resolving" << relative << "from index"; - QCryptographicHash sha1(QCryptographicHash::Algorithm::Sha1); - sha1.addData(data); + auto sha1 = Hashing::hash(data, Hashing::Algorithm::Sha1); - ResolvedFile resolvedFile{ sha1.result().toHex(), sha512.result().toHex(), url.toEncoded(), openFile.size(), - mod->metadata()->side }; + ResolvedFile resolvedFile{ sha1, sha512, url.toEncoded(), openFile.size(), mod->metadata()->side }; resolvedFiles[relative] = resolvedFile; // nice! we've managed to resolve based on local metadata! @@ -142,7 +141,7 @@ void ModrinthPackExportTask::collectHashes() } qDebug() << "Enqueueing" << relative << "for Modrinth query"; - pendingHashes[relative] = sha512.result().toHex(); + pendingHashes[relative] = sha512; } setAbortable(true); @@ -157,8 +156,8 @@ void ModrinthPackExportTask::makeApiRequest() setStatus(tr("Finding versions for hashes...")); auto response = std::make_shared(); task = api.currentVersions(pendingHashes.values(), "sha512", response); - connect(task.get(), &NetJob::succeeded, [this, response]() { parseApiResponse(response); }); - connect(task.get(), &NetJob::failed, this, &ModrinthPackExportTask::emitFailed); + connect(task.get(), &Task::succeeded, [this, response]() { parseApiResponse(response); }); + connect(task.get(), &Task::failed, this, &ModrinthPackExportTask::emitFailed); task->start(); } } @@ -200,7 +199,7 @@ void ModrinthPackExportTask::buildZip() { setStatus(tr("Adding files...")); - auto zipTask = makeShared(output, gameRoot, files, "overrides/", true); + auto zipTask = makeShared(output, gameRoot, files, "overrides/", true, true); zipTask->addExtraFile("modrinth.index.json", generateIndex()); zipTask->setExcludeFiles(resolvedFiles.keys()); diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 7a74619e5..48b27a597 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln + * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,6 +18,7 @@ */ #include "ModrinthPackIndex.h" +#include "FileSystem.h" #include "ModrinthAPI.h" #include "Json.h" @@ -113,16 +115,11 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const BaseInstance* inst) { QVector unsortedVersions; - auto profile = (dynamic_cast(inst))->getPackProfile(); - QString mcVersion = profile->getComponentVersion("net.minecraft"); - auto loaders = profile->getSupportedModLoaders(); - for (auto versionIter : arr) { auto obj = versionIter.toObject(); auto file = loadIndexedPackVersion(obj); - if (file.fileId.isValid() && - (!loaders.has_value() || !file.loaders || loaders.value() & file.loaders)) // Heuristic to check if the returned value is valid + if (file.fileId.isValid()) // Heuristic to check if the returned value is valid unsortedVersions.append(file); } auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { @@ -134,8 +131,9 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArra pack.versionsLoaded = true; } -auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_type, QString preferred_file_name) - -> ModPlatform::IndexedVersion +auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, + QString preferred_hash_type, + QString preferred_file_name) -> ModPlatform::IndexedVersion { ModPlatform::IndexedVersion file; @@ -153,15 +151,15 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t for (auto loader : loaders) { if (loader == "neoforge") file.loaders |= ModPlatform::NeoForge; - if (loader == "forge") + else if (loader == "forge") file.loaders |= ModPlatform::Forge; - if (loader == "cauldron") + else if (loader == "cauldron") file.loaders |= ModPlatform::Cauldron; - if (loader == "liteloader") + else if (loader == "liteloader") file.loaders |= ModPlatform::LiteLoader; - if (loader == "fabric") + else if (loader == "fabric") file.loaders |= ModPlatform::Fabric; - if (loader == "quilt") + else if (loader == "quilt") file.loaders |= ModPlatform::Quilt; } file.version = Json::requireString(obj, "name"); @@ -225,6 +223,7 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t if (parent.contains("url")) { file.downloadUrl = Json::requireString(parent, "url"); file.fileName = Json::requireString(parent, "filename"); + file.fileName = FS::RemoveInvalidPathChars(file.fileName); file.is_preferred = Json::requireBoolean(parent, "primary") || (files.count() == 1); auto hash_list = Json::requireObject(parent, "hashes"); @@ -248,8 +247,9 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t return {}; } -auto Modrinth::loadDependencyVersions([[maybe_unused]] const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst) - -> ModPlatform::IndexedVersion +auto Modrinth::loadDependencyVersions([[maybe_unused]] const ModPlatform::Dependency& m, + QJsonArray& arr, + const BaseInstance* inst) -> ModPlatform::IndexedVersion { auto profile = (dynamic_cast(inst))->getPackProfile(); QString mcVersion = profile->getComponentVersion("net.minecraft"); diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index 7846e966d..f360df43a 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -131,6 +131,10 @@ auto loadIndexedVersion(QJsonObject& obj) -> ModpackVersion file.name = Json::requireString(obj, "name"); file.version = Json::requireString(obj, "version_number"); + auto gameVersions = Json::ensureArray(obj, "game_versions"); + if (!gameVersions.isEmpty()) { + file.gameVersion = Json::ensureString(gameVersions[0]); + } 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 1ffd31d83..2bd61c5d9 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -84,6 +84,7 @@ struct ModpackExtra { struct ModpackVersion { QString name; QString version; + QString gameVersion; ModPlatform::IndexedVersionType version_type; QString changelog; diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 6e1f507fd..0e1f2b58c 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln + * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -112,10 +113,14 @@ auto V1::createModFormat([[maybe_unused]] const QDir& index_dir, mod.provider = mod_pack.provider; mod.file_id = mod_version.fileId; mod.project_id = mod_pack.addonId; - mod.side = stringToSide(mod_pack.side); + mod.side = stringToSide(mod_version.side.isEmpty() ? mod_pack.side : mod_version.side); + mod.loaders = mod_version.loaders; + mod.mcVersions = mod_version.mcVersion; + mod.mcVersions.sort(); + mod.releaseType = mod_version.version_type; mod.version_number = mod_version.version_number; - if (mod.version_number.isNull()) // on CurseForge, there is only a version name - not a versio + if (mod.version_number.isNull()) // on CurseForge, there is only a version name - not a version number mod.version_number = mod_version.version; return mod; @@ -184,6 +189,18 @@ void V1::updateModIndex(const QDir& index_dir, Mod& mod) break; } + 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()); + } + } + toml::array mcVersions; + for (auto version : mod.mcVersions) { + mcVersions.push_back(version.toStdString()); + } + if (!index_file.open(QIODevice::ReadWrite)) { qCritical() << QString("Could not open file %1!").arg(normalized_fname); return; @@ -195,6 +212,9 @@ void V1::updateModIndex(const 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() }, { "download", toml::table{ { "mode", mod.mode.toStdString() }, @@ -279,6 +299,25 @@ auto V1::getIndexForMod(const 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()) { + 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()) { + for (auto&& version : *versions.as_array()) { + if (version.is_string()) { + auto ver = QString::fromStdString(version.as_string()->value_or("")); + if (!ver.isEmpty()) { + mod.mcVersions << ver; + } + } + } + mod.mcVersions.sort(); + } } { // [download] info diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index 19ef22dd8..44896e74c 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln + * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -43,6 +44,9 @@ class V1 { QString name{}; QString filename{}; Side side{ Side::UniversalSide }; + ModPlatform::ModLoaderTypes loaders; + QStringList mcVersions; + ModPlatform::IndexedVersionType releaseType; // [download] QString mode{}; diff --git a/launcher/modplatform/technic/SolderPackInstallTask.cpp b/launcher/modplatform/technic/SolderPackInstallTask.cpp index ed8b0a8a4..ffda05ee9 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.cpp +++ b/launcher/modplatform/technic/SolderPackInstallTask.cpp @@ -114,8 +114,7 @@ void Technic::SolderPackInstallTask::fileListSucceeded() auto dl = Net::ApiDownload::makeFile(mod.url, path); if (!mod.md5.isEmpty()) { - auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); - dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5)); } m_filesNetJob->addNetAction(dl); diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp index 90f59ce54..9050e14d8 100644 --- a/launcher/modplatform/technic/TechnicPackProcessor.cpp +++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp @@ -83,8 +83,10 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, data = file.readAll(); file.close(); } else { - if (minecraftVersion.isEmpty()) + if (minecraftVersion.isEmpty()) { emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but Minecraft version is unknown")); + return; + } components->setComponentVersion("net.minecraft", minecraftVersion, true); components->installJarMods({ modpackJar }); @@ -131,7 +133,9 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, file.close(); } else { // This is the "Vanilla" modpack, excluded by the search code - emit failed(tr("Unable to find a \"version.json\"!")); + components->setComponentVersion("net.minecraft", minecraftVersion, true); + components->saveNow(); + emit succeeded(); return; } @@ -155,8 +159,26 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, auto libraryObject = Json::ensureObject(library, {}, ""); auto libraryName = Json::ensureString(libraryObject, "name", "", ""); - if ((libraryName.startsWith("net.minecraftforge:forge:") || libraryName.startsWith("net.minecraftforge:fmlloader:")) && - libraryName.contains('-')) { + if (libraryName.startsWith("net.neoforged.fancymodloader:")) { // it is neoforge + // no easy way to get the version from the libs so use the arguments + auto arguments = Json::ensureObject(root, "arguments", {}); + bool isVersionArg = false; + QString neoforgeVersion; + for (auto arg : Json::ensureArray(arguments, "game", {})) { + auto argument = Json::ensureString(arg, ""); + if (isVersionArg) { + neoforgeVersion = argument; + break; + } else { + isVersionArg = "--fml.neoForgeVersion" == argument || "--fml.forgeVersion" == argument; + } + } + if (!neoforgeVersion.isEmpty()) { + components->setComponentVersion("net.neoforged", neoforgeVersion); + } + break; + } else if ((libraryName.startsWith("net.minecraftforge:forge:") || libraryName.startsWith("net.minecraftforge:fmlloader:")) && + libraryName.contains('-')) { QString libraryVersion = libraryName.section(':', 2); if (!libraryVersion.startsWith("1.7.10-")) { components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1)); @@ -164,6 +186,7 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, // 1.7.10 versions sometimes look like 1.7.10-10.13.4.1614-1.7.10, this filters out the 10.13.4.1614 part components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1, 1)); } + break; } else { // -> static QMap loaderMap{ { "net.minecraftforge:minecraftforge:", "net.minecraftforge" }, diff --git a/launcher/net/ApiDownload.cpp b/launcher/net/ApiDownload.cpp index aaa8ff650..78eb1f851 100644 --- a/launcher/net/ApiDownload.cpp +++ b/launcher/net/ApiDownload.cpp @@ -18,49 +18,29 @@ */ #include "net/ApiDownload.h" -#include "ByteArraySink.h" -#include "ChecksumValidator.h" -#include "MetaCacheSink.h" -#include "net/NetAction.h" +#include "net/ApiHeaderProxy.h" namespace Net { -auto ApiDownload::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Download::Ptr +Download::Ptr ApiDownload::makeCached(QUrl url, MetaEntryPtr entry, Download::Options options) { - auto dl = makeShared(); - dl->m_url = url; - dl->setObjectName(QString("CACHE:") + url.toString()); - dl->m_options = options; - auto md5Node = new ChecksumValidator(QCryptographicHash::Md5); - auto cachedNode = new MetaCacheSink(entry, md5Node, options.testFlag(Option::MakeEternal)); - dl->m_sink.reset(cachedNode); + auto dl = Download::makeCached(url, entry, options); + dl->addHeaderProxy(new ApiHeaderProxy()); return dl; } -auto ApiDownload::makeByteArray(QUrl url, std::shared_ptr output, Options options) -> Download::Ptr +Download::Ptr ApiDownload::makeByteArray(QUrl url, std::shared_ptr output, Download::Options options) { - auto dl = makeShared(); - dl->m_url = url; - dl->setObjectName(QString("BYTES:") + url.toString()); - dl->m_options = options; - dl->m_sink.reset(new ByteArraySink(output)); + auto dl = Download::makeByteArray(url, output, options); + dl->addHeaderProxy(new ApiHeaderProxy()); return dl; } -auto ApiDownload::makeFile(QUrl url, QString path, Options options) -> Download::Ptr +Download::Ptr ApiDownload::makeFile(QUrl url, QString path, Download::Options options) { - auto dl = makeShared(); - dl->m_url = url; - dl->setObjectName(QString("FILE:") + url.toString()); - dl->m_options = options; - dl->m_sink.reset(new FileSink(path)); + auto dl = Download::makeFile(url, path, options); + dl->addHeaderProxy(new ApiHeaderProxy()); return dl; } -void ApiDownload::init() -{ - qDebug() << "Setting up api download"; - auto api_headers = new ApiHeaderProxy(); - addHeaderProxy(api_headers); -} } // namespace Net diff --git a/launcher/net/ApiDownload.h b/launcher/net/ApiDownload.h index 638c94e11..842c25c56 100644 --- a/launcher/net/ApiDownload.h +++ b/launcher/net/ApiDownload.h @@ -19,20 +19,14 @@ #pragma once -#include "ApiHeaderProxy.h" #include "Download.h" namespace Net { -class ApiDownload : public Download { - public: - virtual ~ApiDownload() = default; - - static auto makeCached(QUrl url, MetaEntryPtr entry, Options options = Option::NoOptions) -> Download::Ptr; - static auto makeByteArray(QUrl url, std::shared_ptr output, Options options = Option::NoOptions) -> Download::Ptr; - static auto makeFile(QUrl url, QString path, Options options = Option::NoOptions) -> Download::Ptr; - - void init() override; -}; +namespace ApiDownload { +Download::Ptr makeCached(QUrl url, MetaEntryPtr entry, Download::Options options = Download::Option::NoOptions); +Download::Ptr makeByteArray(QUrl url, std::shared_ptr output, Download::Options options = Download::Option::NoOptions); +Download::Ptr makeFile(QUrl url, QString path, Download::Options options = Download::Option::NoOptions); +}; // namespace ApiDownload } // namespace Net diff --git a/launcher/net/ApiUpload.cpp b/launcher/net/ApiUpload.cpp index c1221b764..a2b8f357b 100644 --- a/launcher/net/ApiUpload.cpp +++ b/launcher/net/ApiUpload.cpp @@ -18,26 +18,15 @@ */ #include "net/ApiUpload.h" -#include "ByteArraySink.h" -#include "ChecksumValidator.h" -#include "MetaCacheSink.h" -#include "net/NetAction.h" +#include "net/ApiHeaderProxy.h" namespace Net { Upload::Ptr ApiUpload::makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data) { - auto up = makeShared(); - up->m_url = std::move(url); - up->m_sink.reset(new ByteArraySink(output)); - up->m_post_data = std::move(m_post_data); + auto up = Upload::makeByteArray(url, output, m_post_data); + up->addHeaderProxy(new ApiHeaderProxy()); return up; } -void ApiUpload::init() -{ - qDebug() << "Setting up api upload"; - auto api_headers = new ApiHeaderProxy(); - addHeaderProxy(api_headers); -} } // namespace Net diff --git a/launcher/net/ApiUpload.h b/launcher/net/ApiUpload.h index b12842b05..674a3b93f 100644 --- a/launcher/net/ApiUpload.h +++ b/launcher/net/ApiUpload.h @@ -19,18 +19,12 @@ #pragma once -#include "ApiHeaderProxy.h" #include "Upload.h" namespace Net { -class ApiUpload : public Upload { - public: - virtual ~ApiUpload() = default; - - static Upload::Ptr makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data); - - void init() override; +namespace ApiUpload { +Upload::Ptr makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data); }; } // namespace Net diff --git a/launcher/net/ByteArraySink.h b/launcher/net/ByteArraySink.h index 7b8f0f8aa..ac64052b9 100644 --- a/launcher/net/ByteArraySink.h +++ b/launcher/net/ByteArraySink.h @@ -45,7 +45,7 @@ namespace Net { */ class ByteArraySink : public Sink { public: - ByteArraySink(std::shared_ptr output) : m_output(output){}; + ByteArraySink(std::shared_ptr output) : m_output(output) {}; virtual ~ByteArraySink() = default; @@ -74,10 +74,6 @@ class ByteArraySink : public Sink { auto abort() -> Task::State override { - if (m_output) - m_output->clear(); - else - qWarning() << "ByteArraySink did not clear the buffer because it's not addressable"; failAllValidators(); return Task::State::Failed; } diff --git a/launcher/net/ChecksumValidator.h b/launcher/net/ChecksumValidator.h index dfee0aee5..7663d5d12 100644 --- a/launcher/net/ChecksumValidator.h +++ b/launcher/net/ChecksumValidator.h @@ -43,8 +43,11 @@ namespace Net { class ChecksumValidator : public Validator { public: + ChecksumValidator(QCryptographicHash::Algorithm algorithm, QString expectedHex) + : Net::ChecksumValidator(algorithm, QByteArray::fromHex(expectedHex.toLatin1())) + {} ChecksumValidator(QCryptographicHash::Algorithm algorithm, QByteArray expected = QByteArray()) - : m_checksum(algorithm), m_expected(expected){}; + : m_checksum(algorithm), m_expected(expected) {}; virtual ~ChecksumValidator() = default; public: @@ -60,7 +63,11 @@ class ChecksumValidator : public Validator { return true; } - auto abort() -> bool override { return true; } + auto abort() -> bool override + { + m_checksum.reset(); + return true; + } auto validate(QNetworkReply&) -> bool override { diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index bae364f12..49686db98 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -47,8 +47,6 @@ #include "ChecksumValidator.h" #include "MetaCacheSink.h" -#include "net/NetAction.h" - namespace Net { #if defined(LAUNCHER_APPLICATION) diff --git a/launcher/net/FileSink.h b/launcher/net/FileSink.h index 40134b5f4..816254ff9 100644 --- a/launcher/net/FileSink.h +++ b/launcher/net/FileSink.h @@ -42,7 +42,7 @@ namespace Net { class FileSink : public Sink { public: - FileSink(QString filename) : m_filename(filename){}; + FileSink(QString filename) : m_filename(filename) {}; virtual ~FileSink() = default; public: diff --git a/launcher/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp index f37bc0bf8..4985ad080 100644 --- a/launcher/net/HttpMetaCache.cpp +++ b/launcher/net/HttpMetaCache.cpp @@ -84,6 +84,7 @@ auto HttpMetaCache::getEntry(QString base, QString resource_path) -> MetaEntryPt auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString expected_etag) -> MetaEntryPtr { + resource_path = FS::RemoveInvalidPathChars(resource_path); auto entry = getEntry(base, resource_path); // it's not present? generate a default stale entry if (!entry) { @@ -174,6 +175,8 @@ void HttpMetaCache::evictAll() if (!evictEntry(entry)) qCWarning(taskHttpMetaCacheLogC) << "Unexpected missing cache entry" << entry->m_basePath; } + map.entry_list.clear(); + FS::deletePath(map.base_path); } } diff --git a/launcher/net/Logging.cpp b/launcher/net/Logging.cpp index a9b9db7cf..cd0c88d3c 100644 --- a/launcher/net/Logging.cpp +++ b/launcher/net/Logging.cpp @@ -22,5 +22,6 @@ Q_LOGGING_CATEGORY(taskNetLogC, "launcher.task.net") Q_LOGGING_CATEGORY(taskDownloadLogC, "launcher.task.net.download") Q_LOGGING_CATEGORY(taskUploadLogC, "launcher.task.net.upload") +Q_LOGGING_CATEGORY(taskMCSkinsLogC, "launcher.task.minecraft.skins") Q_LOGGING_CATEGORY(taskMetaCacheLogC, "launcher.task.net.metacache") Q_LOGGING_CATEGORY(taskHttpMetaCacheLogC, "launcher.task.net.metacache.http") diff --git a/launcher/net/Logging.h b/launcher/net/Logging.h index 4deed2b49..2536f31aa 100644 --- a/launcher/net/Logging.h +++ b/launcher/net/Logging.h @@ -24,5 +24,6 @@ Q_DECLARE_LOGGING_CATEGORY(taskNetLogC) Q_DECLARE_LOGGING_CATEGORY(taskDownloadLogC) Q_DECLARE_LOGGING_CATEGORY(taskUploadLogC) +Q_DECLARE_LOGGING_CATEGORY(taskMCSkinsLogC) Q_DECLARE_LOGGING_CATEGORY(taskMetaCacheLogC) Q_DECLARE_LOGGING_CATEGORY(taskHttpMetaCacheLogC) diff --git a/launcher/net/NetAction.h b/launcher/net/NetAction.h deleted file mode 100644 index b66b91941..000000000 --- a/launcher/net/NetAction.h +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2022 flowln - * 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 . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include -#include - -#include "QObjectPtr.h" -#include "tasks/Task.h" - -#include "HeaderProxy.h" - -class NetAction : public Task { - Q_OBJECT - protected: - explicit NetAction() : Task() {} - - public: - using Ptr = shared_qobject_ptr; - - virtual ~NetAction() = default; - - QUrl url() { return m_url; } - - void setNetwork(shared_qobject_ptr network) { m_network = network; } - - void addHeaderProxy(Net::HeaderProxy* proxy) { m_headerProxies.push_back(std::shared_ptr(proxy)); } - virtual void init() = 0; - - protected slots: - virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) = 0; - virtual void downloadError(QNetworkReply::NetworkError error) = 0; - virtual void downloadFinished() = 0; - virtual void downloadReadyRead() = 0; - - virtual void sslErrors(const QList& errors) - { - int i = 1; - for (auto error : errors) { - qCritical() << "Network SSL Error #" << i << " : " << error.errorString(); - auto cert = error.certificate(); - qCritical() << "Certificate in question:\n" << cert.toText(); - i++; - } - } - - public slots: - void startAction(shared_qobject_ptr network) - { - m_network = network; - executeTask(); - } - - protected: - void executeTask() override {} - - public: - shared_qobject_ptr m_network; - - /// the network reply - unique_qobject_ptr m_reply; - - /// source URL - QUrl m_url; - std::vector> m_headerProxies; -}; diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp index d027e31c9..e363c911d 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -36,19 +36,26 @@ */ #include "NetJob.h" +#include +#include "net/NetRequest.h" #include "tasks/ConcurrentTask.h" #if defined(LAUNCHER_APPLICATION) #include "Application.h" +#include "ui/dialogs/CustomMessageBox.h" #endif -NetJob::NetJob(QString job_name, shared_qobject_ptr network) : ConcurrentTask(nullptr, job_name), m_network(network) +NetJob::NetJob(QString job_name, shared_qobject_ptr network, int max_concurrent) + : ConcurrentTask(nullptr, job_name), m_network(network) { #if defined(LAUNCHER_APPLICATION) - setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + if (max_concurrent < 0) + max_concurrent = APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt(); #endif + if (max_concurrent > 0) + setMaxConcurrent(max_concurrent); } -auto NetJob::addNetAction(NetAction::Ptr action) -> bool +auto NetJob::addNetAction(Net::NetRequest::Ptr action) -> bool { action->setNetwork(m_network); @@ -62,8 +69,11 @@ void NetJob::executeNextSubTask() // We're finished, check for failures and retry if we can (up to 3 times) if (isRunning() && m_queue.isEmpty() && m_doing.isEmpty() && !m_failed.isEmpty() && m_try < 3) { m_try += 1; - while (!m_failed.isEmpty()) - m_queue.enqueue(m_failed.take(*m_failed.keyBegin())); + while (!m_failed.isEmpty()) { + auto task = m_failed.take(*m_failed.keyBegin()); + m_done.remove(task.get()); + m_queue.enqueue(task); + } } ConcurrentTask::executeNextSubTask(); } @@ -111,11 +121,11 @@ auto NetJob::abort() -> bool return fullyAborted; } -auto NetJob::getFailedActions() -> QList +auto NetJob::getFailedActions() -> QList { - QList failed; + QList failed; for (auto index : m_failed) { - failed.push_back(dynamic_cast(index.get())); + failed.push_back(dynamic_cast(index.get())); } return failed; } @@ -124,7 +134,7 @@ auto NetJob::getFailedFiles() -> QList { QList failed; for (auto index : m_failed) { - failed.append(static_cast(index.get())->url().toString()); + failed.append(static_cast(index.get())->url().toString()); } return failed; } @@ -135,3 +145,45 @@ void NetJob::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()))); } + +bool NetJob::isOnline() +{ + // check some errors that are ussually associated with the lack of internet + for (auto job : getFailedActions()) { + auto err = job->error(); + if (err != QNetworkReply::HostNotFoundError && err != QNetworkReply::NetworkSessionFailedError) { + return true; + } + } + return false; +}; + +void NetJob::emitFailed(QString reason) +{ +#if defined(LAUNCHER_APPLICATION) + if (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" + "Failed urls\n" + + getFailedFiles().join("\n\t") + + ".\n" + "If this continues to happen please check the logs of the application.\n" + "Do you want to retry?", + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response == QMessageBox::Yes) { + m_try = 0; + executeNextSubTask(); + return; + } + } +#endif + ConcurrentTask::emitFailed(reason); +} + +void NetJob::setAskRetry(bool askRetry) +{ + m_ask_retry = askRetry; +} diff --git a/launcher/net/NetJob.h b/launcher/net/NetJob.h index f6c005809..59213ba15 100644 --- a/launcher/net/NetJob.h +++ b/launcher/net/NetJob.h @@ -39,7 +39,7 @@ #include #include -#include "NetAction.h" +#include "net/NetRequest.h" #include "tasks/ConcurrentTask.h" // Those are included so that they are also included by anyone using NetJob @@ -52,29 +52,34 @@ class NetJob : public ConcurrentTask { public: using Ptr = shared_qobject_ptr; - explicit NetJob(QString job_name, shared_qobject_ptr network); + explicit NetJob(QString job_name, shared_qobject_ptr network, int max_concurrent = -1); ~NetJob() override = default; auto size() const -> int; auto canAbort() const -> bool override; - auto addNetAction(NetAction::Ptr action) -> bool; + auto addNetAction(Net::NetRequest::Ptr action) -> bool; - auto getFailedActions() -> QList; + auto getFailedActions() -> QList; auto getFailedFiles() -> QList; + void setAskRetry(bool askRetry); public slots: // Qt can't handle auto at the start for some reason? bool abort() override; + void emitFailed(QString reason) override; protected slots: void executeNextSubTask() override; protected: void updateState() override; + bool isOnline(); private: shared_qobject_ptr m_network; int m_try = 1; + bool m_ask_retry = true; + int m_manual_try = 0; }; diff --git a/launcher/net/NetRequest.cpp b/launcher/net/NetRequest.cpp index 728c0e077..cf2e72858 100644 --- a/launcher/net/NetRequest.cpp +++ b/launcher/net/NetRequest.cpp @@ -5,6 +5,7 @@ * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * 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 @@ -37,10 +38,11 @@ */ #include "NetRequest.h" -#include #include #include +#include +#include #include #if defined(LAUNCHER_APPLICATION) @@ -48,8 +50,6 @@ #endif #include "BuildConfig.h" -#include "net/NetAction.h" - #include "MMCTime.h" #include "StringUtils.h" @@ -62,13 +62,12 @@ void NetRequest::addValidator(Validator* v) void NetRequest::executeTask() { - init(); - setStatus(tr("Requesting %1").arg(StringUtils::truncateUrlHumanFriendly(m_url, 80))); if (getState() == Task::State::AbortedByUser) { qCWarning(logCat) << getUid().toString() << "Attempt to start an aborted Request:" << m_url.toString(); - emitAborted(); + emit aborted(); + emit finished(); return; } @@ -85,10 +84,12 @@ void NetRequest::executeTask() break; case State::Inactive: case State::Failed: - emitFailed(); + emit failed("Failed to initialize sink"); + emit finished(); return; case State::AbortedByUser: - emitAborted(); + emit aborted(); + emit finished(); return; } @@ -102,20 +103,24 @@ void NetRequest::executeTask() for (auto& header_proxy : m_headerProxies) { header_proxy->writeHeaders(request); } - // TODO remove duplication #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) +#if defined(LAUNCHER_APPLICATION) + request.setTransferTimeout(APPLICATION->settings()->get("RequestTimeout").toInt() * 1000); +#else request.setTransferTimeout(); +#endif #endif m_last_progress_time = m_clock.now(); m_last_progress_bytes = 0; - QNetworkReply* rep = getReply(request); + auto rep = getReply(request); if (rep == nullptr) // it failed return; m_reply.reset(rep); - connect(rep, &QNetworkReply::downloadProgress, this, &NetRequest::downloadProgress); + connect(rep, &QNetworkReply::uploadProgress, this, &NetRequest::onProgress); + connect(rep, &QNetworkReply::downloadProgress, this, &NetRequest::onProgress); connect(rep, &QNetworkReply::finished, this, &NetRequest::downloadFinished); #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 connect(rep, &QNetworkReply::errorOccurred, this, &NetRequest::downloadError); @@ -126,7 +131,7 @@ void NetRequest::executeTask() connect(rep, &QNetworkReply::readyRead, this, &NetRequest::downloadReadyRead); } -void NetRequest::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) +void NetRequest::onProgress(qint64 bytesReceived, qint64 bytesTotal) { auto now = m_clock.now(); auto elapsed = now - m_last_progress_time; @@ -169,7 +174,9 @@ void NetRequest::downloadError(QNetworkReply::NetworkError error) } } // error happened during download. - qCCritical(logCat) << getUid().toString() << "Failed " << m_url.toString() << " with reason " << error; + qCCritical(logCat) << getUid().toString() << "Failed" << m_url.toString() << "with reason" << error; + if (m_reply) + qCCritical(logCat) << getUid().toString() << "HTTP Status" << replyStatusCode() << ";error" << errorString(); m_state = State::Failed; } } @@ -234,7 +241,7 @@ auto NetRequest::handleRedirect() -> bool m_url = QUrl(redirect.toString()); qCDebug(logCat) << getUid().toString() << "Following redirect to " << m_url.toString(); - startAction(m_network); + executeTask(); return true; } @@ -252,21 +259,18 @@ void NetRequest::downloadFinished() { qCDebug(logCat) << getUid().toString() << "Request failed but we are allowed to proceed:" << m_url.toString(); m_sink->abort(); - m_reply.reset(); emit succeeded(); emit finished(); return; } else if (m_state == State::Failed) { qCDebug(logCat) << getUid().toString() << "Request failed in previous step:" << m_url.toString(); m_sink->abort(); - m_reply.reset(); - emit failed(""); + emit failed(m_reply->errorString()); emit finished(); return; } else if (m_state == State::AbortedByUser) { qCDebug(logCat) << getUid().toString() << "Request aborted in previous step:" << m_url.toString(); m_sink->abort(); - m_reply.reset(); emit aborted(); emit finished(); return; @@ -280,7 +284,7 @@ void NetRequest::downloadFinished() if (m_state != State::Succeeded) { qCDebug(logCat) << getUid().toString() << "Request failed to write:" << m_url.toString(); m_sink->abort(); - emit failed(""); + emit failed("failed to write in sink"); emit finished(); return; } @@ -291,13 +295,11 @@ void NetRequest::downloadFinished() if (m_state != State::Succeeded) { qCDebug(logCat) << getUid().toString() << "Request failed to finalize:" << m_url.toString(); m_sink->abort(); - m_reply.reset(); - emit failed(""); + emit failed("failed to finalize the request"); emit finished(); return; } - m_reply.reset(); qCDebug(logCat) << getUid().toString() << "Request succeeded:" << m_url.toString(); emit succeeded(); emit finished(); @@ -331,4 +333,23 @@ auto NetRequest::abort() -> bool return true; } +int NetRequest::replyStatusCode() const +{ + return m_reply ? m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() : -1; +} + +QNetworkReply::NetworkError NetRequest::error() const +{ + return m_reply ? m_reply->error() : QNetworkReply::NoError; +} + +QUrl NetRequest::url() const +{ + return m_url; +} + +QString NetRequest::errorString() const +{ + return m_reply ? m_reply->errorString() : ""; +} } // namespace Net diff --git a/launcher/net/NetRequest.h b/launcher/net/NetRequest.h index 0b307b4f6..6b3f643d6 100644 --- a/launcher/net/NetRequest.h +++ b/launcher/net/NetRequest.h @@ -4,6 +4,7 @@ * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * 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 @@ -39,20 +40,23 @@ #pragma once #include +#include +#include #include -#include "NetAction.h" +#include "HeaderProxy.h" #include "Sink.h" #include "Validator.h" #include "QObjectPtr.h" #include "net/Logging.h" +#include "tasks/Task.h" namespace Net { -class NetRequest : public NetAction { +class NetRequest : public Task { Q_OBJECT protected: - explicit NetRequest() : NetAction() {} + explicit NetRequest() : Task() {} public: using Ptr = shared_qobject_ptr; @@ -61,26 +65,29 @@ class NetRequest : public NetAction { public: ~NetRequest() override = default; - - void init() override {} - - public: void addValidator(Validator* v); auto abort() -> bool override; auto canAbort() const -> bool override { return true; } + void setNetwork(shared_qobject_ptr network) { m_network = network; } + void addHeaderProxy(Net::HeaderProxy* proxy) { m_headerProxies.push_back(std::shared_ptr(proxy)); } + + QUrl url() const; + void setUrl(QUrl url) { m_url = url; } + int replyStatusCode() const; + QNetworkReply::NetworkError error() const; + QString errorString() const; + private: auto handleRedirect() -> bool; virtual QNetworkReply* getReply(QNetworkRequest&) = 0; protected slots: - void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; - void downloadError(QNetworkReply::NetworkError error) override; - void sslErrors(const QList& errors) override; - void downloadFinished() override; - void downloadReadyRead() override; - - public slots: + void onProgress(qint64 bytesReceived, qint64 bytesTotal); + void downloadError(QNetworkReply::NetworkError error); + void sslErrors(const QList& errors); + void downloadFinished(); + void downloadReadyRead(); void executeTask() override; protected: @@ -93,6 +100,15 @@ class NetRequest : public NetAction { std::chrono::steady_clock m_clock; std::chrono::time_point m_last_progress_time; qint64 m_last_progress_bytes; + + shared_qobject_ptr m_network; + + /// the network reply + unique_qobject_ptr m_reply; + + /// source URL + QUrl m_url; + std::vector> m_headerProxies; }; } // namespace Net diff --git a/launcher/net/RawHeaderProxy.h b/launcher/net/RawHeaderProxy.h index 09b3d4d02..9de18efc7 100644 --- a/launcher/net/RawHeaderProxy.h +++ b/launcher/net/RawHeaderProxy.h @@ -4,6 +4,7 @@ * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * 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 @@ -27,7 +28,7 @@ namespace Net { class RawHeaderProxy : public HeaderProxy { public: - RawHeaderProxy() : HeaderProxy() {} + RawHeaderProxy(QList headers = {}) : HeaderProxy(), m_headers(std::move(headers)) {}; virtual ~RawHeaderProxy() = default; public: @@ -36,6 +37,7 @@ class RawHeaderProxy : public HeaderProxy { void addHeader(const HeaderPair& header) { m_headers.append(header); } void addHeader(const QByteArray& headerName, const QByteArray& headerValue) { m_headers.append({ headerName, headerValue }); } void addHeaders(const QList& headers) { m_headers.append(headers); } + void setHeaders(QList headers) { m_headers = headers; }; private: QList m_headers; diff --git a/launcher/net/Sink.h b/launcher/net/Sink.h index fcdabf372..d1fd9de10 100644 --- a/launcher/net/Sink.h +++ b/launcher/net/Sink.h @@ -35,9 +35,8 @@ #pragma once -#include "net/NetAction.h" - #include "Validator.h" +#include "tasks/Task.h" namespace Net { class Sink { diff --git a/launcher/net/Upload.cpp b/launcher/net/Upload.cpp index 726572e52..623ec80f4 100644 --- a/launcher/net/Upload.cpp +++ b/launcher/net/Upload.cpp @@ -46,7 +46,8 @@ namespace Net { QNetworkReply* Upload::getReply(QNetworkRequest& request) { - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + if (!request.hasRawHeader("Content-Type")) + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); return m_network->post(request, m_post_data); } diff --git a/launcher/net/Validator.h b/launcher/net/Validator.h index 92ac6ea15..6d1945ee6 100644 --- a/launcher/net/Validator.h +++ b/launcher/net/Validator.h @@ -34,7 +34,7 @@ #pragma once -#include "net/NetAction.h" +#include namespace Net { class Validator { diff --git a/launcher/news/NewsChecker.cpp b/launcher/news/NewsChecker.cpp index 33fb7eceb..169589f78 100644 --- a/launcher/news/NewsChecker.cpp +++ b/launcher/news/NewsChecker.cpp @@ -58,6 +58,7 @@ void NewsChecker::reloadNews() NetJob::Ptr job{ new NetJob("News RSS Feed", m_network) }; job->addNetAction(Net::Download::makeByteArray(m_feedUrl, newsData)); + job->setAskRetry(false); QObject::connect(job.get(), &NetJob::succeeded, this, &NewsChecker::rssDownloadFinished); QObject::connect(job.get(), &NetJob::failed, this, &NewsChecker::rssDownloadFailed); m_newsNetJob.reset(job); diff --git a/launcher/pathmatcher/FSTreeMatcher.h b/launcher/pathmatcher/FSTreeMatcher.h index 52f1404df..689f11979 100644 --- a/launcher/pathmatcher/FSTreeMatcher.h +++ b/launcher/pathmatcher/FSTreeMatcher.h @@ -6,7 +6,7 @@ class FSTreeMatcher : public IPathMatcher { public: - virtual ~FSTreeMatcher(){}; + virtual ~FSTreeMatcher() {}; FSTreeMatcher(SeparatorPrefixTree<'/'>& tree) : m_fsTree(tree) {} bool matches(const QString& string) const override { return m_fsTree.covers(string); } diff --git a/launcher/pathmatcher/MultiMatcher.h b/launcher/pathmatcher/MultiMatcher.h index 101595809..3e2bdb95d 100644 --- a/launcher/pathmatcher/MultiMatcher.h +++ b/launcher/pathmatcher/MultiMatcher.h @@ -4,7 +4,7 @@ class MultiMatcher : public IPathMatcher { public: - virtual ~MultiMatcher(){}; + virtual ~MultiMatcher() {}; MultiMatcher() {} MultiMatcher& add(Ptr add) { diff --git a/launcher/pathmatcher/SimplePrefixMatcher.h b/launcher/pathmatcher/SimplePrefixMatcher.h index fc1f5cede..ff3805179 100644 --- a/launcher/pathmatcher/SimplePrefixMatcher.h +++ b/launcher/pathmatcher/SimplePrefixMatcher.h @@ -7,7 +7,7 @@ class SimplePrefixMatcher : public IPathMatcher { public: - virtual ~SimplePrefixMatcher(){}; + virtual ~SimplePrefixMatcher() {}; SimplePrefixMatcher(const QString& prefix) { m_prefix = prefix; diff --git a/launcher/qtlogging.ini b/launcher/qtlogging.ini index 5266de59b..c12d1e109 100644 --- a/launcher/qtlogging.ini +++ b/launcher/qtlogging.ini @@ -5,7 +5,6 @@ qt.*.debug=false # don't log credentials by default launcher.auth.credentials.debug=false -katabasis.*.debug=false # remove the debug lines, other log levels still get through launcher.task.net.download.debug=false # enable or disable whole catageries diff --git a/launcher/resources/documents/documents.qrc b/launcher/resources/documents/documents.qrc index 007efcde3..489d1d5a2 100644 --- a/launcher/resources/documents/documents.qrc +++ b/launcher/resources/documents/documents.qrc @@ -2,6 +2,7 @@ ../../../COPYING.md + login-qr.svg diff --git a/launcher/resources/documents/login-qr.svg b/launcher/resources/documents/login-qr.svg new file mode 100644 index 000000000..1b88e3c83 --- /dev/null +++ b/launcher/resources/documents/login-qr.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc index eeba32186..25edd09e0 100644 --- a/launcher/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -353,5 +353,11 @@ scalable/instances/neoforged.svg 128x128/instances/forge.png 128x128/instances/liteloader.png + + + scalable/adoptium.svg + scalable/azul.svg + scalable/mojang.svg + diff --git a/launcher/resources/multimc/scalable/adoptium.svg b/launcher/resources/multimc/scalable/adoptium.svg new file mode 100644 index 000000000..d48f8b7d9 --- /dev/null +++ b/launcher/resources/multimc/scalable/adoptium.svg @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/launcher/resources/multimc/scalable/azul.svg b/launcher/resources/multimc/scalable/azul.svg new file mode 100644 index 000000000..1c4356eb7 --- /dev/null +++ b/launcher/resources/multimc/scalable/azul.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/launcher/resources/multimc/scalable/mojang.svg b/launcher/resources/multimc/scalable/mojang.svg new file mode 100644 index 000000000..0c1f48d3d --- /dev/null +++ b/launcher/resources/multimc/scalable/mojang.svg @@ -0,0 +1,55 @@ + + Created with Fabric.js 3.6.3 diff --git a/launcher/screenshots/ImgurAlbumCreation.cpp b/launcher/screenshots/ImgurAlbumCreation.cpp index 7e42ff40c..7ee98760a 100644 --- a/launcher/screenshots/ImgurAlbumCreation.cpp +++ b/launcher/screenshots/ImgurAlbumCreation.cpp @@ -46,14 +46,18 @@ #include #include "BuildConfig.h" -#include "net/StaticHeaderProxy.h" +#include "net/RawHeaderProxy.h" Net::NetRequest::Ptr ImgurAlbumCreation::make(std::shared_ptr output, QList screenshots) { auto up = makeShared(); - up->m_url = BuildConfig.IMGUR_BASE_URL + "album.json"; + up->m_url = BuildConfig.IMGUR_BASE_URL + "album"; up->m_sink.reset(new Sink(output)); up->m_screenshots = screenshots; + up->addHeaderProxy(new Net::RawHeaderProxy( + QList{ { "Content-Type", "application/x-www-form-urlencoded" }, + { "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toUtf8() }, + { "Accept", "application/json" } })); return up; } @@ -65,23 +69,13 @@ QNetworkReply* ImgurAlbumCreation::getReply(QNetworkRequest& request) } const QByteArray data = "deletehashes=" + hashes.join(',').toUtf8() + "&title=Minecraft%20Screenshots&privacy=hidden"; return m_network->post(request, data); -}; - -void ImgurAlbumCreation::init() -{ - qDebug() << "Setting up imgur upload"; - auto api_headers = new Net::StaticHeaderProxy( - QList{ { "Content-Type", "application/x-www-form-urlencoded" }, - { "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str() }, - { "Accept", "application/json" } }); - addHeaderProxy(api_headers); } auto ImgurAlbumCreation::Sink::init(QNetworkRequest& request) -> Task::State { m_output.clear(); return Task::State::Running; -}; +} auto ImgurAlbumCreation::Sink::write(QByteArray& data) -> Task::State { diff --git a/launcher/screenshots/ImgurAlbumCreation.h b/launcher/screenshots/ImgurAlbumCreation.h index 7c292db73..f10409b20 100644 --- a/launcher/screenshots/ImgurAlbumCreation.h +++ b/launcher/screenshots/ImgurAlbumCreation.h @@ -49,7 +49,7 @@ class ImgurAlbumCreation : public Net::NetRequest { class Sink : public Net::Sink { public: - Sink(std::shared_ptr res) : m_result(res){}; + Sink(std::shared_ptr res) : m_result(res) {}; virtual ~Sink() = default; public: @@ -67,8 +67,6 @@ class ImgurAlbumCreation : public Net::NetRequest { static NetRequest::Ptr make(std::shared_ptr output, QList screenshots); QNetworkReply* getReply(QNetworkRequest& request) override; - void init() override; - private: QList m_screenshots; }; diff --git a/launcher/screenshots/ImgurUpload.cpp b/launcher/screenshots/ImgurUpload.cpp index 15fb043e4..8b4ef5327 100644 --- a/launcher/screenshots/ImgurUpload.cpp +++ b/launcher/screenshots/ImgurUpload.cpp @@ -36,7 +36,7 @@ #include "ImgurUpload.h" #include "BuildConfig.h" -#include "net/StaticHeaderProxy.h" +#include "net/RawHeaderProxy.h" #include #include @@ -47,15 +47,6 @@ #include #include -void ImgurUpload::init() -{ - qDebug() << "Setting up imgur upload"; - auto api_headers = new Net::StaticHeaderProxy( - QList{ { "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str() }, - { "Accept", "application/json" } }); - addHeaderProxy(api_headers); -} - QNetworkReply* ImgurUpload::getReply(QNetworkRequest& request) { auto file = new QFile(m_fileInfo.absoluteFilePath(), this); @@ -70,25 +61,25 @@ QNetworkReply* ImgurUpload::getReply(QNetworkRequest& request) QHttpPart filePart; filePart.setBodyDevice(file); filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/png"); - filePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"image\""); + filePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"image\"; filename=\"" + file->fileName() + "\""); multipart->append(filePart); QHttpPart typePart; typePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"type\""); typePart.setBody("file"); multipart->append(typePart); QHttpPart namePart; - namePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"name\""); + namePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"title\""); namePart.setBody(m_fileInfo.baseName().toUtf8()); multipart->append(namePart); return m_network->post(request, multipart); -}; +} auto ImgurUpload::Sink::init(QNetworkRequest& request) -> Task::State { m_output.clear(); return Task::State::Running; -}; +} auto ImgurUpload::Sink::write(QByteArray& data) -> Task::State { @@ -124,7 +115,9 @@ auto ImgurUpload::Sink::finalize(QNetworkReply&) -> Task::State Net::NetRequest::Ptr ImgurUpload::make(ScreenShot::Ptr m_shot) { auto up = makeShared(m_shot->m_file); - up->m_url = std::move(BuildConfig.IMGUR_BASE_URL + "upload.json"); + up->m_url = std::move(BuildConfig.IMGUR_BASE_URL + "image"); up->m_sink.reset(new Sink(m_shot)); + up->addHeaderProxy(new Net::RawHeaderProxy(QList{ + { "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toUtf8() }, { "Accept", "application/json" } })); return up; } diff --git a/launcher/screenshots/ImgurUpload.h b/launcher/screenshots/ImgurUpload.h index 5867ad306..f4f71859d 100644 --- a/launcher/screenshots/ImgurUpload.h +++ b/launcher/screenshots/ImgurUpload.h @@ -43,7 +43,7 @@ class ImgurUpload : public Net::NetRequest { public: class Sink : public Net::Sink { public: - Sink(ScreenShot::Ptr shot) : m_shot(shot){}; + Sink(ScreenShot::Ptr shot) : m_shot(shot) {}; virtual ~Sink() = default; public: @@ -62,8 +62,6 @@ class ImgurUpload : public Net::NetRequest { static NetRequest::Ptr make(ScreenShot::Ptr m_shot); - void init() override; - private: virtual QNetworkReply* getReply(QNetworkRequest&) override; const QFileInfo m_fileInfo; diff --git a/launcher/settings/OverrideSetting.h b/launcher/settings/OverrideSetting.h index faa3e7948..3763b5717 100644 --- a/launcher/settings/OverrideSetting.h +++ b/launcher/settings/OverrideSetting.h @@ -29,7 +29,7 @@ class OverrideSetting : public Setting { Q_OBJECT public: - explicit OverrideSetting(std::shared_ptr overriden, std::shared_ptr gate); + explicit OverrideSetting(std::shared_ptr overridden, std::shared_ptr gate); virtual QVariant defValue() const; virtual QVariant get() const; diff --git a/launcher/settings/PassthroughSetting.h b/launcher/settings/PassthroughSetting.h index c776ca951..3f3474003 100644 --- a/launcher/settings/PassthroughSetting.h +++ b/launcher/settings/PassthroughSetting.h @@ -28,7 +28,7 @@ class PassthroughSetting : public Setting { Q_OBJECT public: - explicit PassthroughSetting(std::shared_ptr overriden, std::shared_ptr gate); + explicit PassthroughSetting(std::shared_ptr overridden, std::shared_ptr gate); virtual QVariant defValue() const; virtual QVariant get() const; diff --git a/launcher/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp index 6f4a94e7f..f2ee40c31 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(QObject* parent, QString task_name, int max_concurrent) : Task(parent), 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..0d6709940 100644 --- a/launcher/tasks/ConcurrentTask.h +++ b/launcher/tasks/ConcurrentTask.h @@ -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/tools/GenericProfiler.cpp b/launcher/tools/GenericProfiler.cpp new file mode 100644 index 000000000..594024a7d --- /dev/null +++ b/launcher/tools/GenericProfiler.cpp @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "GenericProfiler.h" + +#include "BaseInstance.h" +#include "launch/LaunchTask.h" +#include "settings/SettingsObject.h" + +class GenericProfiler : public BaseProfiler { + Q_OBJECT + public: + GenericProfiler(SettingsObjectPtr settings, InstancePtr instance, QObject* parent = 0); + + protected: + void beginProfilingImpl(shared_qobject_ptr process); +}; + +GenericProfiler::GenericProfiler(SettingsObjectPtr settings, InstancePtr instance, QObject* parent) + : BaseProfiler(settings, instance, parent) +{} + +void GenericProfiler::beginProfilingImpl(shared_qobject_ptr process) +{ + emit readyToLaunch(tr("Started process: %1").arg(process->pid())); +} + +BaseExternalTool* GenericProfilerFactory::createTool(InstancePtr instance, QObject* parent) +{ + return new GenericProfiler(globalSettings, instance, parent); +} +#include "GenericProfiler.moc" \ No newline at end of file diff --git a/launcher/tools/GenericProfiler.h b/launcher/tools/GenericProfiler.h new file mode 100644 index 000000000..7868990ea --- /dev/null +++ b/launcher/tools/GenericProfiler.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 "BaseProfiler.h" + +class GenericProfilerFactory : public BaseProfilerFactory { + public: + QString name() const override { return "Generic"; } + void registerSettings([[maybe_unused]] SettingsObjectPtr settings) override {}; + BaseExternalTool* createTool(InstancePtr instance, QObject* parent = 0) override; + bool check([[maybe_unused]] QString* error) override { return true; }; + bool check([[maybe_unused]] const QString& path, [[maybe_unused]] QString* error) override { return true; }; +}; diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index 56ade8e32..d03469b78 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -550,9 +550,10 @@ 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); connect(d->m_index_job.get(), &NetJob::failed, this, &TranslationsModel::indexFailed); connect(d->m_index_job.get(), &NetJob::succeeded, this, &TranslationsModel::indexReceived); d->m_index_job->start(); @@ -590,13 +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 rawHash = QByteArray::fromHex(lang->file_sha1.toLatin1()); - dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawHash)); + 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/ColorCache.cpp b/launcher/ui/ColorCache.cpp deleted file mode 100644 index f941a6093..000000000 --- a/launcher/ui/ColorCache.cpp +++ /dev/null @@ -1,32 +0,0 @@ -#include "ColorCache.h" - -/** - * Blend the color with the front color, adapting to the back color - */ -QColor ColorCache::blend(QColor color) -{ - if (Rainbow::luma(m_front) > Rainbow::luma(m_back)) { - // for dark color schemes, produce a fitting color first - color = Rainbow::tint(m_front, color, 0.5); - } - // adapt contrast - return Rainbow::mix(m_front, color, m_bias); -} - -/** - * Blend the color with the back color - */ -QColor ColorCache::blendBackground(QColor color) -{ - // adapt contrast - return Rainbow::mix(m_back, color, m_bias); -} - -void ColorCache::recolorAll() -{ - auto iter = m_colors.begin(); - while (iter != m_colors.end()) { - iter->front = blend(iter->original); - iter->back = blendBackground(iter->original); - } -} diff --git a/launcher/ui/ColorCache.h b/launcher/ui/ColorCache.h deleted file mode 100644 index 1cf292c13..000000000 --- a/launcher/ui/ColorCache.h +++ /dev/null @@ -1,106 +0,0 @@ -#pragma once -#include -#include -#include -#include - -class ColorCache { - public: - ColorCache(QColor front, QColor back, qreal bias) - { - m_front = front; - m_back = back; - m_bias = bias; - }; - - void addColor(int key, QColor color) { m_colors[key] = { color, blend(color), blendBackground(color) }; } - - void setForeground(QColor front) - { - if (m_front != front) { - m_front = front; - recolorAll(); - } - } - - void setBackground(QColor back) - { - if (m_back != back) { - m_back = back; - recolorAll(); - } - } - - QColor getFront(int key) - { - auto iter = m_colors.find(key); - if (iter == m_colors.end()) { - return QColor(); - } - return (*iter).front; - } - - QColor getBack(int key) - { - auto iter = m_colors.find(key); - if (iter == m_colors.end()) { - return QColor(); - } - return (*iter).back; - } - - /** - * Blend the color with the front color, adapting to the back color - */ - QColor blend(QColor color); - - /** - * Blend the color with the back color - */ - QColor blendBackground(QColor color); - - protected: - void recolorAll(); - - protected: - struct ColorEntry { - QColor original; - QColor front; - QColor back; - }; - - protected: - qreal m_bias; - QColor m_front; - QColor m_back; - QMap m_colors; -}; - -class LogColorCache : public ColorCache { - public: - LogColorCache(QColor front, QColor back) : ColorCache(front, back, 1.0) - { - addColor((int)MessageLevel::Launcher, QColor("purple")); - addColor((int)MessageLevel::Debug, QColor("green")); - addColor((int)MessageLevel::Warning, QColor("orange")); - addColor((int)MessageLevel::Error, QColor("red")); - addColor((int)MessageLevel::Fatal, QColor("red")); - addColor((int)MessageLevel::Message, front); - } - - QColor getFront(MessageLevel::Enum level) - { - if (!m_colors.contains((int)level)) { - return ColorCache::getFront((int)MessageLevel::Message); - } - return ColorCache::getFront((int)level); - } - - QColor getBack(MessageLevel::Enum level) - { - if (level == MessageLevel::Fatal) { - return QColor(Qt::black); - } - return QColor(Qt::transparent); - } -}; diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 71768f316..cce6af187 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -77,7 +77,6 @@ #include #include #include -#include #include #include #include @@ -96,7 +95,6 @@ #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ExportInstanceDialog.h" #include "ui/dialogs/ExportPackDialog.h" -#include "ui/dialogs/ExportToModListDialog.h" #include "ui/dialogs/IconPickerDialog.h" #include "ui/dialogs/ImportResourceDialog.h" #include "ui/dialogs/NewInstanceDialog.h" @@ -209,7 +207,6 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi exportInstanceMenu->addAction(ui->actionExportInstanceZip); exportInstanceMenu->addAction(ui->actionExportInstanceMrPack); exportInstanceMenu->addAction(ui->actionExportInstanceFlamePack); - exportInstanceMenu->addAction(ui->actionExportInstanceToModList); ui->actionExportInstance->setMenu(exportInstanceMenu); } @@ -231,10 +228,14 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi setInstanceActionsEnabled(false); // add a close button at the end of the main toolbar when running on gamescope / steam deck - // FIXME: detect if we don't have server side decorations instead + // this is only needed on gamescope because it defaults to an X11/XWayland session and + // does not implement decorations 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 @@ -870,30 +871,6 @@ void MainWindow::on_actionCopyInstance_triggered() runModalTask(task.get()); } -void MainWindow::finalizeInstance(InstancePtr inst) -{ - view->updateGeometries(); - setSelectedInstanceById(inst->id()); - if (APPLICATION->accounts()->anyAccountIsValid()) { - ProgressDialog loadDialog(this); - auto update = inst->createUpdateTask(Net::Mode::Online); - connect(update.get(), &Task::failed, [this](QString reason) { - QString error = QString("Instance load failed: %1").arg(reason); - CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show(); - }); - if (update) { - loadDialog.setSkipButton(true, tr("Abort")); - loadDialog.execWithTask(update.get()); - } - } else { - CustomMessageBox::selectable(this, tr("Error"), - tr("The launcher cannot download Minecraft or update instances unless you have at least " - "one account added.\nPlease add a Microsoft account."), - QMessageBox::Warning) - ->show(); - } -} - void MainWindow::addInstance(const QString& url, const QMap& extra_info) { QString groupName; @@ -998,6 +975,14 @@ void MainWindow::processURLs(QList urls) dlUrlDialod.execWithTask(job.get()); } + } else if (url.scheme() == BuildConfig.LAUNCHER_APP_BINARY_NAME) { + QVariantMap receivedData; + const QUrlQuery query(url.query()); + const auto items = query.queryItems(); + for (auto it = items.begin(), end = items.end(); it != end; ++it) + receivedData.insert(it->first, it->second); + emit APPLICATION->oauthReplyRecieved(receivedData); + continue; } else { dl_url = url; } @@ -1211,6 +1196,11 @@ void MainWindow::on_actionViewCentralModsFolder_triggered() DesktopServices::openPath(APPLICATION->settings()->get("CentralModsDir").toString(), true); } +void MainWindow::on_actionViewSkinsFolder_triggered() +{ + DesktopServices::openPath(APPLICATION->settings()->get("SkinsDir").toString(), true); +} + void MainWindow::on_actionViewIconThemeFolder_triggered() { DesktopServices::openPath(APPLICATION->themeManager()->getIconThemesFolder().path(), true); @@ -1236,6 +1226,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(); @@ -1415,14 +1410,6 @@ void MainWindow::on_actionExportInstanceMrPack_triggered() } } -void MainWindow::on_actionExportInstanceToModList_triggered() -{ - if (m_selectedInstance) { - ExportToModListDialog dlg(m_selectedInstance, this); - dlg.exec(); - } -} - void MainWindow::on_actionExportInstanceFlamePack_triggered() { if (m_selectedInstance) { @@ -1589,7 +1576,7 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered() QFileDialog fileDialog; // workaround to make sure the portal file dialog opens in the desktop directory fileDialog.setDirectoryUrl(desktopPath); - desktopFilePath = fileDialog.getSaveFileName(this, tr("Create Shortcut"), desktopFilePath, tr("Desktop Entries (*.desktop)")); + desktopFilePath = fileDialog.getSaveFileName(this, tr("Create Shortcut"), desktopFilePath, tr("Desktop Entries") + " (*.desktop)"); if (desktopFilePath.isEmpty()) return; // file dialog canceled by user appPath = "flatpak"; diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 07a6e1eba..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,9 @@ class MainWindow : public QMainWindow { void on_actionViewCatPackFolder_triggered(); void on_actionViewIconsFolder_triggered(); void on_actionViewLogsFolder_triggered(); + void on_actionViewJavaFolder_triggered(); + + void on_actionViewSkinsFolder_triggered(); void on_actionViewSelectedInstFolder_triggered(); @@ -158,7 +160,6 @@ class MainWindow : public QMainWindow { void on_actionExportInstanceZip_triggered(); void on_actionExportInstanceMrPack_triggered(); void on_actionExportInstanceFlamePack_triggered(); - void on_actionExportInstanceToModList_triggered(); void on_actionRenameInstance_triggered(); @@ -228,7 +229,6 @@ class MainWindow : public QMainWindow { void runModalTask(Task* task); void instanceFromInstanceTask(InstanceTask* task); - void finalizeInstance(InstancePtr inst); private: Ui::MainWindow* ui; diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index 889012105..89e536b01 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -131,7 +131,7 @@ 0 0 800 - 20 + 27 @@ -191,6 +191,8 @@ + + @@ -491,15 +493,6 @@ CurseForge (zip) - - - - .. - - - Mod List - - @@ -587,6 +580,18 @@ Open the central mods folder in a file browser. + + + + .. + + + &Skins + + + Open the skins folder in a file browser. + + @@ -784,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 17b79ecaa..b652ba991 100644 --- a/launcher/ui/dialogs/AboutDialog.cpp +++ b/launcher/ui/dialogs/AboutDialog.cpp @@ -38,6 +38,7 @@ #include "Application.h" #include "BuildConfig.h" #include "Markdown.h" +#include "StringUtils.h" #include "ui_AboutDialog.h" #include @@ -139,10 +140,10 @@ AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent), ui(new Ui::AboutDia setWindowTitle(tr("About %1").arg(launcherName)); QString chtml = getCreditsHtml(); - ui->creditsText->setHtml(chtml); + ui->creditsText->setHtml(StringUtils::htmlListPatch(chtml)); QString lhtml = getLicenseHtml(); - ui->licenseText->setHtml(lhtml); + ui->licenseText->setHtml(StringUtils::htmlListPatch(lhtml)); ui->urlLabel->setOpenExternalLinks(true); diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index 7a5a16818..5c93053d1 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -40,6 +40,7 @@ #include #include #include +#include BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList& mods, QString hash_type) : QDialog(parent), ui(new Ui::BlockedModsDialog), m_mods(mods), m_hash_type(hash_type) @@ -60,8 +61,13 @@ BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, cons qDebug() << "[Blocked Mods Dialog] Mods List: " << mods; - setupWatch(); - scanPaths(); + // defer setup of file system watchers until after the dialog is shown + // this allows OS (namely macOS) permission prompts to show after the relevant dialog appears + QTimer::singleShot(0, this, [this] { + setupWatch(); + scanPaths(); + update(); + }); this->setWindowTitle(title); ui->labelDescription->setText(text); @@ -158,7 +164,8 @@ void BlockedModsDialog::update() QString watching; for (auto& dir : m_watcher.directories()) { - watching += QString("%1
    ").arg(dir); + QUrl fileURL = QUrl::fromLocalFile(dir); + watching += QString("%2
    ").arg(fileURL.toString(), dir); } ui->textBrowserWatched->setText(watching); @@ -194,6 +201,10 @@ void BlockedModsDialog::setupWatch() void BlockedModsDialog::watchPath(QString path, bool watch_recursive) { auto to_watch = QFileInfo(path); + if (!to_watch.isReadable()) { + qWarning() << "[Blocked Mods Dialog] Failed to add Watch Path (unable to read):" << path; + return; + } auto to_watch_path = to_watch.canonicalFilePath(); if (m_watcher.directories().contains(to_watch_path)) return; // don't watch the same path twice (no loops!) @@ -255,7 +266,7 @@ void BlockedModsDialog::addHashTask(QString path) /// @param path the path to the local file being hashed void BlockedModsDialog::buildHashTask(QString path) { - auto hash_task = Hashing::createBlockedModHasher(path, ModPlatform::ResourceProvider::FLAME, m_hash_type); + auto hash_task = Hashing::createHasher(path, m_hash_type); qDebug() << "[Blocked Mods Dialog] Creating Hash task for path: " << path; diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp index 703736d68..9f2b3ac42 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.cpp +++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -146,7 +146,7 @@ void ExportInstanceDialog::doExport() return; } - auto task = makeShared(output, m_instance->instanceRoot(), files, "", true); + auto task = makeShared(output, m_instance->instanceRoot(), files, "", true, true); connect(task.get(), &Task::failed, this, [this, output](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); diff --git a/launcher/ui/dialogs/ExportPackDialog.cpp b/launcher/ui/dialogs/ExportPackDialog.cpp index 73e44efb1..0278c6cb0 100644 --- a/launcher/ui/dialogs/ExportPackDialog.cpp +++ b/launcher/ui/dialogs/ExportPackDialog.cpp @@ -129,14 +129,14 @@ void ExportPackDialog::done(int result) QString output; if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(name), FS::PathCombine(QDir::homePath(), filename + ".mrpack"), - "Modrinth pack (*.mrpack *.zip)", nullptr); + tr("Modrinth pack") + " (*.mrpack *.zip)", nullptr); if (output.isEmpty()) return; if (!(output.endsWith(".zip") || output.endsWith(".mrpack"))) output.append(".mrpack"); } else { output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(name), FS::PathCombine(QDir::homePath(), filename + ".zip"), - "CurseForge pack (*.zip)", nullptr); + tr("CurseForge pack") + " (*.zip)", nullptr); if (output.isEmpty()) return; if (!output.endsWith(".zip")) diff --git a/launcher/ui/dialogs/ExportToModListDialog.cpp b/launcher/ui/dialogs/ExportToModListDialog.cpp index a343f555a..908743ab0 100644 --- a/launcher/ui/dialogs/ExportToModListDialog.cpp +++ b/launcher/ui/dialogs/ExportToModListDialog.cpp @@ -22,8 +22,7 @@ #include #include "FileSystem.h" #include "Markdown.h" -#include "minecraft/MinecraftInstance.h" -#include "minecraft/mod/ModFolderModel.h" +#include "StringUtils.h" #include "modplatform/helpers/ExportToModList.h" #include "ui_ExportToModListDialog.h" @@ -41,38 +40,31 @@ const QHash ExportToModListDialog::exampleLin { ExportToModList::CSV, "{name},{url},{version},\"{authors}\"" }, }; -ExportToModListDialog::ExportToModListDialog(InstancePtr instance, QWidget* parent) - : QDialog(parent), m_template_changed(false), name(instance->name()), ui(new Ui::ExportToModListDialog) +ExportToModListDialog::ExportToModListDialog(QString name, QList mods, QWidget* parent) + : QDialog(parent), m_mods(mods), m_template_changed(false), m_name(name), ui(new Ui::ExportToModListDialog) { ui->setupUi(this); enableCustom(false); - MinecraftInstance* mcInstance = dynamic_cast(instance.get()); - if (mcInstance) { - mcInstance->loaderModList()->update(); - connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, [this, mcInstance]() { - m_allMods = mcInstance->loaderModList()->allMods(); - triggerImp(); - }); - } - connect(ui->formatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ExportToModListDialog::formatChanged); connect(ui->authorsCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); connect(ui->versionCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); connect(ui->urlCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); + connect(ui->filenameCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); connect(ui->authorsButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Authors); }); connect(ui->versionButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Version); }); connect(ui->urlButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Url); }); + connect(ui->filenameButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::FileName); }); connect(ui->templateText, &QTextEdit::textChanged, this, [this] { - if (ui->templateText->toPlainText() != exampleLines[format]) + if (ui->templateText->toPlainText() != exampleLines[m_format]) ui->formatComboBox->setCurrentIndex(5); - else - triggerImp(); + triggerImp(); }); connect(ui->copyButton, &QPushButton::clicked, this, [this](bool) { this->ui->finalText->selectAll(); this->ui->finalText->copy(); }); + triggerImp(); } ExportToModListDialog::~ExportToModListDialog() @@ -86,38 +78,38 @@ void ExportToModListDialog::formatChanged(int index) case 0: { enableCustom(false); ui->resultText->show(); - format = ExportToModList::HTML; + m_format = ExportToModList::HTML; break; } case 1: { enableCustom(false); ui->resultText->show(); - format = ExportToModList::MARKDOWN; + m_format = ExportToModList::MARKDOWN; break; } case 2: { enableCustom(false); ui->resultText->hide(); - format = ExportToModList::PLAINTXT; + m_format = ExportToModList::PLAINTXT; break; } case 3: { enableCustom(false); ui->resultText->hide(); - format = ExportToModList::JSON; + m_format = ExportToModList::JSON; break; } case 4: { enableCustom(false); ui->resultText->hide(); - format = ExportToModList::CSV; + m_format = ExportToModList::CSV; break; } case 5: { m_template_changed = true; enableCustom(true); ui->resultText->hide(); - format = ExportToModList::CUSTOM; + m_format = ExportToModList::CUSTOM; break; } } @@ -126,8 +118,8 @@ void ExportToModListDialog::formatChanged(int index) void ExportToModListDialog::triggerImp() { - if (format == ExportToModList::CUSTOM) { - ui->finalText->setPlainText(ExportToModList::exportToModList(m_allMods, ui->templateText->toPlainText())); + if (m_format == ExportToModList::CUSTOM) { + ui->finalText->setPlainText(ExportToModList::exportToModList(m_mods, ui->templateText->toPlainText())); return; } auto opt = 0; @@ -137,16 +129,18 @@ void ExportToModListDialog::triggerImp() opt |= ExportToModList::Version; if (ui->urlCheckBox->isChecked()) opt |= ExportToModList::Url; - auto txt = ExportToModList::exportToModList(m_allMods, format, static_cast(opt)); + if (ui->filenameCheckBox->isChecked()) + opt |= ExportToModList::FileName; + auto txt = ExportToModList::exportToModList(m_mods, m_format, static_cast(opt)); ui->finalText->setPlainText(txt); - switch (format) { + switch (m_format) { case ExportToModList::CUSTOM: return; case ExportToModList::HTML: - ui->resultText->setHtml(txt); + ui->resultText->setHtml(StringUtils::htmlListPatch(txt)); break; case ExportToModList::MARKDOWN: - ui->resultText->setHtml(markdownToHTML(txt)); + ui->resultText->setHtml(StringUtils::htmlListPatch(markdownToHTML(txt))); break; case ExportToModList::PLAINTXT: break; @@ -155,7 +149,7 @@ void ExportToModListDialog::triggerImp() case ExportToModList::CSV: break; } - auto exampleLine = exampleLines[format]; + auto exampleLine = exampleLines[m_format]; if (!m_template_changed && ui->templateText->toPlainText() != exampleLine) ui->templateText->setPlainText(exampleLine); } @@ -163,14 +157,19 @@ void ExportToModListDialog::triggerImp() void ExportToModListDialog::done(int result) { if (result == Accepted) { - const QString filename = FS::RemoveInvalidFilenameChars(name); + const QString filename = FS::RemoveInvalidFilenameChars(m_name); const QString output = - QFileDialog::getSaveFileName(this, tr("Export %1").arg(name), FS::PathCombine(QDir::homePath(), filename + extension()), - "File (*.txt *.html *.md *.json *.csv)", nullptr); + QFileDialog::getSaveFileName(this, tr("Export %1").arg(m_name), FS::PathCombine(QDir::homePath(), filename + extension()), + tr("File") + " (*.txt *.html *.md *.json *.csv)", nullptr); 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); @@ -178,7 +177,7 @@ void ExportToModListDialog::done(int result) QString ExportToModListDialog::extension() { - switch (format) { + switch (m_format) { case ExportToModList::HTML: return ".html"; case ExportToModList::MARKDOWN: @@ -197,7 +196,7 @@ QString ExportToModListDialog::extension() void ExportToModListDialog::addExtra(ExportToModList::OptionalData option) { - if (format != ExportToModList::CUSTOM) + if (m_format != ExportToModList::CUSTOM) return; switch (option) { case ExportToModList::Authors: @@ -209,6 +208,9 @@ void ExportToModListDialog::addExtra(ExportToModList::OptionalData option) case ExportToModList::Version: ui->templateText->insertPlainText("{version}"); break; + case ExportToModList::FileName: + ui->templateText->insertPlainText("{filename}"); + break; } } void ExportToModListDialog::enableCustom(bool enabled) @@ -221,4 +223,7 @@ void ExportToModListDialog::enableCustom(bool enabled) ui->urlCheckBox->setHidden(enabled); ui->urlButton->setHidden(!enabled); + + ui->filenameCheckBox->setHidden(enabled); + ui->filenameButton->setHidden(!enabled); } diff --git a/launcher/ui/dialogs/ExportToModListDialog.h b/launcher/ui/dialogs/ExportToModListDialog.h index 9886ae5a0..4ebe203f7 100644 --- a/launcher/ui/dialogs/ExportToModListDialog.h +++ b/launcher/ui/dialogs/ExportToModListDialog.h @@ -20,7 +20,6 @@ #include #include -#include "BaseInstance.h" #include "minecraft/mod/Mod.h" #include "modplatform/helpers/ExportToModList.h" @@ -32,7 +31,7 @@ class ExportToModListDialog : public QDialog { Q_OBJECT public: - explicit ExportToModListDialog(InstancePtr instance, QWidget* parent = nullptr); + explicit ExportToModListDialog(QString name, QList mods, QWidget* parent = nullptr); ~ExportToModListDialog(); void done(int result) override; @@ -46,10 +45,11 @@ class ExportToModListDialog : public QDialog { private: QString extension(); void enableCustom(bool enabled); - QList m_allMods; + + QList m_mods; bool m_template_changed; - QString name; - ExportToModList::Formats format = ExportToModList::Formats::HTML; + QString m_name; + ExportToModList::Formats m_format = ExportToModList::Formats::HTML; Ui::ExportToModListDialog* ui; static const QHash exampleLines; }; diff --git a/launcher/ui/dialogs/ExportToModListDialog.ui b/launcher/ui/dialogs/ExportToModListDialog.ui index 4f8ab52b5..3afda2fa8 100644 --- a/launcher/ui/dialogs/ExportToModListDialog.ui +++ b/launcher/ui/dialogs/ExportToModListDialog.ui @@ -117,6 +117,13 @@ + + + + Filename + + + @@ -138,6 +145,13 @@ + + + + Filename + + + diff --git a/launcher/ui/dialogs/MSALoginDialog.cpp b/launcher/ui/dialogs/MSALoginDialog.cpp index 7df423412..799b5b332 100644 --- a/launcher/ui/dialogs/MSALoginDialog.cpp +++ b/launcher/ui/dialogs/MSALoginDialog.cpp @@ -34,43 +34,65 @@ */ #include "MSALoginDialog.h" +#include "Application.h" + #include "ui_MSALoginDialog.h" #include "DesktopServices.h" -#include "minecraft/auth/AccountTask.h" +#include "minecraft/auth/AuthFlow.h" #include #include +#include #include #include MSALoginDialog::MSALoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::MSALoginDialog) { ui->setupUi(this); - ui->progressBar->setVisible(false); - ui->actionButton->setVisible(false); - // ui->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false); - connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); - connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + // make font monospace + QFont font; + font.setPixelSize(ui->code->fontInfo().pixelSize()); + font.setFamily(APPLICATION->settings()->get("ConsoleFont").toString()); + font.setStyleHint(QFont::Monospace); + font.setFixedPitch(true); + ui->code->setFont(font); + + connect(ui->copyCode, &QPushButton::clicked, this, [this] { QApplication::clipboard()->setText(ui->code->text()); }); + ui->qr->setPixmap(QIcon((":/documents/login-qr.svg")).pixmap(QSize(150, 150))); + connect(ui->loginButton, &QPushButton::clicked, this, [this] { + if (m_url.isValid()) { + if (!DesktopServices::openUrl(m_url)) { + QApplication::clipboard()->setText(m_url.toString()); + } + } + }); } int MSALoginDialog::exec() { - setUserInputsEnabled(false); - ui->progressBar->setVisible(true); - // Setup the login task and start it m_account = MinecraftAccount::createBlankMSA(); - m_loginTask = m_account->loginMSA(); - connect(m_loginTask.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed); - connect(m_loginTask.get(), &Task::succeeded, this, &MSALoginDialog::onTaskSucceeded); - connect(m_loginTask.get(), &Task::status, this, &MSALoginDialog::onTaskStatus); - connect(m_loginTask.get(), &Task::progress, this, &MSALoginDialog::onTaskProgress); - connect(m_loginTask.get(), &AccountTask::showVerificationUriAndCode, this, &MSALoginDialog::showVerificationUriAndCode); - connect(m_loginTask.get(), &AccountTask::hideVerificationUriAndCode, this, &MSALoginDialog::hideVerificationUriAndCode); - connect(&m_externalLoginTimer, &QTimer::timeout, this, &MSALoginDialog::externalLoginTick); - m_loginTask->start(); + m_authflow_task = m_account->login(false); + 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(), &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)); + 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(), &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); + QMetaObject::invokeMethod(m_authflow_task.get(), &Task::start, Qt::QueuedConnection); + QMetaObject::invokeMethod(m_devicecode_task.get(), &Task::start, Qt::QueuedConnection); return QDialog::exec(); } @@ -80,63 +102,12 @@ MSALoginDialog::~MSALoginDialog() delete ui; } -void MSALoginDialog::externalLoginTick() -{ - m_externalLoginElapsed++; - ui->progressBar->setValue(m_externalLoginElapsed); - ui->progressBar->repaint(); - - if (m_externalLoginElapsed >= m_externalLoginTimeout) { - m_externalLoginTimer.stop(); - } -} - -void MSALoginDialog::showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn) -{ - m_externalLoginElapsed = 0; - m_externalLoginTimeout = expiresIn; - - m_externalLoginTimer.setInterval(1000); - m_externalLoginTimer.setSingleShot(false); - m_externalLoginTimer.start(); - - ui->progressBar->setMaximum(expiresIn); - ui->progressBar->setValue(m_externalLoginElapsed); - - QString urlString = uri.toString(); - QString linkString = QString("%2").arg(urlString, urlString); - if (urlString == "https://www.microsoft.com/link" && !code.isEmpty()) { - urlString += QString("?otc=%1").arg(code); - DesktopServices::openUrl(urlString); - ui->label->setText(tr("

    Please login in the opened browser. If no browser was opened, please open up %1 in " - "a browser and put in the code %2 to proceed with login.

    ") - .arg(linkString, code)); - } else { - ui->label->setText( - tr("

    Please open up %1 in a browser and put in the code %2 to proceed with login.

    ").arg(linkString, code)); - } - ui->actionButton->setVisible(true); - connect(ui->actionButton, &QPushButton::clicked, [=]() { - DesktopServices::openUrl(uri); - QClipboard* cb = QApplication::clipboard(); - cb->setText(code); - }); -} - -void MSALoginDialog::hideVerificationUriAndCode() -{ - m_externalLoginTimer.stop(); - ui->actionButton->setVisible(false); -} - -void MSALoginDialog::setUserInputsEnabled(bool enable) -{ - ui->buttonBox->setEnabled(enable); -} - -void MSALoginDialog::onTaskFailed(const QString& reason) +void MSALoginDialog::onTaskFailed(QString reason) { // Set message + m_authflow_task->disconnect(); + m_devicecode_task->disconnect(); + ui->stackedWidget->setCurrentIndex(0); auto lines = reason.split('\n'); QString processed; for (auto line : lines) { @@ -146,37 +117,53 @@ void MSALoginDialog::onTaskFailed(const QString& reason) processed += "
    "; } } - ui->label->setText(processed); - - // Re-enable user-interaction - setUserInputsEnabled(true); - ui->progressBar->setVisible(false); - ui->actionButton->setVisible(false); + ui->status->setText(processed); + auto task = m_authflow_task; + if (task->failReason().isEmpty()) { + task = m_devicecode_task; + } + if (task) { + ui->loadingLabel->setText(task->getStatus()); + } + disconnect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, m_authflow_task.get(), &Task::abort); + disconnect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, m_devicecode_task.get(), &Task::abort); + connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &MSALoginDialog::reject); } -void MSALoginDialog::onTaskSucceeded() +void MSALoginDialog::authorizeWithBrowser(const QUrl& url) { - QDialog::accept(); + ui->stackedWidget->setCurrentIndex(1); + ui->loginButton->setToolTip(QString("
    %1
    ").arg(url.toString())); + m_url = url; } -void MSALoginDialog::onTaskStatus(const QString& status) +void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn) { - ui->label->setText(status); + ui->stackedWidget->setCurrentIndex(1); + + const auto linkString = QString("%2").arg(url, url); + ui->code->setText(code); + auto isDefaultUrl = url == "https://www.microsoft.com/link"; + ui->qr->setVisible(isDefaultUrl); + if (isDefaultUrl) { + ui->qrMessage->setText(tr("Open %1 or scan the QR and enter the above code.").arg(linkString)); + } else { + ui->qrMessage->setText(tr("Open %1 and enter the above code.").arg(linkString)); + } } -void MSALoginDialog::onTaskProgress(qint64 current, qint64 total) +void MSALoginDialog::onTaskStatus(QString status) { - ui->progressBar->setMaximum(total); - ui->progressBar->setValue(current); + ui->stackedWidget->setCurrentIndex(0); + ui->status->setText(status); } // Public interface -MinecraftAccountPtr MSALoginDialog::newAccount(QWidget* parent, QString msg) +MinecraftAccountPtr MSALoginDialog::newAccount(QWidget* parent) { MSALoginDialog dlg(parent); - dlg.ui->label->setText(msg); if (dlg.exec() == QDialog::Accepted) { return dlg.m_account; } return nullptr; -} +} \ No newline at end of file diff --git a/launcher/ui/dialogs/MSALoginDialog.h b/launcher/ui/dialogs/MSALoginDialog.h index 03e276bc0..70f480ca9 100644 --- a/launcher/ui/dialogs/MSALoginDialog.h +++ b/launcher/ui/dialogs/MSALoginDialog.h @@ -19,6 +19,7 @@ #include #include +#include "minecraft/auth/AuthFlow.h" #include "minecraft/auth/MinecraftAccount.h" namespace Ui { @@ -31,29 +32,23 @@ class MSALoginDialog : public QDialog { public: ~MSALoginDialog(); - static MinecraftAccountPtr newAccount(QWidget* parent, QString message); + static MinecraftAccountPtr newAccount(QWidget* parent); int exec() override; private: explicit MSALoginDialog(QWidget* parent = 0); - void setUserInputsEnabled(bool enable); - protected slots: - void onTaskFailed(const QString& reason); - void onTaskSucceeded(); - void onTaskStatus(const QString& status); - void onTaskProgress(qint64 current, qint64 total); - void showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn); - void hideVerificationUriAndCode(); - - void externalLoginTick(); + void onTaskFailed(QString reason); + void onTaskStatus(QString status); + void authorizeWithBrowser(const QUrl& url); + void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn); private: Ui::MSALoginDialog* ui; MinecraftAccountPtr m_account; - shared_qobject_ptr m_loginTask; - QTimer m_externalLoginTimer; - int m_externalLoginElapsed = 0; - int m_externalLoginTimeout = 0; + shared_qobject_ptr m_devicecode_task; + shared_qobject_ptr m_authflow_task; + + QUrl m_url; }; diff --git a/launcher/ui/dialogs/MSALoginDialog.ui b/launcher/ui/dialogs/MSALoginDialog.ui index c18d01a16..c6821782f 100644 --- a/launcher/ui/dialogs/MSALoginDialog.ui +++ b/launcher/ui/dialogs/MSALoginDialog.ui @@ -6,69 +6,348 @@ 0 0 - 491 - 143 + 440 + 430 - - - 0 - 0 - + + + 0 + 430 + Add Microsoft Account - + - - - Message label placeholder. - -aaaaa - - - Qt::RichText - - - true - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + 1 + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 16 + 75 + true + + + + Please wait... + + + Qt::AlignCenter + + + true + + + + + + + Status + + + Qt::AlignCenter + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 250 + 40 + + + + Sign in with Microsoft + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + + + + + + 16 + + + + Or + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 150 + 150 + + + + + 150 + 150 + + + + + + + true + + + Qt::AlignCenter + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 30 + 75 + true + + + + IBeamCursor + + + CODE + + + Qt::AlignCenter + + + Qt::TextBrowserInteraction + + + + + + + Copy code to clipboard + + + + + + + .. + + + + 22 + 22 + + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Info + + + Qt::AlignCenter + + + true + + + true + + + Qt::TextBrowserInteraction + + + + + - - - 24 - - - false + + + QDialogButtonBox::Cancel - - - - - - Open page and copy code - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel - - - - - diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp index 3524d43f8..2e799d2a8 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.cpp +++ b/launcher/ui/dialogs/NewInstanceDialog.cpp @@ -52,6 +52,7 @@ #include #include #include +#include #include #include @@ -63,6 +64,7 @@ #include "ui/pages/modplatform/modrinth/ModrinthPage.h" #include "ui/pages/modplatform/technic/TechnicPage.h" #include "ui/widgets/PageContainer.h" + NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, const QString& url, const QMap& extra_info, @@ -127,7 +129,17 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, updateDialogState(); - restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toByteArray())); + if (APPLICATION->settings()->get("NewInstanceGeometry").isValid()) { + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toByteArray())); + } else { +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + auto screen = parent->screen(); +#else + auto screen = QGuiApplication::primaryScreen(); +#endif + auto geometry = screen->availableSize(); + resize(width(), qMin(geometry.height() - 50, 710)); + } } void NewInstanceDialog::reject() @@ -188,7 +200,7 @@ void NewInstanceDialog::setSuggestedPack(const QString& name, InstanceTask* task importVersion.clear(); if (!task) { - ui->iconButton->setIcon(APPLICATION->icons()->getIcon("default")); + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); importIcon = false; } @@ -204,7 +216,7 @@ void NewInstanceDialog::setSuggestedPack(const QString& name, QString version, I importVersion = std::move(version); if (!task) { - ui->iconButton->setIcon(APPLICATION->icons()->getIcon("default")); + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); importIcon = false; } @@ -224,6 +236,9 @@ void NewInstanceDialog::setSuggestedIconFromFile(const QString& path, const QStr void NewInstanceDialog::setSuggestedIcon(const QString& key) { + if (key == "default") + return; + auto icon = APPLICATION->icons()->getIcon(key); importIcon = false; diff --git a/launcher/ui/dialogs/OfflineLoginDialog.cpp b/launcher/ui/dialogs/OfflineLoginDialog.cpp index 137620be4..b9d1c2915 100644 --- a/launcher/ui/dialogs/OfflineLoginDialog.cpp +++ b/launcher/ui/dialogs/OfflineLoginDialog.cpp @@ -1,8 +1,6 @@ #include "OfflineLoginDialog.h" #include "ui_OfflineLoginDialog.h" -#include "minecraft/auth/AccountTask.h" - #include OfflineLoginDialog::OfflineLoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::OfflineLoginDialog) @@ -28,7 +26,7 @@ void OfflineLoginDialog::accept() // Setup the login task and start it m_account = MinecraftAccount::createOffline(ui->userTextBox->text()); - m_loginTask = m_account->loginOffline(); + m_loginTask = m_account->login(); connect(m_loginTask.get(), &Task::failed, this, &OfflineLoginDialog::onTaskFailed); connect(m_loginTask.get(), &Task::succeeded, this, &OfflineLoginDialog::onTaskSucceeded); connect(m_loginTask.get(), &Task::status, this, &OfflineLoginDialog::onTaskStatus); diff --git a/launcher/ui/dialogs/ProfileSelectDialog.cpp b/launcher/ui/dialogs/ProfileSelectDialog.cpp index a62238bdb..fe03e1b6b 100644 --- a/launcher/ui/dialogs/ProfileSelectDialog.cpp +++ b/launcher/ui/dialogs/ProfileSelectDialog.cpp @@ -20,7 +20,6 @@ #include #include "Application.h" -#include "SkinUtils.h" #include "ui/dialogs/ProgressDialog.h" diff --git a/launcher/ui/dialogs/ProfileSetupDialog.cpp b/launcher/ui/dialogs/ProfileSetupDialog.cpp index 4b0c5b768..385094e23 100644 --- a/launcher/ui/dialogs/ProfileSetupDialog.cpp +++ b/launcher/ui/dialogs/ProfileSetupDialog.cpp @@ -34,6 +34,7 @@ */ #include "ProfileSetupDialog.h" +#include "net/RawHeaderProxy.h" #include "ui_ProfileSetupDialog.h" #include @@ -45,8 +46,8 @@ #include "ui/dialogs/ProgressDialog.h" #include -#include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/Parsers.h" +#include "net/Upload.h" ProfileSetupDialog::ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidget* parent) : QDialog(parent), m_accountToSetup(accountToSetup), ui(new Ui::ProfileSetupDialog) @@ -150,28 +151,27 @@ void ProfileSetupDialog::checkName(const QString& name) currentCheck = name; isChecking = true; - auto token = m_accountToSetup->accessToken(); + QUrl url(QString("https://api.minecraftservices.com/minecraft/profile/name/%1/available").arg(name)); + auto headers = QList{ { "Content-Type", "application/json" }, + { "Accept", "application/json" }, + { "Authorization", QString("Bearer %1").arg(m_accountToSetup->accessToken()).toUtf8() } }; - auto url = QString("https://api.minecraftservices.com/minecraft/profile/name/%1/available").arg(name); - QNetworkRequest request = QNetworkRequest(url); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Accept", "application/json"); - request.setRawHeader("Authorization", QString("Bearer %1").arg(token).toUtf8()); + m_check_response.reset(new QByteArray()); + if (m_check_task) + disconnect(m_check_task.get(), nullptr, this, nullptr); + m_check_task = Net::Download::makeByteArray(url, m_check_response); + m_check_task->addHeaderProxy(new Net::RawHeaderProxy(headers)); - AuthRequest* requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &ProfileSetupDialog::checkFinished); - requestor->get(request); + connect(m_check_task.get(), &Task::finished, this, &ProfileSetupDialog::checkFinished); + + m_check_task->setNetwork(APPLICATION->network()); + m_check_task->start(); } -void ProfileSetupDialog::checkFinished(QNetworkReply::NetworkError error, - QByteArray profileData, - [[maybe_unused]] QList headers) +void ProfileSetupDialog::checkFinished() { - auto requestor = qobject_cast(QObject::sender()); - requestor->deleteLater(); - - if (error == QNetworkReply::NoError) { - auto doc = QJsonDocument::fromJson(profileData); + if (m_check_task->error() == QNetworkReply::NoError) { + auto doc = QJsonDocument::fromJson(*m_check_response); auto root = doc.object(); auto statusValue = root.value("status").toString("INVALID"); if (statusValue == "AVAILABLE") { @@ -195,20 +195,22 @@ void ProfileSetupDialog::setupProfile(const QString& profileName) return; } - auto token = m_accountToSetup->accessToken(); - - auto url = QString("https://api.minecraftservices.com/minecraft/profile"); - QNetworkRequest request = QNetworkRequest(url); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Accept", "application/json"); - request.setRawHeader("Authorization", QString("Bearer %1").arg(token).toUtf8()); - QString payloadTemplate("{\"profileName\":\"%1\"}"); - auto profileData = payloadTemplate.arg(profileName).toUtf8(); - AuthRequest* requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &ProfileSetupDialog::setupProfileFinished); - requestor->post(request, profileData); + QUrl url("https://api.minecraftservices.com/minecraft/profile"); + auto headers = QList{ { "Content-Type", "application/json" }, + { "Accept", "application/json" }, + { "Authorization", QString("Bearer %1").arg(m_accountToSetup->accessToken()).toUtf8() } }; + + m_profile_response.reset(new QByteArray()); + m_profile_task = Net::Upload::makeByteArray(url, m_profile_response, payloadTemplate.arg(profileName).toUtf8()); + m_profile_task->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + connect(m_profile_task.get(), &Task::finished, this, &ProfileSetupDialog::setupProfileFinished); + + m_profile_task->setNetwork(APPLICATION->network()); + m_profile_task->start(); + isWorking = true; auto button = ui->buttonBox->button(QDialogButtonBox::Cancel); @@ -244,22 +246,17 @@ struct MojangError { } // namespace -void ProfileSetupDialog::setupProfileFinished(QNetworkReply::NetworkError error, - QByteArray errorData, - [[maybe_unused]] QList headers) +void ProfileSetupDialog::setupProfileFinished() { - auto requestor = qobject_cast(QObject::sender()); - requestor->deleteLater(); - isWorking = false; - if (error == QNetworkReply::NoError) { + if (m_profile_task->error() == QNetworkReply::NoError) { /* * data contains the profile in the response * ... we could parse it and update the account, but let's just return back to the normal login flow instead... */ accept(); } else { - auto parsedError = MojangError::fromJSON(errorData); + auto parsedError = MojangError::fromJSON(*m_profile_response); ui->errorLabel->setVisible(true); ui->errorLabel->setText(tr("The server returned the following error:") + "\n\n" + parsedError.errorMessage); qDebug() << parsedError.rawError; diff --git a/launcher/ui/dialogs/ProfileSetupDialog.h b/launcher/ui/dialogs/ProfileSetupDialog.h index 09f8124e2..c005a4138 100644 --- a/launcher/ui/dialogs/ProfileSetupDialog.h +++ b/launcher/ui/dialogs/ProfileSetupDialog.h @@ -22,6 +22,8 @@ #include #include +#include "net/Download.h" +#include "net/Upload.h" namespace Ui { class ProfileSetupDialog; @@ -40,10 +42,10 @@ class ProfileSetupDialog : public QDialog { void on_buttonBox_rejected(); void nameEdited(const QString& name); - void checkFinished(QNetworkReply::NetworkError error, QByteArray data, QList headers); void startCheck(); - void setupProfileFinished(QNetworkReply::NetworkError error, QByteArray data, QList headers); + void checkFinished(); + void setupProfileFinished(); protected: void scheduleCheck(const QString& name); @@ -67,4 +69,10 @@ class ProfileSetupDialog : public QDialog { QString currentCheck; QTimer checkStartTimer; + + std::shared_ptr m_check_response; + Net::Download::Ptr m_check_task; + + std::shared_ptr m_profile_response; + Net::Upload::Ptr m_profile_task; }; diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index 1b7b0ac3b..90d3b79a9 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -27,6 +27,7 @@ #include "Application.h" #include "ResourceDownloadTask.h" +#include "minecraft/PackProfile.h" #include "minecraft/mod/ModFolderModel.h" #include "minecraft/mod/ResourcePackFolderModel.h" #include "minecraft/mod/ShaderPackFolderModel.h" @@ -91,6 +92,19 @@ void ResourceDownloadDialog::accept() void ResourceDownloadDialog::reject() { + auto selected = getTasks(); + if (selected.count() > 0) { + auto reply = CustomMessageBox::selectable(this, tr("Confirmation Needed"), + tr("You have %1 selected resources.\n" + "Are you sure you want to close this dialog?") + .arg(selected.count()), + QMessageBox::Question, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + if (reply != QMessageBox::Yes) { + return; + } + } + if (!geometrySaveKey().isEmpty()) APPLICATION->settings()->set(geometrySaveKey(), saveGeometry().toBase64()); @@ -131,6 +145,7 @@ void ResourceDownloadDialog::confirm() confirm_dialog->retranslateUi(resourcesString()); QHash dependencyExtraInfo; + QStringList depNames; if (auto task = getModDependenciesTask(); task) { connect(task.get(), &Task::failed, this, [&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); @@ -153,8 +168,10 @@ void ResourceDownloadDialog::confirm() QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); return; } else { - for (auto dep : task->getDependecies()) + for (auto dep : task->getDependecies()) { addResource(dep->pack, dep->version); + depNames << dep->pack->name; + } dependencyExtraInfo = task->getExtraInfo(); } } @@ -179,6 +196,9 @@ void ResourceDownloadDialog::confirm() } this->accept(); + } else { + for (auto name : depNames) + removeResource(name); } } @@ -355,4 +375,20 @@ QList ShaderPackDownloadDialog::getPages() return pages; } +void ModDownloadDialog::setModMetadata(std::shared_ptr meta) +{ + switch (meta->provider) { + case ModPlatform::ResourceProvider::MODRINTH: + selectPage(Modrinth::id()); + break; + case ModPlatform::ResourceProvider::FLAME: + selectPage(Flame::id()); + break; + } + setWindowTitle(tr("Change %1 version").arg(meta->name)); + m_container->hidePageList(); + m_buttons.hide(); + auto page = selectedPage(); + page->openProject(meta->project_id); +} } // namespace ResourceDownload diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h index a6efca138..7a0d6e895 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.h +++ b/launcher/ui/dialogs/ResourceDownloadDialog.h @@ -107,6 +107,8 @@ class ModDownloadDialog final : public ResourceDownloadDialog { QList getPages() override; GetModDependenciesTask::Ptr getModDependenciesTask() override; + void setModMetadata(std::shared_ptr); + private: BaseInstance* m_instance; }; diff --git a/launcher/ui/dialogs/ResourceUpdateDialog.cpp b/launcher/ui/dialogs/ResourceUpdateDialog.cpp index 378191378..958bc9683 100644 --- a/launcher/ui/dialogs/ResourceUpdateDialog.cpp +++ b/launcher/ui/dialogs/ResourceUpdateDialog.cpp @@ -3,6 +3,7 @@ #include "CustomMessageBox.h" #include "ProgressDialog.h" #include "ScrollMessageBox.h" +#include "StringUtils.h" #include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "modplatform/ModIndex.h" #include "modplatform/flame/FlameAPI.h" @@ -29,9 +30,9 @@ static std::list mcVersions(BaseInstance* inst) return { static_cast(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() }; } -static std::optional mcLoaders(BaseInstance* inst) +static QList mcLoadersList(BaseInstance* inst) { - return { static_cast(inst)->getPackProfile()->getSupportedModLoaders() }; + return static_cast(inst)->getPackProfile()->getModLoadersList(); } ResourceUpdateDialog::ResourceUpdateDialog(QWidget* parent, @@ -40,7 +41,7 @@ ResourceUpdateDialog::ResourceUpdateDialog(QWidget* parent, QList& search_for, bool include_deps, bool filter_loaders) - : ReviewMessageBox(parent, tr("Confirm mods to update"), "") + : ReviewMessageBox(parent, tr("Confirm resources to update"), "") , m_parent(parent) , m_resource_model(resource_model) , m_candidates(search_for) @@ -52,8 +53,8 @@ ResourceUpdateDialog::ResourceUpdateDialog(QWidget* parent, { ReviewMessageBox::setGeometry(0, 0, 800, 600); - ui->explainLabel->setText(tr("You're about to update the following mods:")); - ui->onlyCheckedLabel->setText(tr("Only mods with a check will be updated!")); + ui->explainLabel->setText(tr("You're about to update the following resources:")); + ui->onlyCheckedLabel->setText(tr("Only resources with a check will be updated!")); } void ResourceUpdateDialog::checkCandidates() @@ -75,8 +76,8 @@ void ResourceUpdateDialog::checkCandidates() } ScrollMessageBox message_dialog(m_parent, tr("Metadata generation failed"), - tr("Could not generate metadata for the following mods:
    " - "Do you wish to proceed without those mods?"), + tr("Could not generate metadata for the following resources:
    " + "Do you wish to proceed without those resources?"), text); message_dialog.setModal(true); if (message_dialog.exec() == QDialog::Rejected) { @@ -87,12 +88,12 @@ void ResourceUpdateDialog::checkCandidates() } auto versions = mcVersions(m_instance); - auto loaders = m_filter_loaders ? mcLoaders(m_instance) : std::optional(); + auto loadersList = m_filter_loaders ? mcLoadersList(m_instance) : QList(); SequentialTask check_task(m_parent, tr("Checking for updates")); if (!m_modrinth_to_update.empty()) { - m_modrinth_check_task.reset(new ModrinthCheckUpdate(m_modrinth_to_update, versions, loaders, m_resource_model)); + m_modrinth_check_task.reset(new ModrinthCheckUpdate(m_modrinth_to_update, versions, loadersList, m_resource_model)); connect(m_modrinth_check_task.get(), &CheckUpdateTask::checkFailed, this, [this](Resource* resource, QString reason, QUrl recover_url) { m_failed_check_update.append({ resource, reason, recover_url }); @@ -101,7 +102,7 @@ void ResourceUpdateDialog::checkCandidates() } if (!m_flame_to_update.empty()) { - m_flame_check_task.reset(new FlameCheckUpdate(m_flame_to_update, versions, loaders, m_resource_model)); + m_flame_check_task.reset(new FlameCheckUpdate(m_flame_to_update, versions, loadersList, m_resource_model)); connect(m_flame_check_task.get(), &CheckUpdateTask::checkFailed, this, [this](Resource* resource, QString reason, QUrl recover_url) { m_failed_check_update.append({ resource, reason, recover_url }); @@ -179,8 +180,8 @@ void ResourceUpdateDialog::checkCandidates() } ScrollMessageBox message_dialog(m_parent, tr("Failed to check for updates"), - tr("Could not check or get the following mods for updates:
    " - "Do you wish to proceed without those mods?"), + tr("Could not check or get the following resources for updates:
    " + "Do you wish to proceed without those resources?"), text); message_dialog.setModal(true); if (message_dialog.exec() == QDialog::Rejected) { @@ -474,7 +475,7 @@ void ResourceUpdateDialog::appendResource(CheckUpdateTask::Update const& info, Q break; } - changelog_area->setHtml(text); + changelog_area->setHtml(StringUtils::htmlListPatch(text)); changelog_area->setOpenExternalLinks(true); changelog_area->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth); changelog_area->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded); diff --git a/launcher/ui/dialogs/SkinUploadDialog.cpp b/launcher/ui/dialogs/SkinUploadDialog.cpp deleted file mode 100644 index 5b3ebfa23..000000000 --- a/launcher/ui/dialogs/SkinUploadDialog.cpp +++ /dev/null @@ -1,164 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Sefa Eyeoglu - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include -#include - -#include - -#include -#include -#include - -#include "CustomMessageBox.h" -#include "ProgressDialog.h" -#include "SkinUploadDialog.h" -#include "ui_SkinUploadDialog.h" - -void SkinUploadDialog::on_buttonBox_rejected() -{ - close(); -} - -void SkinUploadDialog::on_buttonBox_accepted() -{ - QString fileName; - QString input = ui->skinPathTextBox->text(); - ProgressDialog prog(this); - SequentialTask skinUpload; - - if (!input.isEmpty()) { - QRegularExpression urlPrefixMatcher(QRegularExpression::anchoredPattern("^([a-z]+)://.+$")); - bool isLocalFile = false; - // it has an URL prefix -> it is an URL - if (urlPrefixMatcher.match(input).hasMatch()) { - QUrl fileURL = input; - if (fileURL.isValid()) { - // local? - if (fileURL.isLocalFile()) { - isLocalFile = true; - fileName = fileURL.toLocalFile(); - } else { - CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Using remote URLs for setting skins is not implemented yet."), - QMessageBox::Warning) - ->exec(); - close(); - return; - } - } else { - CustomMessageBox::selectable(this, tr("Skin Upload"), tr("You cannot use an invalid URL for uploading skins."), - QMessageBox::Warning) - ->exec(); - close(); - return; - } - } else { - // just assume it's a path then - isLocalFile = true; - fileName = ui->skinPathTextBox->text(); - } - if (isLocalFile && !QFile::exists(fileName)) { - CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning)->exec(); - close(); - return; - } - SkinUpload::Model model = SkinUpload::STEVE; - if (ui->steveBtn->isChecked()) { - model = SkinUpload::STEVE; - } else if (ui->alexBtn->isChecked()) { - model = SkinUpload::ALEX; - } - skinUpload.addTask(shared_qobject_ptr(new SkinUpload(this, m_acct->accessToken(), FS::read(fileName), model))); - } - - auto selectedCape = ui->capeCombo->currentData().toString(); - if (selectedCape != m_acct->accountData()->minecraftProfile.currentCape) { - skinUpload.addTask(shared_qobject_ptr(new CapeChange(this, m_acct->accessToken(), selectedCape))); - } - if (prog.execWithTask(&skinUpload) != QDialog::Accepted) { - CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to upload skin!"), QMessageBox::Warning)->exec(); - close(); - return; - } - CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Success"), QMessageBox::Information)->exec(); - close(); -} - -void SkinUploadDialog::on_skinBrowseBtn_clicked() -{ - auto filter = QMimeDatabase().mimeTypeForName("image/png").filterString(); - QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), filter); - if (raw_path.isEmpty() || !QFileInfo::exists(raw_path)) { - return; - } - QString cooked_path = FS::NormalizePath(raw_path); - ui->skinPathTextBox->setText(cooked_path); -} - -SkinUploadDialog::SkinUploadDialog(MinecraftAccountPtr acct, QWidget* parent) : QDialog(parent), m_acct(acct), ui(new Ui::SkinUploadDialog) -{ - ui->setupUi(this); - - // FIXME: add a model for this, download/refresh the capes on demand - auto& accountData = *acct->accountData(); - int index = 0; - ui->capeCombo->addItem(tr("No Cape"), QVariant()); - auto currentCape = accountData.minecraftProfile.currentCape; - if (currentCape.isEmpty()) { - ui->capeCombo->setCurrentIndex(index); - } - - for (auto& cape : accountData.minecraftProfile.capes) { - index++; - if (cape.data.size()) { - QPixmap capeImage; - if (capeImage.loadFromData(cape.data, "PNG")) { - QPixmap preview = QPixmap(10, 16); - QPainter painter(&preview); - painter.drawPixmap(0, 0, capeImage.copy(1, 1, 10, 16)); - ui->capeCombo->addItem(capeImage, cape.alias, cape.id); - if (currentCape == cape.id) { - ui->capeCombo->setCurrentIndex(index); - } - continue; - } - } - ui->capeCombo->addItem(cape.alias, cape.id); - if (currentCape == cape.id) { - ui->capeCombo->setCurrentIndex(index); - } - } -} diff --git a/launcher/ui/dialogs/SkinUploadDialog.h b/launcher/ui/dialogs/SkinUploadDialog.h deleted file mode 100644 index 81d6140cc..000000000 --- a/launcher/ui/dialogs/SkinUploadDialog.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include -#include - -namespace Ui { -class SkinUploadDialog; -} - -class SkinUploadDialog : public QDialog { - Q_OBJECT - public: - explicit SkinUploadDialog(MinecraftAccountPtr acct, QWidget* parent = 0); - virtual ~SkinUploadDialog(){}; - - public slots: - void on_buttonBox_accepted(); - - void on_buttonBox_rejected(); - - void on_skinBrowseBtn_clicked(); - - protected: - MinecraftAccountPtr m_acct; - - private: - Ui::SkinUploadDialog* ui; -}; diff --git a/launcher/ui/dialogs/SkinUploadDialog.ui b/launcher/ui/dialogs/SkinUploadDialog.ui deleted file mode 100644 index c6df92df3..000000000 --- a/launcher/ui/dialogs/SkinUploadDialog.ui +++ /dev/null @@ -1,95 +0,0 @@ - - - SkinUploadDialog - - - - 0 - 0 - 394 - 360 - - - - Skin Upload - - - - - - Skin File - - - - - - Leave empty to keep current skin - - - - - - - - 0 - 0 - - - - Browse - - - - - - - - - - Player Model - - - - - - Steve Model - - - true - - - - - - - Alex Model - - - - - - - - - - Cape - - - - - - - - - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - diff --git a/launcher/ui/dialogs/UpdateAvailableDialog.cpp b/launcher/ui/dialogs/UpdateAvailableDialog.cpp index 5eebe87a3..810a1f089 100644 --- a/launcher/ui/dialogs/UpdateAvailableDialog.cpp +++ b/launcher/ui/dialogs/UpdateAvailableDialog.cpp @@ -25,6 +25,7 @@ #include "Application.h" #include "BuildConfig.h" #include "Markdown.h" +#include "StringUtils.h" #include "ui_UpdateAvailableDialog.h" UpdateAvailableDialog::UpdateAvailableDialog(const QString& currentVersion, @@ -43,7 +44,7 @@ UpdateAvailableDialog::UpdateAvailableDialog(const QString& currentVersion, ui->icon->setPixmap(APPLICATION->getThemedIcon("checkupdate").pixmap(64)); auto releaseNotesHtml = markdownToHTML(releaseNotes); - ui->releaseNotes->setHtml(releaseNotesHtml); + ui->releaseNotes->setHtml(StringUtils::htmlListPatch(releaseNotesHtml)); ui->releaseNotes->setOpenExternalLinks(true); connect(ui->skipButton, &QPushButton::clicked, this, [this]() { diff --git a/launcher/ui/dialogs/VersionSelectDialog.cpp b/launcher/ui/dialogs/VersionSelectDialog.cpp index c61d10578..876d7470e 100644 --- a/launcher/ui/dialogs/VersionSelectDialog.cpp +++ b/launcher/ui/dialogs/VersionSelectDialog.cpp @@ -121,7 +121,7 @@ void VersionSelectDialog::setResizeOn(int column) int VersionSelectDialog::exec() { QDialog::open(); - m_versionWidget->initialize(m_vlist); + m_versionWidget->initialize(m_vlist, true); m_versionWidget->selectSearch(); if (resizeOnColumn != -1) { m_versionWidget->setResizeOn(resizeOnColumn); diff --git a/launcher/ui/dialogs/VersionSelectDialog.h b/launcher/ui/dialogs/VersionSelectDialog.h index 0ccd45e74..ed1de607b 100644 --- a/launcher/ui/dialogs/VersionSelectDialog.h +++ b/launcher/ui/dialogs/VersionSelectDialog.h @@ -26,10 +26,6 @@ class QDialogButtonBox; class VersionSelectWidget; class QPushButton; -namespace Ui { -class VersionSelectDialog; -} - class VersionProxyModel; class VersionSelectDialog : public QDialog { @@ -37,7 +33,7 @@ class VersionSelectDialog : public QDialog { public: explicit VersionSelectDialog(BaseVersionList* vlist, QString title, QWidget* parent = 0, bool cancelable = true); - virtual ~VersionSelectDialog(){}; + virtual ~VersionSelectDialog() = default; int exec() override; diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp new file mode 100644 index 000000000..6c85ffa96 --- /dev/null +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -0,0 +1,520 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "SkinManageDialog.h" +#include "ui_SkinManageDialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Application.h" +#include "DesktopServices.h" +#include "Json.h" +#include "QObjectPtr.h" + +#include "minecraft/auth/Parsers.h" +#include "minecraft/skins/CapeChange.h" +#include "minecraft/skins/SkinDelete.h" +#include "minecraft/skins/SkinList.h" +#include "minecraft/skins/SkinModel.h" +#include "minecraft/skins/SkinUpload.h" + +#include "net/Download.h" +#include "net/NetJob.h" +#include "tasks/Task.h" + +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" +#include "ui/instanceview/InstanceDelegate.h" + +SkinManageDialog::SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct) + : QDialog(parent), m_acct(acct), ui(new Ui::SkinManageDialog), m_list(this, APPLICATION->settings()->get("SkinsDir").toString(), acct) +{ + ui->setupUi(this); + + setWindowModality(Qt::WindowModal); + + auto contentsWidget = ui->listView; + contentsWidget->setViewMode(QListView::IconMode); + contentsWidget->setFlow(QListView::LeftToRight); + contentsWidget->setIconSize(QSize(48, 48)); + contentsWidget->setMovement(QListView::Static); + contentsWidget->setResizeMode(QListView::Adjust); + contentsWidget->setSelectionMode(QAbstractItemView::SingleSelection); + contentsWidget->setSpacing(5); + contentsWidget->setWordWrap(false); + contentsWidget->setWrapping(true); + contentsWidget->setUniformItemSizes(true); + contentsWidget->setTextElideMode(Qt::ElideRight); + contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + contentsWidget->installEventFilter(this); + contentsWidget->setItemDelegate(new ListViewDelegate(this)); + + contentsWidget->setAcceptDrops(true); + contentsWidget->setDropIndicatorShown(true); + contentsWidget->viewport()->setAcceptDrops(true); + contentsWidget->setDragDropMode(QAbstractItemView::DropOnly); + contentsWidget->setDefaultDropAction(Qt::CopyAction); + + contentsWidget->installEventFilter(this); + contentsWidget->setModel(&m_list); + + connect(contentsWidget, SIGNAL(doubleClicked(QModelIndex)), SLOT(activated(QModelIndex))); + + connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), + SLOT(selectionChanged(QItemSelection, QItemSelection))); + connect(ui->listView, &QListView::customContextMenuRequested, this, &SkinManageDialog::show_context_menu); + + setupCapes(); + + ui->listView->setCurrentIndex(m_list.index(m_list.getSelectedAccountSkin())); +} + +SkinManageDialog::~SkinManageDialog() +{ + delete ui; +} + +void SkinManageDialog::activated(QModelIndex index) +{ + m_selected_skin = index.data(Qt::UserRole).toString(); + accept(); +} + +void SkinManageDialog::selectionChanged(QItemSelection selected, QItemSelection deselected) +{ + if (selected.empty()) + return; + + QString key = selected.first().indexes().first().data(Qt::UserRole).toString(); + if (key.isEmpty()) + return; + m_selected_skin = key; + auto skin = m_list.skin(key); + 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())); + ui->steveBtn->setChecked(skin->getModel() == SkinModel::CLASSIC); + ui->alexBtn->setChecked(skin->getModel() == SkinModel::SLIM); +} + +void SkinManageDialog::delayed_scroll(QModelIndex model_index) +{ + auto contentsWidget = ui->listView; + contentsWidget->scrollTo(model_index); +} + +void SkinManageDialog::on_openDirBtn_clicked() +{ + DesktopServices::openPath(m_list.getDir(), true); +} + +void SkinManageDialog::on_fileBtn_clicked() +{ + auto filter = QMimeDatabase().mimeTypeForName("image/png").filterString(); + QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), filter); + 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(); + return; + } +} + +QPixmap previewCape(QPixmap capeImage) +{ + QPixmap preview = QPixmap(10, 16); + QPainter painter(&preview); + painter.drawPixmap(0, 0, capeImage.copy(1, 1, 10, 16)); + return preview.scaled(80, 128, Qt::IgnoreAspectRatio, Qt::FastTransformation); +} + +void SkinManageDialog::setupCapes() +{ + // FIXME: add a model for this, download/refresh the capes on demand + auto& accountData = *m_acct->accountData(); + int index = 0; + ui->capeCombo->addItem(tr("No Cape"), QVariant()); + auto currentCape = accountData.minecraftProfile.currentCape; + if (currentCape.isEmpty()) { + ui->capeCombo->setCurrentIndex(index); + } + + auto capesDir = FS::PathCombine(m_list.getDir(), "capes"); + NetJob::Ptr job{ new NetJob(tr("Download capes"), APPLICATION->network()) }; + bool needsToDownload = false; + for (auto& cape : accountData.minecraftProfile.capes) { + auto path = FS::PathCombine(capesDir, cape.id + ".png"); + if (cape.data.size()) { + QPixmap capeImage; + if (capeImage.loadFromData(cape.data, "PNG") && capeImage.save(path)) { + m_capes[cape.id] = previewCape(capeImage); + continue; + } + } + if (QFileInfo(path).exists()) { + continue; + } + if (!cape.url.isEmpty()) { + needsToDownload = true; + job->addNetAction(Net::Download::makeFile(cape.url, path)); + } + } + if (needsToDownload) { + ProgressDialog dlg(this); + dlg.execWithTask(job.get()); + } + for (auto& cape : accountData.minecraftProfile.capes) { + index++; + QPixmap capeImage; + if (!m_capes.contains(cape.id)) { + auto path = FS::PathCombine(capesDir, cape.id + ".png"); + if (QFileInfo(path).exists() && capeImage.load(path)) { + capeImage = previewCape(capeImage); + m_capes[cape.id] = capeImage; + } + } + if (!capeImage.isNull()) { + ui->capeCombo->addItem(capeImage, cape.alias, cape.id); + } else { + ui->capeCombo->addItem(cape.alias, cape.id); + } + + m_capes_idx[cape.id] = index; + } +} + +void SkinManageDialog::on_capeCombo_currentIndexChanged(int index) +{ + auto id = ui->capeCombo->currentData(); + 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()); + } +} + +void SkinManageDialog::on_steveBtn_toggled(bool checked) +{ + if (auto skin = m_list.skin(m_selected_skin); skin) { + skin->setModel(checked ? SkinModel::CLASSIC : SkinModel::SLIM); + } +} + +void SkinManageDialog::accept() +{ + auto skin = m_list.skin(m_selected_skin); + if (!skin) { + reject(); + return; + } + auto path = skin->getPath(); + + ProgressDialog prog(this); + NetJob::Ptr skinUpload{ new NetJob(tr("Change skin"), APPLICATION->network(), 1) }; + + if (!QFile::exists(path)) { + CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning)->exec(); + reject(); + return; + } + + skinUpload->addNetAction(SkinUpload::make(m_acct->accessToken(), skin->getPath(), skin->getModelString())); + + auto selectedCape = skin->getCapeId(); + if (selectedCape != m_acct->accountData()->minecraftProfile.currentCape) { + skinUpload->addNetAction(CapeChange::make(m_acct->accessToken(), selectedCape)); + } + + skinUpload->addTask(m_acct->refresh().staticCast()); + if (prog.execWithTask(skinUpload.get()) != QDialog::Accepted) { + CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to upload skin!"), QMessageBox::Warning)->exec(); + reject(); + return; + } + skin->setURL(m_acct->accountData()->minecraftProfile.skin.url); + QDialog::accept(); +} + +void SkinManageDialog::on_resetBtn_clicked() +{ + ProgressDialog prog(this); + NetJob::Ptr skinReset{ new NetJob(tr("Reset skin"), APPLICATION->network(), 1) }; + skinReset->addNetAction(SkinDelete::make(m_acct->accessToken())); + skinReset->addTask(m_acct->refresh().staticCast()); + if (prog.execWithTask(skinReset.get()) != QDialog::Accepted) { + CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to delete current skin!"), QMessageBox::Warning)->exec(); + reject(); + return; + } + QDialog::accept(); +} + +void SkinManageDialog::show_context_menu(const QPoint& pos) +{ + QMenu myMenu(tr("Context menu"), this); + myMenu.addAction(ui->action_Rename_Skin); + myMenu.addAction(ui->action_Delete_Skin); + + myMenu.exec(ui->listView->mapToGlobal(pos)); +} + +bool SkinManageDialog::eventFilter(QObject* obj, QEvent* ev) +{ + if (obj == ui->listView) { + if (ev->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(ev); + switch (keyEvent->key()) { + case Qt::Key_Delete: + on_action_Delete_Skin_triggered(false); + return true; + case Qt::Key_F2: + on_action_Rename_Skin_triggered(false); + return true; + default: + break; + } + } + } + return QDialog::eventFilter(obj, ev); +} + +void SkinManageDialog::on_action_Rename_Skin_triggered(bool checked) +{ + if (!m_selected_skin.isEmpty()) { + ui->listView->edit(ui->listView->currentIndex()); + } +} + +void SkinManageDialog::on_action_Delete_Skin_triggered(bool checked) +{ + if (m_selected_skin.isEmpty()) + return; + + if (m_list.getSkinIndex(m_selected_skin) == m_list.getSelectedAccountSkin()) { + CustomMessageBox::selectable(this, tr("Delete error"), tr("Can not delete skin that is in use."), QMessageBox::Warning)->exec(); + return; + } + + auto skin = m_list.skin(m_selected_skin); + if (!skin) + return; + + auto response = CustomMessageBox::selectable(this, tr("Confirm Deletion"), + tr("You are about to delete \"%1\".\n" + "Are you sure?") + .arg(skin->name()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response == QMessageBox::Yes) { + if (!m_list.deleteSkin(m_selected_skin, true)) { + m_list.deleteSkin(m_selected_skin, false); + } + } +} + +void SkinManageDialog::on_urlBtn_clicked() +{ + auto url = QUrl(ui->urlLine->text()); + if (!url.isValid()) { + CustomMessageBox::selectable(this, tr("Invalid url"), tr("Invalid url"), QMessageBox::Critical)->show(); + return; + } + + NetJob::Ptr job{ new NetJob(tr("Download skin"), APPLICATION->network()) }; + job->setAskRetry(false); + + auto path = FS::PathCombine(m_list.getDir(), url.fileName()); + job->addNetAction(Net::Download::makeFile(url, path)); + ProgressDialog dlg(this); + dlg.execWithTask(job.get()); + SkinModel s(path); + if (!s.isValid()) { + CustomMessageBox::selectable(this, tr("URL is not a valid skin"), + QFileInfo::exists(path) ? tr("Skin images must be 64x64 or 64x32 pixel PNG files.") + : tr("Unable to download the skin: '%1'.").arg(ui->urlLine->text()), + QMessageBox::Critical) + ->show(); + QFile::remove(path); + return; + } + ui->urlLine->setText(""); + if (QFileInfo(path).suffix().isEmpty()) { + QFile::rename(path, path + ".png"); + } +} + +class WaitTask : public Task { + public: + WaitTask() : m_loop(), m_done(false) {}; + virtual ~WaitTask() = default; + + public slots: + void quit() + { + m_done = true; + m_loop.quit(); + } + + protected: + virtual void executeTask() + { + if (!m_done) + m_loop.exec(); + emitSucceeded(); + }; + + private: + QEventLoop m_loop; + bool m_done; +}; + +void SkinManageDialog::on_userBtn_clicked() +{ + auto user = ui->urlLine->text(); + if (user.isEmpty()) { + return; + } + MinecraftProfile mcProfile; + auto path = FS::PathCombine(m_list.getDir(), user + ".png"); + + NetJob::Ptr job{ new NetJob(tr("Download user skin"), APPLICATION->network(), 1) }; + job->setAskRetry(false); + + auto uuidOut = std::make_shared(); + auto profileOut = std::make_shared(); + + auto uuidLoop = makeShared(); + auto profileLoop = makeShared(); + + auto getUUID = Net::Download::makeByteArray("https://api.mojang.com/users/profiles/minecraft/" + user, uuidOut); + auto getProfile = Net::Download::makeByteArray(QUrl(), profileOut); + auto downloadSkin = Net::Download::makeFile(QUrl(), path); + + QString failReason; + + connect(getUUID.get(), &Task::aborted, uuidLoop.get(), &WaitTask::quit); + connect(getUUID.get(), &Task::failed, this, [&failReason](QString reason) { + qCritical() << "Couldn't get user UUID:" << reason; + failReason = tr("failed to get user UUID"); + }); + connect(getUUID.get(), &Task::failed, uuidLoop.get(), &WaitTask::quit); + connect(getProfile.get(), &Task::aborted, profileLoop.get(), &WaitTask::quit); + connect(getProfile.get(), &Task::failed, profileLoop.get(), &WaitTask::quit); + connect(getProfile.get(), &Task::failed, this, [&failReason](QString reason) { + qCritical() << "Couldn't get user profile:" << reason; + failReason = tr("failed to get user profile"); + }); + connect(downloadSkin.get(), &Task::failed, this, [&failReason](QString reason) { + qCritical() << "Couldn't download skin:" << reason; + failReason = tr("failed to download skin"); + }); + + connect(getUUID.get(), &Task::succeeded, this, [uuidLoop, uuidOut, job, getProfile, &failReason] { + try { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*uuidOut, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Minecraft skin service at " << parse_error.offset + << " reason: " << parse_error.errorString(); + failReason = tr("failed to parse get user UUID response"); + uuidLoop->quit(); + return; + } + const auto root = doc.object(); + auto id = Json::ensureString(root, "id"); + if (!id.isEmpty()) { + getProfile->setUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + id); + } else { + failReason = tr("user id is empty"); + job->abort(); + } + } catch (const Exception& e) { + qCritical() << "Couldn't load skin json:" << e.cause(); + failReason = tr("failed to parse get user UUID response"); + } + uuidLoop->quit(); + }); + + connect(getProfile.get(), &Task::succeeded, this, [profileLoop, profileOut, job, getProfile, &mcProfile, downloadSkin, &failReason] { + if (Parsers::parseMinecraftProfileMojang(*profileOut, mcProfile)) { + downloadSkin->setUrl(mcProfile.skin.url); + } else { + failReason = tr("failed to parse get user profile response"); + job->abort(); + } + profileLoop->quit(); + }); + + job->addNetAction(getUUID); + job->addTask(uuidLoop); + job->addNetAction(getProfile); + job->addTask(profileLoop); + job->addNetAction(downloadSkin); + ProgressDialog dlg(this); + dlg.execWithTask(job.get()); + + SkinModel s(path); + if (!s.isValid()) { + if (failReason.isEmpty()) { + failReason = tr("the skin is invalid"); + } + CustomMessageBox::selectable(this, tr("Username not found"), + tr("Unable to find the skin for '%1'\n because: %2.").arg(user, failReason), QMessageBox::Critical) + ->show(); + QFile::remove(path); + return; + } + ui->urlLine->setText(""); + s.setModel(mcProfile.skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); + s.setURL(mcProfile.skin.url); + if (m_capes.contains(mcProfile.currentCape)) { + s.setCapeId(mcProfile.currentCape); + } + m_list.updateSkin(&s); +} + +void SkinManageDialog::resizeEvent(QResizeEvent* event) +{ + QWidget::resizeEvent(event); + QSize s = size() * (1. / 3); + + if (auto skin = m_list.skin(m_selected_skin); skin) { + if (skin->isValid()) { + ui->selectedModel->setPixmap(skin->getTexture().scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); + } + } + auto id = ui->capeCombo->currentData(); + 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/dialogs/skins/SkinManageDialog.h b/launcher/ui/dialogs/skins/SkinManageDialog.h new file mode 100644 index 000000000..cdb37a513 --- /dev/null +++ b/launcher/ui/dialogs/skins/SkinManageDialog.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include + +#include "minecraft/auth/MinecraftAccount.h" +#include "minecraft/skins/SkinList.h" + +namespace Ui { +class SkinManageDialog; +} + +class SkinManageDialog : public QDialog { + Q_OBJECT + public: + explicit SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct); + virtual ~SkinManageDialog(); + void resizeEvent(QResizeEvent* event) override; + + public slots: + void selectionChanged(QItemSelection, QItemSelection); + void activated(QModelIndex); + void delayed_scroll(QModelIndex); + void on_openDirBtn_clicked(); + void on_fileBtn_clicked(); + void on_urlBtn_clicked(); + void on_userBtn_clicked(); + void accept() override; + void on_capeCombo_currentIndexChanged(int index); + void on_steveBtn_toggled(bool checked); + void on_resetBtn_clicked(); + void show_context_menu(const QPoint& pos); + bool eventFilter(QObject* obj, QEvent* ev) override; + void on_action_Rename_Skin_triggered(bool checked); + void on_action_Delete_Skin_triggered(bool checked); + + private: + void setupCapes(); + + MinecraftAccountPtr m_acct; + Ui::SkinManageDialog* ui; + SkinList m_list; + QString m_selected_skin; + QHash m_capes; + QHash m_capes_idx; +}; diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.ui b/launcher/ui/dialogs/skins/SkinManageDialog.ui new file mode 100644 index 000000000..c77eeaaa3 --- /dev/null +++ b/launcher/ui/dialogs/skins/SkinManageDialog.ui @@ -0,0 +1,226 @@ + + + SkinManageDialog + + + + 0 + 0 + 968 + 757 + + + + Skin Upload + + + + + + + + + + + + + false + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + Model + + + + + + Classic + + + true + + + + + + + Slim + + + + + + + + + + Cape + + + + + + + + + + + + false + + + Qt::AlignCenter + + + + + + + + + + + + Qt::CustomContextMenu + + + false + + + 0 + + + + + + + + + + + Open Folder + + + + + + + Reset Skin + + + + + + + + + + + + + + Import URL + + + + + + + Import user + + + + + + + Import File + + + + + + + + 0 + 0 + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + &Delete Skin + + + Deletes selected skin + + + Del + + + + + &Rename Skin + + + Rename selected skin + + + F2 + + + + + + + buttonBox + rejected() + SkinManageDialog + reject() + + + 617 + 736 + + + 483 + 378 + + + + + buttonBox + accepted() + SkinManageDialog + accept() + + + 617 + 736 + + + 483 + 378 + + + + + diff --git a/launcher/ui/instanceview/InstanceView.cpp b/launcher/ui/instanceview/InstanceView.cpp index ed97de17a..c677f3951 100644 --- a/launcher/ui/instanceview/InstanceView.cpp +++ b/launcher/ui/instanceview/InstanceView.cpp @@ -848,7 +848,7 @@ QRegion InstanceView::visualRegionForSelection(const QItemSelection& selection) return region; } -QModelIndex InstanceView::moveCursor(QAbstractItemView::CursorAction cursorAction, [[maybe_unused]] Qt::KeyboardModifiers modifiers) +QModelIndex InstanceView::moveCursor(QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers) { auto current = currentIndex(); if (!current.isValid()) { @@ -865,6 +865,7 @@ QModelIndex InstanceView::moveCursor(QAbstractItemView::CursorAction cursorActio if (m_currentCursorColumn < 0) { m_currentCursorColumn = column; } + // Handle different movement actions. switch (cursorAction) { case MoveUp: { if (row == 0) { @@ -925,16 +926,47 @@ QModelIndex InstanceView::moveCursor(QAbstractItemView::CursorAction cursorActio if (column > 0) { m_currentCursorColumn = column - 1; return cat->rows[row][column - 1]; + } else if (row > 0) { + row -= 1; + int newRowSize = cat->rows[row].size(); + m_currentCursorColumn = newRowSize - 1; + return cat->rows[row][m_currentCursorColumn]; + } else { + int prevGroupIndex = group_index - 1; + while (prevGroupIndex >= 0) { + auto prevGroup = m_groups[prevGroupIndex]; + if (prevGroup->collapsed) { + prevGroupIndex--; + continue; + } + int lastRow = prevGroup->numRows() - 1; + int lastCol = prevGroup->rows[lastRow].size() - 1; + m_currentCursorColumn = lastCol; + return prevGroup->rows[lastRow][lastCol]; + } } - // TODO: moving to previous line return current; } case MoveRight: { if (column < cat->rows[row].size() - 1) { m_currentCursorColumn = column + 1; return cat->rows[row][column + 1]; + } else if (row < cat->rows.size() - 1) { + row += 1; + m_currentCursorColumn = 0; + return cat->rows[row][m_currentCursorColumn]; + } else { + int nextGroupIndex = group_index + 1; + while (nextGroupIndex < m_groups.size()) { + auto nextGroup = m_groups[nextGroupIndex]; + if (nextGroup->collapsed) { + nextGroupIndex++; + continue; + } + m_currentCursorColumn = 0; + return nextGroup->rows[0][0]; + } } - // TODO: moving to next line return current; } case MoveHome: { @@ -947,6 +979,7 @@ QModelIndex InstanceView::moveCursor(QAbstractItemView::CursorAction cursorActio return cat->rows[row][last]; } default: + // For unsupported cursor actions, return the current index. break; } return current; diff --git a/launcher/ui/instanceview/VisualGroup.cpp b/launcher/ui/instanceview/VisualGroup.cpp index 7bff727fe..83103c502 100644 --- a/launcher/ui/instanceview/VisualGroup.cpp +++ b/launcher/ui/instanceview/VisualGroup.cpp @@ -66,6 +66,9 @@ void VisualGroup::update() rows[currentRow].height = maxRowHeight; rows[currentRow].top = offsetFromTop; currentRow++; + if (currentRow >= rows.size()) { + currentRow = rows.size() - 1; + } offsetFromTop += maxRowHeight + 5; positionInRow = 0; maxRowHeight = 0; diff --git a/launcher/ui/java/InstallJavaDialog.cpp b/launcher/ui/java/InstallJavaDialog.cpp new file mode 100644 index 000000000..f01edc5e5 --- /dev/null +++ b/launcher/ui/java/InstallJavaDialog.cpp @@ -0,0 +1,346 @@ +// 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 "InstallJavaDialog.h" + +#include +#include +#include +#include +#include +#include + +#include "Application.h" +#include "BaseVersionList.h" +#include "FileSystem.h" +#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" +#include "ui/widgets/PageContainer.h" +#include "ui/widgets/VersionSelectWidget.h" + +class InstallJavaPage : public QWidget, public BasePage { + public: + Q_OBJECT + public: + explicit InstallJavaPage(const QString& id, const QString& iconName, const QString& name, QWidget* parent = nullptr) + : QWidget(parent), uid(id), iconName(iconName), name(name) + { + setObjectName(QStringLiteral("VersionSelectWidget")); + horizontalLayout = new QHBoxLayout(this); + horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); + horizontalLayout->setContentsMargins(0, 0, 0, 0); + + 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!")); + 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!")); + horizontalLayout->addWidget(javaVersionSelect, 4); + connect(majorVersionSelect, &VersionSelectWidget::selectedVersionChanged, this, &InstallJavaPage::setSelectedVersion); + connect(majorVersionSelect, &VersionSelectWidget::selectedVersionChanged, this, &InstallJavaPage::selectionChanged); + connect(javaVersionSelect, &VersionSelectWidget::selectedVersionChanged, this, &InstallJavaPage::selectionChanged); + + QMetaObject::connectSlotsByName(this); + } + ~InstallJavaPage() + { + delete horizontalLayout; + delete majorVersionSelect; + delete javaVersionSelect; + } + + //! loads the list if needed. + void initialize(Meta::VersionList::Ptr vlist) + { + vlist->setProvidedRoles({ BaseVersionList::JavaMajorRole, BaseVersionList::RecommendedRole, BaseVersionList::VersionPointerRole }); + majorVersionSelect->initialize(vlist.get()); + } + + void setSelectedVersion(BaseVersion::Ptr version) + { + auto dcast = std::dynamic_pointer_cast(version); + if (!dcast) { + return; + } + javaVersionSelect->initialize(new Java::VersionList(dcast, this)); + javaVersionSelect->selectCurrent(); + } + + QString id() const override { return uid; } + QString displayName() const override { return name; } + QIcon icon() const override { return APPLICATION->getThemedIcon(iconName); } + + void openedImpl() override + { + if (loaded) + return; + + const auto versions = APPLICATION->metadataIndex()->get(uid); + if (!versions) + return; + + initialize(versions); + loaded = true; + } + + void setParentContainer(BasePageContainer* container) override + { + auto dialog = dynamic_cast(dynamic_cast(container)->parent()); + connect(javaVersionSelect->view(), &QAbstractItemView::doubleClicked, dialog, &QDialog::accept); + } + + BaseVersion::Ptr selectedVersion() const { return javaVersionSelect->selectedVersion(); } + void selectSearch() { javaVersionSelect->selectSearch(); } + void loadList() + { + majorVersionSelect->loadList(); + javaVersionSelect->loadList(); + } + + public slots: + void setRecommendedMajors(const QStringList& majors) + { + m_recommended_majors = majors; + recommendedFilterChanged(); + } + void setRecomend(bool recomend) + { + m_recommend = recomend; + recommendedFilterChanged(); + } + void recommendedFilterChanged() + { + if (m_recommend) { + majorVersionSelect->setFilter(BaseVersionList::ModelRoles::JavaMajorRole, new ExactListFilter(m_recommended_majors)); + } else { + majorVersionSelect->setFilter(BaseVersionList::ModelRoles::JavaMajorRole, new ExactListFilter()); + } + } + + signals: + void selectionChanged(); + + private: + const QString uid; + const QString iconName; + const QString name; + bool loaded = false; + + QHBoxLayout* horizontalLayout = nullptr; + VersionSelectWidget* majorVersionSelect = nullptr; + VersionSelectWidget* javaVersionSelect = nullptr; + + QStringList m_recommended_majors; + bool m_recommend; +}; + +static InstallJavaPage* pageCast(BasePage* page) +{ + auto result = dynamic_cast(page); + Q_ASSERT(result != nullptr); + return result; +} +namespace Java { +QStringList getRecommendedJavaVersionsFromVersionList(Meta::VersionList::Ptr list) +{ + QStringList recommendedJavas; + for (auto ver : list->versions()) { + auto major = ver->version(); + if (major.startsWith("java")) { + major = "Java " + major.mid(4); + } + recommendedJavas.append(major); + } + return recommendedJavas; +} + +InstallDialog::InstallDialog(const QString& uid, BaseInstance* instance, QWidget* parent) + : QDialog(parent), container(new PageContainer(this, QString(), this)), buttons(new QDialogButtonBox(this)) +{ + auto layout = new QVBoxLayout(this); + + container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + layout->addWidget(container); + + auto buttonLayout = new QHBoxLayout(this); + auto refreshLayout = new QHBoxLayout(this); + + auto refreshButton = new QPushButton(tr("&Refresh"), this); + connect(refreshButton, &QPushButton::clicked, this, [this] { pageCast(container->selectedPage())->loadList(); }); + refreshLayout->addWidget(refreshButton); + + auto recommendedCheckBox = new QCheckBox("Recommended", this); + recommendedCheckBox->setCheckState(Qt::CheckState::Checked); + connect(recommendedCheckBox, &QCheckBox::stateChanged, this, [this](int state) { + for (BasePage* page : container->getPages()) { + pageCast(page)->setRecomend(state == Qt::Checked); + } + }); + + refreshLayout->addWidget(recommendedCheckBox); + buttonLayout->addLayout(refreshLayout); + + buttons->setOrientation(Qt::Horizontal); + buttons->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); + buttons->button(QDialogButtonBox::Ok)->setText(tr("Download")); + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + buttonLayout->addWidget(buttons); + + layout->addLayout(buttonLayout); + + setWindowTitle(dialogTitle()); + setWindowModality(Qt::WindowModal); + resize(840, 480); + + QStringList recommendedJavas; + if (auto mcInst = dynamic_cast(instance); mcInst) { + auto mc = mcInst->getPackProfile()->getComponent("net.minecraft"); + if (mc) { + auto file = mc->getVersionFile(); // no need for load as it should already be loaded + if (file) { + for (auto major : file->compatibleJavaMajors) { + recommendedJavas.append(QString("Java %1").arg(major)); + } + } + } + } else { + const auto versions = APPLICATION->metadataIndex()->get("net.minecraft.java"); + if (versions) { + if (versions->isLoaded()) { + recommendedJavas = getRecommendedJavaVersionsFromVersionList(versions); + } else { + auto newTask = versions->getLoadTask(); + if (newTask) { + connect(newTask.get(), &Task::succeeded, this, [this, versions] { + auto recommendedJavas = getRecommendedJavaVersionsFromVersionList(versions); + for (BasePage* page : container->getPages()) { + pageCast(page)->setRecommendedMajors(recommendedJavas); + } + }); + if (!newTask->isRunning()) + newTask->start(); + } else { + recommendedJavas = getRecommendedJavaVersionsFromVersionList(versions); + } + } + } + } + for (BasePage* page : container->getPages()) { + if (page->id() == uid) + container->selectPage(page->id()); + + auto cast = pageCast(page); + cast->setRecomend(true); + connect(cast, &InstallJavaPage::selectionChanged, this, [this, cast] { validate(cast); }); + if (!recommendedJavas.isEmpty()) { + cast->setRecommendedMajors(recommendedJavas); + } + } + connect(container, &PageContainer::selectedPageChanged, this, [this](BasePage* previous, BasePage* selected) { validate(selected); }); + pageCast(container->selectedPage())->selectSearch(); + validate(container->selectedPage()); +} + +QList InstallDialog::getPages() +{ + return { + // Mojang + new InstallJavaPage("net.minecraft.java", "mojang", tr("Mojang")), + // Adoptium + new InstallJavaPage("net.adoptium.java", "adoptium", tr("Adoptium")), + // Azul + new InstallJavaPage("com.azul.java", "azul", tr("Azul Zulu")), + }; +} + +QString InstallDialog::dialogTitle() +{ + return tr("Install Java"); +} + +void InstallDialog::validate(BasePage* selected) +{ + buttons->button(QDialogButtonBox::Ok)->setEnabled(!!std::dynamic_pointer_cast(pageCast(selected)->selectedVersion())); +} + +void InstallDialog::done(int result) +{ + if (result == Accepted) { + auto* page = pageCast(container->selectedPage()); + if (page->selectedVersion()) { + auto meta = std::dynamic_pointer_cast(page->selectedVersion()); + if (meta) { + Task::Ptr task; + auto final_path = FS::PathCombine(APPLICATION->javaPath(), meta->m_name); + auto deletePath = [final_path] { FS::deletePath(final_path); }; + switch (meta->downloadType) { + case Java::DownloadType::Manifest: + task = makeShared(meta->url, final_path, meta->checksumType, meta->checksumHash); + break; + case Java::DownloadType::Archive: + task = makeShared(meta->url, final_path, meta->checksumType, meta->checksumHash); + break; + case Java::DownloadType::Unknown: + QString error = QString(tr("Could not determine Java download type!")); + CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show(); + deletePath(); + } +#if defined(Q_OS_MACOS) + auto seq = makeShared(this, 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(); + deletePath(); + }); + connect(task.get(), &Task::aborted, this, deletePath); + ProgressDialog pg(this); + pg.setSkipButton(true, tr("Abort")); + pg.execWithTask(task.get()); + } else { + return; + } + } else { + return; + } + } + + QDialog::done(result); +} + +} // namespace Java + +#include "InstallJavaDialog.moc" \ No newline at end of file diff --git a/launcher/ui/java/InstallJavaDialog.h b/launcher/ui/java/InstallJavaDialog.h new file mode 100644 index 000000000..7d0edbfdd --- /dev/null +++ b/launcher/ui/java/InstallJavaDialog.h @@ -0,0 +1,47 @@ +// 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 "BaseInstance.h" +#include "ui/pages/BasePageProvider.h" + +class MinecraftInstance; +class PageContainer; +class PackProfile; +class QDialogButtonBox; + +namespace Java { +class InstallDialog final : public QDialog, private BasePageProvider { + Q_OBJECT + + public: + explicit InstallDialog(const QString& uid = QString(), BaseInstance* instance = nullptr, QWidget* parent = nullptr); + + QList getPages() override; + QString dialogTitle() override; + + void validate(BasePage* selected); + void done(int result) override; + + private: + PageContainer* container; + QDialogButtonBox* buttons; +}; +} // namespace Java diff --git a/launcher/ui/java/VersionList.cpp b/launcher/ui/java/VersionList.cpp new file mode 100644 index 000000000..f2c0cb3b9 --- /dev/null +++ b/launcher/ui/java/VersionList.cpp @@ -0,0 +1,126 @@ +// 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 "VersionList.h" + +#include + +#include "BaseVersionList.h" +#include "SysInfo.h" +#include "java/JavaMetadata.h" +#include "meta/VersionList.h" + +namespace Java { + +VersionList::VersionList(Meta::Version::Ptr version, QObject* parent) : BaseVersionList(parent), m_version(version) +{ + if (version->isLoaded()) + sortVersions(); +} + +Task::Ptr VersionList::getLoadTask() +{ + auto task = m_version->loadTask(Net::Mode::Online); + connect(task.get(), &Task::finished, this, &VersionList::sortVersions); + return task; +} + +const BaseVersion::Ptr VersionList::at(int i) const +{ + return m_vlist.at(i); +} + +bool VersionList::isLoaded() +{ + return m_version->isLoaded(); +} + +int VersionList::count() const +{ + return m_vlist.count(); +} + +QVariant VersionList::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() > count()) + return QVariant(); + + auto version = (m_vlist[index.row()]); + switch (role) { + case SortRole: + return -index.row(); + case VersionPointerRole: + return QVariant::fromValue(std::dynamic_pointer_cast(m_vlist[index.row()])); + case VersionIdRole: + return version->descriptor(); + case VersionRole: + return version->version.toString(); + case RecommendedRole: + return false; // do not recommend any version + case JavaNameRole: + return version->name(); + case JavaMajorRole: { + auto major = version->version.toString(); + if (major.startsWith("java")) { + major = "Java " + major.mid(4); + } + return major; + } + case TypeRole: + return version->packageType; + case Meta::VersionList::TimeRole: + return version->releaseTime; + default: + return QVariant(); + } +} + +BaseVersionList::RoleList VersionList::providesRoles() const +{ + return { VersionPointerRole, VersionIdRole, VersionRole, RecommendedRole, JavaNameRole, TypeRole, Meta::VersionList::TimeRole }; +} + +bool sortJavas(BaseVersion::Ptr left, BaseVersion::Ptr right) +{ + auto rleft = std::dynamic_pointer_cast(right); + auto rright = std::dynamic_pointer_cast(left); + return (*rleft) < (*rright); +} + +void VersionList::sortVersions() +{ + if (!m_version || !m_version->data()) + return; + QString versionStr = SysInfo::getSupportedJavaArchitecture(); + beginResetModel(); + auto runtimes = m_version->data()->runtimes; + m_vlist = {}; + if (!versionStr.isEmpty() && !runtimes.isEmpty()) { + std::copy_if(runtimes.begin(), runtimes.end(), std::back_inserter(m_vlist), + [versionStr](Java::MetadataPtr val) { return val->runtimeOS == versionStr; }); + std::sort(m_vlist.begin(), m_vlist.end(), sortJavas); + } else { + qWarning() << "No Java versions found for your operating system." << SysInfo::currentSystem() << " " << SysInfo::useQTForArch(); + } + endResetModel(); +} + +} // namespace Java diff --git a/launcher/ui/java/VersionList.h b/launcher/ui/java/VersionList.h new file mode 100644 index 000000000..d334ed564 --- /dev/null +++ b/launcher/ui/java/VersionList.h @@ -0,0 +1,50 @@ +// 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 "BaseVersionList.h" +#include "java/JavaMetadata.h" +#include "meta/Version.h" + +namespace Java { + +class VersionList : public BaseVersionList { + Q_OBJECT + + public: + explicit VersionList(Meta::Version::Ptr m_version, QObject* parent = 0); + + Task::Ptr getLoadTask() override; + bool isLoaded() override; + const BaseVersion::Ptr at(int i) const override; + int count() const override; + void sortVersions() override; + + QVariant data(const QModelIndex& index, int role) const override; + RoleList providesRoles() const override; + + protected slots: + void updateListData(QList) override {} + + protected: + Meta::Version::Ptr m_version; + QList m_vlist; +}; + +} // namespace Java diff --git a/launcher/ui/pages/BasePageContainer.h b/launcher/ui/pages/BasePageContainer.h index a497ef7b3..671c2735d 100644 --- a/launcher/ui/pages/BasePageContainer.h +++ b/launcher/ui/pages/BasePageContainer.h @@ -4,7 +4,7 @@ class BasePage; class BasePageContainer { public: - virtual ~BasePageContainer(){}; + virtual ~BasePageContainer() {}; virtual bool selectPage(QString pageId) = 0; virtual BasePage* selectedPage() const = 0; virtual BasePage* getPage(QString pageId) { return nullptr; }; diff --git a/launcher/ui/pages/BasePageProvider.h b/launcher/ui/pages/BasePageProvider.h index 422891e6b..ef3c1cd08 100644 --- a/launcher/ui/pages/BasePageProvider.h +++ b/launcher/ui/pages/BasePageProvider.h @@ -16,7 +16,6 @@ #pragma once #include -#include #include "ui/pages/BasePage.h" class BasePageProvider { diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 93591e440..a7f3f3f72 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -207,7 +207,7 @@ - <html><head/><body><p>Note: you only need to set this to access private data. Read the <a href="https://docs.modrinth.com/api-spec/#section/Authentication">documentation</a> for more information.</p></body></html> + <html><head/><body><p>Note: you only need to set this to access private data. Read the <a href="https://docs.modrinth.com/#section/Authentication">documentation</a> for more information.</p></body></html> true diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index abd8fa228..041b8faff 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -35,22 +35,18 @@ */ #include "AccountListPage.h" -#include "minecraft/auth/AccountData.h" +#include "ui/dialogs/skins/SkinManageDialog.h" #include "ui_AccountListPage.h" #include #include +#include #include #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/MSALoginDialog.h" #include "ui/dialogs/OfflineLoginDialog.h" -#include "ui/dialogs/ProgressDialog.h" -#include "ui/dialogs/SkinUploadDialog.h" - -#include "minecraft/services/SkinDelete.h" -#include "tasks/Task.h" #include "Application.h" @@ -134,9 +130,7 @@ void AccountListPage::listChanged() void AccountListPage::on_actionAddMicrosoft_triggered() { - MinecraftAccountPtr account = - MSALoginDialog::newAccount(this, tr("Please enter your Mojang account email and password to add your account.")); - + auto account = MSALoginDialog::newAccount(this); if (account) { m_accounts->addAccount(account); if (m_accounts->count() == 1) { @@ -221,8 +215,7 @@ void AccountListPage::updateButtonStates() } ui->actionRemove->setEnabled(accountIsReady); ui->actionSetDefault->setEnabled(accountIsReady); - ui->actionUploadSkin->setEnabled(accountIsReady && accountIsOnline); - ui->actionDeleteSkin->setEnabled(accountIsReady && accountIsOnline); + ui->actionManageSkins->setEnabled(accountIsReady && accountIsOnline); ui->actionRefresh->setEnabled(accountIsReady && accountIsOnline); if (m_accounts->defaultAccount().get() == nullptr) { @@ -235,29 +228,13 @@ void AccountListPage::updateButtonStates() ui->listView->resizeColumnToContents(3); } -void AccountListPage::on_actionUploadSkin_triggered() +void AccountListPage::on_actionManageSkins_triggered() { QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); if (selection.size() > 0) { QModelIndex selected = selection.first(); MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); - SkinUploadDialog dialog(account, this); + SkinManageDialog dialog(this, account); dialog.exec(); } } - -void AccountListPage::on_actionDeleteSkin_triggered() -{ - QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); - if (selection.size() <= 0) - return; - - QModelIndex selected = selection.first(); - MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); - ProgressDialog prog(this); - auto deleteSkinTask = std::make_shared(this, account->accessToken()); - if (prog.execWithTask((Task*)deleteSkinTask.get()) != QDialog::Accepted) { - CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to delete current skin!"), QMessageBox::Warning)->exec(); - return; - } -} diff --git a/launcher/ui/pages/global/AccountListPage.h b/launcher/ui/pages/global/AccountListPage.h index f3b80191d..4f02b7df5 100644 --- a/launcher/ui/pages/global/AccountListPage.h +++ b/launcher/ui/pages/global/AccountListPage.h @@ -66,7 +66,7 @@ class AccountListPage : public QMainWindow, public BasePage { return icon; } QString id() const override { return "accounts"; } - QString helpPage() const override { return "Getting-Started#adding-an-account"; } + QString helpPage() const override { return "/getting-started/adding-an-account"; } void retranslate() override; public slots: @@ -76,8 +76,7 @@ class AccountListPage : public QMainWindow, public BasePage { void on_actionRefresh_triggered(); void on_actionSetDefault_triggered(); void on_actionNoDefault_triggered(); - void on_actionUploadSkin_triggered(); - void on_actionDeleteSkin_triggered(); + void on_actionManageSkins_triggered(); void listChanged(); diff --git a/launcher/ui/pages/global/AccountListPage.ui b/launcher/ui/pages/global/AccountListPage.ui index d8cf3ac0a..c9b770ab2 100644 --- a/launcher/ui/pages/global/AccountListPage.ui +++ b/launcher/ui/pages/global/AccountListPage.ui @@ -59,14 +59,8 @@ - - + - - - Remo&ve - - &Set Default @@ -80,17 +74,12 @@ &No Default - + - &Upload Skin - - - - - &Delete Skin + &Manage Skins - Delete the currently active skin and go back to the default one + Manage Skins @@ -111,6 +100,11 @@ Refresh the account tokens
    + + + Remo&ve + + diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index ac50319ec..6699b00c0 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -35,12 +35,18 @@ */ #include "JavaPage.h" +#include "BuildConfig.h" #include "JavaCommon.h" +#include "java/JavaInstall.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/java/InstallJavaDialog.h" #include "ui_JavaPage.h" +#include #include #include #include +#include #include #include "ui/dialogs/VersionSelectDialog.h" @@ -56,7 +62,22 @@ JavaPage::JavaPage(QWidget* parent) : QWidget(parent), ui(new Ui::JavaPage) { ui->setupUi(this); - ui->tabWidget->tabBar()->hide(); + + if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { + 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!")); + connect(ui->autodetectJavaCheckBox, &QCheckBox::stateChanged, this, [this] { + ui->autodownloadCheckBox->setEnabled(ui->autodetectJavaCheckBox->isChecked()); + if (!ui->autodetectJavaCheckBox->isChecked()) + ui->autodownloadCheckBox->setChecked(false); + }); + } else { + ui->autodownloadCheckBox->setHidden(true); + ui->tabWidget->tabBar()->hide(); + } loadSettings(); updateThresholds(); @@ -94,6 +115,8 @@ void JavaPage::applySettings() s->set("JvmArgs", ui->jvmArgsTextBox->toPlainText().replace("\n", " ")); s->set("IgnoreJavaCompatibility", ui->skipCompatibilityCheckbox->isChecked()); s->set("IgnoreJavaWizard", ui->skipJavaWizardCheckbox->isChecked()); + s->set("AutomaticJavaSwitch", ui->autodetectJavaCheckBox->isChecked()); + s->set("AutomaticJavaDownload", ui->autodownloadCheckBox->isChecked()); JavaCommon::checkJVMArgs(s->get("JvmArgs").toString(), this->parentWidget()); } void JavaPage::loadSettings() @@ -116,6 +139,8 @@ void JavaPage::loadSettings() ui->jvmArgsTextBox->setPlainText(s->get("JvmArgs").toString()); ui->skipCompatibilityCheckbox->setChecked(s->get("IgnoreJavaCompatibility").toBool()); ui->skipJavaWizardCheckbox->setChecked(s->get("IgnoreJavaWizard").toBool()); + ui->autodetectJavaCheckBox->setChecked(s->get("AutomaticJavaSwitch").toBool()); + ui->autodownloadCheckBox->setChecked(s->get("AutomaticJavaSwitch").toBool() && s->get("AutomaticJavaDownload").toBool()); } void JavaPage::on_javaDetectBtn_clicked() @@ -134,6 +159,14 @@ void JavaPage::on_javaDetectBtn_clicked() if (vselect.result() == QDialog::Accepted && vselect.selectedVersion()) { java = std::dynamic_pointer_cast(vselect.selectedVersion()); ui->javaPathTextBox->setText(java->path); + if (!java->is_64bit && APPLICATION->settings()->get("MaxMemAlloc").toInt() > 2048) { + CustomMessageBox::selectable(this, tr("Confirm Selection"), + tr("You selected a 32-bit version of Java.\n" + "This installation does not support more than 2048MiB of RAM.\n" + "Please make sure that the maximum memory value is lower."), + QMessageBox::Warning, QMessageBox::Ok, QMessageBox::Ok) + ->exec(); + } } } @@ -166,6 +199,13 @@ void JavaPage::on_javaTestBtn_clicked() checker->run(); } +void JavaPage::on_downloadJavaButton_clicked() +{ + auto jdialog = new Java::InstallDialog({}, nullptr, this); + jdialog->exec(); + ui->managedJavaList->loadList(); +} + void JavaPage::on_maxMemSpinBox_valueChanged([[maybe_unused]] int i) { updateThresholds(); @@ -210,3 +250,35 @@ void JavaPage::updateThresholds() ui->labelMaxMemIcon->setPixmap(pix); } } + +void JavaPage::on_removeJavaButton_clicked() +{ + auto version = ui->managedJavaList->selectedVersion(); + auto dcast = std::dynamic_pointer_cast(version); + if (!dcast) { + return; + } + QDir dir(APPLICATION->javaPath()); + + auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); + for (auto& entry : entries) { + if (dcast->path.startsWith(entry.canonicalFilePath())) { + auto response = CustomMessageBox::selectable(this, tr("Confirm Deletion"), + tr("You are about to remove the Java installation named \"%1\".\n" + "Are you sure?") + .arg(entry.fileName()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response == QMessageBox::Yes) { + FS::deletePath(entry.canonicalFilePath()); + ui->managedJavaList->loadList(); + } + break; + } + } +} +void JavaPage::on_refreshJavaButton_clicked() +{ + ui->managedJavaList->loadList(); +} diff --git a/launcher/ui/pages/global/JavaPage.h b/launcher/ui/pages/global/JavaPage.h index 1a1bd96e1..0a1c4a6be 100644 --- a/launcher/ui/pages/global/JavaPage.h +++ b/launcher/ui/pages/global/JavaPage.h @@ -38,7 +38,7 @@ #include #include #include -#include +#include #include "JavaCommon.h" #include "ui/pages/BasePage.h" @@ -72,6 +72,9 @@ class JavaPage : public QWidget, public BasePage { void on_javaDetectBtn_clicked(); void on_javaTestBtn_clicked(); void on_javaBrowseBtn_clicked(); + void on_downloadJavaButton_clicked(); + void on_removeJavaButton_clicked(); + void on_refreshJavaButton_clicked(); void on_maxMemSpinBox_valueChanged(int i); void checkerFinished(); diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index fd16572d3..e6bbeb15a 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -6,8 +6,8 @@ 0 0 - 545 - 580 + 559 + 659 @@ -34,9 +34,9 @@ 0 - + - Tab 1 + General @@ -160,25 +160,6 @@ Java Runtime - - - - true - - - - 0 - 0 - - - - - 16777215 - 100 - - - - @@ -225,7 +206,7 @@ - + @@ -241,6 +222,45 @@ + + + + If enabled, the launcher will not prompt you to choose a Java version if one isn't found. + + + Skip Java &Wizard + + + + + + + true + + + + 0 + 0 + + + + + 16777215 + 100 + + + + + + + + Automatically selects the Java version that is compatible with the current Minecraft instance, based on the major version required. + + + Autodetect Java version + + + @@ -277,13 +297,16 @@ - - + + + + false + - If enabled, the launcher will not prompt you to choose a Java version if one isn't found. + Automatically downloads and selects the Java version recommended by Mojang. - Skip Java &Wizard + Auto-download Mojang Java @@ -305,16 +328,106 @@ + + + Management + + + + + + Downloaded Java Versions + + + + + + + 0 + 0 + + + + + + + + + + Download + + + + + + + Remove + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Refresh + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + VersionSelectWidget + QWidget +
    ui/widgets/VersionSelectWidget.h
    + 1 +
    +
    minMemSpinBox maxMemSpinBox permGenSpinBox - javaBrowseBtn javaPathTextBox + javaBrowseBtn + javaDetectBtn + javaTestBtn + skipCompatibilityCheckbox + skipJavaWizardCheckbox + jvmArgsTextBox tabWidget diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 78c44380a..8bbed9643 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -4,6 +4,7 @@ * Copyright (c) 2022 Jamie Mansfield * Copyright (c) 2022 dada513 * Copyright (C) 2022 Tayou + * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -50,6 +51,7 @@ #include "DesktopServices.h" #include "settings/SettingsObject.h" #include "ui/themes/ITheme.h" +#include "ui/themes/ThemeManager.h" #include "updater/ExternalUpdater.h" #include @@ -66,9 +68,6 @@ enum InstSortMode { LauncherPage::LauncherPage(QWidget* parent) : QWidget(parent), ui(new Ui::LauncherPage) { ui->setupUi(this); - auto origForeground = ui->fontPreview->palette().color(ui->fontPreview->foregroundRole()); - auto origBackground = ui->fontPreview->palette().color(ui->fontPreview->backgroundRole()); - m_colors.reset(new LogColorCache(origForeground, origBackground)); ui->sortingModeGroup->setId(ui->sortByNameBtn, Sort_Name); ui->sortingModeGroup->setId(ui->sortLastLaunchedBtn, Sort_LastLaunch); @@ -80,8 +79,9 @@ LauncherPage::LauncherPage(QWidget* parent) : QWidget(parent), ui(new Ui::Launch ui->updateSettingsBox->setHidden(!APPLICATION->updater()); - connect(ui->fontSizeBox, SIGNAL(valueChanged(int)), SLOT(refreshFontPreview())); - connect(ui->consoleFont, SIGNAL(currentFontChanged(QFont)), SLOT(refreshFontPreview())); + connect(ui->fontSizeBox, QOverload::of(&QSpinBox::valueChanged), this, &LauncherPage::refreshFontPreview); + connect(ui->consoleFont, &QFontComboBox::currentFontChanged, this, &LauncherPage::refreshFontPreview); + connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentWidgetThemeChanged, this, &LauncherPage::refreshFontPreview); connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentCatChanged, APPLICATION, &Application::currentCatChanged); } @@ -173,6 +173,27 @@ void LauncherPage::on_downloadsDirBrowseBtn_clicked() } } +void LauncherPage::on_javaDirBrowseBtn_clicked() +{ + QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Java Folder"), ui->javaDirTextBox->text()); + + if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) { + QString cooked_dir = FS::NormalizePath(raw_dir); + ui->javaDirTextBox->setText(cooked_dir); + } +} + +void LauncherPage::on_skinsDirBrowseBtn_clicked() +{ + QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Skins Folder"), ui->skinsDirTextBox->text()); + + // do not allow current dir - it's dirty. Do not allow dirs that don't exist + if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) { + QString cooked_dir = FS::NormalizePath(raw_dir); + ui->skinsDirTextBox->setText(cooked_dir); + } +} + void LauncherPage::on_metadataDisableBtn_clicked() { ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); @@ -185,12 +206,15 @@ void LauncherPage::applySettings() // Updates if (APPLICATION->updater()) { APPLICATION->updater()->setAutomaticallyChecksForUpdates(ui->autoUpdateCheckBox->isChecked()); + APPLICATION->updater()->setUpdateCheckInterval(ui->updateIntervalSpinBox->value() * 3600); } s->set("MenuBarInsteadOfToolBar", ui->preferMenuBarCheckBox->isChecked()); s->set("NumberOfConcurrentTasks", ui->numberOfConcurrentTasksSpinBox->value()); s->set("NumberOfConcurrentDownloads", ui->numberOfConcurrentDownloadsSpinBox->value()); + s->set("NumberOfManualRetries", ui->numberOfManualRetriesSpinBox->value()); + s->set("RequestTimeout", ui->timeoutSecondsSpinBox->value()); // Console settings s->set("ShowConsole", ui->showConsoleCheck->isChecked()); @@ -208,6 +232,8 @@ void LauncherPage::applySettings() s->set("CentralModsDir", ui->modsDirTextBox->text()); s->set("IconsDir", ui->iconsDirTextBox->text()); s->set("DownloadsDir", ui->downloadsDirTextBox->text()); + s->set("SkinsDir", ui->skinsDirTextBox->text()); + s->set("JavaDir", ui->javaDirTextBox->text()); s->set("DownloadsDirWatchRecursive", ui->downloadsDirWatchRecursiveCheckBox->isChecked()); auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId(); @@ -227,6 +253,7 @@ void LauncherPage::applySettings() // Mods s->set("ModMetadataDisabled", ui->metadataDisableBtn->isChecked()); s->set("ModDependenciesDisabled", ui->dependenciesDisableBtn->isChecked()); + s->set("SkipModpackUpdatePrompt", ui->skipModpackUpdatePromptBtn->isChecked()); } void LauncherPage::loadSettings() { @@ -234,6 +261,7 @@ void LauncherPage::loadSettings() // Updates if (APPLICATION->updater()) { ui->autoUpdateCheckBox->setChecked(APPLICATION->updater()->getAutomaticallyChecksForUpdates()); + ui->updateIntervalSpinBox->setValue(APPLICATION->updater()->getUpdateCheckInterval() / 3600); } // Toolbar/menu bar settings (not applicable if native menu bar is present) @@ -245,6 +273,8 @@ void LauncherPage::loadSettings() ui->numberOfConcurrentTasksSpinBox->setValue(s->get("NumberOfConcurrentTasks").toInt()); ui->numberOfConcurrentDownloadsSpinBox->setValue(s->get("NumberOfConcurrentDownloads").toInt()); + ui->numberOfManualRetriesSpinBox->setValue(s->get("NumberOfManualRetries").toInt()); + ui->timeoutSecondsSpinBox->setValue(s->get("RequestTimeout").toInt()); // Console settings ui->showConsoleCheck->setChecked(s->get("ShowConsole").toBool()); @@ -269,6 +299,8 @@ void LauncherPage::loadSettings() ui->modsDirTextBox->setText(s->get("CentralModsDir").toString()); ui->iconsDirTextBox->setText(s->get("IconsDir").toString()); ui->downloadsDirTextBox->setText(s->get("DownloadsDir").toString()); + ui->skinsDirTextBox->setText(s->get("SkinsDir").toString()); + ui->javaDirTextBox->setText(s->get("JavaDir").toString()); ui->downloadsDirWatchRecursiveCheckBox->setChecked(s->get("DownloadsDirWatchRecursive").toBool()); QString sortMode = s->get("InstSortMode").toString(); @@ -286,41 +318,52 @@ void LauncherPage::loadSettings() ui->metadataDisableBtn->setChecked(s->get("ModMetadataDisabled").toBool()); ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); ui->dependenciesDisableBtn->setChecked(s->get("ModDependenciesDisabled").toBool()); + ui->skipModpackUpdatePromptBtn->setChecked(s->get("SkipModpackUpdatePrompt").toBool()); } void LauncherPage::refreshFontPreview() { + const LogColors& colors = APPLICATION->themeManager()->getLogColors(); + int fontSize = ui->fontSizeBox->value(); QString fontFamily = ui->consoleFont->currentFont().family(); ui->fontPreview->clear(); defaultFormat->setFont(QFont(fontFamily, fontSize)); - { + + auto print = [this, colors](const QString& message, MessageLevel::Enum level) { QTextCharFormat format(*defaultFormat); - format.setForeground(m_colors->getFront(MessageLevel::Error)); + + QColor bg = colors.background.value(level); + QColor fg = colors.foreground.value(level); + + if (bg.isValid()) + format.setBackground(bg); + + if (fg.isValid()) + format.setForeground(fg); + // append a paragraph/line auto workCursor = ui->fontPreview->textCursor(); workCursor.movePosition(QTextCursor::End); - workCursor.insertText(tr("[Something/ERROR] A spooky error!"), format); + workCursor.insertText(message, format); workCursor.insertBlock(); - } - { - QTextCharFormat format(*defaultFormat); - format.setForeground(m_colors->getFront(MessageLevel::Message)); - // append a paragraph/line - auto workCursor = ui->fontPreview->textCursor(); - workCursor.movePosition(QTextCursor::End); - workCursor.insertText(tr("[Test/INFO] A harmless message..."), format); - workCursor.insertBlock(); - } - { - QTextCharFormat format(*defaultFormat); - format.setForeground(m_colors->getFront(MessageLevel::Warning)); - // append a paragraph/line - auto workCursor = ui->fontPreview->textCursor(); - workCursor.movePosition(QTextCursor::End); - workCursor.insertText(tr("[Something/WARN] A not so spooky warning."), format); - workCursor.insertBlock(); - } + }; + + print(QString("%1 version: %2 (%3)\n") + .arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString(), BuildConfig.BUILD_PLATFORM), + MessageLevel::Launcher); + + QDate today = QDate::currentDate(); + + if (today.month() == 10 && today.day() == 31) + print(tr("[Test/ERROR] OOoooOOOoooo! A spooky error!"), MessageLevel::Error); + else + print(tr("[Test/ERROR] A spooky error!"), MessageLevel::Error); + + print(tr("[Test/INFO] A harmless message..."), MessageLevel::Info); + print(tr("[Test/WARN] A not so spooky warning."), MessageLevel::Warning); + print(tr("[Test/DEBUG] A secret debugging message..."), MessageLevel::Debug); + print(tr("[Test/FATAL] A terrifying fatal error!"), MessageLevel::Fatal); } void LauncherPage::retranslate() diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h index e733224d2..02f371b04 100644 --- a/launcher/ui/pages/global/LauncherPage.h +++ b/launcher/ui/pages/global/LauncherPage.h @@ -41,7 +41,6 @@ #include #include #include "java/JavaChecker.h" -#include "ui/ColorCache.h" #include "ui/pages/BasePage.h" class QTextCharFormat; @@ -74,6 +73,8 @@ class LauncherPage : public QWidget, public BasePage { void on_modsDirBrowseBtn_clicked(); void on_iconsDirBrowseBtn_clicked(); void on_downloadsDirBrowseBtn_clicked(); + void on_javaDirBrowseBtn_clicked(); + void on_skinsDirBrowseBtn_clicked(); void on_metadataDisableBtn_clicked(); /*! @@ -92,7 +93,5 @@ class LauncherPage : public QWidget, public BasePage { // default format for the font preview... QTextCharFormat* defaultFormat; - std::unique_ptr m_colors; - std::shared_ptr m_languageModel; }; diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 18b52e1b8..3cba468ff 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -7,7 +7,7 @@ 0 0 511 - 629 + 726 @@ -58,6 +58,33 @@ + + + + + + Update interval + + + + + + + Set it to 0 to only check on launch + + + h + + + 0 + + + 99999999 + + + + + @@ -67,7 +94,7 @@ Folders - + &Downloads: @@ -77,39 +104,59 @@ - - - - I&nstances: - - - instDirTextBox - - - - - - - - - - - - - + Browse - - + + - - + + + + + - 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 @@ -123,6 +170,22 @@ + + + + + + + + + + + + + Browse + + + @@ -137,23 +200,27 @@ - - + + - &Icons: + I&nstances: - iconsDirTextBox + instDirTextBox - - - - 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 + Browse + + + + + + + Browse @@ -196,6 +263,16 @@ + + + + When creating a new modpack instance, do not suggest updating existing instances instead. + + + Skip modpack update prompt + + + @@ -205,6 +282,13 @@ Miscellaneous + + + + 1 + + + @@ -226,10 +310,34 @@ - - + + + + Number of manual retries + + + + + - 1 + 0 + + + + + + + Seconds to wait until the requests are terminated + + + Timeout for HTTP requests + + + + + + + s @@ -237,7 +345,7 @@ - + Qt::Vertical @@ -251,7 +359,7 @@ - + User Interface @@ -374,7 +482,7 @@ - + Qt::Vertical diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index 1ee473541..62bd6ac04 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -98,6 +98,7 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared connect(selection_model, &QItemSelectionModel::selectionChanged, this, updateExtra); connect(model.get(), &ResourceFolderModel::updateFinished, this, updateExtra); + connect(model.get(), &ResourceFolderModel::parseFinished, this, updateExtra); connect(selection_model, &QItemSelectionModel::selectionChanged, this, [this] { updateActions(); }); connect(m_model.get(), &ResourceFolderModel::rowsInserted, this, [this] { updateActions(); }); diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui index c671efaf8..c33df2c26 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.ui +++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui @@ -60,7 +60,7 @@ true - QAbstractItemView::DropOnly + QAbstractItemView::DragDropMode::DropOnly true @@ -74,7 +74,7 @@ Actions - Qt::ToolButtonTextOnly + Qt::ToolButtonStyle::ToolButtonTextOnly true @@ -177,7 +177,7 @@ Reset Update Metadata - QAction::NoRole + QAction::MenuRole::NoRole @@ -185,7 +185,29 @@ Verify Dependencies - QAction::NoRole + QAction::MenuRole::NoRole + + + + + true + + + Export List + + + Export resource's metadata to text. + + + + + Change Version + + + Change a resource's version. + + + QAction::MenuRole::NoRole diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index 76add9402..cf8d86cd4 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -36,6 +36,11 @@ */ #include "InstanceSettingsPage.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/WorldList.h" +#include "settings/Setting.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/java/InstallJavaDialog.h" #include "ui_InstanceSettingsPage.h" #include @@ -62,6 +67,8 @@ InstanceSettingsPage::InstanceSettingsPage(BaseInstance* inst, QWidget* parent) m_settings = inst->settings(); ui->setupUi(this); + ui->javaDownloadBtn->setHidden(!BuildConfig.JAVA_DOWNLOADER_ENABLED); + connect(ui->openGlobalJavaSettingsButton, &QCommandLinkButton::clicked, this, &InstanceSettingsPage::globalSettingsButtonClicked); connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::applySettings); connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings); @@ -71,6 +78,22 @@ InstanceSettingsPage::InstanceSettingsPage(BaseInstance* inst, QWidget* parent) connect(ui->useNativeGLFWCheck, &QAbstractButton::toggled, this, &InstanceSettingsPage::onUseNativeGLFWChanged); connect(ui->useNativeOpenALCheck, &QAbstractButton::toggled, this, &InstanceSettingsPage::onUseNativeOpenALChanged); + auto mInst = dynamic_cast(inst); + m_world_quickplay_supported = mInst && mInst->traits().contains("feature:is_quick_play_singleplayer"); + if (m_world_quickplay_supported) { + auto worlds = mInst->worldList(); + worlds->update(); + for (const auto& world : worlds->allWorlds()) { + ui->worldsCb->addItem(world.folderName()); + } + } else { + ui->worldsCb->hide(); + ui->worldJoinButton->hide(); + ui->serverJoinAddressButton->setChecked(true); + ui->serverJoinAddress->setEnabled(true); + ui->serverJoinAddressButton->setStyleSheet("QRadioButton::indicator { width: 0px; height: 0px; }"); + } + loadSettings(); updateThresholds(); @@ -186,9 +209,6 @@ void InstanceSettingsPage::applySettings() m_settings->reset("JvmArgs"); } - // old generic 'override both' is removed. - m_settings->reset("OverrideJava"); - // Custom Commands bool custcmd = ui->customCommands->checked(); m_settings->set("OverrideCommands", custcmd); @@ -256,9 +276,16 @@ void InstanceSettingsPage::applySettings() bool joinServerOnLaunch = ui->serverJoinGroupBox->isChecked(); m_settings->set("JoinServerOnLaunch", joinServerOnLaunch); if (joinServerOnLaunch) { - m_settings->set("JoinServerOnLaunchAddress", ui->serverJoinAddress->text()); + if (ui->serverJoinAddressButton->isChecked() || !m_world_quickplay_supported) { + m_settings->set("JoinServerOnLaunchAddress", ui->serverJoinAddress->text()); + m_settings->reset("JoinWorldOnLaunch"); + } else { + m_settings->set("JoinWorldOnLaunch", ui->worldsCb->currentText()); + m_settings->reset("JoinServerOnLaunchAddress"); + } } else { m_settings->reset("JoinServerOnLaunchAddress"); + m_settings->reset("JoinWorldOnLaunch"); } // Use an account for this instance @@ -317,12 +344,16 @@ void InstanceSettingsPage::loadSettings() ui->labelPermgenNote->setVisible(permGenVisible); // Java Settings - bool overrideJava = m_settings->get("OverrideJava").toBool(); - bool overrideLocation = m_settings->get("OverrideJavaLocation").toBool() || overrideJava; - bool overrideArgs = m_settings->get("OverrideJavaArgs").toBool() || overrideJava; + bool overrideLocation = m_settings->get("OverrideJavaLocation").toBool(); + bool overrideArgs = m_settings->get("OverrideJavaArgs").toBool(); + connect(m_settings->getSetting("OverrideJavaLocation").get(), &Setting::SettingChanged, ui->javaSettingsGroupBox, + [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); @@ -379,7 +410,25 @@ void InstanceSettingsPage::loadSettings() ui->recordGameTime->setChecked(m_settings->get("RecordGameTime").toBool()); ui->serverJoinGroupBox->setChecked(m_settings->get("JoinServerOnLaunch").toBool()); - ui->serverJoinAddress->setText(m_settings->get("JoinServerOnLaunchAddress").toString()); + + if (auto server = m_settings->get("JoinServerOnLaunchAddress").toString(); !server.isEmpty()) { + ui->serverJoinAddress->setText(server); + ui->serverJoinAddressButton->setChecked(true); + ui->worldJoinButton->setChecked(false); + ui->serverJoinAddress->setEnabled(true); + ui->worldsCb->setEnabled(false); + } else if (auto world = m_settings->get("JoinWorldOnLaunch").toString(); !world.isEmpty() && m_world_quickplay_supported) { + ui->worldsCb->setCurrentText(world); + ui->serverJoinAddressButton->setChecked(false); + ui->worldJoinButton->setChecked(true); + ui->serverJoinAddress->setEnabled(false); + ui->worldsCb->setEnabled(true); + } else { + ui->serverJoinAddressButton->setChecked(true); + ui->worldJoinButton->setChecked(false); + ui->serverJoinAddress->setEnabled(true); + ui->worldsCb->setEnabled(false); + } ui->instanceAccountGroupBox->setChecked(m_settings->get("UseAccountForInstance").toBool()); updateAccountsMenu(); @@ -388,6 +437,12 @@ void InstanceSettingsPage::loadSettings() ui->onlineFixes->setChecked(m_settings->get("OnlineFixes").toBool()); } +void InstanceSettingsPage::on_javaDownloadBtn_clicked() +{ + auto jdialog = new Java::InstallDialog({}, m_instance, this); + jdialog->exec(); +} + void InstanceSettingsPage::on_javaDetectBtn_clicked() { if (JavaUtils::getJavaCheckPath().isEmpty()) { @@ -409,6 +464,15 @@ void InstanceSettingsPage::on_javaDetectBtn_clicked() ui->labelPermGen->setVisible(visible); ui->labelPermgenNote->setVisible(visible); m_settings->set("PermGenVisible", visible); + + if (!java->is_64bit && m_settings->get("MaxMemAlloc").toInt() > 2048) { + CustomMessageBox::selectable(this, tr("Confirm Selection"), + tr("You selected a 32-bit version of Java.\n" + "This installation does not support more than 2048MiB of RAM.\n" + "Please make sure that the maximum memory value is lower."), + QMessageBox::Warning, QMessageBox::Ok, QMessageBox::Ok) + ->exec(); + } } } @@ -534,3 +598,13 @@ void InstanceSettingsPage::updateThresholds() ui->labelMaxMemIcon->setPixmap(pix); } } + +void InstanceSettingsPage::on_serverJoinAddressButton_toggled(bool checked) +{ + ui->serverJoinAddress->setEnabled(checked); +} + +void InstanceSettingsPage::on_worldJoinButton_toggled(bool checked) +{ + ui->worldsCb->setEnabled(checked); +} diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h index 8b78dcb7f..6499f9e8f 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.h +++ b/launcher/ui/pages/instance/InstanceSettingsPage.h @@ -69,7 +69,10 @@ class InstanceSettingsPage : public QWidget, public BasePage { void on_javaDetectBtn_clicked(); void on_javaTestBtn_clicked(); void on_javaBrowseBtn_clicked(); + void on_javaDownloadBtn_clicked(); void on_maxMemSpinBox_valueChanged(int i); + void on_serverJoinAddressButton_toggled(bool checked); + void on_worldJoinButton_toggled(bool checked); void onUseNativeGLFWChanged(bool checked); void onUseNativeOpenALChanged(bool checked); @@ -90,4 +93,5 @@ class InstanceSettingsPage : public QWidget, public BasePage { BaseInstance* m_instance; SettingsObjectPtr m_settings; unique_qobject_ptr checker; + bool m_world_quickplay_supported; }; diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui index 9490860ae..4905eae87 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.ui +++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui @@ -84,6 +84,13 @@ + + + + Download Java + + + @@ -660,7 +667,7 @@ - Set a server to join on launch + Set a target to join on launch true @@ -668,26 +675,26 @@ false - - - - - - - - 0 - 0 - - - - Server address: - - - - - - - + + + + + Server address: + + + + + + + + + + Singleplayer world + + + + + @@ -764,6 +771,12 @@ openGlobalJavaSettingsButton settingsTabs javaSettingsGroupBox + javaPathTextBox + javaBrowseBtn + javaDownloadBtn + javaDetectBtn + javaTestBtn + skipCompatibilityCheckbox memoryGroupBox minMemSpinBox maxMemSpinBox @@ -783,6 +796,18 @@ useNativeOpenALCheck showGameTime recordGameTime + miscellaneousSettingsBox + closeAfterLaunchCheck + quitAfterGameStopCheck + perfomanceGroupBox + enableFeralGamemodeCheck + enableMangoHud + useDiscreteGpuCheck + gameTimeGroupBox + serverJoinGroupBox + serverJoinAddress + instanceAccountGroupBox + instanceAccountSelector diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index 8e1e53762..0c22d1de6 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -3,7 +3,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (C) 2022 TheKodeToad + * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -47,8 +47,8 @@ #include "launch/LaunchTask.h" #include "settings/Setting.h" -#include "ui/ColorCache.h" #include "ui/GuiUtil.h" +#include "ui/themes/ThemeManager.h" #include @@ -57,26 +57,36 @@ class LogFormatProxyModel : public QIdentityProxyModel { LogFormatProxyModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {} QVariant data(const QModelIndex& index, int role) const override { + const LogColors& colors = APPLICATION->themeManager()->getLogColors(); + switch (role) { case Qt::FontRole: return m_font; case Qt::ForegroundRole: { - MessageLevel::Enum level = (MessageLevel::Enum)QIdentityProxyModel::data(index, LogModel::LevelRole).toInt(); - return m_colors->getFront(level); + auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); + QColor result = colors.foreground.value(level); + + if (result.isValid()) + return result; + + break; } case Qt::BackgroundRole: { - MessageLevel::Enum level = (MessageLevel::Enum)QIdentityProxyModel::data(index, LogModel::LevelRole).toInt(); - return m_colors->getBack(level); + auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); + QColor result = colors.background.value(level); + + if (result.isValid()) + return result; + + break; } - default: - return QIdentityProxyModel::data(index, role); } + + return QIdentityProxyModel::data(index, role); } void setFont(QFont font) { m_font = font; } - void setColors(LogColorCache* colors) { m_colors.reset(colors); } - QModelIndex find(const QModelIndex& start, const QString& value, bool reverse) const { QModelIndex parentIndex = parent(start); @@ -125,7 +135,6 @@ class LogFormatProxyModel : public QIdentityProxyModel { private: QFont m_font; - std::unique_ptr m_colors; }; LogPage::LogPage(InstancePtr instance, QWidget* parent) : QWidget(parent), ui(new Ui::LogPage), m_instance(instance) @@ -134,12 +143,6 @@ LogPage::LogPage(InstancePtr instance, QWidget* parent) : QWidget(parent), ui(ne ui->tabWidget->tabBar()->hide(); m_proxy = new LogFormatProxyModel(this); - // set up text colors in the log proxy and adapt them to the current theme foreground and background - { - auto origForeground = ui->text->palette().color(ui->text->foregroundRole()); - auto origBackground = ui->text->palette().color(ui->text->backgroundRole()); - m_proxy->setColors(new LogColorCache(origForeground, origBackground)); - } // set up fonts in the log proxy { diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp index 2210d0263..a909d10d1 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.cpp +++ b/launcher/ui/pages/instance/ManagedPackPage.cpp @@ -20,6 +20,7 @@ #include "InstanceTask.h" #include "Json.h" #include "Markdown.h" +#include "StringUtils.h" #include "modplatform/modrinth/ModrinthPackManifest.h" @@ -332,7 +333,7 @@ void ModrinthManagedPackPage::suggestVersion() } auto version = m_pack.versions.at(index); - ui->changelogTextBrowser->setHtml(markdownToHTML(version.changelog.toUtf8())); + ui->changelogTextBrowser->setHtml(StringUtils::htmlListPatch(markdownToHTML(version.changelog.toUtf8()))); ManagedPackPage::suggestVersion(); } @@ -370,7 +371,7 @@ void ModrinthManagedPackPage::update() void ModrinthManagedPackPage::updateFromFile() { - auto output = QFileDialog::getOpenFileUrl(this, tr("Choose update file"), QDir::homePath(), "Modrinth pack (*.mrpack *.zip)"); + auto output = QFileDialog::getOpenFileUrl(this, tr("Choose update file"), QDir::homePath(), tr("Modrinth pack") + " (*.mrpack *.zip)"); if (output.isEmpty()) return; QMap extra_info; @@ -420,7 +421,7 @@ void FlameManagedPackPage::parseManagedPack() "Don't worry though, it will ask you to update this instance instead, so you'll not lose this instance!" ""); - ui->changelogTextBrowser->setHtml(message); + ui->changelogTextBrowser->setHtml(StringUtils::htmlListPatch(message)); return; } @@ -502,7 +503,8 @@ void FlameManagedPackPage::suggestVersion() } auto version = m_pack.versions.at(index); - ui->changelogTextBrowser->setHtml(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId)); + ui->changelogTextBrowser->setHtml( + StringUtils::htmlListPatch(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId))); ManagedPackPage::suggestVersion(); } @@ -536,7 +538,7 @@ void FlameManagedPackPage::update() void FlameManagedPackPage::updateFromFile() { - auto output = QFileDialog::getOpenFileUrl(this, tr("Choose update file"), QDir::homePath(), "CurseForge pack (*.zip)"); + auto output = QFileDialog::getOpenFileUrl(this, tr("Choose update file"), QDir::homePath(), tr("CurseForge pack") + " (*.zip)"); if (output.isEmpty()) return; diff --git a/launcher/ui/pages/instance/ManagedPackPage.h b/launcher/ui/pages/instance/ManagedPackPage.h index d77cb97b8..c44f77070 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.h +++ b/launcher/ui/pages/instance/ManagedPackPage.h @@ -50,7 +50,7 @@ class ManagedPackPage : public QWidget, public BasePage { /** Gets the necessary information about the managed pack, such as * available versions*/ - virtual void parseManagedPack(){}; + virtual void parseManagedPack() {}; /** URL of the managed pack. * Not the version-specific one. @@ -64,8 +64,8 @@ class ManagedPackPage : public QWidget, public BasePage { */ virtual void suggestVersion(); - virtual void update(){}; - virtual void updateFromFile(){}; + virtual void update() {}; + virtual void updateFromFile() {}; protected slots: /** Does the necessary UI changes for when something failed. @@ -119,6 +119,7 @@ class ModrinthManagedPackPage final : public ManagedPackPage { void parseManagedPack() override; [[nodiscard]] QString url() const override; + [[nodiscard]] QString helpPage() const override { return "modrinth-managed-pack"; } public slots: void suggestVersion() override; @@ -142,6 +143,7 @@ class FlameManagedPackPage final : public ManagedPackPage { void parseManagedPack() override; [[nodiscard]] QString url() const override; + [[nodiscard]] QString helpPage() const override { return "curseforge-managed-pack"; } public slots: void suggestVersion() override; diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index e647120c2..1ede31d49 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -37,9 +37,11 @@ */ #include "ModFolderPage.h" +#include "ui/dialogs/ExportToModListDialog.h" #include "ui_ExternalResourcesPage.h" #include +#include #include #include #include @@ -66,6 +68,7 @@ #include "Version.h" #include "tasks/ConcurrentTask.h" +#include "tasks/Task.h" #include "ui/dialogs/ProgressDialog.h" ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr model, QWidget* parent) @@ -99,6 +102,16 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr connect(ui->actionResetItemMetadata, &QAction::triggered, this, &ModFolderPage::deleteModMetadata); ui->actionUpdateItem->setMenu(updateMenu); + + ui->actionChangeVersion->setToolTip(tr("Change a mod's version.")); + connect(ui->actionChangeVersion, &QAction::triggered, this, &ModFolderPage::changeModVersion); + ui->actionsToolbar->insertActionAfter(ui->actionUpdateItem, ui->actionChangeVersion); + + ui->actionsToolbar->addSeparator(); + + ui->actionExportMetadata->setToolTip(tr("Export mod's metadata to text.")); + connect(ui->actionExportMetadata, &QAction::triggered, this, &ModFolderPage::exportModMetadata); + ui->actionsToolbar->addAction(ui->actionExportMetadata); } bool ModFolderPage::shouldDisplay() const @@ -275,9 +288,92 @@ void ModFolderPage::deleteModMetadata() m_model->deleteMetadata(selection); } +void ModFolderPage::changeModVersion() +{ + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + auto profile = static_cast(m_instance)->getPackProfile(); + if (!profile->getModLoaders().has_value()) { + QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!")); + return; + } + if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { + QMessageBox::critical(this, tr("Error"), tr("Mod updates are unavailable when metadata is disabled!")); + return; + } + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + auto mods_list = m_model->selectedMods(selection); + if (mods_list.length() != 1 || mods_list[0]->metadata() == nullptr) + return; + + 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()); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::aborted, [this, tasks]() { + CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::succeeded, [this, tasks]() { + QStringList warnings = tasks->warnings(); + if (warnings.count()) + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + + tasks->deleteLater(); + }); + + for (auto& task : mdownload.getTasks()) { + tasks->addTask(task); + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } +} + +void ModFolderPage::exportModMetadata() +{ + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + auto selectedMods = m_model->selectedMods(selection); + if (selectedMods.length() == 0) + selectedMods = m_model->allMods(); + + std::sort(selectedMods.begin(), selectedMods.end(), [](const Mod* a, const Mod* b) { return a->name() < b->name(); }); + ExportToModListDialog dlg(m_instance->name(), selectedMods, this); + dlg.exec(); +} + CoreModFolderPage::CoreModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent) : ModFolderPage(inst, mods, parent) -{} +{ + auto mcInst = dynamic_cast(m_instance); + if (mcInst) { + auto version = mcInst->getPackProfile(); + if (version && version->getComponent("net.minecraftforge") && version->getComponent("net.minecraft")) { + auto minecraftCmp = version->getComponent("net.minecraft"); + if (!minecraftCmp->m_loaded) { + version->reload(Net::Mode::Offline); + auto update = version->getCurrentTask(); + if (update) { + connect(update.get(), &Task::finished, this, [this] { + if (m_container) { + m_container->refreshContainer(); + } + }); + update->start(); + } + } + } + } +} bool CoreModFolderPage::shouldDisplay() const { @@ -287,15 +383,10 @@ bool CoreModFolderPage::shouldDisplay() const return true; auto version = inst->getPackProfile(); - - if (!version) - return true; - if (!version->getComponent("net.minecraftforge")) + if (!version || !version->getComponent("net.minecraftforge") || !version->getComponent("net.minecraft")) return false; - if (!version->getComponent("net.minecraft")) - return false; - if (version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate) - return true; + auto minecraftCmp = version->getComponent("net.minecraft"); + return minecraftCmp->m_loaded && minecraftCmp->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate; } return false; } diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index 4fac80141..534f5185d 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -65,6 +65,8 @@ class ModFolderPage : public ExternalResourcesPage { void downloadMods(); void updateMods(bool includeDeps = false); void deleteModMetadata(); + void exportModMetadata(); + void changeModVersion(); protected: std::shared_ptr m_model; diff --git a/launcher/ui/pages/instance/OtherLogsPage.h b/launcher/ui/pages/instance/OtherLogsPage.h index de42f5a23..85a3a2dbc 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.h +++ b/launcher/ui/pages/instance/OtherLogsPage.h @@ -57,7 +57,7 @@ class OtherLogsPage : public QWidget, public BasePage { QString id() const override { return "logs"; } QString displayName() const override { return tr("Other logs"); } QIcon icon() const override { return APPLICATION->getThemedIcon("log"); } - QString helpPage() const override { return "Minecraft-Logs"; } + QString helpPage() const override { return "other-Logs"; } void retranslate() override; void openedImpl() override; 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/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index 2142e6c9f..d8035e73e 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -168,7 +168,7 @@ class ServersModel : public QAbstractListModel { m_saveTimer.setInterval(5000); connect(&m_saveTimer, &QTimer::timeout, this, &ServersModel::save_internal); } - virtual ~ServersModel(){}; + virtual ~ServersModel() = default; void observe() { @@ -731,7 +731,7 @@ void ServersPage::on_actionMove_Down_triggered() void ServersPage::on_actionJoin_triggered() { const auto& address = m_model->at(currentServer)->m_address; - APPLICATION->launch(m_inst, true, false, std::make_shared(MinecraftServerTarget::parse(address))); + APPLICATION->launch(m_inst, true, false, std::make_shared(MinecraftTarget::parse(address, false))); } #include "ServersPage.moc" diff --git a/launcher/ui/pages/instance/ShaderPackPage.h b/launcher/ui/pages/instance/ShaderPackPage.h index fadffe82d..f35a4b79f 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.h +++ b/launcher/ui/pages/instance/ShaderPackPage.h @@ -48,7 +48,7 @@ class ShaderPackPage : public ExternalResourcesPage { QString displayName() const override { return tr("Shader packs"); } QIcon icon() const override { return APPLICATION->getThemedIcon("shaderpacks"); } QString id() const override { return "shaderpacks"; } - QString helpPage() const override { return "Resource-packs"; } + QString helpPage() const override { return "shader-packs"; } bool shouldDisplay() const override { return true; } diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 487433230..0c25b4c0c 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" @@ -297,7 +299,7 @@ void VersionPage::on_actionRemove_triggered() void VersionPage::on_actionAdd_to_Minecraft_jar_triggered() { - auto list = GuiUtil::BrowseForFiles("jarmod", tr("Select jar mods"), tr("Minecraft.jar mods (*.zip *.jar)"), + auto list = GuiUtil::BrowseForFiles("jarmod", tr("Select jar mods"), tr("Minecraft.jar mods") + " (*.zip *.jar)", APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); if (!list.empty()) { m_profile->installJarMods(list); @@ -307,7 +309,7 @@ void VersionPage::on_actionAdd_to_Minecraft_jar_triggered() void VersionPage::on_actionReplace_Minecraft_jar_triggered() { - auto jarPath = GuiUtil::BrowseForFile("jar", tr("Select jar"), tr("Minecraft.jar replacement (*.jar)"), + auto jarPath = GuiUtil::BrowseForFile("jar", tr("Select jar"), tr("Minecraft.jar replacement") + " (*.jar)", APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); if (!jarPath.isEmpty()) { m_profile->installCustomJar(jarPath); @@ -317,7 +319,7 @@ void VersionPage::on_actionReplace_Minecraft_jar_triggered() void VersionPage::on_actionImport_Components_triggered() { - QStringList list = GuiUtil::BrowseForFiles("component", tr("Select components"), tr("Components (*.json)"), + QStringList list = GuiUtil::BrowseForFiles("component", tr("Select components"), tr("Components") + " (*.json)", APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); if (!list.isEmpty()) { @@ -332,7 +334,7 @@ void VersionPage::on_actionImport_Components_triggered() void VersionPage::on_actionAdd_Agents_triggered() { - QStringList list = GuiUtil::BrowseForFiles("agent", tr("Select agents"), tr("Java agents (*.jar)"), + QStringList list = GuiUtil::BrowseForFiles("agent", tr("Select agents"), tr("Java agents") + " (*.jar)", APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); if (!list.isEmpty()) @@ -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.")); @@ -393,6 +409,11 @@ void VersionPage::on_actionChange_version_triggered() bool important = false; if (uid == "net.minecraft") { important = true; + if (APPLICATION->settings()->get("AutomaticJavaSwitch").toBool() && m_inst->settings()->get("AutomaticJava").toBool() && + m_inst->settings()->get("OverrideJavaLocation").toBool()) { + m_inst->settings()->set("OverrideJavaLocation", false); + m_inst->settings()->set("JavaPath", ""); + } } m_profile->setComponentVersion(uid, vselect.selectedVersion()->descriptor(), important); m_profile->resolve(Net::Mode::Online); @@ -410,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(this); + 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/instance/VersionPage.h b/launcher/ui/pages/instance/VersionPage.h index 951643743..602d09206 100644 --- a/launcher/ui/pages/instance/VersionPage.h +++ b/launcher/ui/pages/instance/VersionPage.h @@ -41,6 +41,7 @@ #pragma once #include +#include #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 692db7ad7..4ed5f1f73 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -82,7 +82,7 @@ class WorldListProxyModel : public QSortFilterProxyModel { } }; -WorldListPage::WorldListPage(BaseInstance* inst, std::shared_ptr worlds, QWidget* parent) +WorldListPage::WorldListPage(InstancePtr inst, std::shared_ptr worlds, QWidget* parent) : QMainWindow(parent), m_inst(inst), ui(new Ui::WorldListPage), m_worlds(worlds) { ui->setupUi(this); @@ -113,6 +113,11 @@ void WorldListPage::openedImpl() { m_worlds->startWatching(); + auto mInst = std::dynamic_pointer_cast(m_inst); + if (!mInst || !mInst->traits().contains("feature:is_quick_play_singleplayer")) { + ui->toolBar->removeAction(ui->actionJoin); + } + auto const setting_name = QString("WideBarVisibility_%1").arg(id()); if (!APPLICATION->settings()->contains(setting_name)) m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name); @@ -339,11 +344,19 @@ void WorldListPage::worldChanged([[maybe_unused]] const QModelIndex& current, [[ ui->actionDatapacks->setEnabled(enable); bool hasIcon = !index.data(WorldList::IconFileRole).isNull(); ui->actionReset_Icon->setEnabled(enable && hasIcon); + + auto mInst = std::dynamic_pointer_cast(m_inst); + auto supportsJoin = mInst && mInst->traits().contains("feature:is_quick_play_singleplayer"); + ui->actionJoin->setEnabled(enable && supportsJoin); + + if (!supportsJoin) { + ui->toolBar->removeAction(ui->actionJoin); + } } void WorldListPage::on_actionAdd_triggered() { - auto list = GuiUtil::BrowseForFiles(displayName(), tr("Select a Minecraft world zip"), tr("Minecraft World Zip File (*.zip)"), + auto list = GuiUtil::BrowseForFiles(displayName(), tr("Select a Minecraft world zip"), tr("Minecraft World Zip File") + " (*.zip)", QString(), this->parentWidget()); if (!list.empty()) { m_worlds->stopWatching(); @@ -418,4 +431,15 @@ void WorldListPage::on_actionRefresh_triggered() m_worlds->update(); } +void WorldListPage::on_actionJoin_triggered() +{ + QModelIndex index = getSelectedWorld(); + if (!index.isValid()) { + return; + } + auto worldVariant = m_worlds->data(index, WorldList::ObjectRole); + auto world = (World*)worldVariant.value(); + APPLICATION->launch(m_inst, true, false, std::make_shared(MinecraftTarget::parse(world->folderName(), true))); +} + #include "WorldListPage.moc" diff --git a/launcher/ui/pages/instance/WorldListPage.h b/launcher/ui/pages/instance/WorldListPage.h index 4f83002f4..84d9cd075 100644 --- a/launcher/ui/pages/instance/WorldListPage.h +++ b/launcher/ui/pages/instance/WorldListPage.h @@ -53,7 +53,7 @@ class WorldListPage : public QMainWindow, public BasePage { Q_OBJECT public: - explicit WorldListPage(BaseInstance* inst, std::shared_ptr worlds, QWidget* parent = 0); + explicit WorldListPage(InstancePtr inst, std::shared_ptr worlds, QWidget* parent = 0); virtual ~WorldListPage(); virtual QString displayName() const override { return tr("Worlds"); } @@ -72,7 +72,7 @@ class WorldListPage : public QMainWindow, public BasePage { QMenu* createPopupMenu() override; protected: - BaseInstance* m_inst; + InstancePtr m_inst; private: QModelIndex getSelectedWorld(); @@ -101,6 +101,7 @@ class WorldListPage : public QMainWindow, public BasePage { void on_actionReset_Icon_triggered(); void worldChanged(const QModelIndex& current, const QModelIndex& previous); void mceditState(LoggedProcess::State state); + void on_actionJoin_triggered(); void ShowContextMenu(const QPoint& pos); }; diff --git a/launcher/ui/pages/instance/WorldListPage.ui b/launcher/ui/pages/instance/WorldListPage.ui index d74dd0796..04344b453 100644 --- a/launcher/ui/pages/instance/WorldListPage.ui +++ b/launcher/ui/pages/instance/WorldListPage.ui @@ -81,6 +81,7 @@ + @@ -97,6 +98,11 @@ Add + + + Join + + Rename diff --git a/launcher/ui/pages/modplatform/CustomPage.cpp b/launcher/ui/pages/modplatform/CustomPage.cpp index 068fb3a36..ba22bd2e6 100644 --- a/launcher/ui/pages/modplatform/CustomPage.cpp +++ b/launcher/ui/pages/modplatform/CustomPage.cpp @@ -49,13 +49,11 @@ CustomPage::CustomPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), dialog(dialog), ui(new Ui::CustomPage) { ui->setupUi(this); - ui->tabWidget->tabBar()->hide(); connect(ui->versionList, &VersionSelectWidget::selectedVersionChanged, this, &CustomPage::setSelectedVersion); filterChanged(); connect(ui->alphaFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); connect(ui->betaFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); connect(ui->snapshotFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); - connect(ui->oldSnapshotFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); connect(ui->releaseFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); connect(ui->experimentsFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); connect(ui->refreshBtn, &QPushButton::clicked, this, &CustomPage::refresh); @@ -96,13 +94,11 @@ void CustomPage::filterChanged() { QStringList out; if (ui->alphaFilter->isChecked()) - out << "(old_alpha)"; + out << "(alpha)"; if (ui->betaFilter->isChecked()) - out << "(old_beta)"; + out << "(beta)"; if (ui->snapshotFilter->isChecked()) out << "(snapshot)"; - if (ui->oldSnapshotFilter->isChecked()) - out << "(old_snapshot)"; if (ui->releaseFilter->isChecked()) out << "(release)"; if (ui->experimentsFilter->isChecked()) diff --git a/launcher/ui/pages/modplatform/CustomPage.ui b/launcher/ui/pages/modplatform/CustomPage.ui index 23351ccd4..39d9aa6dc 100644 --- a/launcher/ui/pages/modplatform/CustomPage.ui +++ b/launcher/ui/pages/modplatform/CustomPage.ui @@ -24,29 +24,21 @@ 0 - - - 0 + + + true - - - - - - - - - - 0 - 0 - - - - Qt::Horizontal - - - - + + + + 0 + 0 + 813 + 605 + + + + @@ -93,16 +85,6 @@ - - - - Old Snapshots - - - true - - - @@ -157,7 +139,20 @@ - + + + + + 0 + 0 + + + + Qt::Horizontal + + + + @@ -283,10 +278,8 @@
    - tabWidget releaseFilter snapshotFilter - oldSnapshotFilter betaFilter alphaFilter experimentsFilter diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index ed7ebfad9..1efc6199e 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -40,6 +40,7 @@ #include "ui_ImportPage.h" #include +#include #include #include @@ -51,6 +52,7 @@ #include "Json.h" #include "InstanceImportTask.h" +#include "net/NetJob.h" class UrlValidator : public QValidator { public: @@ -102,7 +104,7 @@ void ImportPage::updateState() return; } if (ui->modpackEdit->hasAcceptableInput()) { - QString input = ui->modpackEdit->text(); + QString input = ui->modpackEdit->text().trimmed(); auto url = QUrl::fromUserInput(input); if (url.isLocalFile()) { // FIXME: actually do some validation of what's inside here... this is fake AF diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index c628f74ac..e87a423fa 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -25,15 +25,21 @@ ResourceAPI::SearchArgs ModModel::createSearchArguments() Q_ASSERT(m_filter); std::optional> versions{}; + std::optional categories{}; + auto loaders = profile->getSupportedModLoaders(); - { // Version filter - if (!m_filter->versions.empty()) - versions = m_filter->versions; - } + // Version filter + if (!m_filter->versions.empty()) + versions = m_filter->versions; + if (m_filter->loaders) + loaders = m_filter->loaders; + if (!m_filter->categoryIds.empty()) + categories = m_filter->categoryIds; + auto side = m_filter->side; auto sort = getCurrentSortingMethodByIndex(); - return { ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term, sort, profile->getSupportedModLoaders(), versions }; + return { ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term, sort, loaders, versions, side, categories }; } ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& entry) @@ -45,10 +51,13 @@ ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& en Q_ASSERT(m_filter); std::optional> versions{}; + auto loaders = profile->getSupportedModLoaders(); if (!m_filter->versions.empty()) versions = m_filter->versions; + if (m_filter->loaders) + loaders = m_filter->loaders; - return { pack, versions, profile->getSupportedModLoaders() }; + return { pack, versions, loaders }; } ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(QModelIndex& entry) @@ -79,4 +88,54 @@ bool ModModel::isPackInstalled(ModPlatform::IndexedPack::Ptr pack) const }); } +QVariant ModModel::getInstalledPackVersion(ModPlatform::IndexedPack::Ptr pack) const +{ + auto allMods = static_cast(m_base_instance).loaderModList()->allMods(); + for (auto mod : allMods) { + if (auto meta = mod->metadata(); meta && meta->provider == pack->provider && meta->project_id == pack->addonId) { + return meta->version(); + } + } + return {}; +} + +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) + return true; + return !(m_filter->hideInstalled && isPackInstalled(pack)) && checkSide(m_filter->side, pack->side); +} + +bool ModModel::checkVersionFilters(const ModPlatform::IndexedVersion& v) +{ + if (!m_filter) + return true; + auto loaders = static_cast(m_base_instance).getPackProfile()->getSupportedModLoaders(); + if (m_filter->loaders) + loaders = m_filter->loaders; + return (!optedOut(v) && // is opted out(aka curseforge download link) + (!loaders.has_value() || !v.loaders || loaders.value() & v.loaders) && // loaders + 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 +} + } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index dd187aa8d..5c994f373 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -35,6 +35,7 @@ class ModModel : public ResourceModel { virtual ModPlatform::IndexedVersion loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) = 0; void setFilter(std::shared_ptr filter) { m_filter = filter; } + virtual QVariant getInstalledPackVersion(ModPlatform::IndexedPack::Ptr) const override; public slots: ResourceAPI::SearchArgs createSearchArguments() override; @@ -45,6 +46,9 @@ class ModModel : public ResourceModel { auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0; virtual bool isPackInstalled(ModPlatform::IndexedPack::Ptr) const override; + virtual bool checkFilters(ModPlatform::IndexedPack::Ptr) override; + virtual bool checkVersionFilters(const ModPlatform::IndexedVersion&) override; + protected: BaseInstance& m_base_instance; diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 851c1c9e5..c9817cdf7 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -5,6 +5,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad + * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -57,7 +58,6 @@ namespace ResourceDownload { ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) { - connect(m_ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch); connect(m_ui->resourceFilterButton, &QPushButton::clicked, this, &ModPage::filterMods); connect(m_ui->packView, &QListView::doubleClicked, this, &ModPage::onResourceSelected); } @@ -67,17 +67,18 @@ void ModPage::setFilterWidget(unique_qobject_ptr& widget) if (m_filter_widget) disconnect(m_filter_widget.get(), nullptr, nullptr, nullptr); + auto old = m_ui->splitter->replaceWidget(0, widget.get()); + // because we replaced the widget we also need to delete it + if (old) { + delete old; + } + m_filter_widget.swap(widget); - m_ui->gridLayout_3->addWidget(m_filter_widget.get(), 0, 0, 1, m_ui->gridLayout_3->columnCount()); - - m_filter_widget->setInstance(&static_cast(m_base_instance)); m_filter = m_filter_widget->getFilter(); - connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this, - [&] { m_ui->searchButton->setStyleSheet("text-decoration: underline"); }); - connect(m_filter_widget.get(), &ModFilterWidget::filterUnchanged, this, - [&] { m_ui->searchButton->setStyleSheet("text-decoration: none"); }); + connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this, &ModPage::triggerSearch); + prepareProviderCategories(); } /******** Callbacks to events in the UI (set up in the derived classes) ********/ @@ -89,6 +90,7 @@ void ModPage::filterMods() void ModPage::triggerSearch() { + auto changed = m_filter_widget->changed(); m_filter = m_filter_widget->getFilter(); m_ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect); m_ui->packView->clearSelection(); @@ -96,7 +98,7 @@ void ModPage::triggerSearch() m_ui->versionSelectionBox->clear(); updateSelectionButton(); - static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), m_filter_widget->changed()); + static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), changed); m_fetch_progress.watch(m_model->activeSearchJob().get()); } @@ -111,40 +113,6 @@ QMap ModPage::urlHandlers() const /******** Make changes to the UI ********/ -void ModPage::updateVersionList() -{ - m_ui->versionSelectionBox->clear(); - auto packProfile = (dynamic_cast(m_base_instance)).getPackProfile(); - - QString mcVersion = packProfile->getComponentVersion("net.minecraft"); - - auto current_pack = getCurrentPack(); - if (!current_pack) - return; - for (int i = 0; i < current_pack->versions.size(); i++) { - auto version = current_pack->versions[i]; - bool valid = false; - for (auto& mcVer : m_filter->versions) { - if (validateVersion(version, mcVer.toString(), packProfile->getSupportedModLoaders())) { - valid = true; - break; - } - } - - // Only add the version if it's valid or using the 'Any' filter, but never if the version is opted out - if ((valid || m_filter->versions.empty()) && !optedOut(version)) { - auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; - m_ui->versionSelectionBox->addItem(QString("%1%2").arg(version.version, release_type), QVariant(i)); - } - } - if (m_ui->versionSelectionBox->count() == 0) { - m_ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1)); - m_ui->resourceSelectionButton->setText(tr("Cannot select invalid version :(")); - } - - updateSelectionButton(); -} - void ModPage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& version, const std::shared_ptr base_model) diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index f3660dd48..5c9a82303 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -31,8 +31,7 @@ class ModPage : public ResourcePage { auto page = new T(dialog, instance); auto model = static_cast(page->getModel()); - auto filter_widget = - ModFilterWidget::create(static_cast(instance).getPackProfile()->getComponentVersion("net.minecraft"), page); + auto filter_widget = page->createFilterWidget(); page->setFilterWidget(filter_widget); model->setFilter(page->getFilter()); @@ -51,20 +50,17 @@ class ModPage : public ResourcePage { void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, std::shared_ptr) override; - virtual auto validateVersion(ModPlatform::IndexedVersion& ver, - QString mineVer, - std::optional loaders = {}) const -> bool = 0; + virtual unique_qobject_ptr createFilterWidget() = 0; [[nodiscard]] bool supportsFiltering() const override { return true; }; auto getFilter() const -> const std::shared_ptr { return m_filter; } void setFilterWidget(unique_qobject_ptr&); - public slots: - void updateVersionList() override; - protected: ModPage(ModDownloadDialog* dialog, BaseInstance& instance); + virtual void prepareProviderCategories() {}; + protected slots: virtual void filterMods(); void triggerSearch() override; diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index f3c7ff60b..c8eb91570 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -317,8 +317,10 @@ std::optional ResourceModel::getIcon(QModelIndex& index, const QUrl& url) if (QPixmapCache::find(url.toString(), &pixmap)) return { pixmap }; - if (!m_current_icon_job) + if (!m_current_icon_job) { m_current_icon_job.reset(new NetJob("IconJob", APPLICATION->network())); + m_current_icon_job->setAskRetry(false); + } if (m_currently_running_icon_actions.contains(url)) return {}; @@ -331,7 +333,7 @@ std::optional ResourceModel::getIcon(QModelIndex& index, const QUrl& url) auto icon_fetch_action = Net::ApiDownload::makeCached(url, cache_entry); auto full_file_path = cache_entry->getFullPath(); - connect(icon_fetch_action.get(), &NetAction::succeeded, this, [=] { + connect(icon_fetch_action.get(), &Task::succeeded, this, [=] { auto icon = QIcon(full_file_path); QPixmapCache::insert(url.toString(), icon.pixmap(icon.actualSize({ 64, 64 }))); @@ -339,7 +341,7 @@ std::optional ResourceModel::getIcon(QModelIndex& index, const QUrl& url) emit dataChanged(index, index, { Qt::DecorationRole }); }); - connect(icon_fetch_action.get(), &NetAction::failed, this, [=] { + connect(icon_fetch_action.get(), &Task::failed, this, [=] { m_currently_running_icon_actions.remove(url); m_failed_icon_actions.insert(url); }); @@ -410,12 +412,17 @@ void ResourceModel::searchRequestSucceeded(QJsonDocument& doc) m_search_state = SearchState::CanFetchMore; } + QList filteredNewList; + for (auto p : newList) + if (checkFilters(p)) + filteredNewList << p; + // When you have a Qt build with assertions turned on, proceeding here will abort the application - if (newList.size() == 0) + if (filteredNewList.size() == 0) return; - beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + newList.size() - 1); - m_packs.append(newList); + beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + filteredNewList.size() - 1); + m_packs.append(filteredNewList); endInsertRows(); } @@ -558,4 +565,8 @@ void ResourceModel::removePack(const QString& rem) ver.is_currently_selected = false; } +bool ResourceModel::checkVersionFilters(const ModPlatform::IndexedVersion& v) +{ + return (!optedOut(v)); +} } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h index 12db49080..4c7ea33a0 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.h +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -11,6 +11,7 @@ #include "QObjectPtr.h" #include "ResourceDownloadTask.h" +#include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" #include "tasks/ConcurrentTask.h" @@ -55,6 +56,17 @@ class ResourceModel : public QAbstractListModel { [[nodiscard]] auto getSortingMethods() const { return m_api->getSortingMethods(); } + virtual QVariant getInstalledPackVersion(ModPlatform::IndexedPack::Ptr) const { return {}; } + /** Whether the version is opted out or not. Currently only makes sense in CF. */ + virtual bool optedOut(const ModPlatform::IndexedVersion& ver) const + { + Q_UNUSED(ver); + return false; + }; + + virtual bool checkFilters(ModPlatform::IndexedPack::Ptr) { return true; } + virtual bool checkVersionFilters(const ModPlatform::IndexedVersion&); + public slots: void fetchMore(const QModelIndex& parent) override; // NOTE: Can't use [[nodiscard]] here because of https://bugreports.qt.io/browse/QTBUG-58628 on Qt 5.12 diff --git a/launcher/ui/pages/modplatform/ResourcePackPage.cpp b/launcher/ui/pages/modplatform/ResourcePackPage.cpp index fc2dc15f3..849ea1111 100644 --- a/launcher/ui/pages/modplatform/ResourcePackPage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePackPage.cpp @@ -15,7 +15,6 @@ namespace ResourceDownload { ResourcePackResourcePage::ResourcePackResourcePage(ResourceDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) { - connect(m_ui->searchButton, &QPushButton::clicked, this, &ResourcePackResourcePage::triggerSearch); connect(m_ui->packView, &QListView::doubleClicked, this, &ResourcePackResourcePage::onResourceSelected); } diff --git a/launcher/ui/pages/modplatform/ResourcePackPage.h b/launcher/ui/pages/modplatform/ResourcePackPage.h index 6015aec0b..440d91ab0 100644 --- a/launcher/ui/pages/modplatform/ResourcePackPage.h +++ b/launcher/ui/pages/modplatform/ResourcePackPage.h @@ -40,6 +40,8 @@ class ResourcePackResourcePage : public ResourcePage { [[nodiscard]] QMap urlHandlers() const override; + [[nodiscard]] inline auto helpPage() const -> QString override { return "resourcepack-platform"; } + protected: ResourcePackResourcePage(ResourceDownloadDialog* dialog, BaseInstance& instance); diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index ab314c336..c8f5814ef 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -5,6 +5,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad + * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -40,6 +41,7 @@ #include "modplatform/ModIndex.h" #include "ui_ResourcePage.h" +#include #include #include @@ -67,11 +69,13 @@ ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_in connect(&m_search_timer, &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_ui->gridLayout_3->addWidget(&m_fetch_progress, 0, 0, 1, m_ui->gridLayout_3->columnCount()); + m_ui->verticalLayout->insertWidget(1, &m_fetch_progress); m_ui->packView->setItemDelegate(new ProjectItemDelegate(this)); m_ui->packView->installEventFilter(this); @@ -93,8 +97,10 @@ void ResourcePage::retranslate() void ResourcePage::openedImpl() { - if (!supportsFiltering()) + if (!supportsFiltering()) { m_ui->resourceFilterButton->setVisible(false); + m_ui->filterWidget->hide(); + } //: String in the search bar of the mod downloading dialog m_ui->searchEdit->setPlaceholderText(tr("Search for %1...").arg(resourcesString())); @@ -235,8 +241,8 @@ void ResourcePage::updateUi() text += "
    "; - m_ui->packDescription->setHtml( - text + (current_pack->extraData.body.isEmpty() ? current_pack->description : markdownToHTML(current_pack->extraData.body))); + m_ui->packDescription->setHtml(StringUtils::htmlListPatch( + text + (current_pack->extraData.body.isEmpty() ? current_pack->description : markdownToHTML(current_pack->extraData.body)))); m_ui->packDescription->flush(); } @@ -266,18 +272,21 @@ void ResourcePage::updateVersionList() m_ui->versionSelectionBox->clear(); m_ui->versionSelectionBox->blockSignals(false); - if (current_pack) + if (current_pack) { + auto installedVersion = m_model->getInstalledPackVersion(current_pack); + for (int i = 0; i < current_pack->versions.size(); i++) { auto& version = current_pack->versions[i]; - if (optedOut(version)) + if (!m_model->checkVersionFilters(version)) continue; auto release_type = current_pack->versions[i].version_type.isValid() ? QString(" [%1]").arg(current_pack->versions[i].version_type.toString()) : ""; - m_ui->versionSelectionBox->addItem(current_pack->versions[i].version, QVariant(i)); - } + m_ui->versionSelectionBox->addItem(QString("%1%2").arg(version.version, release_type), QVariant(i)); + } + } if (m_ui->versionSelectionBox->count() == 0) { m_ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1)); m_ui->resourceSelectionButton->setText(tr("Cannot select invalid version :(")); @@ -313,14 +322,9 @@ void ResourcePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI updateUi(); } -void ResourcePage::onVersionSelectionChanged(QString versionData) +void ResourcePage::onVersionSelectionChanged(int index) { - if (versionData.isNull() || versionData.isEmpty()) { - m_selected_version_index = -1; - return; - } - - m_selected_version_index = m_ui->versionSelectionBox->currentData().toInt(); + m_selected_version_index = index; updateSelectionButton(); } @@ -395,7 +399,7 @@ void ResourcePage::openUrl(const QUrl& url) } } - if (!page.isNull()) { + if (!page.isNull() && !m_do_not_jump_to_mod) { const QString slug = match.captured(1); // ensure the user isn't opening the same mod @@ -439,4 +443,52 @@ void ResourcePage::openUrl(const QUrl& url) QDesktopServices::openUrl(url); } +void ResourcePage::openProject(QVariant projectID) +{ + m_ui->sortByBox->hide(); + m_ui->searchEdit->hide(); + m_ui->resourceFilterButton->hide(); + m_ui->packView->hide(); + m_ui->resourceSelectionButton->hide(); + m_do_not_jump_to_mod = true; + + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + + auto okBtn = buttonBox->button(QDialogButtonBox::Ok); + okBtn->setDefault(true); + okBtn->setAutoDefault(true); + okBtn->setText(tr("Reinstall")); + okBtn->setShortcut(tr("Ctrl+Return")); + okBtn->setEnabled(false); + + auto cancelBtn = buttonBox->button(QDialogButtonBox::Cancel); + cancelBtn->setDefault(false); + cancelBtn->setAutoDefault(false); + + connect(okBtn, &QPushButton::clicked, this, [this] { + onResourceSelected(); + m_parent_dialog->accept(); + }); + + connect(cancelBtn, &QPushButton::clicked, m_parent_dialog, &ResourceDownloadDialog::reject); + m_ui->gridLayout_4->addWidget(buttonBox, 1, 2); + + auto jump = [this, okBtn] { + 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")); + }; + + m_ui->searchEdit->setText("#" + projectID.toString()); + triggerSearch(); + + if (m_model->hasActiveSearchJob()) + connect(m_model->activeSearchJob().get(), &Task::finished, jump); + else + jump(); +} } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h index 235b44412..b625240eb 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.h +++ b/launcher/ui/pages/modplatform/ResourcePage.h @@ -83,11 +83,13 @@ class ResourcePage : public QWidget, public BasePage { QList selectedPacks() { return m_model->selectedPacks(); } bool hasSelectedPacks() { return !(m_model->selectedPacks().isEmpty()); } + virtual void openProject(QVariant projectID); + protected slots: - virtual void triggerSearch() {} + virtual void triggerSearch() = 0; void onSelectionChanged(QModelIndex first, QModelIndex second); - void onVersionSelectionChanged(QString data); + void onVersionSelectionChanged(int index); void onResourceSelected(); // NOTE: Can't use [[nodiscard]] here because of https://bugreports.qt.io/browse/QTBUG-58628 on Qt 5.12 @@ -96,13 +98,6 @@ class ResourcePage : public QWidget, public BasePage { virtual QMap urlHandlers() const = 0; virtual void openUrl(const QUrl&); - /** Whether the version is opted out or not. Currently only makes sense in CF. */ - virtual bool optedOut(ModPlatform::IndexedVersion& ver) const - { - Q_UNUSED(ver); - return false; - }; - public: BaseInstance& m_base_instance; @@ -118,6 +113,8 @@ class ResourcePage : public QWidget, public BasePage { // Used to do instant searching with a delay to cache quick changes QTimer m_search_timer; + + bool m_do_not_jump_to_mod = false; }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourcePage.ui b/launcher/ui/pages/modplatform/ResourcePage.ui index 73a9d3b1a..491e7d9f0 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.ui +++ b/launcher/ui/pages/modplatform/ResourcePage.ui @@ -10,48 +10,55 @@ 685 - - - - - - - false - - - false + + + + + + + Filter options - - - - Qt::ScrollBarAlwaysOff - - - true - - - - 48 - 48 - - - + + - - - - Search + + + + Qt::Horizontal + + false + + + + + Qt::ScrollBarAlwaysOff + + + true + + + + 48 + 48 + + + + + + false + + + false + + - - - - + @@ -74,20 +81,6 @@ - - - - Filter options - - - - - - - Qt::Vertical - - - @@ -98,8 +91,6 @@
    - searchEdit - searchButton packView packDescription sortByBox diff --git a/launcher/ui/pages/modplatform/ShaderPackPage.cpp b/launcher/ui/pages/modplatform/ShaderPackPage.cpp index 0a6aa6d01..36ff1a518 100644 --- a/launcher/ui/pages/modplatform/ShaderPackPage.cpp +++ b/launcher/ui/pages/modplatform/ShaderPackPage.cpp @@ -17,7 +17,6 @@ namespace ResourceDownload { ShaderPackResourcePage::ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) { - connect(m_ui->searchButton, &QPushButton::clicked, this, &ShaderPackResourcePage::triggerSearch); connect(m_ui->packView, &QListView::doubleClicked, this, &ShaderPackResourcePage::onResourceSelected); } diff --git a/launcher/ui/pages/modplatform/ShaderPackPage.h b/launcher/ui/pages/modplatform/ShaderPackPage.h index c29317e15..4b92c33dc 100644 --- a/launcher/ui/pages/modplatform/ShaderPackPage.h +++ b/launcher/ui/pages/modplatform/ShaderPackPage.h @@ -42,6 +42,8 @@ class ShaderPackResourcePage : public ResourcePage { [[nodiscard]] QMap urlHandlers() const override; + [[nodiscard]] inline auto helpPage() const -> QString override { return "shaderpack-platform"; } + protected: ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance); diff --git a/launcher/ui/pages/modplatform/TexturePackModel.cpp b/launcher/ui/pages/modplatform/TexturePackModel.cpp index fa6369514..cb4cafd41 100644 --- a/launcher/ui/pages/modplatform/TexturePackModel.cpp +++ b/launcher/ui/pages/modplatform/TexturePackModel.cpp @@ -17,9 +17,9 @@ TexturePackResourceModel::TexturePackResourceModel(BaseInstance const& inst, Res { if (!m_version_list->isLoaded()) { qDebug() << "Loading version list..."; - auto task = m_version_list->getLoadTask(); - if (!task->isRunning()) - task->start(); + m_task = m_version_list->getLoadTask(); + if (!m_task->isRunning()) + m_task->start(); } } @@ -35,7 +35,8 @@ void waitOnVersionListLoad(Meta::VersionList::Ptr version_list) auto task = version_list->getLoadTask(); QObject::connect(task.get(), &Task::finished, &load_version_list_loop, &QEventLoop::quit); - + if (!task->isRunning()) + task->start(); load_version_list_loop.exec(); if (time_limit_for_list_load.isActive()) time_limit_for_list_load.stop(); diff --git a/launcher/ui/pages/modplatform/TexturePackModel.h b/launcher/ui/pages/modplatform/TexturePackModel.h index bb2db5cfc..607a03be3 100644 --- a/launcher/ui/pages/modplatform/TexturePackModel.h +++ b/launcher/ui/pages/modplatform/TexturePackModel.h @@ -22,6 +22,7 @@ class TexturePackResourceModel : public ResourcePackResourceModel { protected: Meta::VersionList::Ptr m_version_list; + Task::Ptr m_task; }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/TexturePackPage.h b/launcher/ui/pages/modplatform/TexturePackPage.h index 948e5286b..42aa921c5 100644 --- a/launcher/ui/pages/modplatform/TexturePackPage.h +++ b/launcher/ui/pages/modplatform/TexturePackPage.h @@ -41,7 +41,6 @@ class TexturePackResourcePage : public ResourcePackResourcePage { protected: TexturePackResourcePage(TexturePackDownloadDialog* dialog, BaseInstance& instance) : ResourcePackResourcePage(dialog, instance) { - connect(m_ui->searchButton, &QPushButton::clicked, this, &TexturePackResourcePage::triggerSearch); connect(m_ui->packView, &QListView::doubleClicked, this, &TexturePackResourcePage::onResourceSelected); } }; diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp index d46b97af1..f116ca915 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp @@ -195,6 +195,7 @@ void ListModel::requestLogo(QString file, QString url) MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(file)); auto job = new NetJob(QString("ATLauncher Icon Download %1").arg(file), APPLICATION->network()); + job->setAskRetry(false); job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp index e492830c6..d79b7621a 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -39,6 +39,7 @@ #include "ui_AtlPage.h" #include "BuildConfig.h" +#include "StringUtils.h" #include "AtlUserInteractionSupportImpl.h" #include "modplatform/atlauncher/ATLPackInstallTask.h" @@ -144,7 +145,7 @@ void AtlPage::onSelectionChanged(QModelIndex first, [[maybe_unused]] QModelIndex selected = filterModel->data(first, Qt::UserRole).value(); - ui->packDescription->setHtml(selected.description.replace("\n", "
    ")); + ui->packDescription->setHtml(StringUtils::htmlListPatch(selected.description.replace("\n", "
    "))); for (const auto& version : selected.versions) { ui->versionSelectionBox->addItem(version.version); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui b/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui index 8b6747331..0b1411b96 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui @@ -10,72 +10,8 @@ 685 - - - - - - - - - - Version selected: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - - - - true - - - true - - - - - - - true - - - - 96 - 48 - - - - - - - - - - Search and filter... - - - true - - - - - - - Search - - - - + + @@ -93,6 +29,63 @@ + + + + Search and filter... + + + true + + + + + + + + + true + + + + 96 + 48 + + + + + + + + true + + + true + + + + + + + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index 3b266bcef..a92d5b579 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -110,6 +110,7 @@ void ListModel::requestLogo(QString logo, QString url) MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo)); auto job = new NetJob(QString("Flame Icon Download %1").arg(logo), APPLICATION->network()); + job->setAskRetry(false); job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); @@ -172,7 +173,7 @@ void ListModel::performPaginatedSearch() callbacks.on_succeed = [this](auto& doc, auto& pack) { searchRequestForOneSucceeded(doc); }; callbacks.on_abort = [this] { qCritical() << "Search task aborted by an unknown reason!"; - searchRequestFailed("Abborted"); + searchRequestFailed("Aborted"); }; static const FlameAPI api; if (auto job = api.getProjectInfo({ projectId }, std::move(callbacks)); job) { diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index f1fd9b5d8..decb5de3b 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -43,6 +43,7 @@ #include "FlameModel.h" #include "InstanceImportTask.h" #include "Json.h" +#include "StringUtils.h" #include "modplatform/flame/FlameAPI.h" #include "ui/dialogs/NewInstanceDialog.h" #include "ui/widgets/ProjectItem.h" @@ -55,7 +56,6 @@ FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::FlamePage), dialog(dialog), m_fetch_progress(this, false) { ui->setupUi(this); - connect(ui->searchButton, &QPushButton::clicked, this, &FlamePage::triggerSearch); ui->searchEdit->installEventFilter(this); listModel = new Flame::ListModel(this); ui->packView->setModel(listModel); @@ -72,7 +72,7 @@ FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) m_fetch_progress.setFixedHeight(24); m_fetch_progress.progressFormat(""); - ui->gridLayout->addWidget(&m_fetch_progress, 2, 0, 1, ui->gridLayout->columnCount()); + ui->verticalLayout->insertWidget(2, &m_fetch_progress); // index is used to set the sorting with the curseforge api ui->sortByBox->addItem(tr("Sort by Featured")); @@ -84,7 +84,7 @@ FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlamePage::onSelectionChanged); - connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlamePage::onVersionSelectionChanged); + connect(ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, &FlamePage::onVersionSelectionChanged); ui->packView->setItemDelegate(new ProjectItemDelegate(this)); ui->packDescription->setMetaEntry("FlamePacks"); @@ -178,7 +178,11 @@ void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelInde for (auto version : current.versions) { auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; - ui->versionSelectionBox->addItem(QString("%1%2").arg(version.version, release_type), QVariant(version.downloadUrl)); + auto mcVersion = !version.mcVersion.isEmpty() && !version.version.contains(version.mcVersion) + ? QString(" for %1").arg(version.mcVersion) + : ""; + ui->versionSelectionBox->addItem(QString("%1%2%3").arg(version.version, mcVersion, release_type), + QVariant(version.downloadUrl)); } QVariant current_updated; @@ -236,17 +240,17 @@ void FlamePage::suggestCurrent() [this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); }); } -void FlamePage::onVersionSelectionChanged(QString version) +void FlamePage::onVersionSelectionChanged(int index) { bool is_blocked = false; ui->versionSelectionBox->currentData().toInt(&is_blocked); - if (version.isNull() || version.isEmpty() || is_blocked) { + if (index == -1 || is_blocked) { m_selected_version_index = -1; return; } - m_selected_version_index = ui->versionSelectionBox->currentIndex(); + m_selected_version_index = index; Q_ASSERT(current.versions.at(m_selected_version_index).downloadUrl == ui->versionSelectionBox->currentData().toString()); @@ -292,6 +296,6 @@ void FlamePage::updateUi() text += "
    "; text += api.getModDescription(current.addonId).toUtf8(); - ui->packDescription->setHtml(text + current.description); + ui->packDescription->setHtml(StringUtils::htmlListPatch(text + current.description)); ui->packDescription->flush(); } diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index d35858fbc..7590e1a95 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -78,7 +78,7 @@ class FlamePage : public QWidget, public BasePage { private slots: void triggerSearch(); void onSelectionChanged(QModelIndex first, QModelIndex second); - void onVersionSelectionChanged(QString data); + void onVersionSelectionChanged(int index); private: Ui::FlamePage* ui = nullptr; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.ui b/launcher/ui/pages/modplatform/flame/FlamePage.ui index f9e1fe67f..d4ddb37a4 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.ui +++ b/launcher/ui/pages/modplatform/flame/FlamePage.ui @@ -10,8 +10,8 @@ 600 - - + + @@ -29,25 +29,14 @@ - - - - - - Search and filter... - - - - - - - Search - - - - + + + + Search and filter... + + - + @@ -77,7 +66,7 @@ - + diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp index 7d18e72a6..ae4562be4 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp @@ -6,11 +6,17 @@ #include "Json.h" +#include "minecraft/PackProfile.h" #include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameModIndex.h" namespace ResourceDownload { +static bool isOptedOut(const ModPlatform::IndexedVersion& ver) +{ + return ver.downloadUrl.isEmpty(); +} + FlameModModel::FlameModModel(BaseInstance& base) : ModModel(base, new FlameAPI) {} void FlameModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) @@ -34,6 +40,11 @@ auto FlameModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJs return FlameMod::loadDependencyVersions(m, arr, &m_base_instance); } +bool FlameModModel::optedOut(const ModPlatform::IndexedVersion& ver) const +{ + return isOptedOut(ver); +} + auto FlameModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray { return Json::ensureArray(obj.object(), "data"); @@ -57,6 +68,11 @@ void FlameResourcePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); } +bool FlameResourcePackModel::optedOut(const ModPlatform::IndexedVersion& ver) const +{ + return isOptedOut(ver); +} + auto FlameResourcePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray { return Json::ensureArray(obj.object(), "data"); @@ -116,6 +132,11 @@ ResourceAPI::VersionSearchArgs FlameTexturePackModel::createVersionsArguments(QM return args; } +bool FlameTexturePackModel::optedOut(const ModPlatform::IndexedVersion& ver) const +{ + return isOptedOut(ver); +} + auto FlameTexturePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray { return Json::ensureArray(obj.object(), "data"); @@ -139,6 +160,11 @@ void FlameShaderPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); } +bool FlameShaderPackModel::optedOut(const ModPlatform::IndexedVersion& ver) const +{ + return isOptedOut(ver); +} + auto FlameShaderPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray { return Json::ensureArray(obj.object(), "data"); diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h index 76dbd7b3d..458fd85d0 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h @@ -17,6 +17,8 @@ class FlameModModel : public ModModel { FlameModModel(BaseInstance&); ~FlameModModel() override = default; + bool optedOut(const ModPlatform::IndexedVersion& ver) const override; + private: [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; } [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); } @@ -36,6 +38,8 @@ class FlameResourcePackModel : public ResourcePackResourceModel { FlameResourcePackModel(const BaseInstance&); ~FlameResourcePackModel() override = default; + bool optedOut(const ModPlatform::IndexedVersion& ver) const override; + private: [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; } [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); } @@ -54,6 +58,8 @@ class FlameTexturePackModel : public TexturePackResourceModel { FlameTexturePackModel(const BaseInstance&); ~FlameTexturePackModel() override = default; + bool optedOut(const ModPlatform::IndexedVersion& ver) const override; + private: [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; } [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); } @@ -75,6 +81,8 @@ class FlameShaderPackModel : public ShaderPackResourceModel { FlameShaderPackModel(const BaseInstance&); ~FlameShaderPackModel() override = default; + bool optedOut(const ModPlatform::IndexedVersion& ver) const override; + private: [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; } [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); } diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index 23373ec9d..62c22902e 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -5,6 +5,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad + * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -37,6 +38,10 @@ */ #include "FlameResourcePages.h" +#include +#include +#include "modplatform/ModIndex.h" +#include "modplatform/flame/FlameAPI.h" #include "ui_ResourcePage.h" #include "FlameResourceModels.h" @@ -44,11 +49,6 @@ namespace ResourceDownload { -static bool isOptedOut(ModPlatform::IndexedVersion const& ver) -{ - return ver.downloadUrl.isEmpty(); -} - FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ModPage(dialog, instance) { m_model = new FlameModModel(instance); @@ -60,25 +60,12 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance) : // so it's best not to connect them in the parent's contructor... connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameModPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, &FlameModPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameModPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); } -auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, - QString mineVer, - std::optional loaders) const -> bool -{ - return ver.mcVersion.contains(mineVer) && !ver.downloadUrl.isEmpty() && - (!loaders.has_value() || !ver.loaders || loaders.value() & ver.loaders); -} - -bool FlameModPage::optedOut(ModPlatform::IndexedVersion& ver) const -{ - return isOptedOut(ver); -} - void FlameModPage::openUrl(const QUrl& url) { if (url.scheme().isEmpty()) { @@ -107,17 +94,13 @@ FlameResourcePackPage::FlameResourcePackPage(ResourcePackDownloadDialog* dialog, // so it's best not to connect them in the parent's contructor... connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameResourcePackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameResourcePackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &FlameResourcePackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameResourcePackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); } -bool FlameResourcePackPage::optedOut(ModPlatform::IndexedVersion& ver) const -{ - return isOptedOut(ver); -} - void FlameResourcePackPage::openUrl(const QUrl& url) { if (url.scheme().isEmpty()) { @@ -146,17 +129,13 @@ FlameTexturePackPage::FlameTexturePackPage(TexturePackDownloadDialog* dialog, Ba // so it's best not to connect them in the parent's contructor... connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameTexturePackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameTexturePackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &FlameTexturePackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameTexturePackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); } -bool FlameTexturePackPage::optedOut(ModPlatform::IndexedVersion& ver) const -{ - return isOptedOut(ver); -} - void FlameTexturePackPage::openUrl(const QUrl& url) { if (url.scheme().isEmpty()) { @@ -185,17 +164,13 @@ FlameShaderPackPage::FlameShaderPackPage(ShaderPackDownloadDialog* dialog, BaseI // so it's best not to connect them in the parent's constructor... connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameShaderPackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameShaderPackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &FlameShaderPackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameShaderPackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); } -bool FlameShaderPackPage::optedOut(ModPlatform::IndexedVersion& ver) const -{ - return isOptedOut(ver); -} - void FlameShaderPackPage::openUrl(const QUrl& url) { if (url.scheme().isEmpty()) { @@ -232,4 +207,19 @@ auto FlameShaderPackPage::shouldDisplay() const -> bool return true; } +unique_qobject_ptr FlameModPage::createFilterWidget() +{ + return ModFilterWidget::create(&static_cast(m_base_instance), false, this); +} + +void FlameModPage::prepareProviderCategories() +{ + auto response = std::make_shared(); + auto task = FlameAPI::getModCategories(response); + QObject::connect(task.get(), &Task::succeeded, [this, response]() { + auto categories = FlameAPI::loadModCategories(response); + m_filter_widget->setCategories(categories); + }); + task->start(); +}; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h index f2f5cecad..6eef3e435 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h @@ -5,6 +5,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad + * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -94,12 +95,11 @@ class FlameModPage : public ModPage { [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; } - bool validateVersion(ModPlatform::IndexedVersion& ver, - QString mineVer, - std::optional loaders = {}) const override; - bool optedOut(ModPlatform::IndexedVersion& ver) const override; - void openUrl(const QUrl& url) override; + unique_qobject_ptr createFilterWidget() override; + + protected: + virtual void prepareProviderCategories() override; }; class FlameResourcePackPage : public ResourcePackResourcePage { @@ -124,8 +124,6 @@ class FlameResourcePackPage : public ResourcePackResourcePage { [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } - bool optedOut(ModPlatform::IndexedVersion& ver) const override; - void openUrl(const QUrl& url) override; }; @@ -151,8 +149,6 @@ class FlameTexturePackPage : public TexturePackResourcePage { [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } - bool optedOut(ModPlatform::IndexedVersion& ver) const override; - void openUrl(const QUrl& url) override; }; @@ -178,8 +174,6 @@ class FlameShaderPackPage : public ShaderPackResourcePage { [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } - bool optedOut(ModPlatform::IndexedVersion& ver) const override; - void openUrl(const QUrl& url) override; }; diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp index ac06f4cdd..db59fe10a 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp @@ -21,6 +21,7 @@ #include "ui_ImportFTBPage.h" #include +#include #include #include "FileSystem.h" #include "ListModel.h" @@ -58,8 +59,8 @@ ImportFTBPage::ImportFTBPage(NewInstanceDialog* dialog, QWidget* parent) : QWidg connect(ui->searchEdit, &QLineEdit::textChanged, this, &ImportFTBPage::triggerSearch); connect(ui->browseButton, &QPushButton::clicked, this, [this] { - auto path = listModel->getPath(); - QString dir = QFileDialog::getExistingDirectory(this, tr("Select FTBApp instances directory"), path, QFileDialog::ShowDirsOnly); + QString dir = QFileDialog::getExistingDirectory(this, tr("Select FTBApp instances directory"), listModel->getUserPath(), + QFileDialog::ShowDirsOnly); if (!dir.isEmpty()) listModel->setPath(dir); }); diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h index 8e9661272..00f013f6f 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h @@ -44,7 +44,7 @@ class ImportFTBPage : public QWidget, public BasePage { QString displayName() const override { return tr("FTB App Import"); } QIcon icon() const override { return APPLICATION->getThemedIcon("ftb_logo"); } QString id() const override { return "import_ftb"; } - QString helpPage() const override { return "FTB-platform"; } + QString helpPage() const override { return "FTB-import"; } bool shouldDisplay() const override { return true; } void openedImpl() override; void retranslate() override; diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui index 6613a5939..18c604ca4 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui @@ -10,8 +10,49 @@ 1011 - - + + + + + Note: If your FTB instances are not in the default location, select it using the button next to search. + + + Qt::AlignCenter + + + + + + + + + Search and filter... + + + true + + + + + + + Select FTBApp instances directory + + + + + + + .. + + + true + + + + + + @@ -21,7 +62,7 @@ - + @@ -48,54 +89,6 @@ - - - - - - Search and filter... - - - true - - - - - - - Search - - - - - - - Select FTBApp instances directory - - - - - - - .. - - - true - - - - - - - - - Note: If your FTB instances are not in the default location, select it using the button next to search. - - - Qt::AlignCenter - - - diff --git a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp index e058937a6..f3c737977 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp @@ -24,45 +24,76 @@ #include #include #include "Application.h" +#include "Exception.h" #include "FileSystem.h" +#include "Json.h" #include "StringUtils.h" #include "modplatform/import_ftb/PackHelpers.h" #include "ui/widgets/ProjectItem.h" namespace FTBImportAPP { -QString getStaticPath() +QString getFTBRoot() { - QString partialPath; + QString partialPath = QDir::homePath(); #if defined(Q_OS_OSX) - partialPath = FS::PathCombine(QDir::homePath(), "Library/Application Support"); -#elif defined(Q_OS_WIN32) - partialPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", ""); -#else - partialPath = QDir::homePath(); + partialPath = FS::PathCombine(partialPath, "Library/Application Support"); #endif return FS::PathCombine(partialPath, ".ftba"); } -static const QString FTB_APP_PATH = FS::PathCombine(getStaticPath(), "instances"); +QString getDynamicPath() +{ + auto settingsPath = FS::PathCombine(getFTBRoot(), "storage", "settings.json"); + if (!QFileInfo::exists(settingsPath)) + settingsPath = FS::PathCombine(getFTBRoot(), "bin", "settings.json"); + if (!QFileInfo::exists(settingsPath)) { + qWarning() << "The ftb app setings doesn't exist."; + return {}; + } + try { + auto doc = Json::requireDocument(FS::read(settingsPath)); + return Json::requireString(Json::requireObject(doc), "instanceLocation"); + } catch (const Exception& e) { + qCritical() << "Could not read ftb settings file: " << e.cause(); + } + return {}; +} + +ListModel::ListModel(QObject* parent) : QAbstractListModel(parent), m_instances_path(getDynamicPath()) {} void ListModel::update() { beginResetModel(); - modpacks.clear(); + m_modpacks.clear(); - QString instancesPath = getPath(); - if (auto instancesInfo = QFileInfo(instancesPath); instancesInfo.exists() && instancesInfo.isDir()) { - QDirIterator directoryIterator(instancesPath, QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable | QDir::Hidden, + auto wasPathAdded = [this](QString path) { + for (auto pack : m_modpacks) { + if (pack.path == path) + return true; + } + return false; + }; + + auto scanPath = [this, wasPathAdded](QString path) { + if (path.isEmpty()) + return; + if (auto instancesInfo = QFileInfo(path); !instancesInfo.exists() || !instancesInfo.isDir()) + return; + QDirIterator directoryIterator(path, QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks); while (directoryIterator.hasNext()) { - auto modpack = parseDirectory(directoryIterator.next()); - if (!modpack.path.isEmpty()) - modpacks.append(modpack); + auto currentPath = directoryIterator.next(); + if (!wasPathAdded(currentPath)) { + auto modpack = parseDirectory(currentPath); + if (!modpack.path.isEmpty()) + m_modpacks.append(modpack); + } } - } else { - qDebug() << "Couldn't find ftb instances folder: " << instancesPath; - } + }; + + scanPath(APPLICATION->settings()->get("FTBAppInstancesPath").toString()); + scanPath(m_instances_path); endResetModel(); } @@ -70,11 +101,11 @@ void ListModel::update() QVariant ListModel::data(const QModelIndex& index, int role) const { int pos = index.row(); - if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { + if (pos >= m_modpacks.size() || pos < 0 || !index.isValid()) { return QVariant(); } - auto pack = modpacks.at(pos); + auto pack = m_modpacks.at(pos); if (role == Qt::ToolTipRole) { } @@ -110,9 +141,9 @@ QVariant ListModel::data(const QModelIndex& index, int role) const FilterModel::FilterModel(QObject* parent) : QSortFilterProxyModel(parent) { - currentSorting = Sorting::ByGameVersion; - sortings.insert(tr("Sort by Name"), Sorting::ByName); - sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion); + m_currentSorting = Sorting::ByGameVersion; + m_sortings.insert(tr("Sort by Name"), Sorting::ByName); + m_sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion); } bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const @@ -120,12 +151,12 @@ bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) co Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value(); Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value(); - if (currentSorting == Sorting::ByGameVersion) { + if (m_currentSorting == Sorting::ByGameVersion) { Version lv(leftPack.mcVersion); Version rv(rightPack.mcVersion); return lv < rv; - } else if (currentSorting == Sorting::ByName) { + } else if (m_currentSorting == Sorting::ByName) { return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; } @@ -136,39 +167,39 @@ bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) co bool FilterModel::filterAcceptsRow([[maybe_unused]] int sourceRow, [[maybe_unused]] const QModelIndex& sourceParent) const { - if (searchTerm.isEmpty()) { + if (m_searchTerm.isEmpty()) { return true; } QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); Modpack pack = sourceModel()->data(index, Qt::UserRole).value(); - return pack.name.contains(searchTerm, Qt::CaseInsensitive); + return pack.name.contains(m_searchTerm, Qt::CaseInsensitive); } void FilterModel::setSearchTerm(const QString term) { - searchTerm = term.trimmed(); + m_searchTerm = term.trimmed(); invalidate(); } const QMap FilterModel::getAvailableSortings() { - return sortings; + return m_sortings; } QString FilterModel::translateCurrentSorting() { - return sortings.key(currentSorting); + return m_sortings.key(m_currentSorting); } void FilterModel::setSorting(Sorting s) { - currentSorting = s; + m_currentSorting = s; invalidate(); } FilterModel::Sorting FilterModel::getCurrentSorting() { - return currentSorting; + return m_currentSorting; } void ListModel::setPath(QString path) { @@ -176,11 +207,11 @@ void ListModel::setPath(QString path) update(); } -QString ListModel::getPath() +QString ListModel::getUserPath() { auto path = APPLICATION->settings()->get("FTBAppInstancesPath").toString(); - if (path.isEmpty() || !QFileInfo(path).exists()) - path = FTB_APP_PATH; + if (path.isEmpty()) + path = m_instances_path; return path; } } // namespace FTBImportAPP \ No newline at end of file diff --git a/launcher/ui/pages/modplatform/import_ftb/ListModel.h b/launcher/ui/pages/modplatform/import_ftb/ListModel.h index ed33a88f3..a842ac8ff 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ListModel.h +++ b/launcher/ui/pages/modplatform/import_ftb/ListModel.h @@ -42,28 +42,29 @@ class FilterModel : public QSortFilterProxyModel { bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; private: - QMap sortings; - Sorting currentSorting; - QString searchTerm; + QMap m_sortings; + Sorting m_currentSorting; + QString m_searchTerm; }; class ListModel : public QAbstractListModel { Q_OBJECT public: - ListModel(QObject* parent) : QAbstractListModel(parent) {} + ListModel(QObject* parent); virtual ~ListModel() = default; - int rowCount(const QModelIndex& parent) const { return modpacks.size(); } + int rowCount(const QModelIndex& parent) const { return m_modpacks.size(); } int columnCount(const QModelIndex& parent) const { return 1; } QVariant data(const QModelIndex& index, int role) const; void update(); - QString getPath(); + QString getUserPath(); void setPath(QString path); private: - ModpackList modpacks; + ModpackList m_modpacks; + const QString m_instances_path; }; } // namespace FTBImportAPP \ No newline at end of file diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index 49666cf6e..98922123c 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -264,6 +264,7 @@ void ListModel::requestLogo(QString file) MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(file)); NetJob* job = new NetJob(QString("FTB Icon Download for %1").arg(file), APPLICATION->network()); + job->setAskRetry(false); job->addNetAction(Net::ApiDownload::makeCached(QUrl(QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1").arg(file)), entry)); auto fullPath = entry->getFullPath(); diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp index 0ecaf4625..a587b5baf 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp @@ -35,6 +35,7 @@ */ #include "Page.h" +#include "StringUtils.h" #include "ui/widgets/ProjectItem.h" #include "ui_Page.h" @@ -260,8 +261,9 @@ void Page::onPackSelectionChanged(Modpack* pack) { ui->versionSelectionBox->clear(); if (pack) { - currentModpackInfo->setHtml("Pack by " + pack->author + "" + "
    Minecraft " + pack->mcVersion + "
    " + "
    " + - pack->description + "
    • " + pack->mods.replace(";", "
    • ") + "
    "); + currentModpackInfo->setHtml(StringUtils::htmlListPatch("Pack by " + pack->author + "" + "
    Minecraft " + pack->mcVersion + + "
    " + "
    " + pack->description + "
    • " + + pack->mods.replace(";", "
    • ") + "
    ")); bool currentAdded = false; for (int i = 0; i < pack->oldVersions.size(); i++) { diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.h b/launcher/ui/pages/modplatform/legacy_ftb/Page.h index 4d317b7c0..daef23342 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.h +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.h @@ -66,7 +66,7 @@ class Page : public QWidget, public BasePage { QString displayName() const override { return "FTB Legacy"; } QIcon icon() const override { return APPLICATION->getThemedIcon("ftb_logo"); } QString id() const override { return "legacy_ftb"; } - QString helpPage() const override { return "FTB-platform"; } + QString helpPage() const override { return "FTB-legacy"; } bool shouldDisplay() const override; void openedImpl() override; void retranslate() override; diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.ui b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui index 56cba7485..544ad77d3 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.ui +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui @@ -10,8 +10,8 @@ 602 - - + + @@ -23,16 +23,9 @@ - - - - Search - - - - + 0 @@ -134,22 +127,9 @@ - - - - - - Version selected: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - + + + @@ -159,6 +139,19 @@ + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index bac294b60..b53eea4ef 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -254,6 +254,7 @@ void ModpackListModel::requestLogo(QString logo, QString url) MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo)); auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network()); + job->setAskRetry(false); job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); @@ -352,10 +353,10 @@ void ModpackListModel::searchRequestForOneSucceeded(QJsonDocument& doc) void ModpackListModel::searchRequestFailed(QString reason) { auto failed_action = dynamic_cast(jobPtr.get())->getFailedActions().at(0); - if (!failed_action->m_reply) { + if (failed_action->replyStatusCode() == -1) { // Network error QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load modpacks.")); - } else if (failed_action->m_reply && failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) { + } else if (failed_action->replyStatusCode() == 409) { // 409 Gone, notify user to update QMessageBox::critical(nullptr, tr("Error"), //: %1 refers to the launcher itself diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index da5fe1e7b..03461d85a 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -44,6 +44,7 @@ #include "InstanceImportTask.h" #include "Json.h" #include "Markdown.h" +#include "StringUtils.h" #include "ui/widgets/ProjectItem.h" @@ -58,7 +59,6 @@ ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) { ui->setupUi(this); - connect(ui->searchButton, &QPushButton::clicked, this, &ModrinthPage::triggerSearch); ui->searchEdit->installEventFilter(this); m_model = new Modrinth::ModpackListModel(this); ui->packView->setModel(m_model); @@ -75,7 +75,7 @@ ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) m_fetch_progress.setFixedHeight(24); m_fetch_progress.progressFormat(""); - ui->gridLayout->addWidget(&m_fetch_progress, 2, 0, 1, ui->gridLayout->columnCount()); + ui->verticalLayout->insertWidget(1, &m_fetch_progress); ui->sortByBox->addItem(tr("Sort by Relevance")); ui->sortByBox->addItem(tr("Sort by Total Downloads")); @@ -85,7 +85,7 @@ ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged); - connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthPage::onVersionSelectionChanged); + connect(ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ModrinthPage::onVersionSelectionChanged); ui->packView->setItemDelegate(new ProjectItemDelegate(this)); ui->packDescription->setMetaEntry(metaEntryBase()); @@ -223,11 +223,12 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI } for (auto version : current.versions) { auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; - if (!version.name.contains(version.version)) - ui->versionSelectionBox->addItem(QString("%1 — %2%3").arg(version.name, version.version, release_type), - QVariant(version.id)); - else - ui->versionSelectionBox->addItem(QString("%1%2").arg(version.name, release_type), QVariant(version.id)); + auto mcVersion = !version.gameVersion.isEmpty() && !version.name.contains(version.gameVersion) + ? QString(" for %1").arg(version.gameVersion) + : ""; + auto versionStr = !version.name.contains(version.version) ? version.version : ""; + ui->versionSelectionBox->addItem(QString("%1%2 — %3%4").arg(version.name, mcVersion, versionStr, release_type), + QVariant(version.id)); } QVariant current_updated; @@ -304,7 +305,7 @@ void ModrinthPage::updateUI() text += markdownToHTML(current.extra.body.toUtf8()); - ui->packDescription->setHtml(text + current.description); + ui->packDescription->setHtml(StringUtils::htmlListPatch(text + current.description)); ui->packDescription->flush(); } @@ -341,9 +342,9 @@ void ModrinthPage::triggerSearch() m_fetch_progress.watch(m_model->activeSearchJob().get()); } -void ModrinthPage::onVersionSelectionChanged(QString version) +void ModrinthPage::onVersionSelectionChanged(int index) { - if (version.isNull() || version.isEmpty()) { + if (index == -1) { selectedVersion = ""; return; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 4240dcafb..dadaeb0a0 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -80,7 +80,7 @@ class ModrinthPage : public QWidget, public BasePage { private slots: void onSelectionChanged(QModelIndex first, QModelIndex second); - void onVersionSelectionChanged(QString data); + void onVersionSelectionChanged(int index); void triggerSearch(); private: diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui index 68b1d4e24..7f4f903f6 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -10,8 +10,15 @@ 600 - - + + + + + Search and filter ... + + + + @@ -41,7 +48,7 @@ - + @@ -61,24 +68,6 @@ - - - - - - Search and filter ... - - - - - - - Search - - - - - @@ -89,8 +78,6 @@ - searchEdit - searchButton packView packDescription sortByBox diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index a4197b225..85dcde471 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -4,6 +4,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -57,19 +58,13 @@ ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instan // so it's best not to connect them in the parent's constructor... connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthModPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthModPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &ModrinthModPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthModPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); } -auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, - QString mineVer, - std::optional loaders) const -> bool -{ - return ver.mcVersion.contains(mineVer) && (!loaders.has_value() || !ver.loaders || loaders.value() & ver.loaders); -} - ModrinthResourcePackPage::ModrinthResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance) : ResourcePackResourcePage(dialog, instance) { @@ -82,7 +77,8 @@ ModrinthResourcePackPage::ModrinthResourcePackPage(ResourcePackDownloadDialog* d // so it's best not to connect them in the parent's constructor... connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthResourcePackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthResourcePackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &ModrinthResourcePackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthResourcePackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); @@ -100,7 +96,8 @@ ModrinthTexturePackPage::ModrinthTexturePackPage(TexturePackDownloadDialog* dial // so it's best not to connect them in the parent's constructor... connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthTexturePackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthTexturePackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &ModrinthTexturePackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthTexturePackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); @@ -118,7 +115,8 @@ ModrinthShaderPackPage::ModrinthShaderPackPage(ShaderPackDownloadDialog* dialog, // so it's best not to connect them in the parent's constructor... connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthShaderPackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthShaderPackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &ModrinthShaderPackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthShaderPackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); @@ -144,4 +142,19 @@ auto ModrinthShaderPackPage::shouldDisplay() const -> bool return true; } +unique_qobject_ptr ModrinthModPage::createFilterWidget() +{ + return ModFilterWidget::create(&static_cast(m_base_instance), true, this); +} + +void ModrinthModPage::prepareProviderCategories() +{ + auto response = std::make_shared(); + auto task = ModrinthAPI::getModCategories(response); + QObject::connect(task.get(), &Task::succeeded, [this, response]() { + auto categories = ModrinthAPI::loadModCategories(response); + m_filter_widget->setCategories(categories); + }); + task->start(); +}; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h index 311bcfe32..eaf6129a5 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h @@ -4,6 +4,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -93,8 +94,10 @@ class ModrinthModPage : public ModPage { [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; } - auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional loaders = {}) const - -> bool override; + unique_qobject_ptr createFilterWidget() override; + + protected: + virtual void prepareProviderCategories() override; }; class ModrinthResourcePackPage : public ResourcePackResourcePage { diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index 6f1810d71..4181edab6 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -292,6 +292,7 @@ void Technic::ListModel::requestLogo(QString logo, QString url) MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo)); auto job = new NetJob(QString("Technic Icon Download %1").arg(logo), APPLICATION->network()); + job->setAskRetry(false); job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index 6b1ec8cb5..a8f06619f 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -44,6 +44,7 @@ #include "BuildConfig.h" #include "Json.h" +#include "StringUtils.h" #include "TechnicModel.h" #include "modplatform/technic/SingleZipPackInstallTask.h" #include "modplatform/technic/SolderPackInstallTask.h" @@ -57,7 +58,6 @@ TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::TechnicPage), dialog(dialog), m_fetch_progress(this, false) { ui->setupUi(this); - connect(ui->searchButton, &QPushButton::clicked, this, &TechnicPage::triggerSearch); ui->searchEdit->installEventFilter(this); model = new Technic::ListModel(this); ui->packView->setModel(model); @@ -71,7 +71,7 @@ TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget* parent) m_fetch_progress.setFixedHeight(24); m_fetch_progress.progressFormat(""); - ui->gridLayout->addWidget(&m_fetch_progress, 2, 0, 1, ui->gridLayout->columnCount()); + ui->verticalLayout->insertWidget(1, &m_fetch_progress); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TechnicPage::onSelectionChanged); connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &TechnicPage::onVersionSelectionChanged); @@ -233,7 +233,7 @@ void TechnicPage::metadataLoaded() text += "

    "; - ui->packDescription->setHtml(text + current.description); + ui->packDescription->setHtml(StringUtils::htmlListPatch(text + current.description)); // Strip trailing forward-slashes from Solder URL's if (current.isSolder) { diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.ui b/launcher/ui/pages/modplatform/technic/TechnicPage.ui index b988eda2b..f4e75ae12 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.ui +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.ui @@ -10,23 +10,41 @@ 405 - - - - - - - - - - Version selected: + + + + + Search and filter... + + + + + + + + + true - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 48 + 48 + - + + + + true + + + + + + + + Qt::Horizontal @@ -42,46 +60,21 @@ - - - - - - - - true + + + + Version selected: - - - 48 - 48 - + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - true - - + + - - - - Search and filter... - - - - - - - Search - - - 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..bd72cf695 --- /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/BaseWizardPage.h b/launcher/ui/setupwizard/BaseWizardPage.h index 80cc64969..b5ea06214 100644 --- a/launcher/ui/setupwizard/BaseWizardPage.h +++ b/launcher/ui/setupwizard/BaseWizardPage.h @@ -6,7 +6,7 @@ class BaseWizardPage : public QWizardPage { public: explicit BaseWizardPage(QWidget* parent = Q_NULLPTR) : QWizardPage(parent) {} - virtual ~BaseWizardPage(){}; + virtual ~BaseWizardPage() {}; virtual bool wantsRefreshButton() { return false; } virtual void refresh() {} diff --git a/launcher/ui/setupwizard/JavaWizardPage.cpp b/launcher/ui/setupwizard/JavaWizardPage.cpp index abe4860da..47718d6da 100644 --- a/launcher/ui/setupwizard/JavaWizardPage.cpp +++ b/launcher/ui/setupwizard/JavaWizardPage.cpp @@ -12,12 +12,8 @@ #include -#include "FileSystem.h" #include "JavaCommon.h" -#include "java/JavaInstall.h" -#include "java/JavaUtils.h" -#include "ui/dialogs/CustomMessageBox.h" #include "ui/widgets/JavaSettingsWidget.h" #include "ui/widgets/VersionSelectWidget.h" @@ -57,6 +53,9 @@ bool JavaWizardPage::validatePage() { auto settings = APPLICATION->settings(); 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: { @@ -84,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 6c083dc96..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/BrightTheme.cpp b/launcher/ui/themes/BrightTheme.cpp index ffccdaab1..81bdd773e 100644 --- a/launcher/ui/themes/BrightTheme.cpp +++ b/launcher/ui/themes/BrightTheme.cpp @@ -1,3 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2024 Tayou + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ #include "BrightTheme.h" #include @@ -12,11 +46,6 @@ QString BrightTheme::name() return QObject::tr("Bright"); } -bool BrightTheme::hasColorScheme() -{ - return true; -} - QPalette BrightTheme::colorScheme() { QPalette brightPalette; @@ -55,3 +84,7 @@ QString BrightTheme::appStyleSheet() { return QString(); } +QString BrightTheme::tooltip() +{ + return QString(); +} diff --git a/launcher/ui/themes/BrightTheme.h b/launcher/ui/themes/BrightTheme.h index 44a767492..070eef124 100644 --- a/launcher/ui/themes/BrightTheme.h +++ b/launcher/ui/themes/BrightTheme.h @@ -1,3 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2024 Tayou + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ #pragma once #include "FusionTheme.h" @@ -8,9 +42,9 @@ class BrightTheme : public FusionTheme { QString id() override; QString name() override; + QString tooltip() override; bool hasStyleSheet() override; QString appStyleSheet() override; - bool hasColorScheme() override; QPalette colorScheme() override; double fadeAmount() override; QColor fadeColor() override; diff --git a/launcher/ui/themes/CustomTheme.cpp b/launcher/ui/themes/CustomTheme.cpp index 4859983c6..081ba1900 100644 --- a/launcher/ui/themes/CustomTheme.cpp +++ b/launcher/ui/themes/CustomTheme.cpp @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou + * Copyright (C) 2024 Tayou + * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -39,121 +40,6 @@ const char* themeFile = "theme.json"; -static bool readThemeJson(const QString& path, - QPalette& palette, - double& fadeAmount, - QColor& fadeColor, - QString& name, - QString& widgets, - QString& qssFilePath, - bool& dataIncomplete) -{ - QFileInfo pathInfo(path); - if (pathInfo.exists() && pathInfo.isFile()) { - try { - auto doc = Json::requireDocument(path, "Theme JSON file"); - const QJsonObject root = doc.object(); - dataIncomplete = !root.contains("qssFilePath"); - name = Json::requireString(root, "name", "Theme name"); - widgets = Json::requireString(root, "widgets", "Qt widget theme"); - qssFilePath = Json::ensureString(root, "qssFilePath", "themeStyle.css"); - auto colorsRoot = Json::requireObject(root, "colors", "colors object"); - auto readColor = [&](QString colorName) -> QColor { - auto colorValue = Json::ensureString(colorsRoot, colorName, QString()); - if (!colorValue.isEmpty()) { - QColor color(colorValue); - if (!color.isValid()) { - themeWarningLog() << "Color value" << colorValue << "for" << colorName << "was not recognized."; - return QColor(); - } - return color; - } - return QColor(); - }; - auto readAndSetColor = [&](QPalette::ColorRole role, QString colorName) { - auto color = readColor(colorName); - if (color.isValid()) { - palette.setColor(role, color); - } else { - themeDebugLog() << "Color value for" << colorName << "was not present."; - } - }; - - // palette - readAndSetColor(QPalette::Window, "Window"); - readAndSetColor(QPalette::WindowText, "WindowText"); - readAndSetColor(QPalette::Base, "Base"); - readAndSetColor(QPalette::AlternateBase, "AlternateBase"); - readAndSetColor(QPalette::ToolTipBase, "ToolTipBase"); - readAndSetColor(QPalette::ToolTipText, "ToolTipText"); - readAndSetColor(QPalette::Text, "Text"); - readAndSetColor(QPalette::Button, "Button"); - readAndSetColor(QPalette::ButtonText, "ButtonText"); - readAndSetColor(QPalette::BrightText, "BrightText"); - readAndSetColor(QPalette::Link, "Link"); - readAndSetColor(QPalette::Highlight, "Highlight"); - readAndSetColor(QPalette::HighlightedText, "HighlightedText"); - - // fade - fadeColor = readColor("fadeColor"); - fadeAmount = Json::ensureDouble(colorsRoot, "fadeAmount", 0.5, "fade amount"); - - } catch (const Exception& e) { - themeWarningLog() << "Couldn't load theme json: " << e.cause(); - return false; - } - } else { - themeDebugLog() << "No theme json present."; - return false; - } - return true; -} - -static bool writeThemeJson(const QString& path, - const QPalette& palette, - double fadeAmount, - QColor fadeColor, - QString name, - QString widgets, - QString qssFilePath) -{ - QJsonObject rootObj; - rootObj.insert("name", name); - rootObj.insert("widgets", widgets); - rootObj.insert("qssFilePath", qssFilePath); - - QJsonObject colorsObj; - auto insertColor = [&](QPalette::ColorRole role, QString colorName) { colorsObj.insert(colorName, palette.color(role).name()); }; - - // palette - insertColor(QPalette::Window, "Window"); - insertColor(QPalette::WindowText, "WindowText"); - insertColor(QPalette::Base, "Base"); - insertColor(QPalette::AlternateBase, "AlternateBase"); - insertColor(QPalette::ToolTipBase, "ToolTipBase"); - insertColor(QPalette::ToolTipText, "ToolTipText"); - insertColor(QPalette::Text, "Text"); - insertColor(QPalette::Button, "Button"); - insertColor(QPalette::ButtonText, "ButtonText"); - insertColor(QPalette::BrightText, "BrightText"); - insertColor(QPalette::Link, "Link"); - insertColor(QPalette::Highlight, "Highlight"); - insertColor(QPalette::HighlightedText, "HighlightedText"); - - // fade - colorsObj.insert("fadeColor", fadeColor.name()); - colorsObj.insert("fadeAmount", fadeAmount); - - rootObj.insert("colors", colorsObj); - try { - Json::write(rootObj, path); - return true; - } catch ([[maybe_unused]] const Exception& e) { - themeWarningLog() << "Failed to write theme json to" << path; - return false; - } -} - /// @param baseTheme Base Theme /// @param fileInfo FileInfo object for file to load /// @param isManifest whether to load a theme manifest or a qss file @@ -176,23 +62,22 @@ CustomTheme::CustomTheme(ITheme* baseTheme, QFileInfo& fileInfo, bool isManifest auto themeFilePath = FS::PathCombine(path, themeFile); - bool jsonDataIncomplete = false; - m_palette = baseTheme->colorScheme(); - if (readThemeJson(themeFilePath, m_palette, m_fadeAmount, m_fadeColor, m_name, m_widgets, m_qssFilePath, jsonDataIncomplete)) { + + bool hasCustomLogColors = false; + + if (read(themeFilePath, hasCustomLogColors)) { // If theme data was found, fade "Disabled" color of each role according to FadeAmount m_palette = fadeInactive(m_palette, m_fadeAmount, m_fadeColor); + + if (!hasCustomLogColors) + m_logColors = defaultLogColors(m_palette); } else { themeDebugLog() << "Did not read theme json file correctly, not changing theme, keeping previous."; + m_logColors = defaultLogColors(m_palette); return; } - // FIXME: This is kinda jank, it only actually checks if the qss file path is not present. It should actually check for any relevant - // missing data (e.g. name, colors) - if (jsonDataIncomplete) { - writeThemeJson(fileInfo.absoluteFilePath(), m_palette, m_fadeAmount, m_fadeColor, m_name, m_widgets, m_qssFilePath); - } - auto qssFilePath = FS::PathCombine(path, m_qssFilePath); QFileInfo info(qssFilePath); if (info.isFile()) { @@ -251,11 +136,6 @@ QString CustomTheme::name() return m_name; } -bool CustomTheme::hasColorScheme() -{ - return true; -} - QPalette CustomTheme::colorScheme() { return m_palette; @@ -285,3 +165,103 @@ QString CustomTheme::qtTheme() { return m_widgets; } +QString CustomTheme::tooltip() +{ + return m_tooltip; +} + +bool CustomTheme::read(const QString& path, bool& hasCustomLogColors) +{ + QFileInfo pathInfo(path); + if (pathInfo.exists() && pathInfo.isFile()) { + try { + auto doc = Json::requireDocument(path, "Theme JSON file"); + const QJsonObject root = doc.object(); + m_name = Json::requireString(root, "name", "Theme name"); + m_widgets = Json::requireString(root, "widgets", "Qt widget theme"); + m_qssFilePath = Json::ensureString(root, "qssFilePath", "themeStyle.css"); + + auto readColor = [&](const QJsonObject& colors, const QString& colorName) -> QColor { + auto colorValue = Json::ensureString(colors, colorName, QString()); + if (!colorValue.isEmpty()) { + QColor color(colorValue); + if (!color.isValid()) { + themeWarningLog() << "Color value" << colorValue << "for" << colorName << "was not recognized."; + return {}; + } + return color; + } + return {}; + }; + + if (root.contains("colors")) { + auto colorsRoot = Json::requireObject(root, "colors"); + auto readAndSetPaletteColor = [&](QPalette::ColorRole role, const QString& colorName) { + auto color = readColor(colorsRoot, colorName); + if (color.isValid()) { + m_palette.setColor(role, color); + } else { + themeDebugLog() << "Color value for" << colorName << "was not present."; + } + }; + + // palette + readAndSetPaletteColor(QPalette::Window, "Window"); + readAndSetPaletteColor(QPalette::WindowText, "WindowText"); + readAndSetPaletteColor(QPalette::Base, "Base"); + readAndSetPaletteColor(QPalette::AlternateBase, "AlternateBase"); + readAndSetPaletteColor(QPalette::ToolTipBase, "ToolTipBase"); + readAndSetPaletteColor(QPalette::ToolTipText, "ToolTipText"); + readAndSetPaletteColor(QPalette::Text, "Text"); + readAndSetPaletteColor(QPalette::Button, "Button"); + readAndSetPaletteColor(QPalette::ButtonText, "ButtonText"); + readAndSetPaletteColor(QPalette::BrightText, "BrightText"); + readAndSetPaletteColor(QPalette::Link, "Link"); + readAndSetPaletteColor(QPalette::Highlight, "Highlight"); + readAndSetPaletteColor(QPalette::HighlightedText, "HighlightedText"); + + // fade + m_fadeColor = readColor(colorsRoot, "fadeColor"); + m_fadeAmount = Json::ensureDouble(colorsRoot, "fadeAmount", 0.5, "fade amount"); + } + + if (root.contains("logColors")) { + hasCustomLogColors = true; + + auto logColorsRoot = Json::requireObject(root, "logColors"); + auto readAndSetLogColor = [&](MessageLevel::Enum level, bool fg, const QString& colorName) { + auto color = readColor(logColorsRoot, colorName); + if (color.isValid()) { + if (fg) + m_logColors.foreground[level] = color; + else + m_logColors.background[level] = color; + } else { + themeDebugLog() << "Color value for" << colorName << "was not present."; + } + }; + + readAndSetLogColor(MessageLevel::Message, false, "MessageHighlight"); + readAndSetLogColor(MessageLevel::Launcher, false, "LauncherHighlight"); + readAndSetLogColor(MessageLevel::Debug, false, "DebugHighlight"); + readAndSetLogColor(MessageLevel::Warning, false, "WarningHighlight"); + readAndSetLogColor(MessageLevel::Error, false, "ErrorHighlight"); + readAndSetLogColor(MessageLevel::Fatal, false, "FatalHighlight"); + + readAndSetLogColor(MessageLevel::Message, true, "Message"); + readAndSetLogColor(MessageLevel::Launcher, true, "Launcher"); + readAndSetLogColor(MessageLevel::Debug, true, "Debug"); + readAndSetLogColor(MessageLevel::Warning, true, "Warning"); + readAndSetLogColor(MessageLevel::Error, true, "Error"); + readAndSetLogColor(MessageLevel::Fatal, true, "Fatal"); + } + } catch (const Exception& e) { + themeWarningLog() << "Couldn't load theme json: " << e.cause(); + return false; + } + } else { + themeDebugLog() << "No theme json present."; + return false; + } + return true; +} diff --git a/launcher/ui/themes/CustomTheme.h b/launcher/ui/themes/CustomTheme.h index 3ec4cafa2..b8d073921 100644 --- a/launcher/ui/themes/CustomTheme.h +++ b/launcher/ui/themes/CustomTheme.h @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou + * Copyright (C) 2024 Tayou + * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -44,16 +45,19 @@ class CustomTheme : public ITheme { QString id() override; QString name() override; + QString tooltip() override; bool hasStyleSheet() override; QString appStyleSheet() override; - bool hasColorScheme() override; QPalette colorScheme() override; double fadeAmount() override; QColor fadeColor() override; QString qtTheme() override; + LogColors logColorScheme() override { return m_logColors; } QStringList searchPaths() override; - private: /* data */ + private: + bool read(const QString& path, bool& hasCustomLogColors); + QPalette m_palette; QColor m_fadeColor; double m_fadeAmount; @@ -62,4 +66,11 @@ class CustomTheme : public ITheme { QString m_id; QString m_widgets; QString m_qssFilePath; + LogColors m_logColors; + /** + * The tooltip could be defined in the theme json, + * or composed of other fields that could be in there. + * like author, license, etc. + */ + QString m_tooltip = ""; }; diff --git a/launcher/ui/themes/DarkTheme.cpp b/launcher/ui/themes/DarkTheme.cpp index c3a68a2d4..804126547 100644 --- a/launcher/ui/themes/DarkTheme.cpp +++ b/launcher/ui/themes/DarkTheme.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2024 Tayou + * Copyright (C) 2024 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ #include "DarkTheme.h" #include @@ -12,11 +47,6 @@ QString DarkTheme::name() return QObject::tr("Dark"); } -bool DarkTheme::hasColorScheme() -{ - return true; -} - QPalette DarkTheme::colorScheme() { QPalette darkPalette; @@ -56,3 +86,8 @@ QString DarkTheme::appStyleSheet() { return "QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }"; } + +QString DarkTheme::tooltip() +{ + return ""; +} diff --git a/launcher/ui/themes/DarkTheme.h b/launcher/ui/themes/DarkTheme.h index 431e9a735..c97edbcbe 100644 --- a/launcher/ui/themes/DarkTheme.h +++ b/launcher/ui/themes/DarkTheme.h @@ -1,3 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2024 Tayou + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ #pragma once #include "FusionTheme.h" @@ -8,9 +42,9 @@ class DarkTheme : public FusionTheme { QString id() override; QString name() override; + QString tooltip() override; bool hasStyleSheet() override; QString appStyleSheet() override; - bool hasColorScheme() override; QPalette colorScheme() override; double fadeAmount() override; QColor fadeColor() override; diff --git a/launcher/ui/themes/HintOverrideProxyStyle.cpp b/launcher/ui/themes/HintOverrideProxyStyle.cpp new file mode 100644 index 000000000..80e821349 --- /dev/null +++ b/launcher/ui/themes/HintOverrideProxyStyle.cpp @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2024 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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 "HintOverrideProxyStyle.h" + +int HintOverrideProxyStyle::styleHint(QStyle::StyleHint hint, + const QStyleOption* option, + const QWidget* widget, + QStyleHintReturn* returnData) const +{ + if (hint == QStyle::SH_ItemView_ActivateItemOnSingleClick) + return 0; + + return QProxyStyle::styleHint(hint, option, widget, returnData); +} diff --git a/launcher/ui/themes/HintOverrideProxyStyle.h b/launcher/ui/themes/HintOverrideProxyStyle.h new file mode 100644 index 000000000..09b296018 --- /dev/null +++ b/launcher/ui/themes/HintOverrideProxyStyle.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2024 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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 + +/// Used to override platform-specific behaviours which the launcher does work well with. +class HintOverrideProxyStyle : public QProxyStyle { + Q_OBJECT + public: + HintOverrideProxyStyle(QStyle* style) : QProxyStyle(style) {} + + int styleHint(QStyle::StyleHint hint, + const QStyleOption* option = nullptr, + const QWidget* widget = nullptr, + QStyleHintReturn* returnData = nullptr) const override; +}; diff --git a/launcher/ui/themes/ITheme.cpp b/launcher/ui/themes/ITheme.cpp index 316b0f2ed..cae6e90db 100644 --- a/launcher/ui/themes/ITheme.cpp +++ b/launcher/ui/themes/ITheme.cpp @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Tayou + * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -36,15 +37,14 @@ #include #include #include "Application.h" +#include "HintOverrideProxyStyle.h" #include "rainbow.h" void ITheme::apply(bool) { APPLICATION->setStyleSheet(QString()); - QApplication::setStyle(QStyleFactory::create(qtTheme())); - if (hasColorScheme()) { - QApplication::setPalette(colorScheme()); - } + QApplication::setStyle(new HintOverrideProxyStyle(QStyleFactory::create(qtTheme()))); + QApplication::setPalette(colorScheme()); APPLICATION->setStyleSheet(appStyleSheet()); QDir::setSearchPaths("theme", searchPaths()); } @@ -71,3 +71,30 @@ QPalette ITheme::fadeInactive(QPalette in, qreal bias, QColor color) blend(QPalette::HighlightedText); return in; } + +LogColors ITheme::defaultLogColors(const QPalette& palette) +{ + LogColors result; + + const QColor& bg = palette.color(QPalette::Base); + const QColor& fg = palette.color(QPalette::Text); + + auto blend = [bg, fg](QColor color) { + if (Rainbow::luma(fg) > Rainbow::luma(bg)) { + // for dark color schemes, produce a fitting color first + color = Rainbow::tint(fg, color, 0.5); + } + // adapt contrast + return Rainbow::mix(fg, color, 1); + }; + + result.background[MessageLevel::Fatal] = Qt::black; + + result.foreground[MessageLevel::Launcher] = blend(QColor("purple")); + result.foreground[MessageLevel::Debug] = blend(QColor("green")); + result.foreground[MessageLevel::Warning] = blend(QColor("orange")); + result.foreground[MessageLevel::Error] = blend(QColor("red")); + result.foreground[MessageLevel::Fatal] = blend(QColor("red")); + + return result; +} diff --git a/launcher/ui/themes/ITheme.h b/launcher/ui/themes/ITheme.h index d85e7f983..7dc5fc64a 100644 --- a/launcher/ui/themes/ITheme.h +++ b/launcher/ui/themes/ITheme.h @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Tayou + * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -33,25 +34,35 @@ * limitations under the License. */ #pragma once +#include +#include #include #include class QStyle; +struct LogColors { + QMap background; + QMap foreground; +}; + +// TODO: rename to Theme; this is not an interface as it contains method implementations class ITheme { public: virtual ~ITheme() {} virtual void apply(bool initial); virtual QString id() = 0; virtual QString name() = 0; + virtual QString tooltip() = 0; virtual bool hasStyleSheet() = 0; virtual QString appStyleSheet() = 0; virtual QString qtTheme() = 0; - virtual bool hasColorScheme() = 0; virtual QPalette colorScheme() = 0; virtual QColor fadeColor() = 0; virtual double fadeAmount() = 0; + virtual LogColors logColorScheme() { return defaultLogColors(colorScheme()); } virtual QStringList searchPaths() { return {}; } static QPalette fadeInactive(QPalette in, qreal bias, QColor color); + static LogColors defaultLogColors(const QPalette& palette); }; diff --git a/launcher/ui/themes/SystemTheme.cpp b/launcher/ui/themes/SystemTheme.cpp index 7ad144c7a..a1674455a 100644 --- a/launcher/ui/themes/SystemTheme.cpp +++ b/launcher/ui/themes/SystemTheme.cpp @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou + * Copyright (C) 2024 Tayou + * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -34,59 +35,75 @@ */ #include "SystemTheme.h" #include -#include #include #include +#include "HintOverrideProxyStyle.h" #include "ThemeManager.h" -SystemTheme::SystemTheme() +SystemTheme::SystemTheme(const QString& styleName, const QPalette& palette, bool isDefaultTheme) { - themeDebugLog() << "Determining System Theme..."; - const auto& style = QApplication::style(); - systemPalette = QApplication::palette(); - QString lowerThemeName = style->objectName(); - themeDebugLog() << "System theme seems to be:" << lowerThemeName; - QStringList styles = QStyleFactory::keys(); - for (auto& st : styles) { - themeDebugLog() << "Considering theme from theme factory:" << st.toLower(); - if (st.toLower() == lowerThemeName) { - systemTheme = st; - themeDebugLog() << "System theme has been determined to be:" << systemTheme; - return; - } - } - // fall back to fusion if we can't find the current theme. - systemTheme = "Fusion"; - themeDebugLog() << "System theme not found, defaulted to Fusion"; + themeName = isDefaultTheme ? "system" : styleName; + widgetTheme = styleName; + colorPalette = palette; } void SystemTheme::apply(bool initial) { // See https://github.com/MultiMC/Launcher/issues/1790 // or https://github.com/PrismLauncher/PrismLauncher/issues/490 - if (initial) + if (initial) { + QApplication::setStyle(new HintOverrideProxyStyle(QStyleFactory::create(qtTheme()))); return; + } + ITheme::apply(initial); } QString SystemTheme::id() { - return "system"; + return themeName; } QString SystemTheme::name() { - return QObject::tr("System"); + if (themeName.toLower() == "windowsvista") { + return QObject::tr("Windows Vista"); + } else if (themeName.toLower() == "windows") { + return QObject::tr("Windows 9x"); + } else if (themeName.toLower() == "windows11") { + return QObject::tr("Windows 11"); + } else if (themeName.toLower() == "system") { + return QObject::tr("System"); + } else { + return themeName; + } +} + +QString SystemTheme::tooltip() +{ + if (themeName.toLower() == "windowsvista") { + return QObject::tr("Widget style trying to look like your win32 theme"); + } else if (themeName.toLower() == "windows") { + return QObject::tr("Windows 9x inspired widget style"); + } else if (themeName.toLower() == "windows11") { + return QObject::tr("WinUI 3 inspired Qt widget style"); + } else if (themeName.toLower() == "fusion") { + return QObject::tr("The default Qt widget style"); + } else if (themeName.toLower() == "system") { + return QObject::tr("Your current system theme"); + } else { + return ""; + } } QString SystemTheme::qtTheme() { - return systemTheme; + return widgetTheme; } QPalette SystemTheme::colorScheme() { - return systemPalette; + return colorPalette; } QString SystemTheme::appStyleSheet() @@ -108,8 +125,3 @@ bool SystemTheme::hasStyleSheet() { return false; } - -bool SystemTheme::hasColorScheme() -{ - return true; -} diff --git a/launcher/ui/themes/SystemTheme.h b/launcher/ui/themes/SystemTheme.h index 4f7d83e57..7c260fdc4 100644 --- a/launcher/ui/themes/SystemTheme.h +++ b/launcher/ui/themes/SystemTheme.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou + * Copyright (C) 2024 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -38,21 +38,22 @@ class SystemTheme : public ITheme { public: - SystemTheme(); + SystemTheme(const QString& styleName, const QPalette& palette, bool isDefaultTheme); virtual ~SystemTheme() {} void apply(bool initial) override; QString id() override; QString name() override; + QString tooltip() override; QString qtTheme() override; bool hasStyleSheet() override; QString appStyleSheet() override; - bool hasColorScheme() override; QPalette colorScheme() override; double fadeAmount() override; QColor fadeColor() override; private: - QPalette systemPalette; - QString systemTheme; + QPalette colorPalette; + QString widgetTheme; + QString themeName; }; diff --git a/launcher/ui/themes/ThemeManager.cpp b/launcher/ui/themes/ThemeManager.cpp index a128fc3f5..691a51668 100644 --- a/launcher/ui/themes/ThemeManager.cpp +++ b/launcher/ui/themes/ThemeManager.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou + * Copyright (C) 2024 Tayou * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify @@ -23,6 +23,8 @@ #include #include #include +#include +#include #include "Exception.h" #include "ui/themes/BrightTheme.h" #include "ui/themes/CatPack.h" @@ -34,6 +36,13 @@ ThemeManager::ThemeManager() { + themeDebugLog() << "Determining System Widget Theme..."; + const auto& style = QApplication::style(); + m_defaultStyle = style->objectName(); + themeDebugLog() << "System theme seems to be:" << m_defaultStyle; + + m_defaultPalette = QApplication::palette(); + initializeThemes(); initializeCatPacks(); } @@ -120,13 +129,24 @@ void ThemeManager::initializeIcons() void ThemeManager::initializeWidgets() { themeDebugLog() << "<> Initializing Widget Themes"; - themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique()); + themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique(m_defaultStyle, m_defaultPalette, true)); auto darkThemeId = addTheme(std::make_unique()); themeDebugLog() << "Loading Built-in Theme:" << darkThemeId; themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique()); - // TODO: need some way to differentiate same name themes in different subdirectories (maybe smaller grey text next to theme name in - // dropdown?) + themeDebugLog() << "<> Initializing System Widget Themes"; + QStringList styles = QStyleFactory::keys(); + for (auto& st : styles) { +#ifdef Q_OS_WINDOWS + if (QSysInfo::productVersion() != "11" && st == "windows11") { + continue; + } +#endif + themeDebugLog() << "Loading System Theme:" << addTheme(std::make_unique(st, m_defaultPalette, false)); + } + + // TODO: need some way to differentiate same name themes in different subdirectories + // (maybe smaller grey text next to theme name in dropdown?) if (!m_applicationThemeFolder.mkpath(".")) themeWarningLog() << "Couldn't create theme folder"; @@ -178,8 +198,8 @@ QList ThemeManager::getValidApplicationThemes() QList ThemeManager::getValidCatPacks() { QList ret; - ret.reserve(m_cat_packs.size()); - for (auto&& [id, theme] : m_cat_packs) { + ret.reserve(m_catPacks.size()); + for (auto&& [id, theme] : m_catPacks) { ret.append(theme.get()); } return ret; @@ -228,6 +248,8 @@ void ThemeManager::setApplicationTheme(const QString& name, bool initial) auto& theme = themeIter->second; themeDebugLog() << "applying theme" << theme->name(); theme->apply(initial); + + m_logColors = theme->logColorScheme(); } else { themeWarningLog() << "Tried to set invalid theme:" << name; } @@ -238,14 +260,18 @@ void ThemeManager::applyCurrentlySelectedTheme(bool initial) auto settings = APPLICATION->settings(); setIconTheme(settings->get("IconTheme").toString()); themeDebugLog() << "<> Icon theme set."; - setApplicationTheme(settings->get("ApplicationTheme").toString(), initial); + auto applicationTheme = settings->get("ApplicationTheme").toString(); + if (applicationTheme == "") { + applicationTheme = m_defaultStyle; + } + setApplicationTheme(applicationTheme, initial); themeDebugLog() << "<> Application theme set."; } QString ThemeManager::getCatPack(QString catName) { - auto catIter = m_cat_packs.find(!catName.isEmpty() ? catName : APPLICATION->settings()->get("BackgroundCat").toString()); - if (catIter != m_cat_packs.end()) { + auto catIter = m_catPacks.find(!catName.isEmpty() ? catName : APPLICATION->settings()->get("BackgroundCat").toString()); + if (catIter != m_catPacks.end()) { auto& catPack = catIter->second; themeDebugLog() << "applying catpack" << catPack->id(); return catPack->path(); @@ -253,14 +279,14 @@ QString ThemeManager::getCatPack(QString catName) themeWarningLog() << "Tried to get invalid catPack:" << catName; } - return m_cat_packs.begin()->second->path(); + return m_catPacks.begin()->second->path(); } QString ThemeManager::addCatPack(std::unique_ptr catPack) { QString id = catPack->id(); - if (m_cat_packs.find(id) == m_cat_packs.end()) - m_cat_packs.emplace(id, std::move(catPack)); + if (m_catPacks.find(id) == m_catPacks.end()) + m_catPacks.emplace(id, std::move(catPack)); else themeWarningLog() << "CatPack(" << id << ") not added to prevent id duplication"; return id; @@ -313,3 +339,13 @@ void ThemeManager::initializeCatPacks() } } } + +void ThemeManager::refresh() +{ + m_themes.clear(); + m_icons.clear(); + m_catPacks.clear(); + + initializeThemes(); + initializeCatPacks(); +} \ No newline at end of file diff --git a/launcher/ui/themes/ThemeManager.h b/launcher/ui/themes/ThemeManager.h index b77b5947a..a9036107c 100644 --- a/launcher/ui/themes/ThemeManager.h +++ b/launcher/ui/themes/ThemeManager.h @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou - * Copyright (C) 2023 TheKodeToad + * Copyright (C) 2024 Tayou + * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -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" @@ -55,13 +57,20 @@ class ThemeManager { QString getCatPack(QString catName = ""); QList getValidCatPacks(); + const LogColors& getLogColors() { return m_logColors; } + + void refresh(); + private: std::map> m_themes; std::map m_icons; QDir m_iconThemeFolder{ "iconthemes" }; QDir m_applicationThemeFolder{ "themes" }; QDir m_catPacksFolder{ "catpacks" }; - std::map> m_cat_packs; + std::map> m_catPacks; + QString m_defaultStyle; + QPalette m_defaultPalette; + LogColors m_logColors; void initializeThemes(); void initializeCatPacks(); diff --git a/launcher/ui/widgets/CheckComboBox.cpp b/launcher/ui/widgets/CheckComboBox.cpp new file mode 100644 index 000000000..41def3ba1 --- /dev/null +++ b/launcher/ui/widgets/CheckComboBox.cpp @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "CheckComboBox.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class CheckComboModel : public QIdentityProxyModel { + Q_OBJECT + + public: + explicit CheckComboModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {} + + virtual Qt::ItemFlags flags(const QModelIndex& index) const { return QIdentityProxyModel::flags(index) | Qt::ItemIsUserCheckable; } + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const + { + if (role == Qt::CheckStateRole) { + auto txt = QIdentityProxyModel::data(index, Qt::DisplayRole).toString(); + return checked.contains(txt) ? Qt::Checked : Qt::Unchecked; + } + if (role == Qt::DisplayRole) + return QIdentityProxyModel::data(index, Qt::DisplayRole); + return {}; + } + virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) + { + if (role == Qt::CheckStateRole) { + auto txt = QIdentityProxyModel::data(index, Qt::DisplayRole).toString(); + if (checked.contains(txt)) { + checked.removeOne(txt); + } else { + checked.push_back(txt); + } + emit dataChanged(index, index); + emit checkStateChanged(); + return true; + } + return QIdentityProxyModel::setData(index, value, role); + } + QStringList getChecked() { return checked; } + + signals: + void checkStateChanged(); + + private: + QStringList checked; +}; + +CheckComboBox::CheckComboBox(QWidget* parent) : QComboBox(parent), m_separator(", ") +{ + view()->installEventFilter(this); + view()->window()->installEventFilter(this); + view()->viewport()->installEventFilter(this); + this->installEventFilter(this); +} + +void CheckComboBox::setSourceModel(QAbstractItemModel* new_model) +{ + auto proxy = new CheckComboModel(this); + proxy->setSourceModel(new_model); + model()->disconnect(this); + QComboBox::setModel(proxy); + connect(this, QOverload::of(&QComboBox::activated), this, &CheckComboBox::toggleCheckState); + connect(proxy, &CheckComboModel::checkStateChanged, this, &CheckComboBox::emitCheckedItemsChanged); + connect(model(), &CheckComboModel::rowsInserted, this, &CheckComboBox::emitCheckedItemsChanged); + connect(model(), &CheckComboModel::rowsRemoved, this, &CheckComboBox::emitCheckedItemsChanged); +} + +void CheckComboBox::hidePopup() +{ + if (!containerMousePress) + QComboBox::hidePopup(); +} + +void CheckComboBox::emitCheckedItemsChanged() +{ + emit checkedItemsChanged(checkedItems()); +} + +QString CheckComboBox::defaultText() const +{ + return m_default_text; +} + +void CheckComboBox::setDefaultText(const QString& text) +{ + m_default_text = text; +} + +QString CheckComboBox::separator() const +{ + return m_separator; +} + +void CheckComboBox::setSeparator(const QString& separator) +{ + m_separator = separator; +} + +bool CheckComboBox::eventFilter(QObject* receiver, QEvent* event) +{ + switch (event->type()) { + case QEvent::KeyPress: + case QEvent::KeyRelease: { + QKeyEvent* keyEvent = static_cast(event); + if (receiver == this && (keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Down)) { + showPopup(); + return true; + } else if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Escape) { + QComboBox::hidePopup(); + return (keyEvent->key() != Qt::Key_Escape); + } + break; + } + case QEvent::MouseButtonPress: { + auto ev = static_cast(event); + containerMousePress = ev && view()->indexAt(ev->pos()).isValid(); + break; + } + case QEvent::Wheel: + return receiver == this; + default: + break; + } + return false; +} + +void CheckComboBox::toggleCheckState(int index) +{ + QVariant value = itemData(index, Qt::CheckStateRole); + if (value.isValid()) { + Qt::CheckState state = static_cast(value.toInt()); + setItemData(index, (state == Qt::Unchecked ? Qt::Checked : Qt::Unchecked), Qt::CheckStateRole); + } + emitCheckedItemsChanged(); +} + +Qt::CheckState CheckComboBox::itemCheckState(int index) const +{ + return static_cast(itemData(index, Qt::CheckStateRole).toInt()); +} + +void CheckComboBox::setItemCheckState(int index, Qt::CheckState state) +{ + setItemData(index, state, Qt::CheckStateRole); +} + +QStringList CheckComboBox::checkedItems() const +{ + if (model()) + return dynamic_cast(model())->getChecked(); + return {}; +} + +void CheckComboBox::setCheckedItems(const QStringList& items) +{ + foreach (auto text, items) { + auto index = findText(text); + setItemCheckState(index, index != -1 ? Qt::Checked : Qt::Unchecked); + } +} + +void CheckComboBox::paintEvent(QPaintEvent*) +{ + QStylePainter painter(this); + painter.setPen(palette().color(QPalette::Text)); + + // draw the combobox frame, focusrect and selected etc. + QStyleOptionComboBox opt; + initStyleOption(&opt); + QStringList items = checkedItems(); + if (items.isEmpty()) + opt.currentText = defaultText(); + else + opt.currentText = items.join(separator()); + painter.drawComplexControl(QStyle::CC_ComboBox, opt); + + // draw the icon and text + painter.drawControl(QStyle::CE_ComboBoxLabel, opt); +} + +#include "CheckComboBox.moc" \ No newline at end of file diff --git a/launcher/ui/widgets/CheckComboBox.h b/launcher/ui/widgets/CheckComboBox.h new file mode 100644 index 000000000..876c6e3e1 --- /dev/null +++ b/launcher/ui/widgets/CheckComboBox.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +class CheckComboBox : public QComboBox { + Q_OBJECT + + public: + explicit CheckComboBox(QWidget* parent = nullptr); + virtual ~CheckComboBox() = default; + + void hidePopup() override; + + QString defaultText() const; + void setDefaultText(const QString& text); + + Qt::CheckState itemCheckState(int index) const; + void setItemCheckState(int index, Qt::CheckState state); + + QString separator() const; + void setSeparator(const QString& separator); + + QStringList checkedItems() const; + + void setSourceModel(QAbstractItemModel* model); + + public slots: + void setCheckedItems(const QStringList& items); + + signals: + void checkedItemsChanged(const QStringList& items); + + protected: + void paintEvent(QPaintEvent*) override; + + private: + void emitCheckedItemsChanged(); + bool eventFilter(QObject* receiver, QEvent* event) override; + void toggleCheckState(int index); + + private: + QString m_default_text; + QString m_separator; + bool containerMousePress; +}; \ No newline at end of file diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp index 69f72fea2..44f702659 100644 --- a/launcher/ui/widgets/InfoFrame.cpp +++ b/launcher/ui/widgets/InfoFrame.cpp @@ -36,6 +36,8 @@ #include #include +#include +#include #include #include "InfoFrame.h" @@ -274,12 +276,27 @@ void InfoFrame::setDescription(QString text) } QString labeltext; labeltext.reserve(300); - if (finaltext.length() > 290) { + + // elide rich text by getting characters without formatting + const int maxCharacterElide = 290; + QTextDocument doc; + doc.setHtml(text); + + if (doc.characterCount() > maxCharacterElide) { ui->descriptionLabel->setOpenExternalLinks(false); - ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText); + ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText); // This allows injecting HTML here. m_description = text; - // This allows injecting HTML here. - labeltext.append("" + finaltext.left(287) + "..."); + + // move the cursor to the character elide, doesn't see html + QTextCursor cursor(&doc); + cursor.movePosition(QTextCursor::End); + cursor.setPosition(maxCharacterElide, QTextCursor::KeepAnchor); + cursor.removeSelectedText(); + + // insert the post fix at the cursor + cursor.insertHtml("..."); + + labeltext.append(doc.toHtml()); QObject::connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler); } else { ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText); @@ -316,7 +333,7 @@ void InfoFrame::setLicense(QString text) if (finaltext.length() > 290) { ui->licenseLabel->setOpenExternalLinks(false); ui->licenseLabel->setTextFormat(Qt::TextFormat::RichText); - m_description = text; + m_license = text; // This allows injecting HTML here. labeltext.append("" + finaltext.left(287) + "..."); QObject::connect(ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler); diff --git a/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp index bd6b6b118..2b270c482 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.cpp +++ b/launcher/ui/widgets/JavaSettingsWidget.cpp @@ -3,20 +3,27 @@ #include #include #include +#include #include +#include #include +#include #include #include #include #include +#include "DesktopServices.h" #include "FileSystem.h" #include "JavaCommon.h" +#include "java/JavaChecker.h" #include "java/JavaInstall.h" +#include "java/JavaInstallList.h" #include "java/JavaUtils.h" #include "ui/dialogs/CustomMessageBox.h" +#include "ui/java/InstallJavaDialog.h" #include "ui/widgets/VersionSelectWidget.h" #include "Application.h" @@ -29,15 +36,20 @@ 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); connect(m_javaStatusBtn, &QToolButton::clicked, this, &JavaSettingsWidget::on_javaStatusBtn_clicked); + if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { + connect(m_javaDownloadBtn, &QPushButton::clicked, this, &JavaSettingsWidget::javaDownloadBtn_clicked); + } } void JavaSettingsWidget::setupUi() @@ -47,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")); @@ -65,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); @@ -120,6 +129,57 @@ void JavaSettingsWidget::setupUi() m_verticalLayout->addWidget(m_memoryGroupBox); + m_horizontalBtnLayout = new QHBoxLayout(); + m_horizontalBtnLayout->setObjectName(QStringLiteral("horizontalBtnLayout")); + + if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { + m_javaDownloadBtn = new QPushButton(tr("Download Java"), this); + m_horizontalBtnLayout->addWidget(m_javaDownloadBtn); + } + + m_autoJavaGroupBox = new QGroupBox(this); + m_autoJavaGroupBox->setObjectName(QStringLiteral("autoJavaGroupBox")); + m_veriticalJavaLayout = new QVBoxLayout(m_autoJavaGroupBox); + m_veriticalJavaLayout->setObjectName(QStringLiteral("veriticalJavaLayout")); + + 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(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(); } @@ -137,10 +197,16 @@ void JavaSettingsWidget::initialize() m_maxMemSpinBox->setValue(observedMaxMemory); m_permGenSpinBox->setValue(observedPermGenMemory); updateThresholds(); + if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { + m_autodownloadCheckBox->setChecked(true); + } } void JavaSettingsWidget::refresh() { + if (BuildConfig.JAVA_DOWNLOADER_ENABLED && m_autodownloadCheckBox->isChecked()) { + return; + } if (JavaUtils::getJavaCheckPath().isEmpty()) { JavaCommon::javaCheckNotFound(this); return; @@ -153,20 +219,52 @@ JavaSettingsWidget::ValidationStatus JavaSettingsWidget::validate() switch (javaStatus) { default: case JavaStatus::NotSet: + /* fallthrough */ case JavaStatus::DoesNotExist: + /* fallthrough */ case JavaStatus::DoesNotStart: + /* fallthrough */ case JavaStatus::ReturnedInvalidData: { - int button = CustomMessageBox::selectable(this, tr("No Java version selected"), - tr("You didn't select a Java version or selected something that doesn't work.\n" - "%1 will not be able to start Minecraft.\n" - "Do you wish to proceed without any Java?" - "\n\n" - "You can change the Java version in the settings later.\n") - .arg(BuildConfig.LAUNCHER_DISPLAYNAME), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::NoButton) - ->exec(); - if (button == QMessageBox::No) { - return ValidationStatus::Bad; + if (!(BuildConfig.JAVA_DOWNLOADER_ENABLED && m_autodownloadCheckBox->isChecked())) { // the java will not be autodownloaded + int button = QMessageBox::No; + if (m_result.mojangPlatform == "32" && maxHeapSize() > 2048) { + button = CustomMessageBox::selectable( + this, tr("32-bit Java detected"), + tr("You selected a 32-bit installation of Java, but allocated more than 2048MiB as maximum memory.\n" + "%1 will not be able to start Minecraft.\n" + "Do you wish to proceed?" + "\n\n" + "You can change the Java version in the settings later.\n") + .arg(BuildConfig.LAUNCHER_DISPLAYNAME), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Help, QMessageBox::NoButton) + ->exec(); + + } else { + button = CustomMessageBox::selectable(this, tr("No Java version selected"), + tr("You either didn't select a Java version or selected one that does not work.\n" + "%1 will not be able to start Minecraft.\n" + "Do you wish to proceed without a functional version of Java?" + "\n\n" + "You can change the Java version in the settings later.\n") + .arg(BuildConfig.LAUNCHER_DISPLAYNAME), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Help, + QMessageBox::NoButton) + ->exec(); + } + switch (button) { + case QMessageBox::Yes: + return ValidationStatus::JavaBad; + case QMessageBox::Help: + DesktopServices::openUrl(QUrl(BuildConfig.HELP_URL.arg("java-wizard"))); + /* fallthrough */ + case QMessageBox::No: + /* fallthrough */ + default: + return ValidationStatus::Bad; + } + if (button == QMessageBox::No) { + return ValidationStatus::Bad; + } } return ValidationStatus::JavaBad; } break; @@ -212,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; } @@ -250,21 +349,22 @@ void JavaSettingsWidget::javaVersionSelected(BaseVersion::Ptr version) void JavaSettingsWidget::on_javaBrowseBtn_clicked() { - QString filter; -#if defined Q_OS_WIN32 - filter = "Java (javaw.exe)"; -#else - filter = "Java (java)"; -#endif - QString raw_path = QFileDialog::getOpenFileName(this, tr("Find Java executable"), QString(), filter); + auto filter = QString("Java (%1)").arg(JavaUtils::javaExecutable); + auto raw_path = QFileDialog::getOpenFileName(this, tr("Find Java executable"), QString(), filter); if (raw_path.isEmpty()) { return; } - QString cooked_path = FS::NormalizePath(raw_path); + auto cooked_path = FS::NormalizePath(raw_path); m_javaPathTextBox->setText(cooked_path); checkJavaPath(cooked_path); } +void JavaSettingsWidget::javaDownloadBtn_clicked() +{ + auto jdialog = new Java::InstallDialog({}, nullptr, this); + jdialog->exec(); +} + void JavaSettingsWidget::on_javaStatusBtn_clicked() { QString text; @@ -359,34 +459,30 @@ void JavaSettingsWidget::checkJavaPath(const QString& path) return; } setJavaStatus(JavaStatus::Pending); - m_checker.reset(new JavaChecker()); - m_checker->m_path = path; - m_checker->m_minMem = minHeapSize(); - m_checker->m_maxMem = maxHeapSize(); - if (m_permGenSpinBox->isVisible()) { - m_checker->m_permGen = m_permGenSpinBox->value(); - } + m_checker.reset( + new JavaChecker(path, "", minHeapSize(), maxHeapSize(), m_permGenSpinBox->isVisible() ? m_permGenSpinBox->value() : 0, 0, this)); connect(m_checker.get(), &JavaChecker::checkFinished, this, &JavaSettingsWidget::checkFinished); - m_checker->performCheck(); + m_checker->start(); } -void JavaSettingsWidget::checkFinished(JavaCheckResult result) +void JavaSettingsWidget::checkFinished(const JavaChecker::Result& result) { m_result = result; switch (result.validity) { - case JavaCheckResult::Validity::Valid: { + case JavaChecker::Result::Validity::Valid: { setJavaStatus(JavaStatus::Good); break; } - case JavaCheckResult::Validity::ReturnedInvalidData: { + case JavaChecker::Result::Validity::ReturnedInvalidData: { setJavaStatus(JavaStatus::ReturnedInvalidData); break; } - case JavaCheckResult::Validity::Errored: { + case JavaChecker::Result::Validity::Errored: { setJavaStatus(JavaStatus::DoesNotStart); break; } } + updateThresholds(); m_checker.reset(); if (!queuedCheck.isNull()) { checkJavaPath(queuedCheck); @@ -403,6 +499,11 @@ void JavaSettingsWidget::retranslate() m_minMemSpinBox->setToolTip(tr("The amount of memory Minecraft is started with.")); m_permGenSpinBox->setToolTip(tr("The amount of memory available to store loaded Java classes.")); m_javaBrowseBtn->setText(tr("Browse")); + if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { + m_autodownloadCheckBox->setText(tr("Auto-download Mojang Java")); + } + m_autodetectJavaCheckBox->setText(tr("Autodetect Java version")); + m_autoJavaGroupBox->setTitle(tr("Autodetect Java")); } void JavaSettingsWidget::updateThresholds() @@ -418,6 +519,12 @@ 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.")); } else { iconName = "status-good"; m_labelMaxMemIcon->setToolTip(""); @@ -430,3 +537,23 @@ void JavaSettingsWidget::updateThresholds() m_labelMaxMemIcon->setPixmap(pix); } } + +bool JavaSettingsWidget::autoDownloadJava() const +{ + return m_autodownloadCheckBox && m_autodownloadCheckBox->isChecked(); +} + +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 6ea73da60..35be48478 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.h +++ b/launcher/ui/widgets/JavaSettingsWidget.h @@ -4,6 +4,7 @@ #include #include #include +#include #include class QLineEdit; @@ -16,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. @@ -25,7 +27,7 @@ class JavaSettingsWidget : public QWidget { public: explicit JavaSettingsWidget(QWidget* parent); - virtual ~JavaSettingsWidget(){}; + virtual ~JavaSettingsWidget(); enum class JavaStatus { NotSet, Pending, Good, DoesNotExist, DoesNotStart, ReturnedInvalidData } javaStatus = JavaStatus::NotSet; @@ -41,16 +43,20 @@ class JavaSettingsWidget : public QWidget { int minHeapSize() const; int maxHeapSize() const; QString javaPath() const; + bool autoDetectJava() const; + bool autoDownloadJava() const; 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(); void on_javaStatusBtn_clicked(); - void checkFinished(JavaCheckResult result); + void javaDownloadBtn_clicked(); + void checkFinished(const JavaChecker::Result& result); protected: /* methods */ void checkJavaPathOnEdit(const QString& path); @@ -61,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; @@ -76,15 +83,24 @@ class JavaSettingsWidget : public QWidget { QSpinBox* m_minMemSpinBox = nullptr; QLabel* m_labelPermGen = nullptr; QSpinBox* m_permGenSpinBox = nullptr; + + QHBoxLayout* m_horizontalBtnLayout = nullptr; + QPushButton* m_javaDownloadBtn = nullptr; QIcon goodIcon; QIcon yellowIcon; QIcon badIcon; + QGroupBox* m_autoJavaGroupBox = nullptr; + QVBoxLayout* m_veriticalJavaLayout = nullptr; + QCheckBox* m_autodetectJavaCheckBox = nullptr; + QCheckBox* m_autodownloadCheckBox = nullptr; + unsigned int observedMinMemory = 0; unsigned int observedMaxMemory = 0; unsigned int observedPermGenMemory = 0; QString queuedCheck; uint64_t m_availableMemory = 0ull; shared_qobject_ptr m_checker; - JavaCheckResult m_result; + JavaChecker::Result m_result; + QTimer* m_memoryTimer; }; diff --git a/launcher/ui/widgets/LanguageSelectionWidget.h b/launcher/ui/widgets/LanguageSelectionWidget.h index f034853dd..cf1f5bf3c 100644 --- a/launcher/ui/widgets/LanguageSelectionWidget.h +++ b/launcher/ui/widgets/LanguageSelectionWidget.h @@ -27,7 +27,7 @@ class LanguageSelectionWidget : public QWidget { Q_OBJECT public: explicit LanguageSelectionWidget(QWidget* parent = 0); - virtual ~LanguageSelectionWidget(){}; + virtual ~LanguageSelectionWidget() {}; QString getSelectedLanguageKey() const; void retranslate(); diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp index c2c099eeb..bbb91eac2 100644 --- a/launcher/ui/widgets/ModFilterWidget.cpp +++ b/launcher/ui/widgets/ModFilterWidget.cpp @@ -1,13 +1,139 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "ModFilterWidget.h" +#include +#include +#include +#include +#include +#include "BaseVersionList.h" +#include "Version.h" +#include "meta/Index.h" +#include "modplatform/ModIndex.h" +#include "ui/widgets/CheckComboBox.h" #include "ui_ModFilterWidget.h" #include "Application.h" +#include "minecraft/PackProfile.h" -unique_qobject_ptr ModFilterWidget::create(Version default_version, QWidget* parent) +unique_qobject_ptr ModFilterWidget::create(MinecraftInstance* instance, bool extended, QWidget* parent) { - auto filter_widget = new ModFilterWidget(default_version, parent); + return unique_qobject_ptr(new ModFilterWidget(instance, extended, parent)); +} - if (!filter_widget->versionList()->isLoaded()) { +class VersionBasicModel : public QIdentityProxyModel { + Q_OBJECT + + public: + explicit VersionBasicModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {} + + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override + { + if (role == Qt::DisplayRole) + return QIdentityProxyModel::data(index, BaseVersionList::VersionIdRole); + return {}; + } +}; + +ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended, QWidget* parent) + : QTabWidget(parent), ui(new Ui::ModFilterWidget), m_instance(instance), m_filter(new Filter()) +{ + ui->setupUi(this); + + m_versions_proxy = new VersionProxyModel(this); + m_versions_proxy->setFilter(BaseVersionList::TypeRole, new ExactFilter("release")); + + auto proxy = new VersionBasicModel(this); + proxy->setSourceModel(m_versions_proxy); + + if (extended) { + ui->versions->setSourceModel(proxy); + ui->versions->setSeparator(", "); + ui->version->hide(); + } else { + ui->version->setModel(proxy); + ui->versions->hide(); + ui->showAllVersions->hide(); + ui->environmentGroup->hide(); + } + + ui->versions->setStyleSheet("combobox-popup: 0;"); + ui->version->setStyleSheet("combobox-popup: 0;"); + connect(ui->showAllVersions, &QCheckBox::stateChanged, this, &ModFilterWidget::onShowAllVersionsChanged); + connect(ui->versions, QOverload::of(&QComboBox::currentIndexChanged), this, &ModFilterWidget::onVersionFilterChanged); + connect(ui->versions, &CheckComboBox::checkedItemsChanged, this, [this] { onVersionFilterChanged(0); }); + connect(ui->version, &QComboBox::currentTextChanged, this, &ModFilterWidget::onVersionFilterTextChanged); + + connect(ui->neoForge, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + connect(ui->forge, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + connect(ui->fabric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + connect(ui->quilt, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + + connect(ui->neoForge, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + connect(ui->forge, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + connect(ui->fabric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + connect(ui->quilt, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + + if (extended) { + connect(ui->clientSide, &QCheckBox::stateChanged, this, &ModFilterWidget::onSideFilterChanged); + connect(ui->serverSide, &QCheckBox::stateChanged, this, &ModFilterWidget::onSideFilterChanged); + } + + connect(ui->hideInstalled, &QCheckBox::stateChanged, this, &ModFilterWidget::onHideInstalledFilterChanged); + + setHidden(true); + loadVersionList(); + prepareBasicFilter(); +} + +auto ModFilterWidget::getFilter() -> std::shared_ptr +{ + m_filter_changed = false; + return m_filter; +} + +ModFilterWidget::~ModFilterWidget() +{ + delete ui; +} + +void ModFilterWidget::loadVersionList() +{ + m_version_list = APPLICATION->metadataIndex()->get("net.minecraft"); + if (!m_version_list->isLoaded()) { QEventLoop load_version_list_loop; QTimer time_limit_for_list_load; @@ -16,10 +142,12 @@ unique_qobject_ptr ModFilterWidget::create(Version default_vers time_limit_for_list_load.callOnTimeout(&load_version_list_loop, &QEventLoop::quit); time_limit_for_list_load.start(4000); - auto task = filter_widget->versionList()->getLoadTask(); + auto task = m_version_list->getLoadTask(); - connect(task.get(), &Task::failed, - [filter_widget] { filter_widget->disableVersionButton(VersionButtonID::Major, tr("failed to get version index")); }); + connect(task.get(), &Task::failed, [this] { + ui->versions->setEnabled(false); + ui->showAllVersions->setEnabled(false); + }); connect(task.get(), &Task::finished, &load_version_list_loop, &QEventLoop::quit); if (!task->isRunning()) @@ -29,128 +157,132 @@ unique_qobject_ptr ModFilterWidget::create(Version default_vers if (time_limit_for_list_load.isActive()) time_limit_for_list_load.stop(); } - - return unique_qobject_ptr(filter_widget); + m_versions_proxy->setSourceModel(m_version_list.get()); } -ModFilterWidget::ModFilterWidget(Version def, QWidget* parent) : QTabWidget(parent), m_filter(new Filter()), ui(new Ui::ModFilterWidget) +void ModFilterWidget::prepareBasicFilter() { - ui->setupUi(this); - - m_mcVersion_buttons.addButton(ui->strictVersionButton, VersionButtonID::Strict); - ui->strictVersionButton->click(); - m_mcVersion_buttons.addButton(ui->majorVersionButton, VersionButtonID::Major); - m_mcVersion_buttons.addButton(ui->allVersionsButton, VersionButtonID::All); - // m_mcVersion_buttons.addButton(ui->betweenVersionsButton, VersionButtonID::Between); - - connect(&m_mcVersion_buttons, SIGNAL(idClicked(int)), this, SLOT(onVersionFilterChanged(int))); - - m_filter->versions.push_front(def); - - m_version_list = APPLICATION->metadataIndex()->get("net.minecraft"); - setHidden(true); + 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)); } -void ModFilterWidget::setInstance(MinecraftInstance* instance) +void ModFilterWidget::onShowAllVersionsChanged() { - m_instance = instance; - - ui->strictVersionButton->setText(tr("Strict match (= %1)").arg(mcVersionStr())); - - // we can't do this for snapshots sadly - if (mcVersionStr().contains('.')) { - auto mcVersionSplit = mcVersionStr().split("."); - ui->majorVersionButton->setText(tr("Major version match (= %1.%2.x)").arg(mcVersionSplit[0], mcVersionSplit[1])); - } else { - ui->majorVersionButton->setText(tr("Major version match (unsupported)")); - disableVersionButton(Major); - } - ui->allVersionsButton->setText(tr("Any version")); - // ui->betweenVersionsButton->setText( - // tr("Between two versions")); -} - -auto ModFilterWidget::getFilter() -> std::shared_ptr -{ - m_last_version_id = m_version_id; - emit filterUnchanged(); - return m_filter; -} - -void ModFilterWidget::disableVersionButton(VersionButtonID id, QString reason) -{ - QAbstractButton* btn = nullptr; - - switch (id) { - case (VersionButtonID::Strict): - btn = ui->strictVersionButton; - break; - case (VersionButtonID::Major): - btn = ui->majorVersionButton; - break; - case (VersionButtonID::All): - btn = ui->allVersionsButton; - break; - case (VersionButtonID::Between): - default: - break; - } - - if (btn) { - btn->setEnabled(false); - if (!reason.isEmpty()) - btn->setText(btn->text() + QString(" (%1)").arg(reason)); - } -} - -void ModFilterWidget::onVersionFilterChanged(int id) -{ - // ui->lowerVersionComboBox->setEnabled(id == VersionButtonID::Between); - // ui->upperVersionComboBox->setEnabled(id == VersionButtonID::Between); - - int index = 1; - - auto cast_id = (VersionButtonID)id; - if (cast_id != m_version_id) { - m_version_id = cast_id; - } else { - return; - } - - m_filter->versions.clear(); - - switch (cast_id) { - case (VersionButtonID::Strict): - m_filter->versions.push_front(mcVersion()); - break; - case (VersionButtonID::Major): { - auto versionSplit = mcVersionStr().split("."); - - auto major_version = QString("%1.%2").arg(versionSplit[0], versionSplit[1]); - QString version_str = major_version; - - while (m_version_list->hasVersion(version_str)) { - m_filter->versions.emplace_back(version_str); - version_str = QString("%1.%2").arg(major_version, QString::number(index++)); - } - - break; - } - case (VersionButtonID::All): - // Empty list to avoid enumerating all versions :P - break; - case (VersionButtonID::Between): - // TODO - break; - } - - if (changed()) - emit filterChanged(); + if (ui->showAllVersions->isChecked()) + m_versions_proxy->clearFilters(); else - emit filterUnchanged(); + m_versions_proxy->setFilter(BaseVersionList::TypeRole, new ExactFilter("release")); } -ModFilterWidget::~ModFilterWidget() +void ModFilterWidget::onVersionFilterChanged(int) { - delete ui; + auto versions = ui->versions->checkedItems(); + versions.sort(); + std::list current_list; + + for (const QString& version : versions) + current_list.emplace_back(version); + + m_filter_changed = m_filter->versions.size() != current_list.size() || + !std::equal(m_filter->versions.begin(), m_filter->versions.end(), current_list.begin(), current_list.end()); + m_filter->versions = current_list; + if (m_filter_changed) + emit filterChanged(); } + +void ModFilterWidget::onLoadersFilterChanged() +{ + ModPlatform::ModLoaderTypes loaders; + if (ui->neoForge->isChecked()) + loaders |= ModPlatform::NeoForge; + if (ui->forge->isChecked()) + loaders |= ModPlatform::Forge; + if (ui->fabric->isChecked()) + loaders |= ModPlatform::Fabric; + if (ui->quilt->isChecked()) + loaders |= ModPlatform::Quilt; + m_filter_changed = loaders != m_filter->loaders; + m_filter->loaders = loaders; + if (m_filter_changed) + emit filterChanged(); +} + +void ModFilterWidget::onSideFilterChanged() +{ + QString side; + + if (ui->clientSide->isChecked() != ui->serverSide->isChecked()) { + if (ui->clientSide->isChecked()) + side = "client"; + else + side = "server"; + } else { + // both are checked or none are checked; in either case no filtering will happen + side = ""; + } + + m_filter_changed = side != m_filter->side; + m_filter->side = side; + if (m_filter_changed) + emit filterChanged(); +} + +void ModFilterWidget::onHideInstalledFilterChanged() +{ + auto hide = ui->hideInstalled->isChecked(); + m_filter_changed = hide != m_filter->hideInstalled; + m_filter->hideInstalled = hide; + if (m_filter_changed) + emit filterChanged(); +} + +void ModFilterWidget::onVersionFilterTextChanged(const QString& version) +{ + m_filter->versions.clear(); + m_filter->versions.emplace_back(version); + m_filter_changed = true; + emit filterChanged(); +} + +void ModFilterWidget::setCategories(const QList& categories) +{ + m_categories = categories; + + delete ui->categoryGroup->layout(); + auto layout = new QVBoxLayout(ui->categoryGroup); + + for (const auto& category : categories) { + auto name = category.name; + name.replace("-", " "); + name.replace("&", "&&"); + auto checkbox = new QCheckBox(name); + auto font = checkbox->font(); + font.setCapitalization(QFont::Capitalize); + checkbox->setFont(font); + + layout->addWidget(checkbox); + + const QString id = category.id; + connect(checkbox, &QCheckBox::toggled, this, [this, id](bool checked) { + if (checked) + m_filter->categoryIds.append(id); + else + m_filter->categoryIds.removeOne(id); + + m_filter_changed = true; + emit filterChanged(); + }); + } +} + +#include "ModFilterWidget.moc" \ No newline at end of file diff --git a/launcher/ui/widgets/ModFilterWidget.h b/launcher/ui/widgets/ModFilterWidget.h index ed6cd0ea7..fdfd2c8bb 100644 --- a/launcher/ui/widgets/ModFilterWidget.h +++ b/launcher/ui/widgets/ModFilterWidget.h @@ -1,15 +1,52 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include +#include +#include #include #include "Version.h" -#include "meta/Index.h" +#include "VersionProxyModel.h" #include "meta/VersionList.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/PackProfile.h" +#include "modplatform/ModIndex.h" class MinecraftInstance; @@ -20,59 +57,57 @@ class ModFilterWidget; class ModFilterWidget : public QTabWidget { Q_OBJECT public: - enum VersionButtonID { Strict = 0, Major = 1, All = 2, Between = 3 }; - struct Filter { std::list versions; + std::list releases; + ModPlatform::ModLoaderTypes loaders; + QString side; + bool hideInstalled; + QStringList categoryIds; - bool operator==(const Filter& other) const { return versions == other.versions; } + bool operator==(const Filter& other) const + { + return hideInstalled == other.hideInstalled && side == other.side && loaders == other.loaders && versions == other.versions && + releases == other.releases && categoryIds == other.categoryIds; + } bool operator!=(const Filter& other) const { return !(*this == other); } }; - std::shared_ptr m_filter; - - public: - static unique_qobject_ptr create(Version default_version, QWidget* parent = nullptr); - ~ModFilterWidget(); - - void setInstance(MinecraftInstance* instance); - - /// By default all buttons are enabled - void disableVersionButton(VersionButtonID, QString reason = {}); + static unique_qobject_ptr create(MinecraftInstance* instance, bool extended, QWidget* parent = nullptr); + virtual ~ModFilterWidget(); auto getFilter() -> std::shared_ptr; - auto changed() const -> bool { return m_last_version_id != m_version_id; } + auto changed() const -> bool { return m_filter_changed; } - Meta::VersionList::Ptr versionList() { return m_version_list; } - - private: - ModFilterWidget(Version def, QWidget* parent = nullptr); - - inline auto mcVersionStr() const -> QString - { - return m_instance ? m_instance->getPackProfile()->getComponentVersion("net.minecraft") : ""; - } - inline auto mcVersion() const -> Version { return { mcVersionStr() }; } - - private slots: - void onVersionFilterChanged(int id); - - public: signals: void filterChanged(); - void filterUnchanged(); + + public slots: + void setCategories(const QList&); + + private: + ModFilterWidget(MinecraftInstance* instance, bool extendedSupport, QWidget* parent = nullptr); + + void loadVersionList(); + void prepareBasicFilter(); + + private slots: + void onVersionFilterChanged(int); + void onVersionFilterTextChanged(const QString& version); + void onLoadersFilterChanged(); + void onSideFilterChanged(); + void onHideInstalledFilterChanged(); + void onShowAllVersionsChanged(); private: Ui::ModFilterWidget* ui; MinecraftInstance* m_instance = nullptr; - - /* Version stuff */ - QButtonGroup m_mcVersion_buttons; + std::shared_ptr m_filter; + bool m_filter_changed = false; Meta::VersionList::Ptr m_version_list; + VersionProxyModel* m_versions_proxy = nullptr; - /* Used to tell if the filter was changed since the last getFilter() call */ - VersionButtonID m_last_version_id = VersionButtonID::Strict; - VersionButtonID m_version_id = VersionButtonID::Strict; + QList m_categories; }; diff --git a/launcher/ui/widgets/ModFilterWidget.ui b/launcher/ui/widgets/ModFilterWidget.ui index ebe5d2be1..236847094 100644 --- a/launcher/ui/widgets/ModFilterWidget.ui +++ b/launcher/ui/widgets/ModFilterWidget.ui @@ -1,54 +1,219 @@ ModFilterWidget - + 0 0 - 400 - 300 + 310 + 600 - + 0 0 - - - Minecraft versions - - - - - - - - allVersions - - - - - - - strictVersion - - - - - - - majorVersion - - - - - - - + + + 275 + 0 + + + + + 310 + 16777215 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 275 + 0 + + + + QAbstractScrollArea::AdjustToContentsOnFirstShow + + + true + + + + + 0 + 0 + 308 + 598 + + + + + + + Categories + + + false + + + false + + + + + + + Loaders + + + false + + + false + + + + + + NeoForge + + + + + + + Forge + + + + + + + Fabric + + + + + + + Quilt + + + + + + + + + + Versions + + + false + + + false + + + + + + Show all versions + + + + + + + + + + + + + + + + Environments + + + false + + + false + + + + + + Client + + + + + + + Server + + + + + + + + + + Hide installed items + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + CheckComboBox + QComboBox +
    ui/widgets/CheckComboBox.h
    +
    +
    diff --git a/launcher/ui/widgets/PageContainer.h b/launcher/ui/widgets/PageContainer.h index 05be1c3a5..ab4444c99 100644 --- a/launcher/ui/widgets/PageContainer.h +++ b/launcher/ui/widgets/PageContainer.h @@ -36,6 +36,7 @@ #pragma once +#include #include #include @@ -86,6 +87,8 @@ class PageContainer : public QWidget, public BasePageContainer { void changeEvent(QEvent*) override; + void hidePageList() { m_pageList->hide(); } + private: void createUI(); void retranslate(); diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.cpp b/launcher/ui/widgets/ThemeCustomizationWidget.cpp index 25b91857c..7a54bd390 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.cpp +++ b/launcher/ui/widgets/ThemeCustomizationWidget.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou + * Copyright (C) 2024 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -27,6 +27,7 @@ ThemeCustomizationWidget::ThemeCustomizationWidget(QWidget* parent) : QWidget(pa { ui->setupUi(this); loadSettings(); + ThemeCustomizationWidget::refresh(); connect(ui->iconsComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyIconTheme); connect(ui->widgetStyleComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, @@ -39,6 +40,8 @@ ThemeCustomizationWidget::ThemeCustomizationWidget(QWidget* parent) : QWidget(pa [] { DesktopServices::openPath(APPLICATION->themeManager()->getApplicationThemesFolder().path()); }); connect(ui->catPackFolder, &QPushButton::clicked, this, [] { DesktopServices::openPath(APPLICATION->themeManager()->getCatPacksFolder().path()); }); + + connect(ui->refreshButton, &QPushButton::clicked, this, &ThemeCustomizationWidget::refresh); } ThemeCustomizationWidget::~ThemeCustomizationWidget() @@ -148,6 +151,10 @@ void ThemeCustomizationWidget::loadSettings() int idx = 0; for (auto& theme : themes) { ui->widgetStyleComboBox->addItem(theme->name(), theme->id()); + if (theme->tooltip() != "") { + int index = ui->widgetStyleComboBox->count() - 1; + ui->widgetStyleComboBox->setItemData(index, theme->tooltip(), Qt::ToolTipRole); + } if (currentTheme == theme->id()) { ui->widgetStyleComboBox->setCurrentIndex(idx); } @@ -169,3 +176,22 @@ void ThemeCustomizationWidget::retranslate() { ui->retranslateUi(this); } + +void ThemeCustomizationWidget::refresh() +{ + applySettings(); + disconnect(ui->iconsComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyIconTheme); + disconnect(ui->widgetStyleComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &ThemeCustomizationWidget::applyWidgetTheme); + disconnect(ui->backgroundCatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &ThemeCustomizationWidget::applyCatTheme); + APPLICATION->themeManager()->refresh(); + ui->iconsComboBox->clear(); + ui->widgetStyleComboBox->clear(); + ui->backgroundCatComboBox->clear(); + loadSettings(); + connect(ui->iconsComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyIconTheme); + connect(ui->widgetStyleComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &ThemeCustomizationWidget::applyWidgetTheme); + connect(ui->backgroundCatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyCatTheme); +}; \ No newline at end of file diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.h b/launcher/ui/widgets/ThemeCustomizationWidget.h index cef5fb6c6..6977b8495 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.h +++ b/launcher/ui/widgets/ThemeCustomizationWidget.h @@ -44,6 +44,7 @@ class ThemeCustomizationWidget : public QWidget { void applyIconTheme(int index); void applyWidgetTheme(int index); void applyCatTheme(int index); + void refresh(); signals: int currentIconThemeChanged(int index); diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.ui b/launcher/ui/widgets/ThemeCustomizationWidget.ui index 4503181c2..1faa45c4f 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.ui +++ b/launcher/ui/widgets/ThemeCustomizationWidget.ui @@ -13,7 +13,7 @@ Form - + QLayout::SetMinimumSize @@ -29,141 +29,179 @@ 0 - - - - &Icons - - - iconsComboBox - - - - - - - - - - 0 - 0 - - - - Qt::StrongFocus - - - - - - - View icon themes folder. - + + + + - + &Icons - - - .. - - - true + + iconsComboBox - - - - - - &Widgets - - - widgetStyleComboBox - - - - - - - - - - 0 - 0 - - - - Qt::StrongFocus - - + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + + + + + View icon themes folder. + + + + + + + + + true + + + + - - - - View widget themes folder. - + + - + &Widgets - - - .. - - - true + + widgetStyleComboBox - - - - - - The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar. - - - C&at - - - backgroundCatComboBox - - - - - - - - - - 0 - 0 - - - - Qt::StrongFocus - + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + + + + + View widget themes folder. + + + + + + + + + true + + + + + + + The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar. + + C&at + + + backgroundCatComboBox + + + + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar. + + + + + + + View cat packs folder. + + + + + + + + + true + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Refresh all + - - - View cat packs folder. + + + Qt::Horizontal - - + + + 40 + 20 + - - - .. - - - true - - + diff --git a/launcher/ui/widgets/VariableSizedImageObject.cpp b/launcher/ui/widgets/VariableSizedImageObject.cpp index cebf2a5f1..9723a2c56 100644 --- a/launcher/ui/widgets/VariableSizedImageObject.cpp +++ b/launcher/ui/widgets/VariableSizedImageObject.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include "Application.h" @@ -36,6 +37,30 @@ QSizeF VariableSizedImageObject::intrinsicSize(QTextDocument* doc, int posInDocu auto image = qvariant_cast(format.property(ImageData)); auto size = image.size(); + if (size.isEmpty()) // can't resize an empty image + return { size }; + + // calculate the new image size based on the properties + int width = 0; + int height = 0; + auto widthVar = format.property(QTextFormat::ImageWidth); + if (widthVar.isValid()) { + width = widthVar.toInt(); + } + auto heigthVar = format.property(QTextFormat::ImageHeight); + if (heigthVar.isValid()) { + height = heigthVar.toInt(); + } + if (width != 0 && height != 0) { + size.setWidth(width); + size.setHeight(height); + } else if (width != 0) { + size.setHeight((width * size.height()) / size.width()); + size.setWidth(width); + } else if (height != 0) { + size.setWidth((height * size.width()) / size.height()); + size.setHeight(height); + } // Get the width of the text content to make the image similar sized. // doc->textWidth() includes the margin, so we need to remove it. @@ -46,6 +71,7 @@ QSizeF VariableSizedImageObject::intrinsicSize(QTextDocument* doc, int posInDocu return { size }; } + void VariableSizedImageObject::drawObject(QPainter* painter, const QRectF& rect, QTextDocument* doc, @@ -54,10 +80,23 @@ void VariableSizedImageObject::drawObject(QPainter* painter, { if (!format.hasProperty(ImageData)) { QUrl image_url{ qvariant_cast(format.property(QTextFormat::ImageName)) }; - if (m_fetching_images.contains(image_url)) + if (m_fetching_images.contains(image_url) || image_url.isEmpty()) return; - loadImage(doc, image_url, posInDocument); + auto meta = std::make_shared(); + meta->posInDocument = posInDocument; + meta->url = image_url; + + auto widthVar = format.property(QTextFormat::ImageWidth); + if (widthVar.isValid()) { + meta->width = widthVar.toInt(); + } + auto heigthVar = format.property(QTextFormat::ImageHeight); + if (heigthVar.isValid()) { + meta->height = heigthVar.toInt(); + } + + loadImage(doc, meta); return; } @@ -72,16 +111,19 @@ void VariableSizedImageObject::flush() m_fetching_images.clear(); } -void VariableSizedImageObject::parseImage(QTextDocument* doc, QImage image, int posInDocument) +void VariableSizedImageObject::parseImage(QTextDocument* doc, std::shared_ptr meta) { QTextCursor cursor(doc); - cursor.setPosition(posInDocument); + cursor.setPosition(meta->posInDocument); cursor.setKeepPositionOnInsert(true); auto image_char_format = cursor.charFormat(); image_char_format.setObjectType(QTextFormat::ImageObject); - image_char_format.setProperty(ImageData, image); + image_char_format.setProperty(ImageData, meta->image); + image_char_format.setProperty(QTextFormat::ImageName, meta->url.toDisplayString()); + image_char_format.setProperty(QTextFormat::ImageWidth, meta->width); + image_char_format.setProperty(QTextFormat::ImageHeight, meta->height); // Qt doesn't allow us to modify the properties of an existing object in the document. // So we remove the old one and add the new one with the ImageData property set. @@ -89,23 +131,25 @@ void VariableSizedImageObject::parseImage(QTextDocument* doc, QImage image, int cursor.insertText(QString(QChar::ObjectReplacementCharacter), image_char_format); } -void VariableSizedImageObject::loadImage(QTextDocument* doc, const QUrl& source, int posInDocument) +void VariableSizedImageObject::loadImage(QTextDocument* doc, std::shared_ptr meta) { - m_fetching_images.insert(source); + m_fetching_images.insert(meta->url); MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry( m_meta_entry, - QString("images/%1").arg(QString(QCryptographicHash::hash(source.toEncoded(), QCryptographicHash::Algorithm::Sha1).toHex()))); + QString("images/%1").arg(QString(QCryptographicHash::hash(meta->url.toEncoded(), QCryptographicHash::Algorithm::Sha1).toHex()))); - auto job = new NetJob(QString("Load Image: %1").arg(source.fileName()), APPLICATION->network()); - job->addNetAction(Net::ApiDownload::makeCached(source, entry)); + auto job = new NetJob(QString("Load Image: %1").arg(meta->url.fileName()), APPLICATION->network()); + job->setAskRetry(false); + job->addNetAction(Net::ApiDownload::makeCached(meta->url, entry)); auto full_entry_path = entry->getFullPath(); - auto source_url = source; - auto loadImage = [this, doc, full_entry_path, source_url, posInDocument](const QImage& image) { + auto source_url = meta->url; + auto loadImage = [this, doc, full_entry_path, source_url, meta](const QImage& image) { doc->addResource(QTextDocument::ImageResource, source_url, image); - parseImage(doc, image, posInDocument); + meta->image = image; + parseImage(doc, meta); // This size hack is needed to prevent the content from being laid out in an area smaller // than the total width available (weird). diff --git a/launcher/ui/widgets/VariableSizedImageObject.h b/launcher/ui/widgets/VariableSizedImageObject.h index ca67af0c9..df3ab4f77 100644 --- a/launcher/ui/widgets/VariableSizedImageObject.h +++ b/launcher/ui/widgets/VariableSizedImageObject.h @@ -22,6 +22,7 @@ #include #include #include +#include /** Custom image text object to be used instead of the normal one in ProjectDescriptionPage. * @@ -32,6 +33,14 @@ class VariableSizedImageObject final : public QObject, public QTextObjectInterfa Q_OBJECT Q_INTERFACES(QTextObjectInterface) + struct ImageMetadata { + int posInDocument; + QUrl url; + QImage image; + int width; + int height; + }; + public: QSizeF intrinsicSize(QTextDocument* doc, int posInDocument, const QTextFormat& format) override; void drawObject(QPainter* painter, const QRectF& rect, QTextDocument* doc, int posInDocument, const QTextFormat& format) override; @@ -49,13 +58,13 @@ class VariableSizedImageObject final : public QObject, public QTextObjectInterfa private: /** Adds the image to the document, in the given position. */ - void parseImage(QTextDocument* doc, QImage image, int posInDocument); + void parseImage(QTextDocument* doc, std::shared_ptr meta); /** Loads an image from an external source, and adds it to the document. * * This uses m_meta_entry to cache the image. */ - void loadImage(QTextDocument* doc, const QUrl& source, int posInDocument); + void loadImage(QTextDocument* doc, std::shared_ptr meta); private: QString m_meta_entry; diff --git a/launcher/ui/widgets/VersionSelectWidget.cpp b/launcher/ui/widgets/VersionSelectWidget.cpp index a24630b31..2d735d18f 100644 --- a/launcher/ui/widgets/VersionSelectWidget.cpp +++ b/launcher/ui/widgets/VersionSelectWidget.cpp @@ -105,14 +105,14 @@ bool VersionSelectWidget::eventFilter(QObject* watched, QEvent* event) return QObject::eventFilter(watched, event); } -void VersionSelectWidget::initialize(BaseVersionList* vlist) +void VersionSelectWidget::initialize(BaseVersionList* vlist, bool forceLoad) { m_vlist = vlist; m_proxyModel->setSourceModel(vlist); listView->header()->setSectionResizeMode(QHeaderView::ResizeToContents); listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::Stretch); - if (!m_vlist->isLoaded()) { + if (!m_vlist->isLoaded() || forceLoad) { loadList(); } else { if (m_proxyModel->rowCount() == 0) { @@ -129,16 +129,12 @@ void VersionSelectWidget::closeEvent(QCloseEvent* event) void VersionSelectWidget::loadList() { - auto newTask = m_vlist->getLoadTask(); - if (!newTask) { - return; - } - loadTask = newTask.get(); - connect(loadTask, &Task::succeeded, this, &VersionSelectWidget::onTaskSucceeded); - connect(loadTask, &Task::failed, this, &VersionSelectWidget::onTaskFailed); - connect(loadTask, &Task::progress, this, &VersionSelectWidget::changeProgress); - if (!loadTask->isRunning()) { - loadTask->start(); + m_load_task = m_vlist->getLoadTask(); + connect(m_load_task.get(), &Task::succeeded, this, &VersionSelectWidget::onTaskSucceeded); + connect(m_load_task.get(), &Task::failed, this, &VersionSelectWidget::onTaskFailed); + connect(m_load_task.get(), &Task::progress, this, &VersionSelectWidget::changeProgress); + if (!m_load_task->isRunning()) { + m_load_task->start(); } sneakyProgressBar->setHidden(false); } @@ -150,7 +146,7 @@ void VersionSelectWidget::onTaskSucceeded() } sneakyProgressBar->setHidden(true); preselect(); - loadTask = nullptr; + m_load_task.reset(); } void VersionSelectWidget::onTaskFailed(const QString& reason) diff --git a/launcher/ui/widgets/VersionSelectWidget.h b/launcher/ui/widgets/VersionSelectWidget.h index d5ef1cc9f..c16d4c0dd 100644 --- a/launcher/ui/widgets/VersionSelectWidget.h +++ b/launcher/ui/widgets/VersionSelectWidget.h @@ -54,7 +54,7 @@ class VersionSelectWidget : public QWidget { ~VersionSelectWidget(); //! loads the list if needed. - void initialize(BaseVersionList* vlist); + void initialize(BaseVersionList* vlist, bool forceLoad = false); //! Starts a task that loads the list. void loadList(); @@ -98,7 +98,7 @@ class VersionSelectWidget : public QWidget { BaseVersionList* m_vlist = nullptr; VersionProxyModel* m_proxyModel = nullptr; int resizeOnColumn = 0; - Task* loadTask; + Task::Ptr m_load_task; bool preselectedAlready = false; QVBoxLayout* verticalLayout = nullptr; diff --git a/launcher/ui/widgets/WideBar.cpp b/launcher/ui/widgets/WideBar.cpp index 46caaaef2..2940d7ce7 100644 --- a/launcher/ui/widgets/WideBar.cpp +++ b/launcher/ui/widgets/WideBar.cpp @@ -309,4 +309,15 @@ bool WideBar::checkHash(QByteArray const& old_hash) const return old_hash == getHash(); } +void WideBar::removeAction(QAction* action) +{ + auto iter = getMatching(action); + if (iter == m_entries.end()) + return; + + iter->bar_action->setVisible(false); + removeAction(iter->bar_action); + m_entries.erase(iter); +} + #include "WideBar.moc" diff --git a/launcher/ui/widgets/WideBar.h b/launcher/ui/widgets/WideBar.h index c47f3a596..f4877a89a 100644 --- a/launcher/ui/widgets/WideBar.h +++ b/launcher/ui/widgets/WideBar.h @@ -38,6 +38,8 @@ class WideBar : public QToolBar { [[nodiscard]] QByteArray getVisibilityState() const; void setVisibilityState(QByteArray&&); + void removeAction(QAction* action); + private: struct BarEntry { enum class Type { None, Action, Separator, Spacer } type = Type::None; diff --git a/launcher/updater/PrismExternalUpdater.cpp b/launcher/updater/PrismExternalUpdater.cpp index bee72e3a0..69774dc04 100644 --- a/launcher/updater/PrismExternalUpdater.cpp +++ b/launcher/updater/PrismExternalUpdater.cpp @@ -85,6 +85,11 @@ PrismExternalUpdater::~PrismExternalUpdater() } void PrismExternalUpdater::checkForUpdates() +{ + checkForUpdates(true); +} + +void PrismExternalUpdater::checkForUpdates(bool triggeredByUser) { QProgressDialog progress(tr("Checking for updates..."), "", 0, 0, priv->parent); progress.setCancelButton(nullptr); @@ -160,7 +165,7 @@ void PrismExternalUpdater::checkForUpdates() switch (exit_code) { case 0: // no update available - { + if (triggeredByUser) { qDebug() << "No update available"; auto msgBox = QMessageBox(QMessageBox::Information, tr("No Update Available"), tr("You are running the latest version."), QMessageBox::Ok, priv->parent); @@ -257,7 +262,7 @@ void PrismExternalUpdater::setBetaAllowed(bool allowed) void PrismExternalUpdater::resetAutoCheckTimer() { - if (priv->autoCheck) { + if (priv->autoCheck && priv->updateInterval > 0) { int timeoutDuration = 0; auto now = QDateTime::currentDateTime(); if (priv->lastCheck.isValid()) { @@ -288,7 +293,7 @@ void PrismExternalUpdater::disconnectTimer() void PrismExternalUpdater::autoCheckTimerFired() { qDebug() << "Auto update Timer fired"; - checkForUpdates(); + checkForUpdates(false); } void PrismExternalUpdater::offerUpdate(const QString& version_name, const QString& version_tag, const QString& release_notes) diff --git a/launcher/updater/PrismExternalUpdater.h b/launcher/updater/PrismExternalUpdater.h index bfe94c149..b88676028 100644 --- a/launcher/updater/PrismExternalUpdater.h +++ b/launcher/updater/PrismExternalUpdater.h @@ -41,6 +41,7 @@ class PrismExternalUpdater : public ExternalUpdater { * Check for updates manually, showing the user a progress bar and an alert if no updates are found. */ void checkForUpdates() override; + void checkForUpdates(bool triggeredByUser); /*! * Indicates whether or not to check for updates automatically. diff --git a/launcher/updater/prismupdater/PrismUpdater.cpp b/launcher/updater/prismupdater/PrismUpdater.cpp index 5fe22bdd0..8bf8cb473 100644 --- a/launcher/updater/prismupdater/PrismUpdater.cpp +++ b/launcher/updater/prismupdater/PrismUpdater.cpp @@ -244,8 +244,9 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar auto updater_executable = QCoreApplication::applicationFilePath(); - if (BuildConfig.BUILD_ARTIFACT.toLower() == "macos") - showFatalErrorMessage(tr("MacOS Not Supported"), tr("The updater does not support installations on MacOS")); +#ifdef Q_OS_MACOS + showFatalErrorMessage(tr("MacOS Not Supported"), tr("The updater does not support installations on MacOS")); +#endif if (updater_executable.startsWith("/tmp/.mount_")) { m_isAppimage = true; @@ -327,6 +328,19 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar // on command line adjustedBy = "Command line"; m_dataPath = dirParam; +#ifndef Q_OS_MACOS + if (QDir(FS::PathCombine(m_rootPath, "UserData")).exists()) { + m_isPortable = true; + } + if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { + m_isPortable = true; + } +#endif + } else if (auto dataDirEnv = + QProcessEnvironment::systemEnvironment().value(QString("%1_DATA_DIR").arg(BuildConfig.LAUNCHER_NAME.toUpper())); + !dataDirEnv.isEmpty()) { + adjustedBy = "System environment"; + m_dataPath = dataDirEnv; #ifndef Q_OS_MACOS if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { m_isPortable = true; @@ -338,7 +352,11 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar adjustedBy = "Persistent data path"; #ifndef Q_OS_MACOS - if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { + if (auto portableUserData = FS::PathCombine(m_rootPath, "UserData"); QDir(portableUserData).exists()) { + m_dataPath = portableUserData; + adjustedBy = "Portable user data path"; + m_isPortable = true; + } else if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { m_dataPath = m_rootPath; adjustedBy = "Portable data path"; m_isPortable = true; @@ -352,15 +370,10 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar FS::ensureFolderPathExists(FS::PathCombine(m_dataPath, "logs")); static const QString baseLogFile = BuildConfig.LAUNCHER_NAME + "Updater" + (m_checkOnly ? "-CheckOnly" : "") + "-%0.log"; static const QString logBase = FS::PathCombine(m_dataPath, "logs", baseLogFile); - auto moveFile = [](const QString& oldName, const QString& newName) { - QFile::remove(newName); - QFile::copy(oldName, newName); - QFile::remove(oldName); - }; if (FS::ensureFolderPathExists("logs")) { // enough history to track both launches of the updater during a portable install - moveFile(logBase.arg(1), logBase.arg(2)); - moveFile(logBase.arg(0), logBase.arg(1)); + FS::move(logBase.arg(1), logBase.arg(2)); + FS::move(logBase.arg(0), logBase.arg(1)); } logFile = std::unique_ptr(new QFile(logBase.arg(0))); @@ -474,8 +487,7 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar target_dir = QDir(m_rootPath).absoluteFilePath(".."); } - QMetaObject::invokeMethod( - this, [this, target_dir]() { moveAndFinishUpdate(target_dir); }, Qt::QueuedConnection); + QMetaObject::invokeMethod(this, [this, target_dir]() { moveAndFinishUpdate(target_dir); }, Qt::QueuedConnection); } else { QMetaObject::invokeMethod(this, &PrismUpdaterApp::loadReleaseList, Qt::QueuedConnection); @@ -586,12 +598,6 @@ void PrismUpdaterApp::run() return exit(result ? 0 : 1); } - if (BuildConfig.BUILD_ARTIFACT.toLower() == "linux" && !m_isPortable) { - showFatalErrorMessage(tr("Updating Not Supported"), - tr("Updating non-portable linux installations is not supported. Please use your system package manager")); - return; - } - if (need_update || m_forceUpdate || !m_userSelectedVersion.isEmpty()) { GitHubRelease update_release = latest; if (!m_userSelectedVersion.isEmpty()) { @@ -793,6 +799,10 @@ QList PrismUpdaterApp::validReleaseArtifacts(const GitHubRel if (BuildConfig.BUILD_ARTIFACT.isEmpty()) qWarning() << "Build platform is not set!"; for (auto asset : release.assets) { + if (asset.name.endsWith(".zsync")) { + qDebug() << "Rejecting zsync file" << asset.name; + continue; + } if (!m_isAppimage && asset.name.toLower().endsWith("appimage")) { qDebug() << "Rejecting" << asset.name << "because it is an AppImage"; continue; @@ -924,7 +934,7 @@ bool PrismUpdaterApp::callAppImageUpdate() void PrismUpdaterApp::clearUpdateLog() { - QFile::remove(m_updateLogPath); + FS::deletePath(m_updateLogPath); } void PrismUpdaterApp::logUpdate(const QString& msg) @@ -1026,7 +1036,7 @@ void PrismUpdaterApp::performInstall(QFileInfo file) FS::write(changelog_path, m_install_release.body.toUtf8()); logUpdate(tr("Updating from %1 to %2").arg(m_prismVersion).arg(m_install_release.tag_name)); - if (m_isPortable || file.suffix().toLower() == "zip") { + if (m_isPortable || file.fileName().endsWith(".zip") || file.fileName().endsWith(".tar.gz")) { write_lock_file(update_lock_path, QDateTime::currentDateTime(), m_prismVersion, m_install_release.tag_name, m_rootPath, m_dataPath); logUpdate(tr("Updating portable install at %1").arg(m_rootPath)); unpackAndInstall(file); @@ -1100,7 +1110,7 @@ void PrismUpdaterApp::backupAppDir() if (file_list.isEmpty()) { // best guess - if (BuildConfig.BUILD_ARTIFACT.toLower() == "linux") { + if (BuildConfig.BUILD_ARTIFACT.toLower().contains("linux")) { file_list.append({ "PrismLauncher", "bin", "share", "lib" }); } else { // windows by process of elimination file_list.append({ @@ -1118,7 +1128,6 @@ void PrismUpdaterApp::backupAppDir() "Qt*.dll", }); } - file_list.append("portable.txt"); logUpdate("manifest.txt empty or missing. making best guess at files to back up."); } logUpdate(tr("Backing up:\n %1").arg(file_list.join(",\n "))); @@ -1201,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; @@ -1232,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 395b658db..06dc161b1 100644 --- a/launcher/updater/prismupdater/UpdaterDialogs.cpp +++ b/launcher/updater/prismupdater/UpdaterDialogs.cpp @@ -26,6 +26,7 @@ #include #include "Markdown.h" +#include "StringUtils.h" SelectReleaseDialog::SelectReleaseDialog(const Version& current_version, const QList& releases, QWidget* parent) : QDialog(parent), m_releases(releases), m_currentVersion(current_version), ui(new Ui::SelectReleaseDialog) @@ -96,7 +97,7 @@ void SelectReleaseDialog::selectionChanged(QTreeWidgetItem* current, QTreeWidget QString body = markdownToHTML(release.body.toUtf8()); m_selectedRelease = release; - ui->changelogTextBrowser->setHtml(body); + ui->changelogTextBrowser->setHtml(StringUtils::htmlListPatch(body)); } SelectReleaseAssetDialog::SelectReleaseAssetDialog(const QList& assets, QWidget* parent) diff --git a/libraries/README.md b/libraries/README.md index e75a381ee..67d78dade 100644 --- a/libraries/README.md +++ b/libraries/README.md @@ -32,14 +32,6 @@ Simple Java tool that prints the JVM details - version and platform bitness. Do what you want with it. It is so trivial that noone cares. -## Katabasis - -Oauth2 library customized for Microsoft authentication. - -This is a fork of the [O2 library](https://github.com/pipacs/o2). - -MIT licensed. - ## launcher Java launcher part for Minecraft. diff --git a/libraries/katabasis/.gitignore b/libraries/katabasis/.gitignore deleted file mode 100644 index 35e189c5e..000000000 --- a/libraries/katabasis/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -build/ -*.kdev4 diff --git a/libraries/katabasis/CMakeLists.txt b/libraries/katabasis/CMakeLists.txt deleted file mode 100644 index 643244ede..000000000 --- a/libraries/katabasis/CMakeLists.txt +++ /dev/null @@ -1,58 +0,0 @@ -cmake_minimum_required(VERSION 3.9.4) - -string(COMPARE EQUAL "${CMAKE_SOURCE_DIR}" "${CMAKE_BUILD_DIR}" IS_IN_SOURCE_BUILD) -if(IS_IN_SOURCE_BUILD) - message(FATAL_ERROR "You are building Katabasis in-source. Please separate the build tree from the source tree.") -endif() - -project(Katabasis) -enable_testing() - -set(CMAKE_AUTOMOC ON) -set(CMAKE_INCLUDE_CURRENT_DIR ON) - -set(CMAKE_CXX_STANDARD_REQUIRED true) -set(CMAKE_C_STANDARD_REQUIRED true) -set(CMAKE_CXX_STANDARD 11) -set(CMAKE_C_STANDARD 11) - -if(QT_VERSION_MAJOR EQUAL 5) - find_package(Qt5 COMPONENTS Core Network REQUIRED) -elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) - find_package(Qt6 COMPONENTS Core Network REQUIRED) -endif() - -set( katabasis_PRIVATE - src/DeviceFlow.cpp - src/JsonResponse.cpp - src/JsonResponse.h - src/PollServer.cpp - src/Reply.cpp -) - -set( katabasis_PUBLIC - include/katabasis/DeviceFlow.h - include/katabasis/Globals.h - include/katabasis/PollServer.h - include/katabasis/Reply.h - include/katabasis/RequestParameter.h -) - -ecm_qt_declare_logging_category(katabasis_PRIVATE - HEADER KatabasisLogging.h # NOTE: this won't be in src/, but CMAKE_BINARY_DIR/src isn't included by default so this should be fine - IDENTIFIER katabasisCredentials - CATEGORY_NAME "katabasis.credentials" - DEFAULT_SEVERITY Warning - DESCRIPTION "Secrets and credentials from Katabasis" - EXPORT "Katabasis" -) - -add_library( Katabasis STATIC ${katabasis_PRIVATE} ${katabasis_PUBLIC} ) -target_link_libraries(Katabasis Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network) - -# needed for statically linked Katabasis in shared libs on x86_64 -set_target_properties(Katabasis - PROPERTIES POSITION_INDEPENDENT_CODE TRUE -) - -target_include_directories(Katabasis PUBLIC include PRIVATE src include/katabasis) diff --git a/libraries/katabasis/LICENSE b/libraries/katabasis/LICENSE deleted file mode 100644 index 9ac8d42fb..000000000 --- a/libraries/katabasis/LICENSE +++ /dev/null @@ -1,23 +0,0 @@ -Copyright (c) 2012, Akos Polster -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this -list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright notice, -this list of conditions and the following disclaimer in the documentation -and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/libraries/katabasis/README.md b/libraries/katabasis/README.md deleted file mode 100644 index fe6dd4aca..000000000 --- a/libraries/katabasis/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# Katabasis - MS-flavored OAuth for Qt, derived from the O2 library - -This library's sole purpose is to make interacting with MSA and various MSA and XBox authenticated services less painful. - -It may be possible to backport some of the changes to O2 in the future, but for the sake of going fast, all compatibility concerns have been ignored. - -[You can find the original library's git repository here.](https://github.com/pipacs/o2) - -Notes to contributors: - -* Please follow the coding style of the existing source, where reasonable -* Code contributions are released under Simplified BSD License, as specified in LICENSE. Do not contribute if this license does not suit your code -* If you are interested in working on this, come to the Prism Launcher Discord server and talk first - -## Installation - -Clone the Github repository, integrate the it into your CMake build system. - -The library is static only, dynamic linking and system-wide installation are out of scope and undesirable. - -## Usage - -At this stage, don't, unless you want to help with the library itself. - -This is an experimental fork of the O2 library and is undergoing a big design/architecture shift in order to support different features: - -* Multiple accounts -* Multi-stage authentication/authorization schemes -* Tighter control over token chains and their storage -* Talking to complex APIs and individually authorized microservices -* Token lifetime management, 'offline mode' and resilience in face of network failures -* Token and claims/entitlements validation -* Caching of some API results -* XBox magic -* Mojang magic -* Generally, magic that you would spend weeks on researching while getting confused by contradictory/incomplete documentation (if any is available) diff --git a/libraries/katabasis/acknowledgements.md b/libraries/katabasis/acknowledgements.md deleted file mode 100644 index a6989d15a..000000000 --- a/libraries/katabasis/acknowledgements.md +++ /dev/null @@ -1,108 +0,0 @@ -## O2 library by Akos Polster and contributors - -[The origin of this fork.](https://github.com/pipacs/o2) - -> Copyright (c) 2012, Akos Polster -> All rights reserved. -> -> Redistribution and use in source and binary forms, with or without -> modification, are permitted provided that the following conditions are met: -> -> * Redistributions of source code must retain the above copyright notice, this -> list of conditions and the following disclaimer. -> -> * Redistributions in binary form must reproduce the above copyright notice, -> this list of conditions and the following disclaimer in the documentation -> and/or other materials provided with the distribution. -> -> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -> AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -> IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -> DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -> FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -> DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -> SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -> OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -> OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -## SimpleCrypt by Andre Somers - -Cryptographic methods for Qt. - -> Copyright (c) 2011, Andre Somers -> All rights reserved. -> -> Redistribution and use in source and binary forms, with or without -> modification, are permitted provided that the following conditions are met: -> -> * Redistributions of source code must retain the above copyright -> notice, this list of conditions and the following disclaimer. -> * Redistributions in binary form must reproduce the above copyright -> notice, this list of conditions and the following disclaimer in the -> documentation and/or other materials provided with the distribution. -> * Neither the name of the Rathenau Instituut, Andre Somers nor the -> names of its contributors may be used to endorse or promote products -> derived from this software without specific prior written permission. -> -> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -> ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -> WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -> DISCLAIMED. IN NO EVENT SHALL ANDRE SOMERS BE LIABLE FOR ANY -> DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -> (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -> ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -> (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -> SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -## Mandeep Sandhu - -Configurable settings storage, Twitter XAuth specialization, new demos, cleanups. - -> "Hi Akos, -> -> I'm writing this mail to confirm that my contributions to the O2 library, available here , can be freely distributed according to the project's license (as shown in the LICENSE file). -> -> Regards, -> -mandeep" - -## Sergey Gavrushkin - -FreshBooks specialization - -## Theofilos Intzoglou - -Hubic specialization - -## Dimitar - -SurveyMonkey specialization - -## David Brooks - -CMake related fixes and improvements. - -## Lukas Vogel - -Spotify support - -## Alan Garny - -Windows DLL build support - -## MartinMikita - -Bug fixes - -## Larry Shaffer - -Versioning, shared lib, install target and header support - -## Gilmanov Ildar - -Bug fixes, support for ```qml``` module - -## Fabian Vogt - -Bug fixes, support for building without Qt keywords enabled diff --git a/libraries/katabasis/include/katabasis/Bits.h b/libraries/katabasis/include/katabasis/Bits.h deleted file mode 100644 index 15da2a5a8..000000000 --- a/libraries/katabasis/include/katabasis/Bits.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -namespace Katabasis { -enum class Activity { - Idle, - LoggingIn, - LoggingOut, - Refreshing, - FailedSoft, //!< soft failure. this generally means the user auth details haven't been invalidated - FailedHard, //!< hard failure. auth is invalid - FailedGone, //!< hard failure. auth is invalid, and the account no longer exists - Succeeded -}; - -enum class Validity { None, Assumed, Certain }; - -struct Token { - QDateTime issueInstant; - QDateTime notAfter; - QString token; - QString refresh_token; - QVariantMap extra; - - Validity validity = Validity::None; - bool persistent = true; -}; - -} // namespace Katabasis diff --git a/libraries/katabasis/include/katabasis/DeviceFlow.h b/libraries/katabasis/include/katabasis/DeviceFlow.h deleted file mode 100644 index 98724d81b..000000000 --- a/libraries/katabasis/include/katabasis/DeviceFlow.h +++ /dev/null @@ -1,149 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include "Bits.h" -#include "Reply.h" -#include "RequestParameter.h" - -namespace Katabasis { - -class ReplyServer; -class PollServer; - -/// Simple OAuth2 Device Flow authenticator. -class DeviceFlow : public QObject { - Q_OBJECT - public: - Q_ENUMS(GrantFlow) - - public: - struct Options { - QString userAgent = QStringLiteral("Katabasis/1.0"); - QString responseType = QStringLiteral("code"); - QString scope; - QString clientIdentifier; - QString clientSecret; - QUrl authorizationUrl; - QUrl accessTokenUrl; - }; - - public: - /// Are we authenticated? - bool linked(); - - /// Authentication token. - QString token(); - - /// Provider-specific extra tokens, available after a successful authentication - QVariantMap extraTokens(); - - public: - // TODO: put in `Options` - /// User-defined extra parameters to append to request URL - QVariantMap extraRequestParams(); - void setExtraRequestParams(const QVariantMap& value); - - // TODO: split up the class into multiple, each implementing one OAuth2 flow - /// Grant type (if non-standard) - QString grantType(); - void setGrantType(const QString& value); - - public: - /// Constructor. - /// @param parent Parent object. - explicit DeviceFlow(Options& opts, Token& token, QObject* parent = 0, QNetworkAccessManager* manager = 0); - - /// Get refresh token. - QString refreshToken(); - - /// Get token expiration time - QDateTime expires(); - - public slots: - /// Authenticate. - void login(); - - /// De-authenticate. - void logout(); - - /// Refresh token. - bool refresh(); - - /// Handle situation where reply server has opted to close its connection - void serverHasClosed(bool paramsfound = false); - - signals: - /// Emitted when client needs to open a web browser window, with the given URL. - void openBrowser(const QUrl& url); - - /// Emitted when client can close the browser window. - void closeBrowser(); - - /// Emitted when client needs to show a verification uri and user code - void showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn); - - /// Emitted when the internal state changes - void activityChanged(Activity activity); - - public slots: - /// Handle verification response. - void onVerificationReceived(QMap); - - protected slots: - /// Handle completion of a Device Authorization Request - void onDeviceAuthReplyFinished(); - - /// Handle completion of a refresh request. - void onRefreshFinished(); - - /// Handle failure of a refresh request. - void onRefreshError(QNetworkReply::NetworkError error, QNetworkReply* reply); - - protected: - /// Set refresh token. - void setRefreshToken(const QString& v); - - /// Set token expiration time. - void setExpires(QDateTime v); - - /// Start polling authorization server - void startPollServer(const QVariantMap& params, int expiresIn); - - /// Set authentication token. - void setToken(const QString& v); - - /// Set the linked state - void setLinked(bool v); - - /// Set extra tokens found in OAuth response - void setExtraTokens(QVariantMap extraTokens); - - /// Set local poll server - void setPollServer(PollServer* server); - - PollServer* pollServer() const; - - void updateActivity(Activity activity); - - protected: - Options options_; - - QVariantMap extraReqParams_; - QNetworkAccessManager* manager_ = nullptr; - ReplyList timedReplies_; - QString grantType_; - - protected: - Token& token_; - - private: - PollServer* pollServer_ = nullptr; - Activity activity_ = Activity::Idle; -}; - -} // namespace Katabasis diff --git a/libraries/katabasis/include/katabasis/Globals.h b/libraries/katabasis/include/katabasis/Globals.h deleted file mode 100644 index 02fe1cf45..000000000 --- a/libraries/katabasis/include/katabasis/Globals.h +++ /dev/null @@ -1,59 +0,0 @@ -#pragma once - -namespace Katabasis { - -// Common constants -const char ENCRYPTION_KEY[] = "12345678"; -const char MIME_TYPE_XFORM[] = "application/x-www-form-urlencoded"; -const char MIME_TYPE_JSON[] = "application/json"; - -// OAuth 1/1.1 Request Parameters -const char OAUTH_CALLBACK[] = "oauth_callback"; -const char OAUTH_CONSUMER_KEY[] = "oauth_consumer_key"; -const char OAUTH_NONCE[] = "oauth_nonce"; -const char OAUTH_SIGNATURE[] = "oauth_signature"; -const char OAUTH_SIGNATURE_METHOD[] = "oauth_signature_method"; -const char OAUTH_TIMESTAMP[] = "oauth_timestamp"; -const char OAUTH_VERSION[] = "oauth_version"; -// OAuth 1/1.1 Response Parameters -const char OAUTH_TOKEN[] = "oauth_token"; -const char OAUTH_TOKEN_SECRET[] = "oauth_token_secret"; -const char OAUTH_CALLBACK_CONFIRMED[] = "oauth_callback_confirmed"; -const char OAUTH_VERFIER[] = "oauth_verifier"; - -// OAuth 2 Request Parameters -const char OAUTH2_RESPONSE_TYPE[] = "response_type"; -const char OAUTH2_CLIENT_ID[] = "client_id"; -const char OAUTH2_CLIENT_SECRET[] = "client_secret"; -const char OAUTH2_USERNAME[] = "username"; -const char OAUTH2_PASSWORD[] = "password"; -const char OAUTH2_REDIRECT_URI[] = "redirect_uri"; -const char OAUTH2_SCOPE[] = "scope"; -const char OAUTH2_GRANT_TYPE_CODE[] = "code"; -const char OAUTH2_GRANT_TYPE_TOKEN[] = "token"; -const char OAUTH2_GRANT_TYPE_PASSWORD[] = "password"; -const char OAUTH2_GRANT_TYPE_DEVICE[] = "urn:ietf:params:oauth:grant-type:device_code"; -const char OAUTH2_GRANT_TYPE[] = "grant_type"; -const char OAUTH2_API_KEY[] = "api_key"; -const char OAUTH2_STATE[] = "state"; -const char OAUTH2_CODE[] = "code"; - -// OAuth 2 Response Parameters -const char OAUTH2_ACCESS_TOKEN[] = "access_token"; -const char OAUTH2_REFRESH_TOKEN[] = "refresh_token"; -const char OAUTH2_EXPIRES_IN[] = "expires_in"; -const char OAUTH2_DEVICE_CODE[] = "device_code"; -const char OAUTH2_USER_CODE[] = "user_code"; -const char OAUTH2_VERIFICATION_URI[] = "verification_uri"; -const char OAUTH2_VERIFICATION_URL[] = "verification_url"; // Google sign-in -const char OAUTH2_VERIFICATION_URI_COMPLETE[] = "verification_uri_complete"; -const char OAUTH2_INTERVAL[] = "interval"; - -// Parameter values -const char AUTHORIZATION_CODE[] = "authorization_code"; - -// Standard HTTP headers -const char HTTP_HTTP_HEADER[] = "HTTP"; -const char HTTP_AUTHORIZATION_HEADER[] = "Authorization"; - -} // namespace Katabasis diff --git a/libraries/katabasis/include/katabasis/PollServer.h b/libraries/katabasis/include/katabasis/PollServer.h deleted file mode 100644 index fd6a5351c..000000000 --- a/libraries/katabasis/include/katabasis/PollServer.h +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -class QNetworkAccessManager; - -namespace Katabasis { - -/// Poll an authorization server for token -class PollServer : public QObject { - Q_OBJECT - - public: - explicit PollServer(QNetworkAccessManager* manager, - const QNetworkRequest& request, - const QByteArray& payload, - int expiresIn, - QObject* parent = 0); - - /// Seconds to wait between polling requests - Q_PROPERTY(int interval READ interval WRITE setInterval) - int interval() const; - void setInterval(int interval); - - signals: - void verificationReceived(QMap); - void serverClosed(bool); // whether it has found parameters - - public slots: - void startPolling(); - - protected slots: - void onPollTimeout(); - void onExpiration(); - void onReplyFinished(); - - protected: - QNetworkAccessManager* manager_; - const QNetworkRequest request_; - const QByteArray payload_; - const int expiresIn_; - QTimer expirationTimer; - QTimer pollTimer; -}; - -} // namespace Katabasis diff --git a/libraries/katabasis/include/katabasis/Reply.h b/libraries/katabasis/include/katabasis/Reply.h deleted file mode 100644 index 89ee90e98..000000000 --- a/libraries/katabasis/include/katabasis/Reply.h +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -namespace Katabasis { - -constexpr int defaultTimeout = 30 * 1000; - -/// A network request/reply pair that can time out. -class Reply : public QTimer { - Q_OBJECT - - public: - Reply(QNetworkReply* reply, int timeOut = defaultTimeout, QObject* parent = 0); - - signals: - void error(QNetworkReply::NetworkError); - - public slots: - /// When time out occurs, the QNetworkReply's error() signal is triggered. - void onTimeOut(); - - public: - QNetworkReply* reply; - bool timedOut = false; -}; - -/// List of O2Replies. -class ReplyList { - public: - ReplyList() { ignoreSslErrors_ = false; } - - /// Destructor. - /// Deletes all O2Reply instances in the list. - virtual ~ReplyList(); - - /// Create a new O2Reply from a QNetworkReply, and add it to this list. - void add(QNetworkReply* reply, int timeOut = defaultTimeout); - - /// Add an O2Reply to the list, while taking ownership of it. - void add(Reply* reply); - - /// Remove item from the list that corresponds to a QNetworkReply. - void remove(QNetworkReply* reply); - - /// Find an O2Reply in the list, corresponding to a QNetworkReply. - /// @return Matching O2Reply or NULL. - Reply* find(QNetworkReply* reply); - - bool ignoreSslErrors(); - void setIgnoreSslErrors(bool ignoreSslErrors); - - protected: - QList replies_; - bool ignoreSslErrors_; -}; - -} // namespace Katabasis diff --git a/libraries/katabasis/include/katabasis/RequestParameter.h b/libraries/katabasis/include/katabasis/RequestParameter.h deleted file mode 100644 index 1d23cf0e1..000000000 --- a/libraries/katabasis/include/katabasis/RequestParameter.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -namespace Katabasis { - -/// Request parameter (name-value pair) participating in authentication. -struct RequestParameter { - RequestParameter(const QByteArray& n, const QByteArray& v) : name(n), value(v) {} - bool operator<(const RequestParameter& other) const { return (name == other.name) ? (value < other.value) : (name < other.name); } - QByteArray name; - QByteArray value; -}; - -} // namespace Katabasis diff --git a/libraries/katabasis/src/DeviceFlow.cpp b/libraries/katabasis/src/DeviceFlow.cpp deleted file mode 100644 index 3b9d9c53f..000000000 --- a/libraries/katabasis/src/DeviceFlow.cpp +++ /dev/null @@ -1,467 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "katabasis/DeviceFlow.h" -#include "katabasis/Globals.h" -#include "katabasis/PollServer.h" - -#include "JsonResponse.h" -#include "KatabasisLogging.h" - -namespace { - -// ref: https://tools.ietf.org/html/rfc8628#section-3.2 -// Exception: Google sign-in uses "verification_url" instead of "*_uri" - we'll accept both. -bool hasMandatoryDeviceAuthParams(const QVariantMap& params) -{ - if (!params.contains(Katabasis::OAUTH2_DEVICE_CODE)) - return false; - - if (!params.contains(Katabasis::OAUTH2_USER_CODE)) - return false; - - if (!(params.contains(Katabasis::OAUTH2_VERIFICATION_URI) || params.contains(Katabasis::OAUTH2_VERIFICATION_URL))) - return false; - - if (!params.contains(Katabasis::OAUTH2_EXPIRES_IN)) - return false; - - return true; -} - -QByteArray createQueryParameters(const QList& parameters) -{ - QByteArray ret; - bool first = true; - for (auto& h : parameters) { - if (first) { - first = false; - } else { - ret.append("&"); - } - ret.append(QUrl::toPercentEncoding(h.name) + "=" + QUrl::toPercentEncoding(h.value)); - } - return ret; -} -} // namespace - -namespace Katabasis { - -DeviceFlow::DeviceFlow(Options& opts, Token& token, QObject* parent, QNetworkAccessManager* manager) : QObject(parent), token_(token) -{ - manager_ = manager ? manager : new QNetworkAccessManager(this); - qRegisterMetaType("QNetworkReply::NetworkError"); - options_ = opts; -} - -bool DeviceFlow::linked() -{ - return token_.validity != Validity::None; -} -void DeviceFlow::setLinked(bool v) -{ - qDebug() << "DeviceFlow::setLinked:" << (v ? "true" : "false"); - token_.validity = v ? Validity::Certain : Validity::None; -} - -void DeviceFlow::updateActivity(Activity activity) -{ - if (activity_ == activity) { - return; - } - - activity_ = activity; - switch (activity) { - case Katabasis::Activity::Idle: - case Katabasis::Activity::LoggingIn: - case Katabasis::Activity::LoggingOut: - case Katabasis::Activity::Refreshing: - // non-terminal states... - break; - case Katabasis::Activity::FailedSoft: - // terminal state, tokens did not change - break; - case Katabasis::Activity::FailedHard: - case Katabasis::Activity::FailedGone: - // terminal state, tokens are invalid - token_ = Token(); - break; - case Katabasis::Activity::Succeeded: - setLinked(true); - break; - } - emit activityChanged(activity_); -} - -QString DeviceFlow::token() -{ - return token_.token; -} -void DeviceFlow::setToken(const QString& v) -{ - token_.token = v; -} - -QVariantMap DeviceFlow::extraTokens() -{ - return token_.extra; -} - -void DeviceFlow::setExtraTokens(QVariantMap extraTokens) -{ - token_.extra = extraTokens; -} - -void DeviceFlow::setPollServer(PollServer* server) -{ - if (pollServer_) - pollServer_->deleteLater(); - - pollServer_ = server; -} - -PollServer* DeviceFlow::pollServer() const -{ - return pollServer_; -} - -QVariantMap DeviceFlow::extraRequestParams() -{ - return extraReqParams_; -} - -void DeviceFlow::setExtraRequestParams(const QVariantMap& value) -{ - extraReqParams_ = value; -} - -QString DeviceFlow::grantType() -{ - if (!grantType_.isEmpty()) - return grantType_; - - return OAUTH2_GRANT_TYPE_DEVICE; -} - -void DeviceFlow::setGrantType(const QString& value) -{ - grantType_ = value; -} - -// First get the URL and token to display to the user -void DeviceFlow::login() -{ - qDebug() << "DeviceFlow::link"; - - updateActivity(Activity::LoggingIn); - setLinked(false); - setToken(""); - setExtraTokens(QVariantMap()); - setRefreshToken(QString()); - setExpires(QDateTime()); - - QList parameters; - parameters.append(RequestParameter(OAUTH2_CLIENT_ID, options_.clientIdentifier.toUtf8())); - parameters.append(RequestParameter(OAUTH2_SCOPE, options_.scope.toUtf8())); - QByteArray payload = createQueryParameters(parameters); - - QUrl url(options_.authorizationUrl); - QNetworkRequest deviceRequest(url); - deviceRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - QNetworkReply* tokenReply = manager_->post(deviceRequest, payload); - - connect(tokenReply, &QNetworkReply::finished, this, &DeviceFlow::onDeviceAuthReplyFinished, Qt::QueuedConnection); -} - -// Then, once we get them, present them to the user -void DeviceFlow::onDeviceAuthReplyFinished() -{ - qDebug() << "DeviceFlow::onDeviceAuthReplyFinished"; - QNetworkReply* tokenReply = qobject_cast(sender()); - if (!tokenReply) { - qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: reply is null"; - return; - } - if (tokenReply->error() == QNetworkReply::NoError) { - QByteArray replyData = tokenReply->readAll(); - - // Dump replyData - // SENSITIVE DATA in RelWithDebInfo or Debug builds - // qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: replyData\n"; - // qDebug() << QString( replyData ); - - QVariantMap params = parseJsonResponse(replyData); - - // Dump tokens - qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: Tokens returned:\n"; - foreach (QString key, params.keys()) { - // SENSITIVE DATA in RelWithDebInfo or Debug builds, so it is truncated first - qDebug() << key << ": " << params.value(key).toString(); - } - - // Check for mandatory parameters - if (hasMandatoryDeviceAuthParams(params)) { - qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: Device auth request response"; - - const QString userCode = params.take(OAUTH2_USER_CODE).toString(); - QUrl uri = params.take(OAUTH2_VERIFICATION_URI).toUrl(); - if (uri.isEmpty()) - uri = params.take(OAUTH2_VERIFICATION_URL).toUrl(); - - if (params.contains(OAUTH2_VERIFICATION_URI_COMPLETE)) - emit openBrowser(params.take(OAUTH2_VERIFICATION_URI_COMPLETE).toUrl()); - - bool ok = false; - int expiresIn = params[OAUTH2_EXPIRES_IN].toInt(&ok); - if (!ok) { - qWarning() << "DeviceFlow::startPollServer: No expired_in parameter"; - updateActivity(Activity::FailedHard); - return; - } - - emit showVerificationUriAndCode(uri, userCode, expiresIn); - - startPollServer(params, expiresIn); - } else { - qWarning() << "DeviceFlow::onDeviceAuthReplyFinished: Mandatory parameters missing from response"; - updateActivity(Activity::FailedHard); - } - } - tokenReply->deleteLater(); -} - -// Spin up polling for the user completing the login flow out of band -void DeviceFlow::startPollServer(const QVariantMap& params, int expiresIn) -{ - qDebug() << "DeviceFlow::startPollServer: device_ and user_code expires in" << expiresIn << "seconds"; - - QUrl url(options_.accessTokenUrl); - QNetworkRequest authRequest(url); - authRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - - const QString deviceCode = params[OAUTH2_DEVICE_CODE].toString(); - const QString grantType = grantType_.isEmpty() ? OAUTH2_GRANT_TYPE_DEVICE : grantType_; - - QList parameters; - parameters.append(RequestParameter(OAUTH2_CLIENT_ID, options_.clientIdentifier.toUtf8())); - if (!options_.clientSecret.isEmpty()) { - parameters.append(RequestParameter(OAUTH2_CLIENT_SECRET, options_.clientSecret.toUtf8())); - } - parameters.append(RequestParameter(OAUTH2_CODE, deviceCode.toUtf8())); - parameters.append(RequestParameter(OAUTH2_GRANT_TYPE, grantType.toUtf8())); - QByteArray payload = createQueryParameters(parameters); - - PollServer* pollServer = new PollServer(manager_, authRequest, payload, expiresIn, this); - if (params.contains(OAUTH2_INTERVAL)) { - bool ok = false; - int interval = params[OAUTH2_INTERVAL].toInt(&ok); - if (ok) { - pollServer->setInterval(interval); - } - } - connect(pollServer, &PollServer::verificationReceived, this, &DeviceFlow::onVerificationReceived); - connect(pollServer, &PollServer::serverClosed, this, &DeviceFlow::serverHasClosed); - setPollServer(pollServer); - pollServer->startPolling(); -} - -// Once the user completes the flow, update the internal state and report it to observers -void DeviceFlow::onVerificationReceived(const QMap response) -{ - qDebug() << "DeviceFlow::onVerificationReceived: Emitting closeBrowser()"; - emit closeBrowser(); - - if (response.contains("error")) { - qWarning() << "DeviceFlow::onVerificationReceived: Verification failed:" << response; - updateActivity(Activity::FailedHard); - return; - } - - // Check for mandatory tokens - if (response.contains(OAUTH2_ACCESS_TOKEN)) { - qDebug() << "DeviceFlow::onVerificationReceived: Access token returned for implicit or device flow"; - setToken(response.value(OAUTH2_ACCESS_TOKEN)); - if (response.contains(OAUTH2_EXPIRES_IN)) { - bool ok = false; - int expiresIn = response.value(OAUTH2_EXPIRES_IN).toInt(&ok); - if (ok) { - qDebug() << "DeviceFlow::onVerificationReceived: Token expires in" << expiresIn << "seconds"; - setExpires(QDateTime::currentDateTimeUtc().addSecs(expiresIn)); - } - } - if (response.contains(OAUTH2_REFRESH_TOKEN)) { - setRefreshToken(response.value(OAUTH2_REFRESH_TOKEN)); - } - updateActivity(Activity::Succeeded); - } else { - qWarning() << "DeviceFlow::onVerificationReceived: Access token missing from response for implicit or device flow"; - updateActivity(Activity::FailedHard); - } -} - -// Or if the flow fails or the polling times out, update the internal state with error and report it to observers -void DeviceFlow::serverHasClosed(bool paramsfound) -{ - if (!paramsfound) { - // server has probably timed out after receiving first response - updateActivity(Activity::FailedHard); - } - // poll server is not re-used for later auth requests - setPollServer(NULL); -} - -void DeviceFlow::logout() -{ - qDebug() << "DeviceFlow::unlink"; - updateActivity(Activity::LoggingOut); - // FIXME: implement logout flows... if they exist - token_ = Token(); - updateActivity(Activity::FailedHard); -} - -QDateTime DeviceFlow::expires() -{ - return token_.notAfter; -} -void DeviceFlow::setExpires(QDateTime v) -{ - token_.notAfter = v; -} - -QString DeviceFlow::refreshToken() -{ - return token_.refresh_token; -} - -void DeviceFlow::setRefreshToken(const QString& v) -{ - qCDebug(katabasisCredentials) << "new refresh token:" << v; - token_.refresh_token = v; -} - -namespace { -QByteArray buildRequestBody(const QMap& parameters) -{ - QByteArray body; - bool first = true; - foreach (QString key, parameters.keys()) { - if (first) { - first = false; - } else { - body.append("&"); - } - QString value = parameters.value(key); - body.append(QUrl::toPercentEncoding(key) + QString("=").toUtf8() + QUrl::toPercentEncoding(value)); - } - return body; -} -} // namespace - -bool DeviceFlow::refresh() -{ - qDebug() << "DeviceFlow::refresh: Token: ..." << refreshToken().right(7); - - updateActivity(Activity::Refreshing); - - if (refreshToken().isEmpty()) { - qWarning() << "DeviceFlow::refresh: No refresh token"; - onRefreshError(QNetworkReply::AuthenticationRequiredError, nullptr); - return false; - } - if (options_.accessTokenUrl.isEmpty()) { - qWarning() << "DeviceFlow::refresh: Refresh token URL not set"; - onRefreshError(QNetworkReply::AuthenticationRequiredError, nullptr); - return false; - } - - QNetworkRequest refreshRequest(options_.accessTokenUrl); - refreshRequest.setHeader(QNetworkRequest::ContentTypeHeader, MIME_TYPE_XFORM); - QMap parameters; - parameters.insert(OAUTH2_CLIENT_ID, options_.clientIdentifier); - if (!options_.clientSecret.isEmpty()) { - parameters.insert(OAUTH2_CLIENT_SECRET, options_.clientSecret); - } - parameters.insert(OAUTH2_REFRESH_TOKEN, refreshToken()); - parameters.insert(OAUTH2_GRANT_TYPE, OAUTH2_REFRESH_TOKEN); - - QByteArray data = buildRequestBody(parameters); - QNetworkReply* refreshReply = manager_->post(refreshRequest, data); - timedReplies_.add(refreshReply); - connect(refreshReply, &QNetworkReply::finished, this, &DeviceFlow::onRefreshFinished, Qt::QueuedConnection); - return true; -} - -void DeviceFlow::onRefreshFinished() -{ - QNetworkReply* refreshReply = qobject_cast(sender()); - - auto networkError = refreshReply->error(); - if (networkError == QNetworkReply::NoError) { - QByteArray reply = refreshReply->readAll(); - QVariantMap tokens = parseJsonResponse(reply); - setToken(tokens.value(OAUTH2_ACCESS_TOKEN).toString()); - setExpires(QDateTime::currentDateTimeUtc().addSecs(tokens.value(OAUTH2_EXPIRES_IN).toInt())); - QString refreshToken = tokens.value(OAUTH2_REFRESH_TOKEN).toString(); - if (!refreshToken.isEmpty()) { - setRefreshToken(refreshToken); - } else { - qDebug() << "No new refresh token. Keep the old one."; - } - timedReplies_.remove(refreshReply); - refreshReply->deleteLater(); - updateActivity(Activity::Succeeded); - qDebug() << "New token expires in" << expires() << "seconds"; - } else { - // FIXME: differentiate the error more here - onRefreshError(networkError, refreshReply); - } -} - -void DeviceFlow::onRefreshError(QNetworkReply::NetworkError error, QNetworkReply* refreshReply) -{ - QString errorString = "No Reply"; - if (refreshReply) { - timedReplies_.remove(refreshReply); - errorString = refreshReply->errorString(); - } - - switch (error) { - // used for invalid credentials and similar errors. Fall through. - case QNetworkReply::AuthenticationRequiredError: - case QNetworkReply::ContentAccessDenied: - case QNetworkReply::ContentOperationNotPermittedError: - case QNetworkReply::ProtocolInvalidOperationError: - updateActivity(Activity::FailedHard); - break; - case QNetworkReply::ContentGoneError: { - updateActivity(Activity::FailedGone); - break; - } - case QNetworkReply::TimeoutError: - case QNetworkReply::OperationCanceledError: - case QNetworkReply::SslHandshakeFailedError: - default: - updateActivity(Activity::FailedSoft); - return; - } - if (refreshReply) { - refreshReply->deleteLater(); - } - qDebug() << "DeviceFlow::onRefreshFinished: Error" << static_cast(error) << " - " << errorString; -} - -} // namespace Katabasis diff --git a/libraries/katabasis/src/JsonResponse.cpp b/libraries/katabasis/src/JsonResponse.cpp deleted file mode 100644 index 6840627ac..000000000 --- a/libraries/katabasis/src/JsonResponse.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include "JsonResponse.h" - -#include -#include -#include -#include - -namespace Katabasis { - -QVariantMap parseJsonResponse(const QByteArray& data) -{ - QJsonParseError err; - QJsonDocument doc = QJsonDocument::fromJson(data, &err); - if (err.error != QJsonParseError::NoError) { - qWarning() << "parseTokenResponse: Failed to parse token response due to err:" << err.errorString(); - return QVariantMap(); - } - - if (!doc.isObject()) { - qWarning() << "parseTokenResponse: Token response is not an object"; - return QVariantMap(); - } - - return doc.object().toVariantMap(); -} - -} // namespace Katabasis diff --git a/libraries/katabasis/src/JsonResponse.h b/libraries/katabasis/src/JsonResponse.h deleted file mode 100644 index ff3471752..000000000 --- a/libraries/katabasis/src/JsonResponse.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include - -class QByteArray; - -namespace Katabasis { - -/// Parse JSON data into a QVariantMap -QVariantMap parseJsonResponse(const QByteArray& data); - -} // namespace Katabasis diff --git a/libraries/katabasis/src/PollServer.cpp b/libraries/katabasis/src/PollServer.cpp deleted file mode 100644 index c1c316df9..000000000 --- a/libraries/katabasis/src/PollServer.cpp +++ /dev/null @@ -1,118 +0,0 @@ -#include -#include - -#include "JsonResponse.h" -#include "katabasis/PollServer.h" - -namespace { -QMap toVerificationParams(const QVariantMap& map) -{ - QMap params; - for (QVariantMap::const_iterator i = map.constBegin(); i != map.constEnd(); ++i) { - params[i.key()] = i.value().toString(); - } - return params; -} -} // namespace - -namespace Katabasis { - -PollServer::PollServer(QNetworkAccessManager* manager, - const QNetworkRequest& request, - const QByteArray& payload, - int expiresIn, - QObject* parent) - : QObject(parent), manager_(manager), request_(request), payload_(payload), expiresIn_(expiresIn) -{ - expirationTimer.setTimerType(Qt::VeryCoarseTimer); - expirationTimer.setInterval(expiresIn * 1000); - expirationTimer.setSingleShot(true); - connect(&expirationTimer, SIGNAL(timeout()), this, SLOT(onExpiration())); - expirationTimer.start(); - - pollTimer.setTimerType(Qt::VeryCoarseTimer); - pollTimer.setInterval(5 * 1000); - pollTimer.setSingleShot(true); - connect(&pollTimer, SIGNAL(timeout()), this, SLOT(onPollTimeout())); -} - -int PollServer::interval() const -{ - return pollTimer.interval() / 1000; -} - -void PollServer::setInterval(int interval) -{ - pollTimer.setInterval(interval * 1000); -} - -void PollServer::startPolling() -{ - if (expirationTimer.isActive()) { - pollTimer.start(); - } -} - -void PollServer::onPollTimeout() -{ - qDebug() << "PollServer::onPollTimeout: retrying"; - QNetworkReply* reply = manager_->post(request_, payload_); - connect(reply, SIGNAL(finished()), this, SLOT(onReplyFinished())); -} - -void PollServer::onExpiration() -{ - pollTimer.stop(); - emit serverClosed(false); -} - -void PollServer::onReplyFinished() -{ - QNetworkReply* reply = qobject_cast(sender()); - - if (!reply) { - qDebug() << "PollServer::onReplyFinished: reply is null"; - return; - } - - QByteArray replyData = reply->readAll(); - QMap params = toVerificationParams(parseJsonResponse(replyData)); - - // Dump replyData - // SENSITIVE DATA in RelWithDebInfo or Debug builds - // qDebug() << "PollServer::onReplyFinished: replyData\n"; - // qDebug() << QString( replyData ); - - if (reply->error() == QNetworkReply::TimeoutError) { - // rfc8628#section-3.2 - // "On encountering a connection timeout, clients MUST unilaterally - // reduce their polling frequency before retrying. The use of an - // exponential backoff algorithm to achieve this, such as doubling the - // polling interval on each such connection timeout, is RECOMMENDED." - setInterval(interval() * 2); - pollTimer.start(); - } else { - QString error = params.value("error"); - if (error == "slow_down") { - // rfc8628#section-3.2 - // "A variant of 'authorization_pending', the authorization request is - // still pending and polling should continue, but the interval MUST - // be increased by 5 seconds for this and all subsequent requests." - setInterval(interval() + 5); - pollTimer.start(); - } else if (error == "authorization_pending") { - // keep trying - rfc8628#section-3.2 - // "The authorization request is still pending as the end user hasn't - // yet completed the user-interaction steps (Section 3.3)." - pollTimer.start(); - } else { - expirationTimer.stop(); - emit serverClosed(true); - // let O2 handle the other cases - emit verificationReceived(params); - } - } - reply->deleteLater(); -} - -} // namespace Katabasis diff --git a/libraries/katabasis/src/Reply.cpp b/libraries/katabasis/src/Reply.cpp deleted file mode 100644 index 4a5017e22..000000000 --- a/libraries/katabasis/src/Reply.cpp +++ /dev/null @@ -1,74 +0,0 @@ -#include -#include - -#include "katabasis/Reply.h" - -namespace Katabasis { - -Reply::Reply(QNetworkReply* r, int timeOut, QObject* parent) : QTimer(parent), reply(r) -{ - setSingleShot(true); - connect(this, &Reply::timeout, this, &Reply::onTimeOut, Qt::QueuedConnection); - start(timeOut); -} - -void Reply::onTimeOut() -{ - timedOut = true; - reply->abort(); -} - -// ---------------------------- - -ReplyList::~ReplyList() -{ - foreach (Reply* timedReply, replies_) { - delete timedReply; - } -} - -void ReplyList::add(QNetworkReply* reply, int timeOut) -{ - if (reply && ignoreSslErrors()) { - reply->ignoreSslErrors(); - } - add(new Reply(reply, timeOut)); -} - -void ReplyList::add(Reply* reply) -{ - replies_.append(reply); -} - -void ReplyList::remove(QNetworkReply* reply) -{ - Reply* o2Reply = find(reply); - if (o2Reply) { - o2Reply->stop(); - (void)replies_.removeOne(o2Reply); - // we took ownership, we must free - delete o2Reply; - } -} - -Reply* ReplyList::find(QNetworkReply* reply) -{ - foreach (Reply* timedReply, replies_) { - if (timedReply->reply == reply) { - return timedReply; - } - } - return 0; -} - -bool ReplyList::ignoreSslErrors() -{ - return ignoreSslErrors_; -} - -void ReplyList::setIgnoreSslErrors(bool ignoreSslErrors) -{ - ignoreSslErrors_ = ignoreSslErrors; -} - -} // namespace Katabasis diff --git a/libraries/launcher/org/prismlauncher/launcher/impl/AbstractLauncher.java b/libraries/launcher/org/prismlauncher/launcher/impl/AbstractLauncher.java index de28a0401..a5f027ba6 100644 --- a/libraries/launcher/org/prismlauncher/launcher/impl/AbstractLauncher.java +++ b/libraries/launcher/org/prismlauncher/launcher/impl/AbstractLauncher.java @@ -70,7 +70,7 @@ public abstract class AbstractLauncher implements Launcher { // secondary parameters protected final int width, height; protected final boolean maximize; - protected final String serverAddress, serverPort; + protected final String serverAddress, serverPort, worldName; protected final String mainClassName; @@ -80,6 +80,7 @@ public abstract class AbstractLauncher implements Launcher { serverAddress = params.getString("serverAddress", null); serverPort = params.getString("serverPort", null); + worldName = params.getString("worldName", null); String windowParams = params.getString("windowParams", null); diff --git a/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java b/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java index 49e5d518f..dc518be64 100644 --- a/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java +++ b/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java @@ -62,13 +62,15 @@ import java.util.Collections; import java.util.List; public final class StandardLauncher extends AbstractLauncher { - private final boolean quickPlaySupported; + private final boolean quickPlayMultiplayerSupported; + private final boolean quickPlaySingleplayerSupported; public StandardLauncher(Parameters params) { super(params); List traits = params.getList("traits", Collections.emptyList()); - quickPlaySupported = traits.contains("feature:is_quick_play_multiplayer"); + quickPlayMultiplayerSupported = traits.contains("feature:is_quick_play_multiplayer"); + quickPlaySingleplayerSupported = traits.contains("feature:is_quick_play_singleplayer"); } @Override @@ -83,7 +85,7 @@ public final class StandardLauncher extends AbstractLauncher { } if (serverAddress != null) { - if (quickPlaySupported) { + if (quickPlayMultiplayerSupported) { // as of 23w14a gameArgs.add("--quickPlayMultiplayer"); gameArgs.add(serverAddress + ':' + serverPort); @@ -93,6 +95,9 @@ public final class StandardLauncher extends AbstractLauncher { gameArgs.add("--port"); gameArgs.add(serverPort); } + } else if (worldName != null && quickPlaySingleplayerSupported) { + gameArgs.add("--quickPlaySingleplayer"); + gameArgs.add(worldName); } // find and invoke the main method diff --git a/libraries/murmur2/src/MurmurHash2.cpp b/libraries/murmur2/src/MurmurHash2.cpp index e73127953..99befd107 100644 --- a/libraries/murmur2/src/MurmurHash2.cpp +++ b/libraries/murmur2/src/MurmurHash2.cpp @@ -8,14 +8,14 @@ #include "MurmurHash2.h" -//----------------------------------------------------------------------------- +namespace Murmur2 { // 'm' and 'r' are mixing constants generated offline. // They're not really 'magic', they just happen to work well. const uint32_t m = 0x5bd1e995; const int r = 24; -uint32_t MurmurHash2(std::ifstream&& file_stream, std::size_t buffer_size, std::function filter_out) +uint32_t hash(Reader* file_stream, std::size_t buffer_size, std::function filter_out) { auto* buffer = new char[buffer_size]; char data[4]; @@ -26,24 +26,21 @@ uint32_t MurmurHash2(std::ifstream&& file_stream, std::size_t buffer_size, std:: // We need the size without the filtered out characters before actually calculating the hash, // to setup the initial value for the hash. do { - file_stream.read(buffer, buffer_size); - read = file_stream.gcount(); + read = file_stream->read(buffer, buffer_size); for (int i = 0; i < read; i++) { if (!filter_out(buffer[i])) size += 1; } - } while (!file_stream.eof()); + } while (!file_stream->eof()); - file_stream.clear(); - file_stream.seekg(0, file_stream.beg); + file_stream->goToBeginning(); int index = 0; // This forces a seed of 1. IncrementalHashInfo info{ (uint32_t)1 ^ size, (uint32_t)size }; do { - file_stream.read(buffer, buffer_size); - read = file_stream.gcount(); + read = file_stream->read(buffer, buffer_size); for (int i = 0; i < read; i++) { char c = buffer[i]; @@ -57,14 +54,13 @@ uint32_t MurmurHash2(std::ifstream&& file_stream, std::size_t buffer_size, std:: if (index == 0) FourBytes_MurmurHash2(reinterpret_cast(&data), info); } - } while (!file_stream.eof()); + } while (!file_stream->eof()); // Do one last bit shuffle in the hash FourBytes_MurmurHash2(reinterpret_cast(&data), info); delete[] buffer; - file_stream.close(); return info.h; } @@ -109,4 +105,4 @@ void FourBytes_MurmurHash2(const unsigned char* data, IncrementalHashInfo& prev) } } -//----------------------------------------------------------------------------- +} // namespace Murmur2 \ No newline at end of file diff --git a/libraries/murmur2/src/MurmurHash2.h b/libraries/murmur2/src/MurmurHash2.h index 5d4f48713..e6c196fd1 100644 --- a/libraries/murmur2/src/MurmurHash2.h +++ b/libraries/murmur2/src/MurmurHash2.h @@ -9,19 +9,22 @@ #pragma once #include -#include - #include -//----------------------------------------------------------------------------- +namespace Murmur2 { #define KiB 1024 #define MiB 1024 * KiB -uint32_t MurmurHash2( - std::ifstream&& file_stream, - std::size_t buffer_size = 4 * MiB, - std::function filter_out = [](char) { return false; }); +class Reader { + public: + virtual ~Reader() = default; + virtual int read(char* s, int n) = 0; + virtual bool eof() = 0; + virtual void goToBeginning() = 0; +}; + +uint32_t hash(Reader* file_stream, std::size_t buffer_size = 4 * MiB, std::function filter_out = [](char) { return false; }); struct IncrementalHashInfo { uint32_t h; @@ -29,5 +32,4 @@ struct IncrementalHashInfo { }; void FourBytes_MurmurHash2(const unsigned char* data, IncrementalHashInfo& prev); - -//----------------------------------------------------------------------------- +} // namespace Murmur2 diff --git a/nix/README.md b/nix/README.md index f7923577f..76cb8bf27 100644 --- a/nix/README.md +++ b/nix/README.md @@ -15,7 +15,6 @@ to temporarily enable it when using `nix` commands. Example (NixOS): ```nix -{...}: { nix.settings = { trusted-substituters = [ @@ -29,9 +28,9 @@ Example (NixOS): } ``` -### Using the overlay +### Installing the package directly -After adding `github:PrismLauncher/PrismLauncher` to your flake inputs, you can add the `default` overlay to your nixpkgs instance. +After adding `github:PrismLauncher/PrismLauncher` to your flake inputs, you can access the flake's `packages` output. Example: @@ -39,34 +38,47 @@ Example: { inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + prismlauncher = { url = "github:PrismLauncher/PrismLauncher"; + # Optional: Override the nixpkgs input of prismlauncher to use the same revision as the rest of your flake - # Note that overriding any input of prismlauncher may break reproducibility + # Note that this may break the reproducibility mentioned above, and you might not be able to access the binary cache + # # inputs.nixpkgs.follows = "nixpkgs"; + + # This is not required for Flakes + inputs.flake-compat.follows = ""; }; }; - outputs = {nixpkgs, prismlauncher}: { - nixosConfigurations.foo = nixpkgs.lib.nixosSystem { - system = "x86_64-linux"; + outputs = + { nixpkgs, prismlauncher, ... }: + { + nixosConfigurations.foo = nixpkgs.lib.nixosSystem { + modules = [ + ./configuration.nix - modules = [ - ({pkgs, ...}: { - nixpkgs.overlays = [prismlauncher.overlays.default]; - - environment.systemPackages = [pkgs.prismlauncher]; - }) - ]; + ( + { pkgs, ... }: + { + environment.systemPackages = [ prismlauncher.packages.${pkgs.system}.prismlauncher ]; + } + ) + ]; + }; }; - } } ``` -### Installing the package directly +### Using the overlay -Alternatively, if you don't want to use an overlay, you can install Prism Launcher directly by installing the `prismlauncher` package. -This way the installed package is fully reproducible. +Alternatively, if you don't want to use our `packages` output, you can add our overlay to your nixpkgs instance. +This will ensure Prism is built with your system's packages. + +> [!WARNING] +> Depending on what revision of nixpkgs your system uses, this may result in binaries that differ from the above `packages` output +> If this is the case, you will not be able to use the binary cache Example: @@ -74,25 +86,38 @@ Example: { inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + prismlauncher = { url = "github:PrismLauncher/PrismLauncher"; + # Optional: Override the nixpkgs input of prismlauncher to use the same revision as the rest of your flake - # Note that overriding any input of prismlauncher may break reproducibility + # Note that this may break the reproducibility mentioned above, and you might not be able to access the binary cache + # # inputs.nixpkgs.follows = "nixpkgs"; + + # This is not required for Flakes + inputs.flake-compat.follows = ""; }; }; - outputs = {nixpkgs, prismlauncher}: { - nixosConfigurations.foo = nixpkgs.lib.nixosSystem { - system = "x86_64-linux"; + outputs = + { nixpkgs, prismlauncher, ... }: + { + nixosConfigurations.foo = nixpkgs.lib.nixosSystem { + modules = [ + ./configuration.nix - modules = [ - ({pkgs, ...}: { - environment.systemPackages = [prismlauncher.packages.${pkgs.system}.prismlauncher]; - }) - ]; + ( + { pkgs, ... }: + { + nixpkgs.overlays = [ prismlauncher.overlays.default ]; + + environment.systemPackages = [ pkgs.prismlauncher ]; + } + ) + ]; + }; }; - } } ``` @@ -118,7 +143,6 @@ If you want to avoid rebuilds you may add the garnix cache to your substitutors. Example (NixOS): ```nix -{...}: { nix.settings = { trusted-substituters = [ @@ -132,30 +156,40 @@ Example (NixOS): } ``` -### Using the overlay (`fetchTarball`) +### Installing the package directly (`fetchTarball`) We use flake-compat to allow using this Flake on a system that doesn't use flakes. Example: ```nix -{pkgs, ...}: { - nixpkgs.overlays = [(import (builtins.fetchTarball "https://github.com/PrismLauncher/PrismLauncher/archive/develop.tar.gz")).overlays.default]; - - environment.systemPackages = [pkgs.prismlauncher]; +{ pkgs, ... }: +{ + environment.systemPackages = [ + (import ( + builtins.fetchTarball "https://github.com/PrismLauncher/PrismLauncher/archive/develop.tar.gz" + )).packages.${pkgs.system}.prismlauncher + ]; } ``` -### Installing the package directly (`fetchTarball`) +### Using the overlay (`fetchTarball`) -Alternatively, if you don't want to use an overlay, you can install Prism Launcher directly by installing the `prismlauncher` package. -This way the installed package is fully reproducible. +Alternatively, if you don't want to use our `packages` output, you can add our overlay to your instance of nixpkgs. +This results in Prism using your system's libraries Example: ```nix -{pkgs, ...}: { - environment.systemPackages = [(import (builtins.fetchTarball "https://github.com/PrismLauncher/PrismLauncher/archive/develop.tar.gz")).packages.${pkgs.system}.prismlauncher]; +{ pkgs, ... }: +{ + nixpkgs.overlays = [ + (import ( + builtins.fetchTarball "https://github.com/PrismLauncher/PrismLauncher/archive/develop.tar.gz" + )).overlays.default + ]; + + environment.systemPackages = [ pkgs.prismlauncher ]; } ``` @@ -177,18 +211,20 @@ nix-env -iA prismlauncher.prismlauncher Both Nixpkgs and this repository offer the following packages: -- `prismlauncher` - Preferred build using Qt 6 -- `prismlauncher-qt5` - Legacy build using Qt 5 (i.e. for Qt 5 theming support) - -Both of these packages also have `-unwrapped` counterparts, that are not wrapped and can therefore be customized even further than what the wrapper packages offer. +- `prismlauncher` - The preferred build, wrapped with everything necessary to run the launcher and Minecraft +- `prismlauncher-unwrapped` - A minimal build that allows for advanced customization of the launcher's runtime environment ### Customizing wrapped packages -The wrapped packages (`prismlauncher` and `prismlauncher-qt5`) offer some build parameters to further customize the launcher's environment. +The wrapped package (`prismlauncher`) offers some build parameters to further customize the launcher's environment. The following parameters can be overridden: -- `msaClientID` (default: `null`, requires full rebuild!) Client ID used for Microsoft Authentication -- `gamemodeSupport` (default: `true`) Turn on/off support for [Feral GameMode](https://github.com/FeralInteractive/gamemode) -- `jdks` (default: `[ jdk17 jdk8 ]`) Java runtimes added to `PRISMLAUNCHER_JAVA_PATHS` variable - `additionalLibs` (default: `[ ]`) Additional libraries that will be added to `LD_LIBRARY_PATH` +- `additionalPrograms` (default: `[ ]`) Additional libraries that will be added to `PATH` +- `controllerSupport` (default: `isLinux`) Turn on/off support for controllers on Linux (macOS will always have this) +- `gamemodeSupport` (default: `isLinux`) Turn on/off support for [Feral GameMode](https://github.com/FeralInteractive/gamemode) on Linux +- `jdks` (default: `[ jdk21 jdk17 jdk8 ]`) Java runtimes added to `PRISMLAUNCHER_JAVA_PATHS` variable +- `msaClientID` (default: `null`, requires full rebuild!) Client ID used for Microsoft Authentication +- `textToSpeechSupport` (default: `isLinux`) Turn on/off support for text-to-speech on Linux (macOS will always have this) +- `withWaylandGLFW` (default: `isLinux`) Build with support for native Wayland via a custom GLFW diff --git a/nix/checks.nix b/nix/checks.nix new file mode 100644 index 000000000..40a2e272f --- /dev/null +++ b/nix/checks.nix @@ -0,0 +1,42 @@ +{ + runCommand, + deadnix, + llvmPackages_18, + markdownlint-cli, + nixfmt-rfc-style, + statix, + self, +}: +{ + formatting = + runCommand "check-formatting" + { + nativeBuildInputs = [ + deadnix + llvmPackages_18.clang-tools + markdownlint-cli + nixfmt-rfc-style + statix + ]; + } + '' + cd ${self} + + echo "Running clang-format...." + clang-format -i --style='file' --Werror */**.{c,cc,cpp,h,hh,hpp} + + echo "Running deadnix..." + deadnix --fail + + echo "Running markdownlint..." + markdownlint --dot . + + echo "Running nixfmt..." + nixfmt --check . + + echo "Running statix" + statix check . + + touch $out + ''; +} diff --git a/nix/dev.nix b/nix/dev.nix deleted file mode 100644 index c476ed10f..000000000 --- a/nix/dev.nix +++ /dev/null @@ -1,36 +0,0 @@ -{ - perSystem = { - config, - lib, - pkgs, - ... - }: { - pre-commit.settings = { - hooks = { - markdownlint.enable = true; - - alejandra.enable = true; - deadnix.enable = true; - nil.enable = true; - - clang-format = { - enable = true; - types_or = ["c" "c++" "java" "json" "objective-c"]; - }; - }; - - tools.clang-tools = lib.mkForce pkgs.clang-tools_16; - }; - - devShells.default = pkgs.mkShell { - shellHook = '' - ${config.pre-commit.installationScript} - ''; - - inputsFrom = [config.packages.prismlauncher-unwrapped]; - buildInputs = with pkgs; [ccache ninja]; - }; - - formatter = pkgs.alejandra; - }; -} diff --git a/nix/distribution.nix b/nix/distribution.nix deleted file mode 100644 index 01c90f783..000000000 --- a/nix/distribution.nix +++ /dev/null @@ -1,49 +0,0 @@ -{ - inputs, - self, - ... -}: { - perSystem = { - lib, - pkgs, - ... - }: { - packages = let - ourPackages = lib.fix (final: self.overlays.default final pkgs); - in { - inherit - (ourPackages) - prismlauncher-qt5-unwrapped - prismlauncher-qt5 - prismlauncher-unwrapped - prismlauncher - ; - default = ourPackages.prismlauncher; - }; - }; - - flake = { - overlays.default = final: prev: let - version = builtins.substring 0 8 self.lastModifiedDate or "dirty"; - - # common args for prismlauncher evaluations - unwrappedArgs = { - inherit (inputs) libnbtplusplus; - inherit ((final.darwin or prev.darwin).apple_sdk.frameworks) Cocoa; - inherit version; - }; - in { - prismlauncher-qt5-unwrapped = prev.libsForQt5.callPackage ./pkg unwrappedArgs; - - prismlauncher-qt5 = prev.libsForQt5.callPackage ./pkg/wrapper.nix { - prismlauncher-unwrapped = final.prismlauncher-qt5-unwrapped; - }; - - prismlauncher-unwrapped = prev.qt6Packages.callPackage ./pkg unwrappedArgs; - - prismlauncher = prev.qt6Packages.callPackage ./pkg/wrapper.nix { - inherit (final) prismlauncher-unwrapped; - }; - }; - }; -} diff --git a/nix/pkg/default.nix b/nix/pkg/default.nix deleted file mode 100644 index 0078def8c..000000000 --- a/nix/pkg/default.nix +++ /dev/null @@ -1,85 +0,0 @@ -{ - lib, - stdenv, - canonicalize-jars-hook, - cmake, - cmark, - Cocoa, - ninja, - jdk17, - zlib, - qtbase, - quazip, - extra-cmake-modules, - tomlplusplus, - ghc_filesystem, - gamemode, - msaClientID ? null, - gamemodeSupport ? stdenv.isLinux, - version, - libnbtplusplus, -}: -assert lib.assertMsg (stdenv.isLinux || !gamemodeSupport) "gamemodeSupport is only available on Linux"; - stdenv.mkDerivation rec { - pname = "prismlauncher-unwrapped"; - inherit version; - - src = lib.fileset.toSource { - root = ../../.; - fileset = lib.fileset.unions (map (fileName: ../../${fileName}) [ - "buildconfig" - "cmake" - "launcher" - "libraries" - "program_info" - "tests" - "COPYING.md" - "CMakeLists.txt" - ]); - }; - - nativeBuildInputs = [extra-cmake-modules cmake jdk17 ninja canonicalize-jars-hook]; - buildInputs = - [ - qtbase - zlib - quazip - ghc_filesystem - tomlplusplus - cmark - ] - ++ lib.optional gamemodeSupport gamemode - ++ lib.optionals stdenv.isDarwin [Cocoa]; - - hardeningEnable = lib.optionals stdenv.isLinux ["pie"]; - - cmakeFlags = - [ - "-DLauncher_BUILD_PLATFORM=nixpkgs" - ] - ++ lib.optionals (msaClientID != null) ["-DLauncher_MSA_CLIENT_ID=${msaClientID}"] - ++ lib.optionals (lib.versionOlder qtbase.version "6") ["-DLauncher_QT_VERSION_MAJOR=5"] - ++ lib.optionals stdenv.isDarwin ["-DINSTALL_BUNDLE=nodeps" "-DMACOSX_SPARKLE_UPDATE_FEED_URL=''"]; - - postUnpack = '' - rm -rf source/libraries/libnbtplusplus - ln -s ${libnbtplusplus} source/libraries/libnbtplusplus - ''; - - dontWrapQtApps = true; - - meta = with lib; { - mainProgram = "prismlauncher"; - homepage = "https://prismlauncher.org/"; - description = "A free, open source launcher for Minecraft"; - longDescription = '' - Allows you to have multiple, separate instances of Minecraft (each with - their own mods, texture packs, saves, etc) and helps you manage them and - their associated options with a simple interface. - ''; - platforms = with platforms; linux ++ darwin; - changelog = "https://github.com/PrismLauncher/PrismLauncher/releases/tag/${version}"; - license = licenses.gpl3Only; - maintainers = with maintainers; [minion3665 Scrumplex getchoo]; - }; - } diff --git a/nix/pkg/wrapper.nix b/nix/pkg/wrapper.nix deleted file mode 100644 index 1bcff1f9b..000000000 --- a/nix/pkg/wrapper.nix +++ /dev/null @@ -1,96 +0,0 @@ -{ - lib, - stdenv, - symlinkJoin, - prismlauncher-unwrapped, - wrapQtAppsHook, - addOpenGLRunpath, - qtbase, # needed for wrapQtAppsHook - qtsvg, - qtwayland, - xorg, - libpulseaudio, - libGL, - glfw, - openal, - jdk8, - jdk17, - jdk21, - gamemode, - flite, - mesa-demos, - udev, - libusb1, - msaClientID ? null, - gamemodeSupport ? stdenv.isLinux, - textToSpeechSupport ? stdenv.isLinux, - controllerSupport ? stdenv.isLinux, - jdks ? [jdk21 jdk17 jdk8], - additionalLibs ? [], - additionalPrograms ? [], -}: let - prismlauncherFinal = prismlauncher-unwrapped.override { - inherit msaClientID gamemodeSupport; - }; -in - symlinkJoin { - name = "prismlauncher-${prismlauncherFinal.version}"; - - paths = [prismlauncherFinal]; - - nativeBuildInputs = [ - wrapQtAppsHook - ]; - - buildInputs = - [ - qtbase - qtsvg - ] - ++ lib.optional (lib.versionAtLeast qtbase.version "6" && stdenv.isLinux) qtwayland; - - postBuild = '' - wrapQtAppsHook - ''; - - qtWrapperArgs = let - runtimeLibs = - (with xorg; [ - libX11 - libXext - libXcursor - libXrandr - libXxf86vm - ]) - ++ [ - # lwjgl - libpulseaudio - libGL - glfw - openal - stdenv.cc.cc.lib - - # oshi - udev - ] - ++ lib.optional gamemodeSupport gamemode.lib - ++ lib.optional textToSpeechSupport flite - ++ lib.optional controllerSupport libusb1 - ++ additionalLibs; - - runtimePrograms = - [ - xorg.xrandr - mesa-demos # need glxinfo - ] - ++ 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}" - # xorg.xrandr needed for LWJGL [2.9.2, 3) https://github.com/LWJGL/lwjgl/issues/128 - "--prefix PATH : ${lib.makeBinPath runtimePrograms}" - ]; - - inherit (prismlauncherFinal) meta; - } diff --git a/nix/unwrapped.nix b/nix/unwrapped.nix new file mode 100644 index 000000000..f75acf1de --- /dev/null +++ b/nix/unwrapped.nix @@ -0,0 +1,108 @@ +{ + lib, + stdenv, + cmake, + cmark, + darwin, + extra-cmake-modules, + gamemode, + ghc_filesystem, + jdk17, + kdePackages, + ninja, + nix-filter, + self, + stripJavaArchivesHook, + tomlplusplus, + zlib, + msaClientID ? null, + gamemodeSupport ? stdenv.isLinux, + version, + libnbtplusplus, +}: + +assert lib.assertMsg ( + gamemodeSupport -> stdenv.isLinux +) "gamemodeSupport is only available on Linux."; + +stdenv.mkDerivation { + pname = "prismlauncher-unwrapped"; + inherit version; + + src = nix-filter.lib { + root = self; + include = [ + "buildconfig" + "cmake" + "launcher" + "libraries" + "program_info" + "tests" + ../COPYING.md + ../CMakeLists.txt + ]; + }; + + postUnpack = '' + rm -rf source/libraries/libnbtplusplus + ln -s ${libnbtplusplus} source/libraries/libnbtplusplus + ''; + + nativeBuildInputs = [ + cmake + ninja + extra-cmake-modules + jdk17 + stripJavaArchivesHook + ]; + + buildInputs = + [ + cmark + ghc_filesystem + kdePackages.qtbase + kdePackages.qtnetworkauth + kdePackages.quazip + tomlplusplus + zlib + ] + ++ lib.optionals stdenv.isDarwin [ darwin.apple_sdk.frameworks.Cocoa ] + ++ lib.optional gamemodeSupport gamemode; + + hardeningEnable = lib.optionals stdenv.isLinux [ "pie" ]; + + cmakeFlags = + [ (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 [ + # we wrap our binary manually + (lib.cmakeFeature "INSTALL_BUNDLE" "nodeps") + # disable built-in updater + (lib.cmakeFeature "MACOSX_SPARKLE_UPDATE_FEED_URL" "''") + (lib.cmakeFeature "CMAKE_INSTALL_PREFIX" "${placeholder "out"}/Applications/") + ]; + + dontWrapQtApps = true; + + meta = { + description = "Free, open source launcher for Minecraft"; + longDescription = '' + Allows you to have multiple, separate instances of Minecraft (each with + their own mods, texture packs, saves, etc) and helps you manage them and + their associated options with a simple interface. + ''; + homepage = "https://prismlauncher.org/"; + license = lib.licenses.gpl3Only; + maintainers = with lib.maintainers; [ + Scrumplex + getchoo + ]; + mainProgram = "prismlauncher"; + platforms = lib.platforms.linux ++ lib.platforms.darwin; + }; +} diff --git a/nix/wrapper.nix b/nix/wrapper.nix new file mode 100644 index 000000000..5632d483b --- /dev/null +++ b/nix/wrapper.nix @@ -0,0 +1,147 @@ +{ + lib, + stdenv, + symlinkJoin, + prismlauncher-unwrapped, + addOpenGLRunpath, + flite, + gamemode, + glfw, + glfw-wayland-minecraft, + glxinfo, + jdk8, + jdk17, + jdk21, + kdePackages, + libGL, + libpulseaudio, + libusb1, + makeWrapper, + openal, + pciutils, + udev, + vulkan-loader, + xorg, + additionalLibs ? [ ], + additionalPrograms ? [ ], + controllerSupport ? stdenv.isLinux, + gamemodeSupport ? stdenv.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, +}: + +assert lib.assertMsg ( + controllerSupport -> stdenv.isLinux +) "controllerSupport only has an effect on Linux."; + +assert lib.assertMsg ( + textToSpeechSupport -> stdenv.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; + + buildInputs = + [ + kdePackages.qtbase + kdePackages.qtsvg + ] + ++ lib.optional ( + lib.versionAtLeast kdePackages.qtbase.version "6" && stdenv.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 + ''; + + qtWrapperArgs = + let + runtimeLibs = + [ + # lwjgl + glfw + libpulseaudio + libGL + openal + stdenv.cc.cc.lib + + vulkan-loader # VulkanMod's lwjgl + + udev # oshi + + xorg.libX11 + xorg.libXext + xorg.libXcursor + xorg.libXrandr + xorg.libXxf86vm + ] + ++ lib.optional textToSpeechSupport flite + ++ lib.optional gamemodeSupport gamemode.lib + ++ lib.optional controllerSupport libusb1 + ++ additionalLibs; + + runtimePrograms = [ + glxinfo + pciutils # need lspci + xorg.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}" + "--prefix PATH : ${lib.makeBinPath runtimePrograms}" + ]; + + meta = { + inherit (prismlauncher'.meta) + description + longDescription + homepage + changelog + license + maintainers + mainProgram + platforms + ; + }; +} diff --git a/program_info/org.prismlauncher.PrismLauncher.desktop.in b/program_info/org.prismlauncher.PrismLauncher.desktop.in index 76f4b19c0..c0e4e9458 100644 --- a/program_info/org.prismlauncher.PrismLauncher.desktop.in +++ b/program_info/org.prismlauncher.PrismLauncher.desktop.in @@ -1,13 +1,13 @@ [Desktop Entry] Version=1.0 -Name=Prism Launcher -Comment=A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once. +Name=@Launcher_DisplayName@ +Comment=Discover, manage, and play Minecraft instances Type=Application Terminal=false Exec=@Launcher_APP_BINARY_NAME@ %U StartupNotify=true -Icon=org.prismlauncher.PrismLauncher +Icon=org.@Launcher_APP_BINARY_NAME@.@Launcher_CommonName@ Categories=Game;ActionGame;AdventureGame;Simulation; Keywords=game;minecraft;mc; -StartupWMClass=PrismLauncher -MimeType=application/zip;application/x-modrinth-modpack+zip;x-scheme-handler/curseforge; +StartupWMClass=@Launcher_CommonName@ +MimeType=application/zip;application/x-modrinth-modpack+zip;x-scheme-handler/curseforge;x-scheme-handler/@Launcher_APP_BINARY_NAME@; diff --git a/program_info/win_install.nsi.in b/program_info/win_install.nsi.in index eda85821b..cc56b9bd5 100644 --- a/program_info/win_install.nsi.in +++ b/program_info/win_install.nsi.in @@ -368,6 +368,10 @@ Section "@Launcher_DisplayName@" WriteRegStr HKCU Software\Classes\curseforge "URL Protocol" "" WriteRegStr HKCU Software\Classes\curseforge\shell\open\command "" '"$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "%1"' +; Write the URL Handler into registry for prismlauncher + WriteRegStr HKCU Software\Classes\prismlauncher "URL Protocol" "" + WriteRegStr HKCU Software\Classes\prismlauncher\shell\open\command "" '"$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "%1"' + ; Write the uninstall keys for Windows ${GetParameters} $R0 ${GetOptions} $R0 "/NoUninstaller" $R1 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 59e0e3144..2dedb47cc 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -57,5 +57,8 @@ ecm_add_test(Index_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}: ecm_add_test(Version_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME Version) +ecm_add_test(MetaComponentParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME MetaComponentParse) + ecm_add_test(CatPack_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME CatPack) diff --git a/tests/Library_test.cpp b/tests/Library_test.cpp index 8b8d4c55c..9826abbdf 100644 --- a/tests/Library_test.cpp +++ b/tests/Library_test.cpp @@ -95,8 +95,8 @@ class LibraryTest : public QObject { auto downloads = test.getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(downloads.size(), 1); QCOMPARE(failedFiles, {}); - NetAction::Ptr dl = downloads[0]; - QCOMPARE(dl->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion.jar")); + Net::NetRequest::Ptr dl = downloads[0]; + QCOMPARE(dl->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion.jar")); } void test_legacy_url_local_broken() { @@ -147,7 +147,7 @@ class LibraryTest : public QObject { QCOMPARE(dls.size(), 1); QCOMPARE(failedFiles, {}); auto dl = dls[0]; - QCOMPARE(dl->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-linux.jar")); + QCOMPARE(dl->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-linux.jar")); } } void test_legacy_native_arch() @@ -170,8 +170,8 @@ class LibraryTest : public QObject { auto dls = test.getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(dls.size(), 2); QCOMPARE(failedFiles, {}); - QCOMPARE(dls[0]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-linux-32.jar")); - QCOMPARE(dls[1]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-linux-64.jar")); + QCOMPARE(dls[0]->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-linux-32.jar")); + QCOMPARE(dls[1]->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-linux-64.jar")); } r.system = "windows"; { @@ -185,8 +185,8 @@ class LibraryTest : public QObject { auto dls = test.getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(dls.size(), 2); QCOMPARE(failedFiles, {}); - QCOMPARE(dls[0]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-windows-32.jar")); - QCOMPARE(dls[1]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-windows-64.jar")); + QCOMPARE(dls[0]->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-windows-32.jar")); + QCOMPARE(dls[1]->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-windows-64.jar")); } r.system = "osx"; { @@ -200,8 +200,8 @@ class LibraryTest : public QObject { auto dls = test.getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(dls.size(), 2); QCOMPARE(failedFiles, {}); - QCOMPARE(dls[0]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-osx-32.jar")); - QCOMPARE(dls[1]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-osx-64.jar")); + QCOMPARE(dls[0]->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-osx-32.jar")); + QCOMPARE(dls[1]->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-osx-64.jar")); } } void test_legacy_native_arch_local_override() @@ -244,7 +244,7 @@ class LibraryTest : public QObject { auto dls = test->getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(dls.size(), 1); QCOMPARE(failedFiles, {}); - QCOMPARE(dls[0]->m_url, QUrl("https://libraries.minecraft.net/com/paulscode/codecwav/20101023/codecwav-20101023.jar")); + QCOMPARE(dls[0]->url(), QUrl("https://libraries.minecraft.net/com/paulscode/codecwav/20101023/codecwav-20101023.jar")); } r.system = "osx"; test->setHint("local"); @@ -300,7 +300,7 @@ class LibraryTest : public QObject { auto dls = test->getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(dls.size(), 1); QCOMPARE(failedFiles, {}); - QCOMPARE(dls[0]->m_url, QUrl("https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/" + QCOMPARE(dls[0]->url(), QUrl("https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/" "lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar")); } void test_onenine_native_arch() @@ -317,9 +317,9 @@ class LibraryTest : public QObject { auto dls = test->getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(dls.size(), 2); QCOMPARE(failedFiles, {}); - QCOMPARE(dls[0]->m_url, + QCOMPARE(dls[0]->url(), QUrl("https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-32.jar")); - QCOMPARE(dls[1]->m_url, + QCOMPARE(dls[1]->url(), QUrl("https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-64.jar")); } diff --git a/tests/MetaComponentParse_test.cpp b/tests/MetaComponentParse_test.cpp new file mode 100644 index 000000000..9979a9fa6 --- /dev/null +++ b/tests/MetaComponentParse_test.cpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include + +#include + +#include + +class MetaComponentParseTest : public QObject { + Q_OBJECT + + void doTest(QString name) + { + QString source = QFINDTESTDATA("testdata/MetaComponentParse"); + + QString comp_rp = FS::PathCombine(source, name); + + QFile file; + file.setFileName(comp_rp); + QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text)); + QString data = file.readAll(); + file.close(); + + QJsonDocument doc = QJsonDocument::fromJson(data.toUtf8()); + QJsonObject obj = doc.object(); + + QJsonValue description_json = obj.value("description"); + QJsonValue expected_json = obj.value("expected_output"); + + QVERIFY(!description_json.isUndefined()); + QVERIFY(expected_json.isString()); + + QString expected = expected_json.toString(); + + QString processed = ResourcePackUtils::processComponent(description_json); + + QCOMPARE(processed, expected); + } + + private slots: + void test_parseComponentBasic() { doTest("component_basic.json"); } + void test_parseComponentWithFormat() { doTest("component_with_format.json"); } + void test_parseComponentWithExtra() { doTest("component_with_extra.json"); } + void test_parseComponentWithLink() { doTest("component_with_link.json"); } + void test_parseComponentWithMixed() { doTest("component_with_mixed.json"); } +}; + +QTEST_GUILESS_MAIN(MetaComponentParseTest) + +#include "MetaComponentParse_test.moc" diff --git a/tests/ResourceFolderModel_test.cpp b/tests/ResourceFolderModel_test.cpp index 57c2cbdff..350ab615e 100644 --- a/tests/ResourceFolderModel_test.cpp +++ b/tests/ResourceFolderModel_test.cpp @@ -203,7 +203,10 @@ class ResourceFolderModelTest : public QObject { QCOMPARE(model.size(), 0); - { EXEC_UPDATE_TASK(model.installResource(folder_resource), QVERIFY) } { + { + EXEC_UPDATE_TASK(model.installResource(folder_resource), QVERIFY) + } + { EXEC_UPDATE_TASK(model.installResource(file_mod), QVERIFY) } diff --git a/tests/testdata/MetaComponentParse/component_basic.json b/tests/testdata/MetaComponentParse/component_basic.json new file mode 100644 index 000000000..908cb353c --- /dev/null +++ b/tests/testdata/MetaComponentParse/component_basic.json @@ -0,0 +1,8 @@ +{ + "description": [ + { + "text": "Hello, Component!" + } + ], + "expected_output": "Hello, Component!" +} diff --git a/tests/testdata/MetaComponentParse/component_with_extra.json b/tests/testdata/MetaComponentParse/component_with_extra.json new file mode 100644 index 000000000..887becdbe --- /dev/null +++ b/tests/testdata/MetaComponentParse/component_with_extra.json @@ -0,0 +1,21 @@ +{ + "description": [ + { + "text": "Hello, ", + "color": "red", + "bold": true, + "italic": true, + "extra": [ + { + "extra": [ + "Component!" + ], + "bold": false, + "italic": false + } + ] + } + ], + "expected_output": + "Hello, Component!" +} \ No newline at end of file diff --git a/tests/testdata/MetaComponentParse/component_with_format.json b/tests/testdata/MetaComponentParse/component_with_format.json new file mode 100644 index 000000000..1078886a6 --- /dev/null +++ b/tests/testdata/MetaComponentParse/component_with_format.json @@ -0,0 +1,13 @@ +{ + "description": [ + { + "text": "Hello, Component!", + "color": "blue", + "bold": true, + "italic": true, + "underlined": true, + "strikethrough": true + } + ], + "expected_output": "Hello, Component!" +} \ No newline at end of file diff --git a/tests/testdata/MetaComponentParse/component_with_link.json b/tests/testdata/MetaComponentParse/component_with_link.json new file mode 100644 index 000000000..188c004cd --- /dev/null +++ b/tests/testdata/MetaComponentParse/component_with_link.json @@ -0,0 +1,12 @@ +{ + "description": [ + { + "text": "Hello, Component!", + "clickEvent": { + "action": "open_url", + "value": "https://google.com" + } + } + ], + "expected_output": "Hello, Component!" +} diff --git a/tests/testdata/MetaComponentParse/component_with_mixed.json b/tests/testdata/MetaComponentParse/component_with_mixed.json new file mode 100644 index 000000000..661fc1a3e --- /dev/null +++ b/tests/testdata/MetaComponentParse/component_with_mixed.json @@ -0,0 +1,45 @@ +{ + "description": [ + { + "text": "The quick ", + "color": "blue", + "italic": true + }, + { + "text": "brown fox ", + "color": "#873600", + "bold": true, + "underlined": true, + "extra": [ + { + "text": "jumped over ", + "color": "blue", + "bold": false, + "underlined": false, + "italic": true, + "strikethrough": true + } + ] + }, + { + "text": "the lazy dog's back. ", + "color": "green", + "bold": true, + "italic": true, + "underlined": true, + "strikethrough": true, + "extra": [ + { + "text": "1234567890 ", + "color": "black", + "strikethrough": false, + "extra": [ + "How vexingly quick daft zebras jump!" + ] + } + ] + } + ], + "expected_output": + "The quick brown fox jumped over the lazy dog's back. 1234567890 How vexingly quick daft zebras jump!" +}