diff --git a/.clang-tidy b/.clang-tidy index 436dcf244..ef5166da4 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,5 +1,23 @@ Checks: - modernize-use-using - readability-avoid-const-params-in-decls + - misc-unused-parameters, + - readability-identifier-naming -SystemHeaders: false +# ^ Without unused-parameters the readability-identifier-naming check doesn't cause any warnings. + +CheckOptions: + - { key: readability-identifier-naming.ClassCase, value: PascalCase } + - { key: readability-identifier-naming.EnumCase, value: PascalCase } + - { key: readability-identifier-naming.FunctionCase, value: camelCase } + - { key: readability-identifier-naming.GlobalVariableCase, value: camelCase } + - { key: readability-identifier-naming.GlobalFunctionCase, value: camelCase } + - { key: readability-identifier-naming.GlobalConstantCase, value: SCREAMING_SNAKE_CASE } + - { key: readability-identifier-naming.MacroDefinitionCase, value: SCREAMING_SNAKE_CASE } + - { key: readability-identifier-naming.ClassMemberCase, value: camelCase } + - { key: readability-identifier-naming.PrivateMemberPrefix, value: m_ } + - { key: readability-identifier-naming.ProtectedMemberPrefix, value: m_ } + - { key: readability-identifier-naming.PrivateStaticMemberPrefix, value: s_ } + - { key: readability-identifier-naming.ProtectedStaticMemberPrefix, value: s_ } + - { key: readability-identifier-naming.PublicStaticConstantCase, value: SCREAMING_SNAKE_CASE } + - { key: readability-identifier-naming.EnumConstantCase, value: SCREAMING_SNAKE_CASE } \ No newline at end of file diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 2163db45b..528b128b1 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -2,3 +2,6 @@ # tabs -> spaces bbb3b3e6f6e3c0f95873f22e6d0a4aaf350f49d9 + +# (nix) alejandra -> nixfmt +4c81d8c53d09196426568c4a31a4e752ed05397a diff --git a/.github/scripts/prepare_JREs.sh b/.github/scripts/prepare_JREs.sh deleted file mode 100755 index ee713f81f..000000000 --- a/.github/scripts/prepare_JREs.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash - -URL_JDK8="https://api.adoptium.net/v3/binary/version/jdk8u312-b07/linux/x64/jre/hotspot/normal/eclipse" -URL_JDK17="https://api.adoptium.net/v3/binary/latest/17/ga/linux/x64/jre/hotspot/normal/eclipse" - -mkdir -p JREs -pushd JREs - -wget --content-disposition "$URL_JDK8" -wget --content-disposition "$URL_JDK17" - -for file in *; -do - mkdir temp - - re='(OpenJDK([[:digit:]]+)U-jre_x64_linux_hotspot_([[:digit:]]+)(.*).tar.gz)' - if [[ $file =~ $re ]]; - then - version_major=${BASH_REMATCH[2]} - version_trailing=${BASH_REMATCH[4]} - - if [ $version_major = 17 ]; - then - hyphen='-' - else - hyphen='' - fi - - version_edit=$(echo $version_trailing | sed -e 's/_/+/g' | sed -e 's/b/-b/g') - dir_name=jdk$hyphen$version_major$version_edit-jre - mkdir jre$version_major - tar -xzf $file -C temp - pushd temp/$dir_name - cp -r . ../../jre$version_major - popd - fi - - rm -rf temp -done - -popd diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index e5443439d..4146cddf4 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -16,6 +16,7 @@ jobs: permissions: contents: write # for korthout/backport-action to create branch pull-requests: write # for korthout/backport-action to create PR to backport + actions: write # for korthout/backport-action to create PR with workflow changes name: Backport Pull Request if: github.repository_owner == 'PrismLauncher' && github.event.pull_request.merged == true && (github.event_name != 'labeled' || startsWith('backport', github.event.label.name)) runs-on: ubuntu-latest @@ -24,7 +25,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} - name: Create backport PRs - uses: korthout/backport-action@v2.1.1 + 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 4fb2d6794..ae4deb7a5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,8 +21,23 @@ on: WINDOWS_CODESIGN_PASSWORD: description: Password for signing Windows builds required: false - CACHIX_AUTH_TOKEN: - description: Private token for authenticating against Cachix cache + APPLE_CODESIGN_CERT: + description: Certificate for signing macOS builds + required: false + APPLE_CODESIGN_PASSWORD: + description: Password for signing macOS builds + required: false + APPLE_CODESIGN_ID: + description: Certificate ID for signing macOS builds + required: false + APPLE_NOTARIZE_APPLE_ID: + description: Apple ID used for notarizing macOS builds + required: false + APPLE_NOTARIZE_TEAM_ID: + description: Team ID used for notarizing macOS builds + required: false + APPLE_NOTARIZE_PASSWORD: + description: Password used for notarizing macOS builds required: false GPG_PRIVATE_KEY: description: Private key for AppImage signing @@ -39,14 +54,17 @@ jobs: include: - os: ubuntu-20.04 qt_ver: 5 + qt_host: linux + qt_arch: "" + qt_version: "5.15.2" + qt_modules: "qtnetworkauth" - - os: ubuntu-20.04 + - os: ubuntu-22.04 qt_ver: 6 qt_host: linux qt_arch: "" - qt_version: "6.2.4" - qt_modules: "qt5compat qtimageformats" - qt_tools: "" + qt_version: "6.5.3" + qt_modules: "qt5compat qtimageformats qtnetworkauth" - os: windows-2022 name: "Windows-MinGW-w64" @@ -59,11 +77,12 @@ jobs: architecture: "x64" vcvars_arch: "amd64" qt_ver: 6 - qt_host: windows - qt_arch: '' - qt_version: '6.6.0' - qt_modules: 'qt5compat qtimageformats' - qt_tools: '' + qt_host: "windows" + qt_arch: "win64_msvc2022_64" + qt_version: "6.8.1" + qt_modules: "qt5compat qtimageformats qtnetworkauth" + nscurl_tag: "v24.9.26.122" + nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0" - os: windows-2022 name: "Windows-MSVC-arm64" @@ -71,30 +90,29 @@ jobs: architecture: "arm64" vcvars_arch: "amd64_arm64" qt_ver: 6 - qt_host: windows - qt_arch: 'win64_msvc2019_arm64' - qt_version: '6.6.0' - qt_modules: 'qt5compat qtimageformats' - qt_tools: '' + qt_host: "windows" + qt_arch: "win64_msvc2022_arm64_cross_compiled" + qt_version: "6.8.1" + qt_modules: "qt5compat qtimageformats qtnetworkauth" + nscurl_tag: "v24.9.26.122" + nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0" - - os: macos-12 + - os: macos-14 name: macOS macosx_deployment_target: 11.0 qt_ver: 6 qt_host: mac - qt_arch: '' - qt_version: '6.6.0' - qt_modules: 'qt5compat qtimageformats' - qt_tools: '' + qt_arch: "" + qt_version: "6.8.1" + 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 }} @@ -136,6 +154,7 @@ jobs: quazip-qt6:p ccache:p qt6-5compat:p + qt6-networkauth:p cmark:p - name: Force newer ccache @@ -145,13 +164,13 @@ jobs: - name: Setup ccache if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug' - uses: hendrikmuhs/ccache-action@v1.2.10 + uses: hendrikmuhs/ccache-action@v1.2.17 with: key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }} - name: Retrieve ccache cache (Windows MinGW-w64) if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' - uses: actions/cache@v3.3.2 + uses: actions/cache@v4.2.1 with: path: '${{ github.workspace }}\.ccache' key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }} @@ -184,7 +203,7 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get -y update - sudo apt-get -y install ninja-build extra-cmake-modules scdoc appstream + sudo apt-get -y install ninja-build extra-cmake-modules scdoc appstream libxcb-cursor-dev - name: Install Dependencies (macOS) if: runner.os == 'macOS' @@ -192,36 +211,29 @@ 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 + uses: jurplel/install-qt-action@v4 with: aqtversion: "==3.1.*" py7zrversion: ">=0.20.2" version: ${{ matrix.qt_version }} host: "windows" target: "desktop" - arch: "" + arch: ${{ matrix.qt_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 == '') - uses: jurplel/install-qt-action@v3 + - name: Install Qt (macOS, Linux & Windows MSVC) + if: matrix.msystem == '' + uses: jurplel/install-qt-action@v4 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 }} @@ -244,14 +256,19 @@ jobs: wget "https://github.com/AppImageCommunity/AppImageUpdate/releases/download/continuous/AppImageUpdate-x86_64.AppImage" - ${{ github.workspace }}/.github/scripts/prepare_JREs.sh - sudo apt install libopengl0 + sudo apt install libopengl0 libfuse2 - name: Add QT_HOST_PATH var (Windows MSVC arm64) if: runner.os == 'Windows' && matrix.architecture == 'arm64' run: | - echo "QT_HOST_PATH=${{ github.workspace }}\HostQt\Qt\${{ matrix.qt_version }}\msvc2019_64" >> $env:GITHUB_ENV + echo "QT_HOST_PATH=${{ github.workspace }}\HostQt\Qt\${{ matrix.qt_version }}\msvc2022_64" >> $env:GITHUB_ENV + - name: Setup java (macOS) + if: runner.os == 'macOS' + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "17" ## # CONFIGURE ## @@ -259,23 +276,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 }}") { @@ -290,7 +307,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 @@ -336,6 +353,20 @@ jobs: # PACKAGE BUILDS ## + - name: Fetch codesign certificate (macOS) + if: runner.os == 'macOS' + run: | + echo '${{ secrets.APPLE_CODESIGN_CERT }}' | base64 --decode > codesign.p12 + if [ -n '${{ secrets.APPLE_CODESIGN_ID }}' ]; then + security create-keychain -p '${{ secrets.APPLE_CODESIGN_PASSWORD }}' build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p '${{ secrets.APPLE_CODESIGN_PASSWORD }}' build.keychain + security import codesign.p12 -k build.keychain -P '${{ secrets.APPLE_CODESIGN_PASSWORD }}' -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k '${{ secrets.APPLE_CODESIGN_PASSWORD }}' build.keychain + else + echo ":warning: Using ad-hoc code signing for macOS, as certificate was not present." >> $GITHUB_STEP_SUMMARY + fi + - name: Package (macOS) if: runner.os == 'macOS' run: | @@ -343,17 +374,43 @@ jobs: cd ${{ env.INSTALL_DIR }} chmod +x "PrismLauncher.app/Contents/MacOS/prismlauncher" - sudo codesign --sign - --deep --force --entitlements "../program_info/App.entitlements" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher" + + if [ -n '${{ secrets.APPLE_CODESIGN_ID }}' ]; then + APPLE_CODESIGN_ID='${{ secrets.APPLE_CODESIGN_ID }}' + ENTITLEMENTS_FILE='../program_info/App.entitlements' + else + APPLE_CODESIGN_ID='-' + ENTITLEMENTS_FILE='../program_info/AdhocSignedApp.entitlements' + fi + + sudo codesign --sign "$APPLE_CODESIGN_ID" --deep --force --entitlements "$ENTITLEMENTS_FILE" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher" mv "PrismLauncher.app" "Prism Launcher.app" - tar -czf ../PrismLauncher.tar.gz * + + - name: Notarize (macOS) + if: runner.os == 'macOS' + run: | + cd ${{ env.INSTALL_DIR }} + + if [ -n '${{ secrets.APPLE_NOTARIZE_PASSWORD }}' ]; then + ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip + xcrun notarytool submit ../PrismLauncher.zip \ + --wait --progress \ + --apple-id '${{ secrets.APPLE_NOTARIZE_APPLE_ID }}' \ + --team-id '${{ secrets.APPLE_NOTARIZE_TEAM_ID }}' \ + --password '${{ secrets.APPLE_NOTARIZE_PASSWORD }}' + + xcrun stapler staple "Prism Launcher.app" + else + echo ":warning: Skipping notarization as credentials are not present." >> $GITHUB_STEP_SUMMARY + fi + ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip - name: Make Sparkle signature (macOS) if: matrix.name == 'macOS' run: | if [ '${{ secrets.SPARKLE_ED25519_KEY }}' != '' ]; then - brew install openssl@3 echo '${{ secrets.SPARKLE_ED25519_KEY }}' > ed25519-priv.pem - signature=$(/usr/local/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.tar.gz -inkey ed25519-priv.pem | openssl base64 | tr -d \\n) + signature=$(/opt/homebrew/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.zip -inkey ed25519-priv.pem | openssl base64 | tr -d \\n) rm ed25519-priv.pem cat >> $GITHUB_STEP_SUMMARY << EOF ### Artifact Information :information_source: @@ -379,12 +436,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 @@ -425,6 +476,16 @@ jobs: - name: Package (Windows, installer) if: runner.os == 'Windows' run: | + if ('${{ matrix.nscurl_tag }}') { + New-Item -Name NSISPlugins -ItemType Directory + Invoke-Webrequest https://github.com/negrutiu/nsis-nscurl/releases/download/${{ matrix.nscurl_tag }}/NScurl.zip -OutFile NSISPlugins\NScurl.zip + $nscurl_hash = Get-FileHash NSISPlugins\NScurl.zip -Algorithm Sha256 | Select-Object -ExpandProperty Hash + if ( $nscurl_hash -ne "${{ matrix.nscurl_sha256 }}") { + echo "::error:: NSCurl.zip sha256 mismatch" + exit 1 + } + Expand-Archive -Path NSISPlugins\NScurl.zip -DestinationPath NSISPlugins\NScurl + } cd ${{ env.INSTALL_DIR }} makensis -NOCD "${{ github.workspace }}/${{ env.BUILD_DIR }}/program_info/win_install.nsi" @@ -437,26 +498,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 - 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 @@ -472,24 +513,16 @@ jobs: chmod +x linuxdeploy-*.AppImage - mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-{8,17}-openjdk + mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines - cp -r ${{ github.workspace }}/JREs/jre8/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk - - cp -r ${{ github.workspace }}/JREs/jre17/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-17-openjdk - cp -r ${{ runner.workspace }}/Qt/${{ matrix.qt_version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines - cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ - cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ + cp /usr/lib/x86_64-linux-gnu/libcrypto.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ + cp /usr/lib/x86_64-linux-gnu/libssl.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ cp /usr/lib/x86_64-linux-gnu/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib" - LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64/server" - LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64" - LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-17-openjdk/lib/server" - LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-17-openjdk/lib" export LD_LIBRARY_PATH chmod +x AppImageUpdate-x86_64.AppImage @@ -511,76 +544,81 @@ 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.* ${{ env.INSTALL_PORTABLE_DIR }}/lib + cp /usr/lib/x86_64-linux-gnu/libssl.so.* ${{ env.INSTALL_PORTABLE_DIR }}/lib + cp /usr/lib/x86_64-linux-gnu/libffi.so.*.* ${{ 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 ## - name: Upload binary tarball (macOS) if: runner.os == 'macOS' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }} - path: PrismLauncher.tar.gz + path: PrismLauncher.zip - name: Upload binary zip (Windows) if: runner.os == 'Windows' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }} path: ${{ env.INSTALL_DIR }}/** - name: Upload binary zip (Windows, portable) if: runner.os == 'Windows' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: PrismLauncher-${{ matrix.name }}-Portable-${{ env.VERSION }}-${{ inputs.build_type }} path: ${{ env.INSTALL_PORTABLE_DIR }}/** - name: Upload installer (Windows) if: runner.os == 'Windows' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: 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@v3 - 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@v3 + uses: actions/upload-artifact@v4 with: 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@v3 - 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@v3 + uses: actions/upload-artifact@v4 with: name: PrismLauncher-${{ runner.os }}-Qt6-Portable-${{ env.VERSION }}-${{ inputs.build_type }} path: PrismLauncher-portable.tar.gz - name: Upload AppImage (Linux) if: runner.os == 'Linux' && matrix.qt_ver != 5 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage path: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage - name: Upload AppImage Zsync (Linux) if: runner.os == 'Linux' && matrix.qt_ver != 5 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage.zsync path: PrismLauncher-Linux-x86_64.AppImage.zsync @@ -590,21 +628,3 @@ jobs: shell: msys2 {0} run: | ccache -s - - flatpak: - runs-on: ubuntu-latest - container: - image: bilelmoussaoui/flatpak-github-actions:kde-5.15-23.08 - options: --privileged - steps: - - name: Checkout - uses: actions/checkout@v4 - if: inputs.build_type == 'Debug' - with: - submodules: "true" - - name: Build Flatpak (Linux) - if: inputs.build_type == 'Debug' - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 - with: - bundle: "Prism Launcher.flatpak" - manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a77b4ae1e..5255f865b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,7 +13,7 @@ jobs: submodules: 'true' - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: config-file: ./.github/codeql/codeql-config.yml queries: security-and-quality @@ -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: | @@ -32,4 +32,4 @@ jobs: cmake --build build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml new file mode 100644 index 000000000..41cc2a51d --- /dev/null +++ b/.github/workflows/flatpak.yml @@ -0,0 +1,62 @@ +name: Flatpak + +on: + push: + paths-ignore: + - "**.md" + - "**/LICENSE" + - ".github/ISSUE_TEMPLATE/**" + - ".markdownlint**" + - "nix/**" + # We don't do anything with these artifacts on releases. They go to Flathub + tags-ignore: + - "*" + pull_request: + paths-ignore: + - "**.md" + - "**/LICENSE" + - ".github/ISSUE_TEMPLATE/**" + - ".markdownlint**" + - "nix/**" + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: Build (${{ matrix.arch }}) + + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-22.04 + arch: x86_64 + + - os: ubuntu-22.04-arm + arch: aarch64 + + runs-on: ${{ matrix.os }} + + container: + image: ghcr.io/flathub-infra/flatpak-github-actions:kde-6.8 + options: --privileged + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: true + + - name: Set short version + shell: bash + run: | + echo "VERSION=${GITHUB_SHA::7}" >> "$GITHUB_ENV" + + - name: Build Flatpak + uses: flatpak/flatpak-github-actions/flatpak-builder@v6 + with: + bundle: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-Flatpak.flatpak + manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml + arch: ${{ matrix.arch }} diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml new file mode 100644 index 000000000..b44011711 --- /dev/null +++ b/.github/workflows/nix.yml @@ -0,0 +1,90 @@ +name: Nix + +on: + push: + paths-ignore: + - "**.md" + - "**/LICENSE" + - ".github/ISSUE_TEMPLATE/**" + - ".markdownlint**" + - "flatpak/**" + pull_request_target: + paths-ignore: + - "**.md" + - "**/LICENSE" + - ".github/ISSUE_TEMPLATE/**" + - ".markdownlint**" + - "flatpak/**" + workflow_dispatch: + +permissions: + contents: read + +env: + DEBUG: ${{ github.ref_type != 'tag' }} + USE_DETERMINATE: ${{ github.event_name == 'pull_request' }} + +jobs: + build: + name: Build (${{ matrix.system }}) + + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-22.04 + system: x86_64-linux + + - os: ubuntu-22.04-arm + system: aarch64-linux + + - os: macos-13 + system: x86_64-darwin + + - os: macos-14 + system: aarch64-darwin + + runs-on: ${{ matrix.os }} + + permissions: + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v16 + with: + determinate: ${{ env.USE_DETERMINATE }} + + # For PRs + - name: Setup Nix Magic Cache + if: ${{ env.USE_DETERMINATE }} + uses: DeterminateSystems/flakehub-cache-action@v1 + + # For in-tree builds + - name: Setup Cachix + if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} + uses: cachix/cachix-action@v15 + with: + name: prismlauncher + authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} + + - name: Run Flake checks + run: | + nix flake check --print-build-logs --show-trace + + - name: Build debug package + if: ${{ env.DEBUG }} + run: | + nix build \ + --no-link --print-build-logs --print-out-paths \ + .#prismlauncher-debug >> "$GITHUB_STEP_SUMMARY" + + - name: Build release package + if: ${{ !env.DEBUG }} + run: | + nix build \ + --no-link --print-build-logs --print-out-paths \ + .#prismlauncher >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..d49eb4b8d --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,45 @@ +name: Publish + +on: + release: + types: [ released ] + +permissions: + contents: read + +jobs: + flakehub: + name: FlakeHub + + runs-on: ubuntu-latest + + permissions: + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + + - name: Install Nix + uses: cachix/install-nix-action@v30 + + - name: Publish on FlakeHub + uses: determinatesystems/flakehub-push@v5 + with: + visibility: "public" + + winget: + name: Winget + + runs-on: windows-latest + + steps: + - name: Publish on Winget + uses: vedantmgoyal2009/winget-releaser@v2 + with: + identifier: PrismLauncher.PrismLauncher + version: ${{ github.event.release.tag_name }} + installers-regex: 'PrismLauncher-Windows-MSVC(:?-arm64|-Legacy)?-Setup-.+\.exe$' + token: ${{ secrets.WINGET_TOKEN }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..106a7844f --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,29 @@ +name: Stale + +on: + schedule: + # run weekly on sunday + - cron: "0 0 * * 0" + workflow_dispatch: + +jobs: + label: + name: Label issues and PRs + + runs-on: ubuntu-latest + + permissions: + issues: write + pull-requests: write + + steps: + - uses: actions/stale@v9 + with: + days-before-stale: 60 + days-before-close: -1 # Don't close anything + exempt-issue-labels: rfc,nostale,help wanted + exempt-all-milestones: true + exempt-all-assignees: true + operations-per-run: 1000 + stale-issue-label: inactive + stale-pr-label: inactive diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml index 70fda60ed..9efafc8cc 100644 --- a/.github/workflows/trigger_builds.yml +++ b/.github/workflows/trigger_builds.yml @@ -32,6 +32,11 @@ jobs: SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }} WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }} WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} - CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} + APPLE_CODESIGN_CERT: ${{ secrets.APPLE_CODESIGN_CERT }} + APPLE_CODESIGN_PASSWORD: ${{ secrets.APPLE_CODESIGN_PASSWORD }} + APPLE_CODESIGN_ID: ${{ secrets.APPLE_CODESIGN_ID }} + APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }} + APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }} + APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }} diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index 28578165f..134281b2c 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -16,7 +16,12 @@ jobs: SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }} WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }} WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} - CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} + APPLE_CODESIGN_CERT: ${{ secrets.APPLE_CODESIGN_CERT }} + APPLE_CODESIGN_PASSWORD: ${{ secrets.APPLE_CODESIGN_PASSWORD }} + APPLE_CODESIGN_ID: ${{ secrets.APPLE_CODESIGN_ID }} + APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }} + APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }} + APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }} @@ -32,7 +37,7 @@ jobs: submodules: "true" path: "PrismLauncher-source" - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 - name: Grab and store version run: | tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$") @@ -41,13 +46,11 @@ 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.tar.gz PrismLauncher-macOS-Legacy-${{ env.VERSION }}.tar.gz - mv PrismLauncher-macOS*/PrismLauncher.tar.gz PrismLauncher-macOS-${{ env.VERSION }}.tar.gz + mv PrismLauncher-macOS-Legacy*/PrismLauncher.zip PrismLauncher-macOS-Legacy-${{ env.VERSION }}.zip + mv PrismLauncher-macOS*/PrismLauncher.zip PrismLauncher-macOS-${{ env.VERSION }}.zip tar --exclude='.git' -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }} @@ -79,7 +82,7 @@ jobs: - name: Create release id: create_release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: token: ${{ secrets.GITHUB_TOKEN }} tag_name: ${{ github.ref }} @@ -87,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 @@ -102,6 +103,6 @@ jobs: PrismLauncher-Windows-MSVC-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-Setup-${{ env.VERSION }}.exe - PrismLauncher-macOS-${{ env.VERSION }}.tar.gz - PrismLauncher-macOS-Legacy-${{ env.VERSION }}.tar.gz + PrismLauncher-macOS-${{ env.VERSION }}.zip + PrismLauncher-macOS-Legacy-${{ env.VERSION }}.zip PrismLauncher-${{ env.VERSION }}.tar.gz diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index 538972968..5e978f356 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@7ac1ec25491415c381d9b62f0657c7a028df52a7 # v24 + - uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30 - - uses: DeterminateSystems/update-flake-lock@v20 + - uses: DeterminateSystems/update-flake-lock@v24 with: commit-msg: "chore(nix): update lockfile" pr-title: "chore(nix): update lockfile" diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml deleted file mode 100644 index eacf23099..000000000 --- a/.github/workflows/winget.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Publish to WinGet -on: - release: - types: [released] - -jobs: - publish: - runs-on: windows-latest - steps: - - uses: vedantmgoyal2009/winget-releaser@v2 - with: - identifier: PrismLauncher.PrismLauncher - version: ${{ github.event.release.tag_name }} - installers-regex: 'PrismLauncher-Windows-MSVC(:?-arm64|-Legacy)?-Setup-.+\.exe$' - token: ${{ secrets.WINGET_TOKEN }} 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 faeb1c44e..a9d543e83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,13 @@ else() # ATL's pack list needs more than the default 1 Mib stack on windows if(WIN32) set(CMAKE_EXE_LINKER_FLAGS "-Wl,--stack,8388608 ${CMAKE_EXE_LINKER_FLAGS}") + + # -ffunction-sections and -fdata-sections help reduce binary size + # -mguard=cf enables Control Flow Guard + # TODO: Look into -gc-sections to further reduce binary size + foreach(lang C CXX) + set("CMAKE_${lang}_FLAGS_RELEASE" "-ffunction-sections -fdata-sections -mguard=cf") + endforeach() endif() endif() @@ -99,21 +106,21 @@ if ((CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebI message(STATUS "Address Sanitizer enabled for Debug builds, Turn it off with -DDEBUG_ADDRESS_SANITIZER=off") if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") - # using clang with clang-cl front end + # using clang with clang-cl front end message(STATUS "Address Sanitizer available on Clang MSVC frontend") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /Oy-") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /Oy-") else() # AppleClang and Clang message(STATUS "Address Sanitizer available on Clang") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover=null") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover=null") endif() elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") # GCC message(STATUS "Address Sanitizer available on GCC") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover") link_libraries("asan") elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") message(STATUS "Address Sanitizer available on MSVC") @@ -176,9 +183,11 @@ 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 8) +set(Launcher_VERSION_MAJOR 10) set(Launcher_VERSION_MINOR 0) set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}") @@ -205,6 +214,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 +229,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 +305,9 @@ 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) + find_package(Qt5 COMPONENTS DBus) + list(APPEND Launcher_QT_DBUS Qt5::DBus) if(NOT Launcher_FORCE_BUNDLED_LIBS) find_package(QuaZip-Qt5 1.3 QUIET) @@ -296,7 +321,9 @@ 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) + find_package(Qt6 COMPONENTS DBus) + list(APPEND Launcher_QT_DBUS Qt6::DBus) list(APPEND Launcher_QT_LIBS Qt6::Core5Compat) if(NOT Launcher_FORCE_BUNDLED_LIBS) @@ -377,12 +404,12 @@ if(UNIX AND APPLE) set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${Launcher_VERSION_NAME}") set(MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_NAME}") set(MACOSX_BUNDLE_ICON_FILE ${Launcher_Name}.icns) - set(MACOSX_BUNDLE_COPYRIGHT "© 2022-2023 ${Launcher_Copyright_Mac}") + set(MACOSX_BUNDLE_COPYRIGHT "${Launcher_Copyright_Mac}") set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "v55ZWWD6QlPoXGV6VLzOTZxZUggWeE51X8cRQyQh6vA=" CACHE STRING "Public key for Sparkle update feed") set(MACOSX_SPARKLE_UPDATE_FEED_URL "https://prismlauncher.org/feed/appcast.xml" CACHE STRING "URL for Sparkle update feed") - set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.1.0/Sparkle-2.1.0.tar.xz" CACHE STRING "URL to Sparkle release archive") - set(MACOSX_SPARKLE_SHA256 "bf6ac1caa9f8d321d5784859c88da874f28412f37fb327bc21b7b14c5d61ef94" CACHE STRING "SHA256 checksum for Sparkle release archive") + set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.6.4/Sparkle-2.6.4.tar.xz" CACHE STRING "URL to Sparkle release archive") + set(MACOSX_SPARKLE_SHA256 "50612a06038abc931f16011d7903b8326a362c1074dabccb718404ce8e585f0b" CACHE STRING "SHA256 checksum for Sparkle release archive") set(MACOSX_SPARKLE_DIR "${CMAKE_BINARY_DIR}/frameworks/Sparkle") # directories to look for dependencies @@ -417,7 +444,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() @@ -467,7 +506,7 @@ if(FORCE_BUNDLED_ZLIB) set(SKIP_INSTALL_ALL ON) add_subdirectory(libraries/zlib EXCLUDE_FROM_ALL) - # On OS where unistd.h exists, zlib's generated header defines `Z_HAVE_UNISTD_H`, while the included header does not. + # On OS where unistd.h exists, zlib's generated header defines `Z_HAVE_UNISTD_H`, while the included header does not. # We cannot safely undo the rename on those systems, and they generally have packages for zlib anyway. check_include_file(unistd.h NEED_GENERATED_ZCONF) if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h.included" AND NOT NEED_GENERATED_ZCONF) @@ -504,15 +543,15 @@ else() endif() if(NOT cmark_FOUND) message(STATUS "Using bundled cmark") - set(CMARK_STATIC ON CACHE BOOL "Build static libcmark library" FORCE) - set(CMARK_SHARED OFF CACHE BOOL "Build shared libcmark library" FORCE) - set(CMARK_TESTS OFF CACHE BOOL "Build cmark tests and enable testing" FORCE) + set(ORIGINAL_BUILD_TESTING ${BUILD_TESTING}) + set(BUILD_TESTING 0) + set(BUILD_SHARED_LIBS 0) add_subdirectory(libraries/cmark EXCLUDE_FROM_ALL) # Markdown parser - add_library(cmark::cmark ALIAS cmark_static) + add_library(cmark::cmark ALIAS cmark) + set(BUILD_TESTING ${ORIGINAL_BUILD_TESTING}) 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/CONTRIBUTING.md b/CONTRIBUTING.md index 072916772..5965f4d8e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,16 +2,59 @@ ## Code formatting -Try to follow the existing formatting. -If there is no existing formatting, you may use `clang-format` with our included `.clang-format` configuration. +All files are formatted with `clang-format` using the configuration in `.clang-format`. Ensure it is run on changed files before committing! -In general, in order of importance: +Please also follow the project's conventions for C++: -- Make sure your IDE is not messing up line endings or whitespace and avoid using linters. -- Prefer readability over dogma. -- Keep to the existing formatting. -- Indent with 4 space unless it's in a submodule. -- Keep lists (of arguments, parameters, initializers...) as lists, not paragraphs. It should either read from top to bottom, or left to right. Not both. +- Class and type names should be formatted as `PascalCase`: `MyClass`. +- Private or protected class data members should be formatted as `camelCase` prefixed with `m_`: `m_myCounter`. +- Private or protected `static` class data members should be formatted as `camelCase` prefixed with `s_`: `s_instance`. +- Public class data members should be formatted as `camelCase` without the prefix: `dateOfBirth`. +- Public, private or protected `static const` class data members should be formatted as `SCREAMING_SNAKE_CASE`: `MAX_VALUE`. +- Class function members should be formatted as `camelCase` without a prefix: `incrementCounter`. +- Global functions and non-`const` global variables should be formatted as `camelCase` without a prefix: `globalData`. +- `const` global variables, macros, and enum constants should be formatted as `SCREAMING_SNAKE_CASE`: `LIGHT_GRAY`. +- Avoid inventing acronyms or abbreviations especially for a name of multiple words - like `tp` for `texturePack`. + +Most of these rules are included in the `.clang-tidy` file, so you can run `clang-tidy` to check for any violations. + +Here is what these conventions with the formatting configuration look like: + +```c++ +#define AWESOMENESS 10 + +constexpr double PI = 3.14159; + +enum class PizzaToppings { HAM_AND_PINEAPPLE, OREO_AND_KETCHUP }; + +struct Person { + QString name; + QDateTime dateOfBirth; + + long daysOld() const { return dateOfBirth.daysTo(QDateTime::currentDateTime()); } +}; + +class ImportantClass { + public: + void incrementCounter() + { + if (m_counter + 1 > MAX_COUNTER_VALUE) + throw std::runtime_error("Counter has reached limit!"); + + ++m_counter; + } + + int counter() const { return m_counter; } + + private: + static constexpr int MAX_COUNTER_VALUE = 100; + int m_counter; +}; + +ImportantClass importantClassInstance; +``` + +If you see any names which do not follow these conventions, it is preferred that you leave them be - renames increase the number of changes therefore make reviewing harder and make your PR more prone to conflicts. However, if you're refactoring a whole class anyway, it's fine. ## Signing your work diff --git a/COPYING.md b/COPYING.md index 0221d1b08..111587060 100644 --- a/COPYING.md +++ b/COPYING.md @@ -1,7 +1,7 @@ ## Prism Launcher Prism Launcher - Minecraft Launcher - Copyright (C) 2022-2023 Prism Launcher Contributors + Copyright (C) 2022-2024 Prism Launcher Contributors 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 @@ -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 @@ -436,7 +410,7 @@ Copyright (C) 2007 Johann Ollivier Lapeyre Copyright (C) 2007 Kenneth Wimer Copyright (C) 2007 Riccardo Iaconelli - + and others This library is free software; you can redistribute it and/or diff --git a/README.md b/README.md index b32132d49..7b2878c2f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +

This is a fork of PrismLauncher, unlocked to be able to use offline accounts without owning the game.

+

@@ -61,7 +63,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..3a8c8fbfe 100644 --- a/cmake/MacOSXBundleInfo.plist.in +++ b/cmake/MacOSXBundleInfo.plist.in @@ -6,6 +6,10 @@ 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. + NSLocalNetworkUsageDescription + Minecraft uses the local network to find and connect to LAN servers. NSPrincipalClass NSApplication NSHighResolutionCapable @@ -77,6 +81,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 18ae9c463..dd50b5395 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1696426674, - "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", "owner": "edolstra", "repo": "flake-compat", - "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", "type": "github" }, "original": { @@ -16,63 +16,6 @@ "type": "github" } }, - "flake-parts": { - "inputs": { - "nixpkgs-lib": "nixpkgs-lib" - }, - "locked": { - "lastModified": 1698882062, - "narHash": "sha256-HkhafUayIqxXyHH1X8d9RDl1M2CkFgZLjKD3MzabiEo=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "8c9fa2545007b49a5db5f650ae91f227672c3877", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "flake-parts", - "type": "github" - } - }, - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1685518550, - "narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "gitignore": { - "inputs": { - "nixpkgs": [ - "pre-commit-hooks", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1660459072, - "narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=", - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "a20de23b925fd8264fd7fad6454652e142fd7f73", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", - "type": "github" - } - }, "libnbtplusplus": { "flake": false, "locked": { @@ -91,11 +34,11 @@ }, "nix-filter": { "locked": { - "lastModified": 1694857738, - "narHash": "sha256-bxxNyLHjhu0N8T3REINXQ2ZkJco0ABFPn6PIe2QUfqo=", + "lastModified": 1731533336, + "narHash": "sha256-oRam5PS1vcrr5UPgALW0eo1m/5/pls27Z/pabHNy2Ms=", "owner": "numtide", "repo": "nix-filter", - "rev": "41fd48e00c22b4ced525af521ead8792402de0ea", + "rev": "f7653272fd234696ae94229839a99b73c9ab7de0", "type": "github" }, "original": { @@ -106,89 +49,26 @@ }, "nixpkgs": { "locked": { - "lastModified": 1700856099, - "narHash": "sha256-RnEA7iJ36Ay9jI0WwP+/y4zjEhmeN6Cjs9VOFBH7eVQ=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "0bd59c54ef06bc34eca01e37d689f5e46b3fe2f1", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-lib": { - "locked": { - "dir": "lib", - "lastModified": 1698611440, - "narHash": "sha256-jPjHjrerhYDy3q9+s5EAsuhyhuknNfowY6yt6pjn9pc=", + "lastModified": 1739446958, + "narHash": "sha256-+/bYK3DbPxMIvSL4zArkMX0LQvS7rzBKXnDXLfKyRVc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "0cbe9f69c234a7700596e943bfae7ef27a31b735", + "rev": "2ff53fe64443980e139eaa286017f53f88336dd0", "type": "github" }, "original": { - "dir": "lib", "owner": "NixOS", "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } }, - "pre-commit-hooks": { - "inputs": { - "flake-compat": [ - "flake-compat" - ], - "flake-utils": "flake-utils", - "gitignore": "gitignore", - "nixpkgs": [ - "nixpkgs" - ], - "nixpkgs-stable": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1700922917, - "narHash": "sha256-ej2fch/T584b5K9sk1UhmZF7W6wEfDHuoUYpFN8dtvM=", - "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "rev": "e5ee5c5f3844550c01d2131096c7271cec5e9b78", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "type": "github" - } - }, "root": { "inputs": { "flake-compat": "flake-compat", - "flake-parts": "flake-parts", "libnbtplusplus": "libnbtplusplus", "nix-filter": "nix-filter", - "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" + "nixpkgs": "nixpkgs" } } }, diff --git a/flake.nix b/flake.nix index afb0ec63a..54add656d 100644 --- a/flake.nix +++ b/flake.nix @@ -1,44 +1,137 @@ { 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://prismlauncher.cachix.org" ]; + extra-trusted-public-keys = [ + "prismlauncher.cachix.org-1:9/n/FGyABA2jLUVfY+DEp4hKds/rwO+SCOtbOkDzd+c=" + ]; + }; + inputs = { - nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; - flake-parts.url = "github:hercules-ci/flake-parts"; - nix-filter.url = "github:numtide/nix-filter"; - pre-commit-hooks = { - url = "github:cachix/pre-commit-hooks.nix"; - inputs.nixpkgs.follows = "nixpkgs"; - inputs.nixpkgs-stable.follows = "nixpkgs"; - inputs.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: { + prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix { + inherit + libnbtplusplus + nix-filter + self + ; + }; + + 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 + ); + + # We put these under legacyPackages as they are meant for CI, not end user consumption + legacyPackages = forAllSystems ( + system: + let + prismPackages = self.packages.${system}; + legacyPackages = self.legacyPackages.${system}; + in + { + prismlauncher-debug = prismPackages.prismlauncher.override { + prismlauncher-unwrapped = legacyPackages.prismlauncher-unwrapped-debug; + }; + + prismlauncher-unwrapped-debug = prismPackages.prismlauncher-unwrapped.overrideAttrs { + cmakeBuildType = "Debug"; + dontStrip = true; + }; + } + ); }; } diff --git a/flatpak/flite.json b/flatpak/flite.json new file mode 100644 index 000000000..1bf280af1 --- /dev/null +++ b/flatpak/flite.json @@ -0,0 +1,20 @@ +{ + "name": "flite", + "config-opts": [ + "--enable-shared", + "--with-audio=pulseaudio" + ], + "no-parallel-make": true, + "sources": [ + { + "type": "git", + "url": "https://github.com/festvox/flite.git", + "tag": "v2.2", + "commit": "e9e2e37c329dbe98bfeb27a1828ef9a71fa84f88", + "x-checker-data": { + "type": "git", + "tag-pattern": "^v([\\d.]+)$" + } + } + ] +} diff --git a/flatpak/libdecor.json b/flatpak/libdecor.json index 589310a35..1652a2f04 100644 --- a/flatpak/libdecor.json +++ b/flatpak/libdecor.json @@ -1,22 +1,18 @@ { - "name": "libdecor", - "buildsystem": "meson", - "config-opts": [ - "-Ddemo=false" - ], - "sources": [ - { - "type": "git", - "url": "https://gitlab.freedesktop.org/libdecor/libdecor.git", - "commit": "73260393a97291c887e1074ab7f318e031be0ac6" - }, - { - "type": "patch", - "path": "patches/weird_libdecor.patch" - } - ], - "cleanup": [ - "/include", - "/lib/pkgconfig" - ] + "name": "libdecor", + "buildsystem": "meson", + "config-opts": [ + "-Ddemo=false" + ], + "sources": [ + { + "type": "git", + "url": "https://gitlab.freedesktop.org/libdecor/libdecor.git", + "commit": "c2bd8ad6fa42c0cb17553ce77ad8a87d1f543b1f" + } + ], + "cleanup": [ + "/include", + "/lib/pkgconfig" + ] } diff --git a/flatpak/org.prismlauncher.PrismLauncher.yml b/flatpak/org.prismlauncher.PrismLauncher.yml index 89727751e..136aef91a 100644 --- a/flatpak/org.prismlauncher.PrismLauncher.yml +++ b/flatpak/org.prismlauncher.PrismLauncher.yml @@ -1,10 +1,9 @@ id: org.prismlauncher.PrismLauncher runtime: org.kde.Platform -runtime-version: "5.15-23.08" +runtime-version: '6.8' sdk: org.kde.Sdk sdk-extensions: - org.freedesktop.Sdk.Extension.openjdk17 - - org.freedesktop.Sdk.Extension.openjdk8 command: prismlauncher finish-args: @@ -20,9 +19,12 @@ finish-args: - --filesystem=xdg-download:ro # FTBApp import - --filesystem=~/.ftba:ro - -cleanup: - - /lib/libGLU* + # Userspace visibility for manual hugepages configuration + # Required for -XX:+UseLargePages + - --filesystem=/sys/kernel/mm/hugepages:ro + # Userspace visibility for transparent hugepages configuration + # Required for -XX:+UseTransparentHugePages + - --filesystem=/sys/kernel/mm/transparent_hugepage:ro modules: # Might be needed by some Controller mods (see https://github.com/isXander/Controlify/issues/31) @@ -31,48 +33,39 @@ modules: # Needed for proper Wayland support - libdecor.json + # Text to Speech in the game + - flite.json + - name: prismlauncher buildsystem: cmake-ninja builddir: true config-opts: - -DLauncher_BUILD_PLATFORM=flatpak + # This allows us to manage and update Java independently of this Flatpak + - -DLauncher_ENABLE_JAVA_DOWNLOADER=ON - -DCMAKE_BUILD_TYPE=RelWithDebInfo - - -DLauncher_QT_VERSION_MAJOR=5 build-options: env: JAVA_HOME: /usr/lib/sdk/openjdk17/jvm/openjdk-17 JAVA_COMPILER: /usr/lib/sdk/openjdk17/jvm/openjdk-17/bin/javac + run-tests: true sources: - type: dir path: ../ - - name: openjdk - buildsystem: simple - build-commands: - - mkdir -p /app/jdk/ - - /usr/lib/sdk/openjdk17/install.sh - - mv /app/jre /app/jdk/17 - - /usr/lib/sdk/openjdk8/install.sh - - mv /app/jre /app/jdk/8 - cleanup: - - /jre - - name: glfw buildsystem: cmake-ninja config-opts: - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DBUILD_SHARED_LIBS:BOOL=ON - - -DGLFW_USE_WAYLAND=ON + - -DGLFW_BUILD_WAYLAND:BOOL=ON + - -DGLFW_BUILD_DOCS:BOOL=OFF sources: - type: git url: https://github.com/glfw/glfw.git - commit: 3fa2360720eeba1964df3c0ecf4b5df8648a8e52 + commit: 7b6aead9fb88b3623e3b3725ebb42670cbe4c579 # 3.4 - type: patch - path: patches/0003-Don-t-crash-on-calls-to-focus-or-icon.patch - - type: patch - path: patches/0005-Add-warning-about-being-an-unofficial-patch.patch - - type: patch - path: patches/0007-Platform-Prefer-Wayland-over-X11.patch + path: patches/0009-Defer-setting-cursor-position-until-the-cursor-is-lo.patch cleanup: - /include - /lib/cmake @@ -82,8 +75,8 @@ modules: buildsystem: autotools sources: - type: archive - url: https://xorg.freedesktop.org/archive/individual/app/xrandr-1.5.2.tar.xz - sha256: c8bee4790d9058bacc4b6246456c58021db58a87ddda1a9d0139bf5f18f1f240 + url: https://xorg.freedesktop.org/archive/individual/app/xrandr-1.5.3.tar.xz + sha256: f8dd7566adb74147fab9964680b6bbadee87cf406a7fcff51718a5e6949b841c x-checker-data: type: anitya project-id: 14957 @@ -104,18 +97,15 @@ modules: - install -Dm755 ../data/gamemoderun -t /app/bin sources: - type: archive - archive-type: tar-gzip - url: https://api.github.com/repos/FeralInteractive/gamemode/tarball/1.7 - sha256: 57ce73ba605d1cf12f8d13725006a895182308d93eba0f69f285648449641803 + dest-filename: gamemode.tar.gz + url: https://api.github.com/repos/FeralInteractive/gamemode/tarball/1.8.2 + sha256: 2886d4ce543c78bd2a364316d5e7fd59ef06b71de63f896b37c6d3dc97658f60 x-checker-data: type: json url: https://api.github.com/repos/FeralInteractive/gamemode/releases/latest version-query: .tag_name url-query: .tarball_url timestamp-query: .published_at - # from https://github.com/flathub/net.gaijin.WarThunder/blob/7ea6f7a9f84b9c77150c003a7059dc03f8dcbc7f/gamemode.patch - - type: patch - path: patches/gamemode.patch cleanup: - /include - /lib/pkgconfig diff --git a/flatpak/patches/0003-Don-t-crash-on-calls-to-focus-or-icon.patch b/flatpak/patches/0003-Don-t-crash-on-calls-to-focus-or-icon.patch deleted file mode 100644 index 9130e856c..000000000 --- a/flatpak/patches/0003-Don-t-crash-on-calls-to-focus-or-icon.patch +++ /dev/null @@ -1,24 +0,0 @@ -diff --git a/src/wl_window.c b/src/wl_window.c -index 52d3b9eb..4ac4eb5d 100644 ---- a/src/wl_window.c -+++ b/src/wl_window.c -@@ -2117,8 +2117,7 @@ void _glfwSetWindowTitleWayland(_GLFWwindow* window, const char* title) - void _glfwSetWindowIconWayland(_GLFWwindow* window, - int count, const GLFWimage* images) - { -- _glfwInputError(GLFW_FEATURE_UNAVAILABLE, -- "Wayland: The platform does not support setting the window icon"); -+ fprintf(stderr, "!!! Ignoring Error: Wayland: The platform does not support setting the window icon\n"); - } - - void _glfwGetWindowPosWayland(_GLFWwindow* window, int* xpos, int* ypos) -@@ -2361,8 +2360,7 @@ void _glfwRequestWindowAttentionWayland(_GLFWwindow* window) - - void _glfwFocusWindowWayland(_GLFWwindow* window) - { -- _glfwInputError(GLFW_FEATURE_UNAVAILABLE, -- "Wayland: The platform does not support setting the input focus"); -+ fprintf(stderr, "!!! Ignoring Error: Wayland: The platform does not support setting the input focus\n"); - } - - void _glfwSetWindowMonitorWayland(_GLFWwindow* window, diff --git a/flatpak/patches/0005-Add-warning-about-being-an-unofficial-patch.patch b/flatpak/patches/0005-Add-warning-about-being-an-unofficial-patch.patch deleted file mode 100644 index b031d739f..000000000 --- a/flatpak/patches/0005-Add-warning-about-being-an-unofficial-patch.patch +++ /dev/null @@ -1,17 +0,0 @@ -diff --git a/src/init.c b/src/init.c -index 06dbb3f2..a7c6da86 100644 ---- a/src/init.c -+++ b/src/init.c -@@ -449,6 +449,12 @@ GLFWAPI int glfwInit(void) - _glfw.initialized = GLFW_TRUE; - - glfwDefaultWindowHints(); -+ -+ fprintf(stderr, "!!! Patched GLFW from https://github.com/Admicos/minecraft-wayland\n" -+ "!!! If any issues with the window, or some issues with rendering, occur, " -+ "first try with the built-in GLFW, and if that solves the issue, report there first.\n" -+ "!!! Use outside Minecraft is untested, and things might break.\n"); -+ - return GLFW_TRUE; - } - diff --git a/flatpak/patches/0007-Platform-Prefer-Wayland-over-X11.patch b/flatpak/patches/0007-Platform-Prefer-Wayland-over-X11.patch deleted file mode 100644 index 4eeb81309..000000000 --- a/flatpak/patches/0007-Platform-Prefer-Wayland-over-X11.patch +++ /dev/null @@ -1,20 +0,0 @@ -diff --git a/src/platform.c b/src/platform.c -index c5966ae7..3e7442f9 100644 ---- a/src/platform.c -+++ b/src/platform.c -@@ -49,12 +49,12 @@ static const struct - #if defined(_GLFW_COCOA) - { GLFW_PLATFORM_COCOA, _glfwConnectCocoa }, - #endif --#if defined(_GLFW_X11) -- { GLFW_PLATFORM_X11, _glfwConnectX11 }, --#endif - #if defined(_GLFW_WAYLAND) - { GLFW_PLATFORM_WAYLAND, _glfwConnectWayland }, - #endif -+#if defined(_GLFW_X11) -+ { GLFW_PLATFORM_X11, _glfwConnectX11 }, -+#endif - }; - - GLFWbool _glfwSelectPlatform(int desiredID, _GLFWplatform* platform) diff --git a/flatpak/patches/0009-Defer-setting-cursor-position-until-the-cursor-is-lo.patch b/flatpak/patches/0009-Defer-setting-cursor-position-until-the-cursor-is-lo.patch new file mode 100644 index 000000000..70cec9981 --- /dev/null +++ b/flatpak/patches/0009-Defer-setting-cursor-position-until-the-cursor-is-lo.patch @@ -0,0 +1,59 @@ +From 9997ae55a47de469ea26f8437c30b51483abda5f Mon Sep 17 00:00:00 2001 +From: Dan Klishch +Date: Sat, 30 Sep 2023 23:38:05 -0400 +Subject: Defer setting cursor position until the cursor is locked + +--- + src/wl_platform.h | 3 +++ + src/wl_window.c | 14 ++++++++++++-- + 2 files changed, 15 insertions(+), 2 deletions(-) + +diff --git a/src/wl_platform.h b/src/wl_platform.h +index ca34f66e..cd1f227f 100644 +--- a/src/wl_platform.h ++++ b/src/wl_platform.h +@@ -403,6 +403,9 @@ typedef struct _GLFWwindowWayland + int scaleSize; + int compositorPreferredScale; + ++ double askedCursorPosX, askedCursorPosY; ++ GLFWbool didAskForSetCursorPos; ++ + struct zwp_relative_pointer_v1* relativePointer; + struct zwp_locked_pointer_v1* lockedPointer; + struct zwp_confined_pointer_v1* confinedPointer; +diff --git a/src/wl_window.c b/src/wl_window.c +index 1de26558..0df16747 100644 +--- a/src/wl_window.c ++++ b/src/wl_window.c +@@ -2586,8 +2586,9 @@ void _glfwGetCursorPosWayland(_GLFWwindow* window, double* xpos, double* ypos) + + void _glfwSetCursorPosWayland(_GLFWwindow* window, double x, double y) + { +- _glfwInputError(GLFW_FEATURE_UNAVAILABLE, +- "Wayland: The platform does not support setting the cursor position"); ++ window->wl.didAskForSetCursorPos = true; ++ window->wl.askedCursorPosX = x; ++ window->wl.askedCursorPosY = y; + } + + void _glfwSetCursorModeWayland(_GLFWwindow* window, int mode) +@@ -2819,6 +2820,15 @@ static const struct zwp_relative_pointer_v1_listener relativePointerListener = + static void lockedPointerHandleLocked(void* userData, + struct zwp_locked_pointer_v1* lockedPointer) + { ++ _GLFWwindow* window = userData; ++ ++ if (window->wl.didAskForSetCursorPos) ++ { ++ window->wl.didAskForSetCursorPos = false; ++ zwp_locked_pointer_v1_set_cursor_position_hint(window->wl.lockedPointer, ++ wl_fixed_from_double(window->wl.askedCursorPosX), ++ wl_fixed_from_double(window->wl.askedCursorPosY)); ++ } + } + + static void lockedPointerHandleUnlocked(void* userData, +-- +2.42.0 + diff --git a/flatpak/patches/gamemode.patch b/flatpak/patches/gamemode.patch deleted file mode 100644 index 3cc0d7412..000000000 --- a/flatpak/patches/gamemode.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff -ruN a/common/common-pidfds.c b/common/common-pidfds.c ---- a/common/common-pidfds.c 2021-02-18 20:00:12.000000000 +0100 -+++ b/common/common-pidfds.c 2023-09-07 08:57:42.954362763 +0200 -@@ -58,6 +58,8 @@ - { - return (int)syscall(__NR_pidfd_open, pid, flags); - } -+#else -+#include - #endif - - /* pidfd functions */ diff --git a/flatpak/patches/weird_libdecor.patch b/flatpak/patches/weird_libdecor.patch deleted file mode 100644 index 3a400b820..000000000 --- a/flatpak/patches/weird_libdecor.patch +++ /dev/null @@ -1,40 +0,0 @@ -diff --git a/src/libdecor.c b/src/libdecor.c -index a9c1106..1aa38b3 100644 ---- a/src/libdecor.c -+++ b/src/libdecor.c -@@ -1391,22 +1391,32 @@ calculate_priority(const struct libdecor_plugin_description *plugin_description) - static bool - check_symbol_conflicts(const struct libdecor_plugin_description *plugin_description) - { -+ bool ret = true; - char * const *symbol; -+ void* main_prog = dlopen(NULL, RTLD_LAZY); -+ if (!main_prog) { -+ fprintf(stderr, "Plugin \"%s\" couldn't check conflicting symbols: \"%s\".\n", -+ plugin_description->description, dlerror()); -+ return false; -+ } -+ - - symbol = plugin_description->conflicting_symbols; - while (*symbol) { - dlerror(); -- dlsym (RTLD_DEFAULT, *symbol); -+ dlsym (main_prog, *symbol); - if (!dlerror()) { - fprintf(stderr, "Plugin \"%s\" uses conflicting symbol \"%s\".\n", - plugin_description->description, *symbol); -- return false; -+ ret = false; -+ break; - } - - symbol++; - } - -- return true; -+ dlclose(main_prog); -+ return ret; - } - - static struct plugin_loader * diff --git a/flatpak/shared-modules b/flatpak/shared-modules index 45094ca57..f5d368a31 160000 --- a/flatpak/shared-modules +++ b/flatpak/shared-modules @@ -1 +1 @@ -Subproject commit 45094ca570be383d06df729b6972830ec63bd3df +Subproject commit f5d368a31d6ef046eb2955c74ec6f54f32ed5c4e diff --git a/garnix.yaml b/garnix.yaml deleted file mode 100644 index 6cf8f7214..000000000 --- a/garnix.yaml +++ /dev/null @@ -1,7 +0,0 @@ -builds: - exclude: - - "*.x86_64-darwin.*" - include: - - "checks.x86_64-linux.*" - - "devShells.*.*" - - "packages.*.*" diff --git a/launcher/Application.cpp b/launcher/Application.cpp index cfede9dc1..6f675dcee 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -44,10 +44,12 @@ #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 "tasks/Task.h" +#include "tools/GenericProfiler.h" #include "ui/InstanceWindow.h" #include "ui/MainWindow.h" @@ -57,8 +59,6 @@ #include "ui/pages/BasePageProvider.h" #include "ui/pages/global/APIPage.h" #include "ui/pages/global/AccountListPage.h" -#include "ui/pages/global/CustomCommandsPage.h" -#include "ui/pages/global/EnvironmentVariablesPage.h" #include "ui/pages/global/ExternalToolsPage.h" #include "ui/pages/global/JavaPage.h" #include "ui/pages/global/LanguagePage.h" @@ -66,8 +66,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 +107,7 @@ #include "icons/IconList.h" #include "net/HttpMetaCache.h" -#include "java/JavaUtils.h" +#include "java/JavaInstallList.h" #include "updater/ExternalUpdater.h" @@ -125,6 +127,7 @@ #include #include +#include "SysInfo.h" #ifdef Q_OS_LINUX #include @@ -150,6 +153,7 @@ #endif #if defined Q_OS_WIN32 +#include #include "WindowsConsole.h" #endif @@ -225,6 +229,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // Don't quit on hiding the last window this->setQuitOnLastWindowClosed(false); + this->setQuitLockEnabled(false); // Commandline parsing QCommandLineParser parser; @@ -234,7 +239,9 @@ 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" }, + { { "o", "offline" }, "Launch offline, with given player name (only valid in combination with --launch)", "offline" }, { "alive", "Write a small '" + liveCheckFile + "' file after the launcher starts" }, { { "I", "import" }, "Import instance or resource from specified local path or URL", "url" }, { "show", "Opens the window for the specified instance (by instance ID)", "show" } }); @@ -248,7 +255,12 @@ 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"); + if (parser.isSet("offline")) { + m_offline = true; + m_offlineName = parser.value("offline"); + } m_liveCheck = parser.isSet("alive"); m_instanceIdToShowWindowOf = parser.value("show"); @@ -263,8 +275,9 @@ 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()) { - std::cerr << "--server and --profile can only be used in combination with --launch!" << std::endl; + if ((!m_serverToJoin.isEmpty() || !m_worldToJoin.isEmpty() || !m_profileToUse.isEmpty() || m_offline) && + m_instanceIdToLaunch.isEmpty()) { + std::cerr << "--server, --profile and --offline can only be used in combination with --launch!" << std::endl; m_status = Application::Failed; return; } @@ -290,12 +303,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()) { @@ -308,7 +326,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) 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()) { + dataPath = portableUserData; + adjustedBy = "Portable user data path"; + m_portable = true; + } else if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { dataPath = m_rootPath; adjustedBy = "Portable data path"; m_portable = true; @@ -374,10 +396,16 @@ 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; } + if (m_offline) { + launch.args["offline_enabled"] = "true"; + launch.args["offline_name"] = m_offlineName; + } m_peerInstance->sendMessage(launch.serialize(), timeout); } m_status = Application::Succeeded; @@ -389,20 +417,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)) { @@ -442,7 +465,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 << "..."; @@ -494,8 +517,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } { - qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME) << ", (c) 2022-2023 " - << qPrintable(QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", ")); + qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + ", " + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", ")); qDebug() << "Version : " << BuildConfig.printableVersionString(); qDebug() << "Platform : " << BuildConfig.BUILD_PLATFORM; qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT; @@ -518,6 +540,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."; } @@ -554,6 +578,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; @@ -588,6 +614,9 @@ 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("MoveModsFromDownloadsDir", false); + m_settings->registerSetting("SkinsDir", "skins"); + m_settings->registerSetting("JavaDir", "java"); // Editors m_settings->registerSetting("JsonEditor", QString()); @@ -616,7 +645,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 @@ -630,6 +659,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); @@ -640,10 +673,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("UseNativeGLFW", false); m_settings->registerSetting("CustomGLFWPath", ""); - // Peformance related options + // Performance related options m_settings->registerSetting("EnableFeralGamemode", false); m_settings->registerSetting("EnableMangoHud", false); m_settings->registerSetting("UseDiscreteGpu", false); + m_settings->registerSetting("UseZink", false); // Game time m_settings->registerSetting("ShowGameTime", true); @@ -654,6 +688,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", ""); @@ -667,6 +702,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // The cat m_settings->registerSetting("TheCat", false); + m_settings->registerSetting("CatOpacity", 100); + + m_settings->registerSetting("StatusBarVisible", true); m_settings->registerSetting("ToolbarsLocked", false); @@ -749,6 +787,12 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("ModrinthToken", ""); m_settings->registerSetting("UserAgentOverride", ""); + // FTBApp instances + m_settings->registerSetting("FTBAppInstancesPath", ""); + + // Custom Technic Client ID + m_settings->registerSetting("TechnicClientID", ""); + // Init page provider { m_globalSettingsProvider = std::make_shared(tr("Settings")); @@ -756,8 +800,6 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); @@ -801,8 +843,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) ":/icons/multimc/128x128/instances/", ":/icons/multimc/scalable/instances/" }; 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."; + [this](const Setting&, QVariant value) { m_icons->directoryChanged(value.toString()); }); + qDebug() << "<> Instance icons initialized."; } // Themes @@ -839,25 +881,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."; } @@ -868,6 +904,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); } @@ -936,8 +973,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; } } @@ -969,8 +1005,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; } } @@ -982,7 +1017,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); @@ -998,7 +1033,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } // notify user if /tmp is mounted with `noexec` (#1693) - { + QString jvmArgs = m_settings->get("JvmArgs").toString(); + if (jvmArgs.indexOf("java.io.tmpdir") == -1) { /* java.io.tmpdir is a valid workaround, so don't annoy */ bool is_tmp_noexec = false; #if defined(Q_OS_LINUX) @@ -1018,7 +1054,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) if (is_tmp_noexec) { auto infoMsg = tr("Your /tmp directory is currently mounted with the 'noexec' flag enabled.\n" - "Some versions of Minecraft may not launch.\n"); + "Some versions of Minecraft may not launch.\n" + "\n" + "You may solve this issue by remounting /tmp as 'exec' or setting " + "the java.io.tmpdir JVM argument to a writeable directory in a " + "filesystem where the 'exec' flag is set (e.g., /home/user/.local/tmp)\n"); auto msgBox = new QMessageBox(QMessageBox::Information, tr("Incompatible system configuration"), infoMsg, QMessageBox::Ok); msgBox->setDefaultButton(QMessageBox::Ok); msgBox->setAttribute(Qt::WA_DeleteOnClose); @@ -1038,8 +1078,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) bool Application::createSetupWizard() { - bool javaRequired = [&]() { - bool ignoreJavaWizard = m_settings->get("IgnoreJavaWizard").toBool(); + bool javaRequired = [this]() { + if (BuildConfig.JAVA_DOWNLOADER_ENABLED && settings()->get("AutomaticJavaDownload").toBool()) { + return false; + } + bool ignoreJavaWizard = settings()->get("IgnoreJavaWizard").toBool(); if (ignoreJavaWizard) { return false; } @@ -1051,18 +1094,17 @@ bool Application::createSetupWizard() } QString currentJavaPath = settings()->get("JavaPath").toString(); QString actualPath = FS::ResolveExecutable(currentJavaPath); - if (actualPath.isNull()) { - return true; - } - return false; + return actualPath.isNull(); }(); + bool askjava = BuildConfig.JAVA_DOWNLOADER_ENABLED && !javaRequired && !settings()->get("AutomaticJavaDownload").toBool() && + !settings()->get("AutomaticJavaSwitch").toBool() && !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) @@ -1079,6 +1121,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) { @@ -1089,11 +1133,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() @@ -1149,14 +1196,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()) { @@ -1167,7 +1217,7 @@ void Application::performMainStartupAction() qDebug() << " Launching with account" << m_profileToUse; } - launch(inst, true, false, serverToJoin, accountToUse); + launch(inst, !m_offline, false, targetToJoin, accountToUse, m_offlineName); return; } } @@ -1198,6 +1248,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); @@ -1229,16 +1285,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") { @@ -1251,7 +1314,10 @@ 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"]; + bool offline = received.args["offline_enabled"] == "true"; + QString offlineName = received.args["offline_name"]; InstancePtr instance; if (!id.isEmpty()) { @@ -1265,11 +1331,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); @@ -1280,7 +1347,7 @@ void Application::messageReceived(const QByteArray& message) } } - launch(instance, true, false, serverObject, accountObject); + launch(instance, !offline, false, serverObject, accountObject, offlineName); } else { qWarning() << "Received invalid message" << message; } @@ -1321,12 +1388,14 @@ bool Application::openJsonEditor(const QString& filename) bool Application::launch(InstancePtr instance, bool online, bool demo, - MinecraftServerTargetPtr serverToJoin, - MinecraftAccountPtr accountToUse) + MinecraftTarget::Ptr targetToJoin, + MinecraftAccountPtr accountToUse, + const QString& offlineName) { if (m_updateRunning) { qDebug() << "Cannot launch instances while an update is running. Please try again when updates are completed."; } else if (instance->canLaunch()) { + QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[instance->id()]; auto window = extras.window; if (window) { @@ -1340,8 +1409,9 @@ 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); + controller->setOfflineName(offlineName); if (window) { controller->setParentWidget(window); } else if (m_mainWindow) { @@ -1351,7 +1421,7 @@ bool Application::launch(InstancePtr instance, connect(controller.get(), &LaunchController::failed, this, &Application::controllerFailed); connect(controller.get(), &LaunchController::aborted, this, [this] { controllerFailed(tr("Aborted")); }); addRunningInstance(); - controller->start(); + QMetaObject::invokeMethod(controller.get(), &Task::start, Qt::QueuedConnection); return true; } else if (instance->isRunning()) { showInstanceWindow(instance, "console"); @@ -1369,9 +1439,11 @@ bool Application::kill(InstancePtr instance) qWarning() << "Attempted to kill instance" << instance->id() << ", which isn't running."; return false; } + QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[instance->id()]; // NOTE: copy of the shared pointer keeps it alive auto controller = extras.controller; + locker.unlock(); if (controller) { return controller->abort(); } @@ -1425,12 +1497,14 @@ void Application::controllerSucceeded() if (!controller) return; auto id = controller->id(); + + QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[id]; // on success, do... if (controller->instance()->settings()->get("AutoCloseConsole").toBool()) { if (extras.window) { - extras.window->close(); + QMetaObject::invokeMethod(extras.window, &QWidget::close, Qt::QueuedConnection); } } extras.controller.reset(); @@ -1450,6 +1524,7 @@ void Application::controllerFailed(const QString& error) if (!controller) return; auto id = controller->id(); + QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[id]; // on failure, do... nothing @@ -1507,10 +1582,22 @@ InstanceWindow* Application::showInstanceWindow(InstancePtr instance, QString pa if (!instance) return nullptr; auto id = instance->id(); + QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[id]; auto& window = extras.window; if (window) { +// If the window is minimized on macOS or Windows, activate and bring it up +#ifdef Q_OS_MACOS + if (window->isMinimized()) { + window->setWindowState(window->windowState() & ~Qt::WindowMinimized); + } +#elif defined(Q_OS_WIN) + if (window->isMinimized()) { + window->showNormal(); + } +#endif + window->raise(); window->activateWindow(); } else { @@ -1518,6 +1605,7 @@ InstanceWindow* Application::showInstanceWindow(InstancePtr instance, QString pa m_openWindows++; connect(window, &InstanceWindow::isClosing, this, &Application::on_windowClose); } + if (!page.isEmpty()) { window->selectPage(page); } @@ -1532,6 +1620,7 @@ void Application::on_windowClose() m_openWindows--; auto instWindow = qobject_cast(QObject::sender()); if (instWindow) { + QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[instWindow->instanceId()]; extras.window = nullptr; if (extras.controller) { @@ -1646,8 +1735,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) { @@ -1708,20 +1796,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, @@ -1794,7 +1868,7 @@ bool Application::handleDataMigration(const QString& currentData, matcher->add(std::make_shared("themes/")); ProgressDialog diag; - DataMigrationTask task(nullptr, oldData, currentData, matcher); + DataMigrationTask task(oldData, currentData, matcher); if (diag.execWithTask(&task)) { qDebug() << "<> Migration succeeded"; setDoNotMigrate(); @@ -1828,3 +1902,36 @@ QUrl Application::normalizeImportUrl(QString const& url) return QUrl::fromUserInput(url); } } + +const QString Application::javaPath() +{ + return m_settings->get("JavaDir").toString(); +} + +void Application::addQSavePath(QString path) +{ + QMutexLocker locker(&m_qsaveResourcesMutex); + m_qsaveResources[path] = m_qsaveResources.value(path, 0) + 1; +} + +void Application::removeQSavePath(QString path) +{ + QMutexLocker locker(&m_qsaveResourcesMutex); + auto count = m_qsaveResources.value(path, 0) - 1; + if (count <= 0) { + m_qsaveResources.remove(path); + } else { + m_qsaveResources[path] = count; + } +} + +bool Application::checkQSavePath(QString path) +{ + QMutexLocker locker(&m_qsaveResourcesMutex); + for (auto partialPath : m_qsaveResources.keys()) { + if (path.startsWith(partialPath) && m_qsaveResources.value(partialPath, 0) > 0) { + return true; + } + } + return false; +} \ No newline at end of file diff --git a/launcher/Application.h b/launcher/Application.h index 7669e08ec..dad76d702 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -42,13 +42,13 @@ #include #include #include +#include #include #include #include -#include "minecraft/launch/MinecraftServerTarget.h" -#include "ui/themes/CatPack.h" +#include "minecraft/launch/MinecraftTarget.h" class LaunchController; class LocalPeer; @@ -82,6 +82,12 @@ class Index; #endif #define APPLICATION (static_cast(QCoreApplication::instance())) +// Used for checking if is a test +#if defined(APPLICATION_DYN) +#undef APPLICATION_DYN +#endif +#define APPLICATION_DYN (dynamic_cast(QCoreApplication::instance())) + class Application : public QApplication { // friends for the purpose of limiting access to deprecated stuff Q_OBJECT @@ -162,6 +168,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 +189,6 @@ class Application : public QApplication { void ShowGlobalSettings(class QWidget* parent, QString open_page = QString()); - int suitableMaxMem(); - bool updaterEnabled(); QString updaterBinaryName(); @@ -193,6 +200,8 @@ class Application : public QApplication { void globalSettingsClosed(); int currentCatChanged(int index); + void oauthReplyRecieved(QVariantMap); + #ifdef Q_OS_MACOS void clickedOnDock(); #endif @@ -201,8 +210,9 @@ class Application : public QApplication { bool launch(InstancePtr instance, bool online = true, bool demo = false, - MinecraftServerTargetPtr serverToJoin = nullptr, - MinecraftAccountPtr accountToUse = nullptr); + MinecraftTarget::Ptr targetToJoin = nullptr, + MinecraftAccountPtr accountToUse = nullptr, + const QString& offlineName = QString()); bool kill(InstancePtr instance); void closeCurrentWindow(); @@ -270,6 +280,7 @@ class Application : public QApplication { shared_qobject_ptr controller; }; std::map m_instanceExtras; + mutable QMutex m_instanceExtrasMutex; // main state variables size_t m_openWindows = 0; @@ -289,9 +300,21 @@ class Application : public QApplication { QString m_detectedOpenALPath; QString m_instanceIdToLaunch; QString m_serverToJoin; + QString m_worldToJoin; QString m_profileToUse; + bool m_offline = false; + QString m_offlineName; bool m_liveCheck = false; QList m_urlsToImport; QString m_instanceIdToShowWindowOf; std::unique_ptr logFile; + + public: + void addQSavePath(QString); + void removeQSavePath(QString); + bool checkQSavePath(QString); + + private: + QHash m_qsaveResources; + mutable QMutex m_qsaveResourcesMutex; }; diff --git a/launcher/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 33dc3f741..ccfd0b847 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -64,6 +64,8 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s m_settings->registerSetting("lastLaunchTime", 0); m_settings->registerSetting("totalTimePlayed", 0); + if (m_settings->get("totalTimePlayed").toLongLong() < 0) + m_settings->reset("totalTimePlayed"); m_settings->registerSetting("lastTimePlayed", 0); m_settings->registerSetting("linkedInstances", "[]"); @@ -267,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(); @@ -283,8 +290,6 @@ void BaseInstance::setRunning(bool running) emit propertiesChanged(this); } - - emit runningStatusChanged(running); } int64_t BaseInstance::totalTimePlayed() const @@ -406,3 +411,8 @@ void BaseInstance::updateRuntimeContext() { // NOOP } + +bool BaseInstance::isLegacy() +{ + return traits().contains("legacyLaunch") || traits().contains("alphaLaunch"); +} diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index f4ed9113c..9827a08b4 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 99acf8fc5..a375e0bdf 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -24,10 +24,13 @@ set(CORE_SOURCES NullInstance.h MMCZip.h MMCZip.cpp + Untar.h + Untar.cpp StringUtils.h StringUtils.cpp QVariantUtils.h RuntimeContext.h + PSaveFile.h # Basic instance manipulation tasks (derived from InstanceTask) InstanceCreationTask.h @@ -126,7 +129,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 +141,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 +161,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 +208,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 +237,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 +270,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 +280,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 +296,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 @@ -346,13 +345,12 @@ set(MINECRAFT_SOURCES minecraft/mod/TexturePackFolderModel.h minecraft/mod/TexturePackFolderModel.cpp minecraft/mod/ShaderPackFolderModel.h - minecraft/mod/tasks/BasicFolderLoadTask.h - minecraft/mod/tasks/ModFolderLoadTask.h - minecraft/mod/tasks/ModFolderLoadTask.cpp + minecraft/mod/tasks/ResourceFolderLoadTask.h + minecraft/mod/tasks/ResourceFolderLoadTask.cpp minecraft/mod/tasks/LocalModParseTask.h minecraft/mod/tasks/LocalModParseTask.cpp - minecraft/mod/tasks/LocalModUpdateTask.h - minecraft/mod/tasks/LocalModUpdateTask.cpp + minecraft/mod/tasks/LocalResourceUpdateTask.h + minecraft/mod/tasks/LocalResourceUpdateTask.cpp minecraft/mod/tasks/LocalDataPackParseTask.h minecraft/mod/tasks/LocalDataPackParseTask.cpp minecraft/mod/tasks/LocalResourcePackParseTask.h @@ -372,13 +370,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) @@ -422,8 +424,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 @@ -432,6 +432,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 @@ -453,6 +467,8 @@ set(TOOLS_SOURCES tools/JVisualVM.h tools/MCEditTool.cpp tools/MCEditTool.h + tools/GenericProfiler.cpp + tools/GenericProfiler.h ) set(META_SOURCES @@ -592,7 +608,7 @@ set(PRISMUPDATER_SOURCES updater/prismupdater/UpdaterDialogs.cpp updater/prismupdater/GitHubRelease.h updater/prismupdater/GitHubRelease.cpp - + Json.h Json.cpp FileSystem.h @@ -609,7 +625,7 @@ set(PRISMUPDATER_SOURCES # Zip MMCZip.h MMCZip.cpp - + # Time MMCTime.h MMCTime.cpp @@ -624,7 +640,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 @@ -653,6 +668,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" @@ -665,7 +696,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}" ) @@ -673,14 +704,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}" ) @@ -750,6 +781,8 @@ SET(LAUNCHER_SOURCES DataMigrationTask.cpp ApplicationMessage.h ApplicationMessage.cpp + SysInfo.h + SysInfo.cpp # GUI - general utilities DesktopServices.h @@ -788,16 +821,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 @@ -815,6 +844,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 @@ -827,6 +860,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 @@ -873,7 +908,6 @@ SET(LAUNCHER_SOURCES ui/pages/instance/NotesPage.h ui/pages/instance/LogPage.cpp ui/pages/instance/LogPage.h - ui/pages/instance/InstanceSettingsPage.cpp ui/pages/instance/InstanceSettingsPage.h ui/pages/instance/ScreenshotsPage.cpp ui/pages/instance/ScreenshotsPage.h @@ -887,17 +921,12 @@ SET(LAUNCHER_SOURCES # GUI - global settings pages ui/pages/global/AccountListPage.cpp ui/pages/global/AccountListPage.h - ui/pages/global/CustomCommandsPage.cpp - ui/pages/global/CustomCommandsPage.h - ui/pages/global/EnvironmentVariablesPage.cpp - ui/pages/global/EnvironmentVariablesPage.h ui/pages/global/ExternalToolsPage.cpp ui/pages/global/ExternalToolsPage.h ui/pages/global/JavaPage.cpp ui/pages/global/JavaPage.h ui/pages/global/LanguagePage.cpp ui/pages/global/LanguagePage.h - ui/pages/global/MinecraftPage.cpp ui/pages/global/MinecraftPage.h ui/pages/global/LauncherPage.cpp ui/pages/global/LauncherPage.h @@ -930,6 +959,9 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/ShaderPackPage.cpp ui/pages/modplatform/ShaderPackModel.cpp + + ui/pages/modplatform/ModpackProviderBasePage.h + ui/pages/modplatform/atlauncher/AtlFilterModel.cpp ui/pages/modplatform/atlauncher/AtlFilterModel.h ui/pages/modplatform/atlauncher/AtlListModel.cpp @@ -992,8 +1024,6 @@ SET(LAUNCHER_SOURCES ui/dialogs/CopyInstanceDialog.h ui/dialogs/CustomMessageBox.cpp ui/dialogs/CustomMessageBox.h - ui/dialogs/EditAccountDialog.cpp - ui/dialogs/EditAccountDialog.h ui/dialogs/ExportInstanceDialog.cpp ui/dialogs/ExportInstanceDialog.h ui/dialogs/ExportPackDialog.cpp @@ -1022,8 +1052,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 @@ -1032,26 +1060,29 @@ SET(LAUNCHER_SOURCES ui/dialogs/BlockedModsDialog.h ui/dialogs/ChooseProviderDialog.h ui/dialogs/ChooseProviderDialog.cpp - ui/dialogs/ModUpdateDialog.cpp - ui/dialogs/ModUpdateDialog.h + ui/dialogs/ResourceUpdateDialog.cpp + ui/dialogs/ResourceUpdateDialog.h 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 ui/widgets/CustomCommands.h ui/widgets/EnvironmentVariables.cpp ui/widgets/EnvironmentVariables.h - ui/widgets/DropLabel.cpp - ui/widgets/DropLabel.h ui/widgets/FocusLineEdit.cpp ui/widgets/FocusLineEdit.h ui/widgets/IconLabel.cpp ui/widgets/IconLabel.h - ui/widgets/JavaSettingsWidget.cpp - ui/widgets/JavaSettingsWidget.h + ui/widgets/JavaWizardWidget.cpp + ui/widgets/JavaWizardWidget.h ui/widgets/LabeledToolButton.cpp ui/widgets/LabeledToolButton.h ui/widgets/LanguageSelectionWidget.cpp @@ -1087,6 +1118,10 @@ SET(LAUNCHER_SOURCES ui/widgets/WideBar.cpp ui/widgets/ThemeCustomizationWidget.h ui/widgets/ThemeCustomizationWidget.cpp + ui/widgets/MinecraftSettingsWidget.h + ui/widgets/MinecraftSettingsWidget.cpp + ui/widgets/JavaSettingsWidget.h + ui/widgets/JavaSettingsWidget.cpp # GUI - instance group view ui/instanceview/InstanceProxyModel.cpp @@ -1122,13 +1157,14 @@ 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 ui/pages/global/LauncherPage.ui ui/pages/global/APIPage.ui ui/pages/global/ProxyPage.ui - ui/pages/global/MinecraftPage.ui ui/pages/global/ExternalToolsPage.ui ui/pages/instance/ExternalResourcesPage.ui ui/pages/instance/NotesPage.ui @@ -1136,7 +1172,6 @@ qt_wrap_ui(LAUNCHER_UI ui/pages/instance/ServersPage.ui ui/pages/instance/GameOptionsPage.ui ui/pages/instance/OtherLogsPage.ui - ui/pages/instance/InstanceSettingsPage.ui ui/pages/instance/VersionPage.ui ui/pages/instance/ManagedPackPage.ui ui/pages/instance/WorldListPage.ui @@ -1159,6 +1194,8 @@ qt_wrap_ui(LAUNCHER_UI ui/widgets/ModFilterWidget.ui ui/widgets/SubTaskProgressBar.ui ui/widgets/ThemeCustomizationWidget.ui + ui/widgets/MinecraftSettingsWidget.ui + ui/widgets/JavaSettingsWidget.ui ui/dialogs/CopyInstanceDialog.ui ui/dialogs/ProfileSetupDialog.ui ui/dialogs/ProgressDialog.ui @@ -1166,7 +1203,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 @@ -1175,11 +1211,11 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/MSALoginDialog.ui ui/dialogs/OfflineLoginDialog.ui ui/dialogs/AboutDialog.ui - ui/dialogs/EditAccountDialog.ui ui/dialogs/ReviewMessageBox.ui ui/dialogs/ScrollMessageBox.ui ui/dialogs/BlockedModsDialog.ui ui/dialogs/ChooseProviderDialog.ui + ui/dialogs/skins/SkinManageDialog.ui ) qt_wrap_ui(PRISM_UPDATE_UI @@ -1221,14 +1257,10 @@ include(CompilerWarnings) # Add executable add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHER_UI} ${LAUNCHER_RESOURCES}) -if(BUILD_TESTING) -target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_TEST) -endif() set_project_warnings(Launcher_logic "${Launcher_MSVC_WARNINGS}" "${Launcher_CLANG_WARNINGS}" "${Launcher_GCC_WARNINGS}") -target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_APPLICATION) target_include_directories(Launcher_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_APPLICATION) target_link_libraries(Launcher_logic @@ -1239,7 +1271,6 @@ target_link_libraries(Launcher_logic tomlplusplus::tomlplusplus qdcss BuildConfig - Katabasis Qt${QT_VERSION_MAJOR}::Widgets ghcFilesystem::ghc_filesystem ) @@ -1257,6 +1288,8 @@ 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_DBUS} ${Launcher_QT_LIBS} ) target_link_libraries(Launcher_logic @@ -1265,6 +1298,10 @@ target_link_libraries(Launcher_logic LocalPeer Launcher_rainbow ) +if (TARGET ${Launcher_QT_DBUS}) + add_compile_definitions(WITH_QTDBUS) +endif() + if(APPLE) set(CMAKE_MACOSX_RPATH 1) set(CMAKE_INSTALL_RPATH "@loader_path/../Frameworks/") @@ -1327,13 +1364,12 @@ 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) target_sources("${Launcher_Name}_updater" PRIVATE updater/prismupdater/updater.exe.manifest) target_link_libraries("${Launcher_Name}_updater" prism_updater_logic) - + if(DEFINED Launcher_APP_BINARY_NAME) set_target_properties("${Launcher_Name}_updater" PROPERTIES OUTPUT_NAME "${Launcher_APP_BINARY_NAME}_updater") endif() @@ -1492,7 +1528,6 @@ if(INSTALL_BUNDLE STREQUAL "full") CONFIGURATIONS Debug RelWithDebInfo "" DESTINATION ${PLUGIN_DEST_DIR} COMPONENT Runtime - PATTERN "*qopensslbackend*" EXCLUDE PATTERN "*qcertonlybackend*" EXCLUDE ) install( @@ -1503,10 +1538,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/DataMigrationTask.cpp b/launcher/DataMigrationTask.cpp index 27ce5f01b..c03302319 100644 --- a/launcher/DataMigrationTask.cpp +++ b/launcher/DataMigrationTask.cpp @@ -12,11 +12,8 @@ #include -DataMigrationTask::DataMigrationTask(QObject* parent, - const QString& sourcePath, - const QString& targetPath, - const IPathMatcher::Ptr pathMatcher) - : Task(parent), m_sourcePath(sourcePath), m_targetPath(targetPath), m_pathMatcher(pathMatcher), m_copy(sourcePath, targetPath) +DataMigrationTask::DataMigrationTask(const QString& sourcePath, const QString& targetPath, const IPathMatcher::Ptr pathMatcher) + : Task(), m_sourcePath(sourcePath), m_targetPath(targetPath), m_pathMatcher(pathMatcher), m_copy(sourcePath, targetPath) { m_copy.matcher(m_pathMatcher.get()).whitelist(true); } @@ -27,7 +24,7 @@ void DataMigrationTask::executeTask() // 1. Scan // Check how many files we gotta copy - m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [&] { + m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { return m_copy(true); // dry run to collect amount of files }); connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::dryRunFinished); @@ -60,7 +57,7 @@ void DataMigrationTask::dryRunFinished() setProgress(m_copy.totalCopied(), m_toCopy); setStatus(tr("Copying %1…").arg(shortenedName)); }); - m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [&] { + m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { return m_copy(false); // actually copy now }); connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::copyFinished); diff --git a/launcher/DataMigrationTask.h b/launcher/DataMigrationTask.h index aba9f2399..fc613cd5e 100644 --- a/launcher/DataMigrationTask.h +++ b/launcher/DataMigrationTask.h @@ -18,7 +18,7 @@ class DataMigrationTask : public Task { Q_OBJECT public: - explicit DataMigrationTask(QObject* parent, const QString& sourcePath, const QString& targetPath, IPathMatcher::Ptr pathmatcher); + explicit DataMigrationTask(const QString& sourcePath, const QString& targetPath, IPathMatcher::Ptr pathmatcher); ~DataMigrationTask() override = default; protected: diff --git a/launcher/DesktopServices.cpp b/launcher/DesktopServices.cpp index 17eb7c2df..841c1399c 100644 --- a/launcher/DesktopServices.cpp +++ b/launcher/DesktopServices.cpp @@ -37,140 +37,33 @@ #include #include #include - -/** - * This shouldn't exist, but until QTBUG-9328 and other unreported bugs are fixed, it needs to be a thing. - */ -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) - -#include -#include -#include -#include - -template -bool IndirectOpen(T callable, qint64* pid_forked = nullptr) -{ - auto pid = fork(); - if (pid_forked) { - if (pid > 0) - *pid_forked = pid; - else - *pid_forked = 0; - } - if (pid == -1) { - qWarning() << "IndirectOpen failed to fork: " << errno; - return false; - } - // child - do the stuff - if (pid == 0) { - // unset all this garbage so it doesn't get passed to the child process - qunsetenv("LD_PRELOAD"); - qunsetenv("LD_LIBRARY_PATH"); - qunsetenv("LD_DEBUG"); - qunsetenv("QT_PLUGIN_PATH"); - qunsetenv("QT_FONTPATH"); - - // open the URL - auto status = callable(); - - // detach from the parent process group. - setsid(); - - // die. now. do not clean up anything, it would just hang forever. - _exit(status ? 0 : 1); - } else { - // parent - assume it worked. - int status; - while (waitpid(pid, &status, 0)) { - if (WIFEXITED(status)) { - return WEXITSTATUS(status) == 0; - } - if (WIFSIGNALED(status)) { - return false; - } - } - return true; - } -} -#endif +#include "FileSystem.h" namespace DesktopServices { -bool openDirectory(const QString& path, [[maybe_unused]] bool ensureExists) +bool openPath(const QFileInfo& path, bool ensureFolderPathExists) { - qDebug() << "Opening directory" << path; - QDir parentPath; - QDir dir(path); - if (ensureExists && !dir.exists()) { - parentPath.mkpath(dir.absolutePath()); + qDebug() << "Opening path" << path; + if (ensureFolderPathExists) { + FS::ensureFolderPathExists(path); } - auto f = [&]() { return QDesktopServices::openUrl(QUrl::fromLocalFile(dir.absolutePath())); }; -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) - if (!isSandbox()) { - return IndirectOpen(f); - } -#endif - return f(); + return openUrl(QUrl::fromLocalFile(QFileInfo(path).absoluteFilePath())); } -bool openFile(const QString& path) +bool openPath(const QString& path, bool ensureFolderPathExists) { - qDebug() << "Opening file" << path; - auto f = [&]() { return QDesktopServices::openUrl(QUrl::fromLocalFile(path)); }; -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) - if (!isSandbox()) { - return IndirectOpen(f); - } else { - return f(); - } -#else - return f(); -#endif -} - -bool openFile(const QString& application, const QString& path, const QString& workingDirectory, qint64* pid) -{ - qDebug() << "Opening file" << path << "using" << application; -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) - // FIXME: the pid here is fake. So if something depends on it, it will likely misbehave - if (!isSandbox()) { - return IndirectOpen([&]() { return QProcess::startDetached(application, QStringList() << path, workingDirectory); }, pid); - } else { - return QProcess::startDetached(application, QStringList() << path, workingDirectory, pid); - } -#else - return QProcess::startDetached(application, QStringList() << path, workingDirectory, pid); -#endif + return openPath(QFileInfo(path), ensureFolderPathExists); } bool run(const QString& application, const QStringList& args, const QString& workingDirectory, qint64* pid) { qDebug() << "Running" << application << "with args" << args.join(' '); -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) - if (!isSandbox()) { - // FIXME: the pid here is fake. So if something depends on it, it will likely misbehave - return IndirectOpen([&]() { return QProcess::startDetached(application, args, workingDirectory); }, pid); - } else { - return QProcess::startDetached(application, args, workingDirectory, pid); - } -#else return QProcess::startDetached(application, args, workingDirectory, pid); -#endif } bool openUrl(const QUrl& url) { qDebug() << "Opening URL" << url.toString(); - auto f = [&]() { return QDesktopServices::openUrl(url); }; -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) - if (!isSandbox()) { - return IndirectOpen(f); - } else { - return f(); - } -#else - return f(); -#endif + return QDesktopServices::openUrl(url); } bool isFlatpak() @@ -191,9 +84,4 @@ bool isSnap() #endif } -bool isSandbox() -{ - return isSnap() || isFlatpak(); -} - } // namespace DesktopServices diff --git a/launcher/DesktopServices.h b/launcher/DesktopServices.h index 151db5557..6c6208e82 100644 --- a/launcher/DesktopServices.h +++ b/launcher/DesktopServices.h @@ -3,31 +3,30 @@ #include #include +class QFileInfo; + /** * This wraps around QDesktopServices and adds workarounds where needed * Use this instead of QDesktopServices! */ namespace DesktopServices { /** - * Open a file in whatever application is applicable + * Open a path in whatever application is applicable. + * @param ensureFolderPathExists Make sure the path exists */ -bool openFile(const QString& path); +bool openPath(const QFileInfo& path, bool ensureFolderPathExists = false); /** - * Open a file in the specified application + * Open a path in whatever application is applicable. + * @param ensureFolderPathExists Make sure the path exists */ -bool openFile(const QString& application, const QString& path, const QString& workingDirectory = QString(), qint64* pid = 0); +bool openPath(const QString& path, bool ensureFolderPathExists = false); /** * Run an application */ bool run(const QString& application, const QStringList& args, const QString& workingDirectory = QString(), qint64* pid = 0); -/** - * Open a directory - */ -bool openDirectory(const QString& path, bool ensureExists = false); - /** * Open the URL, most likely in a browser. Maybe. */ @@ -42,9 +41,4 @@ bool isFlatpak(); * Determine whether the launcher is running in a Snap environment */ bool isSnap(); - -/** - * Determine whether the launcher is running in a sandboxed (Flatpak or Snap) environment - */ -bool isSandbox(); } // namespace DesktopServices diff --git a/launcher/Exception.h b/launcher/Exception.h index ef1e4e0d8..55b40fdc8 100644 --- a/launcher/Exception.h +++ b/launcher/Exception.h @@ -1,4 +1,37 @@ -// Licensed under the Apache-2.0 license. See README.md for details. +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +/* + * 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 . + * + * 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 @@ -8,12 +41,12 @@ class Exception : public std::exception { public: - Exception(const QString& message) : std::exception(), m_message(message) { qCritical() << "Exception:" << message; } - Exception(const Exception& other) : std::exception(), m_message(other.cause()) {} + Exception(const QString& message) : std::exception(), m_message(message.toUtf8()) { qCritical() << "Exception:" << message; } + Exception(const Exception& other) : std::exception(), m_message(other.m_message) {} virtual ~Exception() noexcept {} - const char* what() const noexcept { return m_message.toLatin1().constData(); } - QString cause() const { return m_message; } + const char* what() const noexcept { return m_message.constData(); } + QString cause() const { return QString::fromUtf8(m_message); } private: - QString m_message; + QByteArray m_message; }; diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index a30d0ae0b..954e7936e 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -45,7 +45,6 @@ #include #include #include -#include #include #include #include @@ -54,6 +53,7 @@ #include #include "DesktopServices.h" +#include "PSaveFile.h" #include "StringUtils.h" #if defined Q_OS_WIN32 @@ -191,8 +191,8 @@ void ensureExists(const QDir& dir) void write(const QString& filename, const QByteArray& data) { ensureExists(QFileInfo(filename).dir()); - QSaveFile file(filename); - if (!file.open(QSaveFile::WriteOnly)) { + PSaveFile file(filename); + if (!file.open(PSaveFile::WriteOnly)) { throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString()); } if (data.size() != file.write(data)) { @@ -213,8 +213,8 @@ void appendSafe(const QString& filename, const QByteArray& data) buffer = QByteArray(); } buffer.append(data); - QSaveFile file(filename); - if (!file.open(QSaveFile::WriteOnly)) { + PSaveFile file(filename); + if (!file.open(PSaveFile::WriteOnly)) { throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString()); } if (buffer.size() != file.write(buffer)) { @@ -272,15 +272,22 @@ bool ensureFilePathExists(QString filenamepath) return success; } -bool ensureFolderPathExists(QString foldernamepath) +bool ensureFolderPathExists(const QFileInfo folderPath) { - QFileInfo a(foldernamepath); QDir dir; - QString ensuredPath = a.filePath(); + QString ensuredPath = folderPath.filePath(); + if (folderPath.exists()) + return true; + bool success = dir.mkpath(ensuredPath); return success; } +bool ensureFolderPathExists(const QString folderPathName) +{ + return ensureFolderPathExists(QFileInfo(folderPathName)); +} + bool copyFileAttributes(QString src, QString dst) { #ifdef Q_OS_WIN32 @@ -334,7 +341,7 @@ bool copy::operator()(const QString& offset, bool dryRun) opt |= copy_opts::overwrite_existing; // Function that'll do the actual copying - auto copy_file = [&](QString src_path, QString relative_dst_path) { + auto copy_file = [this, dryRun, src, dst, opt, &err](QString src_path, QString relative_dst_path) { if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist)) return; @@ -421,7 +428,7 @@ void create_link::make_link_list(const QString& offset) m_recursive = true; // Function that'll do the actual linking - auto link_file = [&](QString src_path, QString relative_dst_path) { + auto link_file = [this, dst](QString src_path, QString relative_dst_path) { if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist)) { qDebug() << "path" << relative_dst_path << "in black list or not in whitelist"; return; @@ -516,7 +523,7 @@ void create_link::runPrivileged(const QString& offset) QString serverName = BuildConfig.LAUNCHER_APP_BINARY_NAME + "_filelink_server" + StringUtils::getRandomAlphaNumeric(); - connect(&m_linkServer, &QLocalServer::newConnection, this, [&]() { + connect(&m_linkServer, &QLocalServer::newConnection, this, [this, &gotResults]() { qDebug() << "Client connected, sending out pairs"; // construct block of data to send QByteArray block; @@ -598,7 +605,7 @@ void create_link::runPrivileged(const QString& offset) } ExternalLinkFileProcess* linkFileProcess = new ExternalLinkFileProcess(serverName, m_useHardLinks, this); - connect(linkFileProcess, &ExternalLinkFileProcess::processExited, this, [&]() { emit finishedPrivileged(gotResults); }); + connect(linkFileProcess, &ExternalLinkFileProcess::processExited, this, [this, gotResults]() { emit finishedPrivileged(gotResults); }); connect(linkFileProcess, &ExternalLinkFileProcess::finished, linkFileProcess, &QObject::deleteLater); linkFileProcess->start(); @@ -643,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; @@ -650,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) @@ -797,16 +818,68 @@ QString NormalizePath(QString path) } } -QString badFilenameChars = "\"\\/?<>:;*|!+\r\n"; +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 (badFilenameChars.contains(string[i])) { + 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 path, QChar replaceWith) +{ + QString invalidChars; +#ifdef Q_OS_WIN + invalidChars = BAD_WIN_CHARS; +#endif + + // 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 string; + + return path; } QString DirNameFromString(QString string, QString inDir) @@ -848,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 = @@ -894,8 +971,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri if (!args.empty()) argstring = " \"" + args.join("\" \"") + "\""; - stream << "#!/bin/bash" - << "\n"; + stream << "#!/bin/bash" << "\n"; stream << "\"" << target << "\" " << argstring << "\n"; stream.flush(); @@ -939,12 +1015,9 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri if (!args.empty()) argstring = " '" + args.join("' '") + "'"; - stream << "[Desktop Entry]" - << "\n"; - stream << "Type=Application" - << "\n"; - stream << "Categories=Game;ActionGame;AdventureGame;Simulation" - << "\n"; + stream << "[Desktop Entry]" << "\n"; + stream << "Type=Application" << "\n"; + stream << "Categories=Game;ActionGame;AdventureGame;Simulation" << "\n"; stream << "Exec=\"" << target.toLocal8Bit() << "\"" << argstring.toLocal8Bit() << "\n"; stream << "Name=" << name.toLocal8Bit() << "\n"; if (!icon.isEmpty()) { @@ -1222,7 +1295,7 @@ bool clone::operator()(const QString& offset, bool dryRun) std::error_code err; // Function that'll do the actual cloneing - auto cloneFile = [&](QString src_path, QString relative_dst_path) { + auto cloneFile = [this, dryRun, dst, &err](QString src_path, QString relative_dst_path) { if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist)) return; @@ -1581,4 +1654,70 @@ uintmax_t hardLinkCount(const QString& path) return count; } +#ifdef Q_OS_WIN +// returns 8.3 file format from long path +QString shortPathName(const QString& file) +{ + auto input = file.toStdWString(); + std::wstring output; + long length = GetShortPathNameW(input.c_str(), NULL, 0); + if (length == 0) + return {}; + // NOTE: this resizing might seem weird... + // when GetShortPathNameW fails, it returns length including null character + // when it succeeds, it returns length excluding null character + // See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa364989(v=vs.85).aspx + output.resize(length); + if (GetShortPathNameW(input.c_str(), (LPWSTR)output.c_str(), length) == 0) + return {}; + output.resize(length - 1); + QString ret = QString::fromStdWString(output); + return ret; +} + +// if the string survives roundtrip through local 8bit encoding... +bool fitsInLocal8bit(const QString& string) +{ + return string == QString::fromLocal8Bit(string.toLocal8Bit()); +} + +QString getPathNameInLocal8bit(const QString& file) +{ + if (!fitsInLocal8bit(file)) { + auto path = shortPathName(file); + if (!path.isEmpty()) { + return path; + } + // in case shortPathName fails just return the path as is + } + return 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 861cfa267..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); @@ -91,7 +91,13 @@ bool ensureFilePathExists(QString filenamepath); * Creates all the folders in a path for the specified path * last segment of the path is treated as a folder name and is created! */ -bool ensureFolderPathExists(QString filenamepath); +bool ensureFolderPathExists(const QFileInfo folderPath); + +/** + * Creates all the folders in a path for the specified path + * last segment of the path is treated as a folder name and is created! + */ +bool ensureFolderPathExists(const QString folderPathName); /** * @brief Copies a directory and it's contents from src to dest @@ -234,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); @@ -336,6 +343,8 @@ QString NormalizePath(QString path); QString RemoveInvalidFilenameChars(QString string, QChar replaceWith = '-'); +QString RemoveInvalidPathChars(QString string, QChar replaceWith = '-'); + QString DirNameFromString(QString string, QString inDir = "."); /// Checks if the a given Path contains "!" @@ -370,6 +379,7 @@ enum class FilesystemType { HFSX, FUSEBLK, F2FS, + BCACHEFS, UNKNOWN }; @@ -398,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" } } }; /** @@ -450,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 @@ -545,4 +556,10 @@ bool canLink(const QString& src, const QString& dst); uintmax_t hardLinkCount(const QString& path); +#ifdef Q_OS_WIN +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 8abf30640..d335b11c4 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 (mcDir.exists() && !dotMCDir.exists()) - staging_mc_dir = mcDir.filePath(); - else - staging_mc_dir = dotMCDir.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"; @@ -77,7 +91,7 @@ void InstanceCopyTask::executeTask() QEventLoop loop; bool got_priv_results = false; - connect(&folderLink, &FS::create_link::finishedPrivileged, this, [&](bool gotResults) { + connect(&folderLink, &FS::create_link::finishedPrivileged, this, [&got_priv_results, &loop](bool gotResults) { if (!gotResults) { qDebug() << "Privileged run exited without results!"; } @@ -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); @@ -142,9 +156,8 @@ void InstanceCopyTask::copyFinished() if (!m_keepPlaytime) { inst->resetTimePlayed(); } - if (m_useLinks) - inst->addLinkedInstanceId(m_origInstance->id()); if (m_useLinks) { + inst->addLinkedInstanceId(m_origInstance->id()); auto allowed_symlinks_file = QFileInfo(FS::PathCombine(inst->gameRoot(), "allowed_symlinks.txt")); QByteArray allowed_symlinks; @@ -160,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(); @@ -171,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..bd3514798 100644 --- a/launcher/InstanceCreationTask.cpp +++ b/launcher/InstanceCreationTask.cpp @@ -2,8 +2,7 @@ #include #include - -InstanceCreationTask::InstanceCreationTask() = default; +#include "FileSystem.h" void InstanceCreationTask::executeTask() { @@ -39,22 +38,29 @@ void InstanceCreationTask::executeTask() // files scheduled to, and we'd better not let the user abort in the middle of it, since it'd // put the instance in an invalid state. if (shouldOverride()) { + bool deleteFailed = false; + setAbortable(false); setStatus(tr("Removing old conflicting files...")); qDebug() << "Removing old files"; - for (auto path : m_files_to_remove) { + for (const QString& path : m_files_to_remove) { if (!QFile::exists(path)) continue; + qDebug() << "Removing" << path; + if (!QFile::remove(path)) { - qCritical() << "Couldn't remove the old conflicting files."; - emitFailed(tr("Failed to remove old conflicting files.")); - return; + qCritical() << "Could not remove" << path; + deleteFailed = true; } } - } - emitSucceeded(); - return; + if (deleteFailed) { + emitFailed(tr("Failed to remove old conflicting files.")); + return; + } + } + if (!m_abort) + emitSucceeded(); } diff --git a/launcher/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 9f0cf7460..71630656d 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -56,6 +56,7 @@ #include #include +#include #include @@ -68,16 +69,11 @@ 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(); - } - - return Task::abort(); + bool wasAborted = false; + if (m_task) + wasAborted = m_task->abort(); + Task::abort(); + return wasAborted; } void InstanceImportTask::executeTask() @@ -89,7 +85,6 @@ void InstanceImportTask::executeTask() processZipPack(); } else { setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString())); - m_downloadRequired = true; downloadFromUrl(); } @@ -97,115 +92,133 @@ 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); + m_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; - extractDir.mkpath(".minecraft"); - extractDir.cd(".minecraft"); + 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, Qt::QueuedConnection); + 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); + }); + m_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; - } - + setAbortable(false); QDir extractDir(m_stagingPath); qDebug() << "Fixing permissions for extracted pack files..."; @@ -279,8 +292,11 @@ void InstanceImportTask::processFlame() inst_creation_task->setGroup(m_instGroup); inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); - connect(inst_creation_task.get(), &Task::succeeded, this, [this, inst_creation_task] { - setOverride(inst_creation_task->shouldOverride(), inst_creation_task->originalInstanceID()); + auto weak = inst_creation_task.toWeakRef(); + connect(inst_creation_task.get(), &Task::succeeded, this, [this, weak] { + if (auto sp = weak.lock()) { + setOverride(sp->shouldOverride(), sp->originalInstanceID()); + } emitSucceeded(); }); connect(inst_creation_task.get(), &Task::failed, this, &InstanceImportTask::emitFailed); @@ -289,11 +305,12 @@ void InstanceImportTask::processFlame() connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus); connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails); - connect(this, &Task::aborted, inst_creation_task.get(), &InstanceCreationTask::abort); connect(inst_creation_task.get(), &Task::aborted, this, &Task::abort); connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); - inst_creation_task->start(); + m_task.reset(inst_creation_task); + setAbortable(true); + m_task->start(); } void InstanceImportTask::processTechnic() @@ -324,13 +341,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(); @@ -338,7 +357,7 @@ void InstanceImportTask::processMultiMC() void InstanceImportTask::processModrinth() { - ModrinthCreationTask* inst_creation_task = nullptr; + shared_qobject_ptr inst_creation_task = nullptr; if (!m_extra_info.isEmpty()) { auto pack_id_it = m_extra_info.constFind("pack_id"); Q_ASSERT(pack_id_it != m_extra_info.constEnd()); @@ -355,7 +374,7 @@ void InstanceImportTask::processModrinth() original_instance_id = original_instance_id_it.value(); inst_creation_task = - new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id); + makeShared(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id); } else { QString pack_id; if (!m_sourceUrl.isEmpty()) { @@ -364,7 +383,7 @@ void InstanceImportTask::processModrinth() } // FIXME: Find a way to get the ID in directly imported ZIPs - inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id); + inst_creation_task = makeShared(m_stagingPath, m_globalSettings, m_parent, pack_id); } inst_creation_task->setName(*this); @@ -372,20 +391,23 @@ void InstanceImportTask::processModrinth() inst_creation_task->setGroup(m_instGroup); inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); - connect(inst_creation_task, &Task::succeeded, this, [this, inst_creation_task] { - setOverride(inst_creation_task->shouldOverride(), inst_creation_task->originalInstanceID()); + auto weak = inst_creation_task.toWeakRef(); + connect(inst_creation_task.get(), &Task::succeeded, this, [this, weak] { + if (auto sp = weak.lock()) { + setOverride(sp->shouldOverride(), sp->originalInstanceID()); + } emitSucceeded(); }); - connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed); - connect(inst_creation_task, &Task::progress, this, &InstanceImportTask::setProgress); - connect(inst_creation_task, &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress); - connect(inst_creation_task, &Task::status, this, &InstanceImportTask::setStatus); - connect(inst_creation_task, &Task::details, this, &InstanceImportTask::setDetails); - connect(inst_creation_task, &Task::finished, inst_creation_task, &InstanceCreationTask::deleteLater); + connect(inst_creation_task.get(), &Task::failed, this, &InstanceImportTask::emitFailed); + connect(inst_creation_task.get(), &Task::progress, this, &InstanceImportTask::setProgress); + connect(inst_creation_task.get(), &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress); + connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus); + connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails); - connect(this, &Task::aborted, inst_creation_task, &InstanceCreationTask::abort); - connect(inst_creation_task, &Task::aborted, this, &Task::abort); - connect(inst_creation_task, &Task::abortStatusChanged, this, &Task::setAbortable); + connect(inst_creation_task.get(), &Task::aborted, this, &Task::abort); + connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); - inst_creation_task->start(); + m_task.reset(inst_creation_task); + setAbortable(true); + m_task->start(); } diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index a1cf2560b..8884e0801 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -39,54 +39,35 @@ #include #include #include "InstanceTask.h" -#include "QObjectPtr.h" -#include "modplatform/flame/PackManifest.h" -#include "net/NetJob.h" -#include "settings/SettingsObject.h" - -#include class QuaZip; -namespace Flame { -class FileResolvingTask; -} class InstanceImportTask : public InstanceTask { Q_OBJECT public: explicit InstanceImportTask(const QUrl& sourceUrl, QWidget* parent = nullptr, QMap&& extra_info = {}); - + virtual ~InstanceImportTask() = default; 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; - shared_qobject_ptr m_modIdResolver; 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 m_task; enum class ModpackType { Unknown, MultiMC, diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index c884a4f12..918fa1073 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -371,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); @@ -486,7 +487,7 @@ InstanceList::InstListError InstanceList::loadList() int front_bookmark = -1; int back_bookmark = -1; int currentItem = -1; - auto removeNow = [&]() { + auto removeNow = [this, &front_bookmark, &back_bookmark, ¤tItem]() { beginRemoveRows(QModelIndex(), front_bookmark, back_bookmark); m_instances.erase(m_instances.begin() + front_bookmark, m_instances.begin() + back_bookmark + 1); endRemoveRows(); @@ -634,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 { @@ -709,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()); @@ -804,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."; } @@ -847,14 +864,16 @@ class InstanceStaging : public Task { const unsigned maxBackoff = 16; public: - InstanceStaging(InstanceList* parent, InstanceTask* child, QString stagingPath, InstanceName const& instanceName, QString groupName) - : m_parent(parent) - , backoff(minBackoff, maxBackoff) - , m_stagingPath(std::move(stagingPath)) - , m_instance_name(std::move(instanceName)) - , m_groupName(std::move(groupName)) + InstanceStaging(InstanceList* parent, InstanceTask* child, SettingsObjectPtr settings) + : m_parent(parent), backoff(minBackoff, maxBackoff) { + m_stagingPath = parent->getStagedInstancePath(); + m_child.reset(child); + + m_child->setStagingPath(m_stagingPath); + m_child->setParentSettings(std::move(settings)); + connect(child, &Task::succeeded, this, &InstanceStaging::childSucceeded); connect(child, &Task::failed, this, &InstanceStaging::childFailed); connect(child, &Task::aborted, this, &InstanceStaging::childAborted); @@ -866,7 +885,7 @@ class InstanceStaging : public Task { connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceeded); } - virtual ~InstanceStaging(){}; + virtual ~InstanceStaging() {} // FIXME/TODO: add ability to abort during instance commit retries bool abort() override @@ -881,14 +900,22 @@ class InstanceStaging : public Task { bool canAbort() const override { return (m_child && m_child->canAbort()); } protected: - virtual void executeTask() override { m_child->start(); } + virtual void executeTask() override + { + if (m_stagingPath.isNull()) { + emitFailed(tr("Could not create staging folder")); + return; + } + + m_child->start(); + } QStringList warnings() const override { return m_child->warnings(); } private slots: void childSucceeded() { unsigned sleepTime = backoff(); - if (m_parent->commitStagedInstance(m_stagingPath, m_instance_name, m_groupName, *m_child.get())) { + if (m_parent->commitStagedInstance(m_stagingPath, *m_child.get(), m_child->group(), *m_child.get())) { emitSucceeded(); return; } @@ -897,7 +924,7 @@ class InstanceStaging : public Task { emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something.")); return; } - qDebug() << "Failed to commit instance" << m_instance_name.name() << "Initiating backoff:" << sleepTime; + qDebug() << "Failed to commit instance" << m_child->name() << "Initiating backoff:" << sleepTime; m_backoffTimer.start(sleepTime * 500); } void childFailed(const QString& reason) @@ -906,7 +933,11 @@ class InstanceStaging : public Task { emitFailed(reason); } - void childAborted() { emitAborted(); } + void childAborted() + { + m_parent->destroyStagingPath(m_stagingPath); + emitAborted(); + } private: InstanceList* m_parent; @@ -918,34 +949,35 @@ class InstanceStaging : public Task { ExponentialSeries backoff; QString m_stagingPath; unique_qobject_ptr m_child; - InstanceName m_instance_name; - QString m_groupName; QTimer m_backoffTimer; }; Task* InstanceList::wrapInstanceTask(InstanceTask* task) { - auto stagingPath = getStagedInstancePath(); - task->setStagingPath(stagingPath); - task->setParentSettings(m_globalSettings); - return new InstanceStaging(this, task, stagingPath, *task, task->group()); + return new InstanceStaging(this, task, m_globalSettings); } QString InstanceList::getStagedInstancePath() { - QString key = QUuid::createUuid().toString(QUuid::WithoutBraces); - QString tempDir = ".LAUNCHER_TEMP/"; - QString relPath = FS::PathCombine(tempDir, key); - QDir rootPath(m_instDir); - auto path = FS::PathCombine(m_instDir, relPath); - if (!rootPath.mkpath(relPath)) { - return QString(); - } + const QString tempRoot = FS::PathCombine(m_instDir, ".tmp"); + + QString result; + int tries = 0; + + do { + if (++tries > 256) + return {}; + + const QString key = QUuid::createUuid().toString(QUuid::Id128).left(6); + result = FS::PathCombine(tempRoot, key); + } while (QFileInfo::exists(result)); + + if (!QDir::current().mkpath(result)) + return {}; #ifdef Q_OS_WIN32 - auto tempPath = FS::PathCombine(m_instDir, tempDir); - SetFileAttributesA(tempPath.toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); + SetFileAttributesA(tempRoot.toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); #endif - return path; + return result; } bool InstanceList::commitStagedInstance(const QString& path, @@ -956,7 +988,6 @@ bool InstanceList::commitStagedInstance(const QString& path, if (groupName.isEmpty() && !groupName.isNull()) groupName = QString(); - QDir dir; QString instID; InstancePtr inst; @@ -980,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..1d7c193f8 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,11 +39,11 @@ 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"))); - values.append(new InstanceSettingsPage(onesix.get())); + values.append(new InstanceSettingsPage(onesix)); auto logMatcher = inst->getLogFileMatcher(); if (logMatcher) { values.append(new OtherLogsPage(inst->getLogFileRoot(), logMatcher)); diff --git a/launcher/InstanceTask.cpp b/launcher/InstanceTask.cpp index 5b2398268..be10bbe07 100644 --- a/launcher/InstanceTask.cpp +++ b/launcher/InstanceTask.cpp @@ -1,7 +1,11 @@ #include "InstanceTask.h" +#include "Application.h" +#include "settings/SettingsObject.h" #include "ui/dialogs/CustomMessageBox.h" +#include + InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name) { auto dialog = @@ -20,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( @@ -27,16 +34,15 @@ ShouldUpdate askIfShouldUpdate(QWidget* parent, QString original_version_name) "separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before " "updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).") .arg(original_version_name), - QMessageBox::Information, QMessageBox::Ok | QMessageBox::Reset | QMessageBox::Abort); - info->setButtonText(QMessageBox::Ok, QObject::tr("Update existing instance")); - info->setButtonText(QMessageBox::Abort, QObject::tr("Create new instance")); - info->setButtonText(QMessageBox::Reset, QObject::tr("Cancel")); + QMessageBox::Information, QMessageBox::Cancel); + QAbstractButton* update = info->addButton(QObject::tr("Update existing instance"), QMessageBox::AcceptRole); + QAbstractButton* skip = info->addButton(QObject::tr("Create new instance"), QMessageBox::ResetRole); info->exec(); - if (info->clickedButton() == info->button(QMessageBox::Ok)) + if (info->clickedButton() == update) return ShouldUpdate::Update; - if (info->clickedButton() == info->button(QMessageBox::Abort)) + if (info->clickedButton() == skip) return ShouldUpdate::SkipUpdating; return ShouldUpdate::Cancel; } diff --git a/launcher/JavaCommon.cpp b/launcher/JavaCommon.cpp index e16ac9255..188edb943 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)); 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)); 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 8a8fa00a8..0aded4a95 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -36,13 +36,14 @@ #include "LaunchController.h" #include "Application.h" +#include "launch/steps/PrintServers.h" #include "minecraft/auth/AccountData.h" #include "minecraft/auth/AccountList.h" #include "ui/InstanceWindow.h" #include "ui/MainWindow.h" #include "ui/dialogs/CustomMessageBox.h" -#include "ui/dialogs/EditAccountDialog.h" +#include "ui/dialogs/MSALoginDialog.h" #include "ui/dialogs/ProfileSelectDialog.h" #include "ui/dialogs/ProfileSetupDialog.h" #include "ui/dialogs/ProgressDialog.h" @@ -53,15 +54,15 @@ #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) {} +LaunchController::LaunchController() : Task() {} void LaunchController::executeTask() { @@ -86,7 +87,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 " @@ -130,12 +131,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; } @@ -144,6 +196,13 @@ void LaunchController::login() bool tryagain = true; unsigned int tries = 0; + 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(); + accounts->requestRefresh(m_accountToUse->internalId()); + } while (tryagain) { if (tries > 0 && tries % 3 == 0) { auto result = @@ -176,24 +235,17 @@ 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 name; + if (m_offlineName.isEmpty()) { + name = askOfflineName(m_session->player_name, m_session->demo, ok); + if (!ok) { + tryagain = false; + break; + } + } else { + name = m_offlineName; } - - 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); - 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()) { @@ -213,20 +265,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.")); @@ -250,17 +292,9 @@ void LaunchController::login() progDialog.execWithTask(task.get()); continue; } - // FIXME: this is missing - the meaning is that the account is queued for refresh and we should wait for that - /* - case AccountState::Queued: { - return; - } - */ case AccountState::Expired: { - auto errorString = tr("The account has expired and needs to be logged into manually again."); - QMessageBox::warning(m_parentWidget, tr("Account refresh failed"), errorString, QMessageBox::StandardButton::Ok, - QMessageBox::StandardButton::Ok); - emitFailed(errorString); + if (reauthenticateCurrentAccount()) + continue; return; } case AccountState::Disabled: { @@ -284,6 +318,33 @@ void LaunchController::login() emitFailed(tr("Failed to launch.")); } +bool LaunchController::reauthenticateCurrentAccount() +{ + auto button = + QMessageBox::warning(m_parentWidget, tr("Account refresh failed"), + tr("The account has expired and needs to be reauthenticated. Do you want to reauthenticate this account?"), + QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No, QMessageBox::StandardButton::Yes); + if (button == QMessageBox::StandardButton::Yes) { + auto accounts = APPLICATION->accounts(); + bool isDefault = accounts->defaultAccount() == m_accountToUse; + accounts->removeAccount(accounts->index(accounts->findAccountByProfileId(m_accountToUse->profileId()))); + if (m_accountToUse->accountType() == AccountType::MSA) { + auto newAccount = MSALoginDialog::newAccount(m_parentWidget); + accounts->addAccount(newAccount); + if (isDefault) { + accounts->setDefaultAccount(newAccount); + } + m_accountToUse = nullptr; + decideAccount(); + return true; + } + emitFailed(tr("Account expired and re-login attempt failed")); + } else { + emitFailed(tr("The account has expired and needs to be reauthenticated")); + } + return false; +} + void LaunchController::launchInstance() { Q_ASSERT_X(m_instance != NULL, "launchInstance", "instance is NULL"); @@ -295,7 +356,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; @@ -317,26 +378,9 @@ void LaunchController::launchInstance() online_mode = "online"; // Prepend Server Status - QStringList servers = { "authserver.mojang.com", "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..7e6a27d91 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 { @@ -47,8 +47,8 @@ class LaunchController : public Task { public: void executeTask() override; - LaunchController(QObject* parent = nullptr); - virtual ~LaunchController(){}; + LaunchController(); + virtual ~LaunchController() = default; void setInstance(InstancePtr instance) { m_instance = instance; } @@ -56,13 +56,15 @@ class LaunchController : public Task { void setOnline(bool online) { m_online = online; } + void setOfflineName(const QString& offlineName) { m_offlineName = offlineName; } + void setDemo(bool demo) { m_demo = demo; } void setProfiler(BaseProfilerFactory* profiler) { m_profiler = profiler; } 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 +76,9 @@ class LaunchController : public Task { void login(); void launchInstance(); void decideAccount(); + bool askPlayDemo(); + QString askOfflineName(QString playerName, bool demo, bool& ok); + bool reauthenticateCurrentAccount(); private slots: void readyForLaunch(); @@ -85,6 +90,7 @@ class LaunchController : public Task { private: BaseProfilerFactory* m_profiler = nullptr; bool m_online = true; + QString m_offlineName; bool m_demo = false; InstancePtr m_instance; QWidget* m_parentWidget = nullptr; @@ -92,5 +98,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/Launcher.in b/launcher/Launcher.in index 1a23f2555..706d7022b 100755 --- a/launcher/Launcher.in +++ b/launcher/Launcher.in @@ -39,8 +39,16 @@ if [ "x$DEPS_LIST" = "x" ]; then # Just to be sure... chmod +x "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" + ARGS=("${LAUNCHER_DIR}/${LAUNCHER_NAME}" "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}") + + if [ -f portable.txt ]; then + ARGS+=("-d" "${LAUNCHER_DIR}") + fi + + ARGS+=("$@") + # Run the launcher - exec -a "${LAUNCHER_DIR}/${LAUNCHER_NAME}" "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" -d "${LAUNCHER_DIR}" "$@" + exec -a "${ARGS[@]}" # Run the launcher in valgrind # valgrind --log-file="valgrind.log" --leak-check=full --track-origins=yes "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" -d "${LAUNCHER_DIR}" "$@" diff --git a/launcher/LoggedProcess.cpp b/launcher/LoggedProcess.cpp index fadd64e68..35ce4e0e5 100644 --- a/launcher/LoggedProcess.cpp +++ b/launcher/LoggedProcess.cpp @@ -39,7 +39,8 @@ #include #include "MessageLevel.h" -LoggedProcess::LoggedProcess(QObject* parent) : QProcess(parent) +LoggedProcess::LoggedProcess(const QTextCodec* output_codec, QObject* parent) + : QProcess(parent), m_err_decoder(output_codec), m_out_decoder(output_codec) { // QProcess has a strange interface... let's map a lot of those into a few. connect(this, &QProcess::readyReadStandardOutput, this, &LoggedProcess::on_stdOut); diff --git a/launcher/LoggedProcess.h b/launcher/LoggedProcess.h index 46bdaa830..75ba15dfd 100644 --- a/launcher/LoggedProcess.h +++ b/launcher/LoggedProcess.h @@ -49,7 +49,7 @@ class LoggedProcess : public QProcess { enum State { NotRunning, Starting, FailedToStart, Running, Finished, Crashed, Aborted }; public: - explicit LoggedProcess(QObject* parent = 0); + explicit LoggedProcess(const QTextCodec* output_codec = QTextCodec::codecForLocale(), QObject* parent = 0); virtual ~LoggedProcess(); State state() const; @@ -80,8 +80,8 @@ class LoggedProcess : public QProcess { QStringList reprocess(const QByteArray& data, QTextDecoder& decoder); private: - QTextDecoder m_err_decoder = QTextDecoder(QTextCodec::codecForLocale()); - QTextDecoder m_out_decoder = QTextDecoder(QTextCodec::codecForLocale()); + QTextDecoder m_err_decoder; + QTextDecoder m_out_decoder; QString m_leftover_line; bool m_killed = false; State m_state = NotRunning; diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index ce2573329..b38aca17a 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) @@ -119,9 +120,10 @@ bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool follow bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks) { QuaZip zip(fileCompressed); + zip.setUtf8Enabled(true); QDir().mkpath(QFileInfo(fileCompressed).absolutePath()); if (!zip.open(QuaZip::mdCreate)) { - QFile::remove(fileCompressed); + FS::deletePath(fileCompressed); return false; } @@ -129,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; } @@ -141,8 +143,9 @@ bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods) { QuaZip zipOut(targetJarPath); + zipOut.setUtf8Enabled(true); if (!zipOut.open(QuaZip::mdCreate)) { - QFile::remove(targetJarPath); + FS::deletePath(targetJarPath); qCritical() << "Failed to open the minecraft.jar for modding"; return false; } @@ -160,7 +163,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; } @@ -169,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; } @@ -192,7 +195,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QListfileinfo().fileName() << "to the jar."; return false; } @@ -200,7 +203,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QListfileinfo().fileName() << "to the jar."; return false; } @@ -208,7 +211,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList extractSubDir(QuaZip* zip, const QString& subdir, con do { QString file_name = zip->getCurrentFileName(); + file_name = FS::RemoveInvalidPathChars(file_name); if (!file_name.startsWith(subdir)) continue; - auto relative_file_name = QDir::fromNativeSeparators(file_name.remove(0, subdir.size())); + auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(subdir.size())); auto original_name = relative_file_name; // Fix subdirs/files ending with a / getting transformed into absolute paths @@ -327,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()); @@ -352,7 +378,7 @@ std::optional extractDir(QString fileCompressed, QString dir) if (fileInfo.size() == 22) { return QStringList(); } - qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError(); + qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError(); ; return std::nullopt; } @@ -369,7 +395,7 @@ std::optional extractDir(QString fileCompressed, QString subdir, QS if (fileInfo.size() == 22) { return QStringList(); } - qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError(); + qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError(); ; return std::nullopt; } @@ -386,7 +412,7 @@ bool extractFile(QString fileCompressed, QString file, QString target) if (fileInfo.size() == 22) { return true; } - qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError(); + qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError(); return false; } return extractRelFile(&zip, file, target); @@ -463,7 +489,7 @@ auto ExportToZipTask::exportZip() -> ZipResult auto absolute = file.absoluteFilePath(); auto relative = m_dir.relativeFilePath(absolute); - setStatus("Compresing: " + relative); + setStatus("Compressing: " + relative); setProgress(m_progress + 1, m_progressTotal); if (m_follow_symlinks) { if (file.isSymLink()) @@ -487,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(); @@ -507,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("Unpacking: " + 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 93692d0d2..d81df9d81 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 @@ -153,8 +153,14 @@ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, Q #if defined(LAUNCHER_APPLICATION) class ExportToZipTask : public Task { + Q_OBJECT public: - ExportToZipTask(QString outputPath, QDir dir, 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,9 +169,15 @@ class ExportToZipTask : public Task { , m_follow_symlinks(followSymlinks) { setAbortable(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; @@ -194,5 +206,34 @@ class ExportToZipTask : public Task { QFuture m_build_zip_future; QFutureWatcher m_build_zip_watcher; }; + +class ExtractZipTask : public Task { + Q_OBJECT + 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/MTPixmapCache.h b/launcher/MTPixmapCache.h index b6bd13045..0ba9c5ac8 100644 --- a/launcher/MTPixmapCache.h +++ b/launcher/MTPixmapCache.h @@ -101,7 +101,7 @@ class PixmapCache final : public QObject { */ bool _markCacheMissByEviciton() { - static constexpr uint maxInt = static_cast(std::numeric_limits::max()); + static constexpr uint maxCache = static_cast(std::numeric_limits::max()) / 4; static constexpr uint step = 10240; static constexpr int oneSecond = 1000; @@ -118,8 +118,8 @@ class PixmapCache final : public QObject { if (m_consecutive_fast_evicitons >= m_consecutive_fast_evicitons_threshold) { // increase the cache size uint newSize = _cacheLimit() + step; - if (newSize >= maxInt) { // increase it until you overflow :D - newSize = maxInt; + if (newSize >= maxCache) { // increase it until you overflow :D + newSize = maxCache; qDebug() << m_consecutive_fast_evicitons << tr("pixmap cache misses by eviction happened too fast, doing nothing as the cache size reached it's limit"); } else { diff --git a/launcher/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/PSaveFile.h b/launcher/PSaveFile.h new file mode 100644 index 000000000..ba6154ad8 --- /dev/null +++ b/launcher/PSaveFile.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023-2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include +#include +#include "Application.h" + +#if defined(LAUNCHER_APPLICATION) + +/* PSaveFile + * A class that mimics QSaveFile for Windows. + * + * When reading resources, we need to avoid accessing temporary files + * generated by QSaveFile. If we start reading such a file, we may + * inadvertently keep it open while QSaveFile is trying to remove it, + * or we might detect the file just before it is removed, leading to + * race conditions and errors. + * + * Unfortunately, QSaveFile doesn't provide a way to retrieve the + * temporary file name or to set a specific template for the temporary + * file name it uses. By default, QSaveFile appends a `.XXXXXX` suffix + * to the original file name, where the `XXXXXX` part is dynamically + * generated to ensure uniqueness. + * + * This class acts like a lock by adding and removing the target file + * name into/from a global string set, helping to manage access to + * files during critical operations. + * + * Note: Please do not use the `setFileName` function directly, as it + * is not virtual and cannot be overridden. + */ +class PSaveFile : public QSaveFile { + public: + PSaveFile(const QString& name) : QSaveFile(name) { addPath(name); } + PSaveFile(const QString& name, QObject* parent) : QSaveFile(name, parent) { addPath(name); } + virtual ~PSaveFile() + { + if (auto app = APPLICATION_DYN) { + app->removeQSavePath(m_absoluteFilePath); + } + } + + private: + void addPath(const QString& path) + { + m_absoluteFilePath = QFileInfo(path).absoluteFilePath() + "."; // add dot for tmp files only + if (auto app = APPLICATION_DYN) { + app->addQSavePath(m_absoluteFilePath); + } + } + QString m_absoluteFilePath; +}; +#else +#define PSaveFile QSaveFile +#endif \ No newline at end of file diff --git a/launcher/QObjectPtr.h b/launcher/QObjectPtr.h index a1c64b433..88c17c0b2 100644 --- a/launcher/QObjectPtr.h +++ b/launcher/QObjectPtr.h @@ -33,7 +33,7 @@ class shared_qobject_ptr : public QSharedPointer { {} void reset() { QSharedPointer::reset(); } - void reset(T*&& other) + void reset(T* other) { shared_qobject_ptr t(other); this->swap(t); diff --git a/launcher/ResourceDownloadTask.cpp b/launcher/ResourceDownloadTask.cpp index a02151ca1..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, @@ -33,9 +35,9 @@ ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, QString custom_target_folder) : m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs), m_custom_target_folder(custom_target_folder) { - if (auto model = dynamic_cast(m_pack_model.get()); model && is_indexed) { - m_update_task.reset(new LocalModUpdateTask(model->indexDir(), *m_pack, m_pack_version)); - connect(m_update_task.get(), &LocalModUpdateTask::hasOldMod, this, &ResourceDownloadTask::hasOldResource); + if (is_indexed) { + m_update_task.reset(new LocalResourceUpdateTask(m_pack_model->indexDir(), *m_pack, m_pack_version)); + connect(m_update_task.get(), &LocalResourceUpdateTask::hasOldResource, this, &ResourceDownloadTask::hasOldResource); addTask(m_update_task); } @@ -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); @@ -67,12 +91,8 @@ void ResourceDownloadTask::downloadSucceeded() m_filesNetJob.reset(); auto name = std::get<0>(to_delete); auto filename = std::get<1>(to_delete); - if (!name.isEmpty() && filename != m_pack_version.fileName) { - if (auto model = dynamic_cast(m_pack_model.get()); model) - model->uninstallMod(filename, true); - else - m_pack_model->uninstallResource(filename); - } + if (!name.isEmpty() && filename != m_pack_version.fileName) + m_pack_model->uninstallResource(filename, true); } void ResourceDownloadTask::downloadFailed(QString reason) diff --git a/launcher/ResourceDownloadTask.h b/launcher/ResourceDownloadTask.h index f686e819a..a10e0ac2c 100644 --- a/launcher/ResourceDownloadTask.h +++ b/launcher/ResourceDownloadTask.h @@ -22,7 +22,7 @@ #include "net/NetJob.h" #include "tasks/SequentialTask.h" -#include "minecraft/mod/tasks/LocalModUpdateTask.h" +#include "minecraft/mod/tasks/LocalResourceUpdateTask.h" #include "modplatform/ModIndex.h" class ResourceFolderModel; @@ -50,7 +50,7 @@ class ResourceDownloadTask : public SequentialTask { QString m_custom_target_folder; NetJob::Ptr m_filesNetJob; - LocalModUpdateTask::Ptr m_update_task; + LocalResourceUpdateTask::Ptr m_update_task; void downloadProgressChanged(qint64 current, qint64 total); void downloadFailed(QString reason); 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..cfcf63805 --- /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-x64"; + if (arch.contains("86")) + return "mac-os-x86"; + // 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/Version.cpp b/launcher/Version.cpp index 511aa9c35..03a16e8a0 100644 --- a/launcher/Version.cpp +++ b/launcher/Version.cpp @@ -79,7 +79,7 @@ void Version::parse() if (m_string.isEmpty()) return; - auto classChange = [&](QChar lastChar, QChar currentChar) { + auto classChange = [¤tSection](QChar lastChar, QChar currentChar) { if (lastChar.isNull()) return false; if (lastChar.isDigit() != currentChar.isDigit()) @@ -123,8 +123,7 @@ QDebug operator<<(QDebug debug, const Version& v) first = false; } - debug.nospace() << " ]" - << " }"; + debug.nospace() << " ]" << " }"; return debug; } diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp index 0ab9ae2c3..12a82f73d 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/filelink/FileLink.cpp b/launcher/filelink/FileLink.cpp index bdf173ebc..b641b41d5 100644 --- a/launcher/filelink/FileLink.cpp +++ b/launcher/filelink/FileLink.cpp @@ -104,11 +104,11 @@ void FileLinkApp::joinServer(QString server) in.setDevice(&socket); - connect(&socket, &QLocalSocket::connected, this, [&]() { qDebug() << "connected to server"; }); + connect(&socket, &QLocalSocket::connected, this, []() { qDebug() << "connected to server"; }); connect(&socket, &QLocalSocket::readyRead, this, &FileLinkApp::readPathPairs); - connect(&socket, &QLocalSocket::errorOccurred, this, [&](QLocalSocket::LocalSocketError socketError) { + connect(&socket, &QLocalSocket::errorOccurred, this, [this](QLocalSocket::LocalSocketError socketError) { m_status = Failed; switch (socketError) { case QLocalSocket::ServerNotFoundError: @@ -132,7 +132,7 @@ void FileLinkApp::joinServer(QString server) } }); - connect(&socket, &QLocalSocket::disconnected, this, [&]() { + connect(&socket, &QLocalSocket::disconnected, this, [this]() { qDebug() << "disconnected from server, should exit"; m_status = Succeeded; exit(); diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp index 5576b9745..f4022e0fb 100644 --- a/launcher/icons/IconList.cpp +++ b/launcher/icons/IconList.cpp @@ -47,24 +47,24 @@ #define MAX_SIZE 1024 -IconList::IconList(const QStringList& builtinPaths, QString path, QObject* parent) : QAbstractListModel(parent) +IconList::IconList(const QStringList& builtinPaths, const QString& path, QObject* parent) : QAbstractListModel(parent) { QSet builtinNames; // add builtin icons - for (auto& builtinPath : builtinPaths) { - QDir instance_icons(builtinPath); - auto file_info_list = instance_icons.entryInfoList(QDir::Files, QDir::Name); - for (auto file_info : file_info_list) { - builtinNames.insert(file_info.completeBaseName()); + for (const auto& builtinPath : builtinPaths) { + QDir instanceIcons(builtinPath); + auto fileInfoList = instanceIcons.entryInfoList(QDir::Files, QDir::Name); + for (const auto& fileInfo : fileInfoList) { + builtinNames.insert(fileInfo.baseName()); } } - for (auto& builtinName : builtinNames) { + for (const auto& builtinName : builtinNames) { addThemeIcon(builtinName); } m_watcher.reset(new QFileSystemWatcher()); - is_watching = false; + m_isWatching = false; connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &IconList::directoryChanged); connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &IconList::fileChanged); @@ -77,91 +77,131 @@ IconList::IconList(const QStringList& builtinPaths, QString path, QObject* paren void IconList::sortIconList() { qDebug() << "Sorting icon list..."; - std::sort(icons.begin(), icons.end(), [](const MMCIcon& a, const MMCIcon& b) { return a.m_key.localeAwareCompare(b.m_key) < 0; }); + std::sort(m_icons.begin(), m_icons.end(), [](const MMCIcon& a, const MMCIcon& b) { + bool aIsSubdir = a.m_key.contains(QDir::separator()); + bool bIsSubdir = b.m_key.contains(QDir::separator()); + if (aIsSubdir != bIsSubdir) { + return !aIsSubdir; // root-level icons come first + } + return a.m_key.localeAwareCompare(b.m_key) < 0; + }); reindex(); } +// Helper function to add directories recursively +bool IconList::addPathRecursively(const QString& path) +{ + QDir dir(path); + if (!dir.exists()) + return false; + + // Add the directory itself + bool watching = m_watcher->addPath(path); + + // Add all subdirectories + QFileInfoList entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QFileInfo& entry : entries) { + if (addPathRecursively(entry.absoluteFilePath())) { + watching = true; + } + } + return watching; +} + +QStringList IconList::getIconFilePaths() const +{ + QStringList iconFiles{}; + QStringList directories{ m_dir.absolutePath() }; + while (!directories.isEmpty()) { + QString first = directories.takeFirst(); + QDir dir(first); + for (QFileInfo& fileInfo : dir.entryInfoList(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot, QDir::Name)) { + if (fileInfo.isDir()) + directories.push_back(fileInfo.absoluteFilePath()); + else + iconFiles.push_back(fileInfo.absoluteFilePath()); + } + } + return iconFiles; +} + +QString formatName(const QDir& iconsDir, const QFileInfo& iconFile) +{ + if (iconFile.dir() == iconsDir) + return iconFile.baseName(); + + constexpr auto delimiter = " » "; + QString relativePathWithoutExtension = iconsDir.relativeFilePath(iconFile.dir().path()) + QDir::separator() + iconFile.baseName(); + return relativePathWithoutExtension.replace(QDir::separator(), delimiter); +} + +/// Split into a separate function because the preprocessing impedes readability +QSet toStringSet(const QList& list) +{ +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + QSet set(list.begin(), list.end()); +#else + QSet set = list.toSet(); +#endif + return set; +} + void IconList::directoryChanged(const QString& path) { - QDir new_dir(path); - if (m_dir.absolutePath() != new_dir.absolutePath()) { - m_dir.setPath(path); + QDir newDir(path); + if (m_dir.absolutePath() != newDir.absolutePath()) { + if (!path.startsWith(m_dir.absolutePath())) + m_dir.setPath(path); m_dir.refresh(); - if (is_watching) + if (m_isWatching) stopWatching(); startWatching(); } - if (!m_dir.exists()) - if (!FS::ensureFolderPathExists(m_dir.absolutePath())) - return; + if (!m_dir.exists() && !FS::ensureFolderPathExists(m_dir.absolutePath())) + return; m_dir.refresh(); - auto new_list = m_dir.entryList(QDir::Files, QDir::Name); - for (auto it = new_list.begin(); it != new_list.end(); it++) { - QString& foo = (*it); - foo = m_dir.filePath(foo); - } -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - QSet new_set(new_list.begin(), new_list.end()); -#else - auto new_set = new_list.toSet(); -#endif - QList current_list; - for (auto& it : icons) { + const QStringList newFileNamesList = getIconFilePaths(); + const QSet newSet = toStringSet(newFileNamesList); + QSet currentSet; + for (const MMCIcon& it : m_icons) { if (!it.has(IconType::FileBased)) continue; - current_list.push_back(it.m_images[IconType::FileBased].filename); + currentSet.insert(it.m_images[IconType::FileBased].filename); } -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - QSet current_set(current_list.begin(), current_list.end()); -#else - QSet current_set = current_list.toSet(); -#endif + QSet toRemove = currentSet - newSet; + QSet toAdd = newSet - currentSet; - QSet to_remove = current_set; - to_remove -= new_set; - - QSet to_add = new_set; - to_add -= current_set; - - for (auto remove : to_remove) { - qDebug() << "Removing " << remove; - QFileInfo rmfile(remove); - QString key = rmfile.completeBaseName(); - - QString suffix = rmfile.suffix(); - // The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well - if (!IconUtils::isIconSuffix(suffix)) - key = rmfile.fileName(); + for (const QString& removedPath : toRemove) { + qDebug() << "Removing icon " << removedPath; + QFileInfo removedFile(removedPath); + QString key = m_dir.relativeFilePath(removedFile.absoluteFilePath()); int idx = getIconIndex(key); if (idx == -1) continue; - icons[idx].remove(IconType::FileBased); - if (icons[idx].type() == IconType::ToBeDeleted) { + m_icons[idx].remove(FileBased); + if (m_icons[idx].type() == ToBeDeleted) { beginRemoveRows(QModelIndex(), idx, idx); - icons.remove(idx); + m_icons.remove(idx); reindex(); endRemoveRows(); } else { dataChanged(index(idx), index(idx)); } - m_watcher->removePath(remove); + m_watcher->removePath(removedPath); emit iconUpdated(key); } - for (auto add : to_add) { - qDebug() << "Adding " << add; + for (const QString& addedPath : toAdd) { + qDebug() << "Adding icon " << addedPath; - QFileInfo addfile(add); - QString key = addfile.completeBaseName(); + QFileInfo addfile(addedPath); + QString relativePath = m_dir.relativeFilePath(addfile.absoluteFilePath()); + QString key = QFileInfo(relativePath).completeBaseName(); + QString name = formatName(m_dir, addfile); - QString suffix = addfile.suffix(); - // The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well - if (!IconUtils::isIconSuffix(suffix)) - key = addfile.fileName(); - - if (addIcon(key, QString(), addfile.filePath(), IconType::FileBased)) { - m_watcher->addPath(add); + if (addIcon(key, name, addfile.filePath(), IconType::FileBased)) { + m_watcher->addPath(addedPath); emit iconUpdated(key); } } @@ -171,24 +211,24 @@ void IconList::directoryChanged(const QString& path) void IconList::fileChanged(const QString& path) { - qDebug() << "Checking " << path; + qDebug() << "Checking icon " << path; QFileInfo checkfile(path); if (!checkfile.exists()) return; - QString key = checkfile.completeBaseName(); + QString key = m_dir.relativeFilePath(checkfile.absoluteFilePath()); int idx = getIconIndex(key); if (idx == -1) return; QIcon icon(path); - if (!icon.availableSizes().size()) + if (icon.availableSizes().empty()) return; - icons[idx].m_images[IconType::FileBased].icon = icon; + m_icons[idx].m_images[IconType::FileBased].icon = icon; dataChanged(index(idx), index(idx)); emit iconUpdated(key); } -void IconList::SettingChanged(const Setting& setting, QVariant value) +void IconList::SettingChanged(const Setting& setting, const QVariant& value) { if (setting.id() != "IconsDir") return; @@ -200,8 +240,8 @@ void IconList::startWatching() { auto abs_path = m_dir.absolutePath(); FS::ensureFolderPathExists(abs_path); - is_watching = m_watcher->addPath(abs_path); - if (is_watching) { + m_isWatching = addPathRecursively(abs_path); + if (m_isWatching) { qDebug() << "Started watching " << abs_path; } else { qDebug() << "Failed to start watching " << abs_path; @@ -212,7 +252,7 @@ void IconList::stopWatching() { m_watcher->removePaths(m_watcher->files()); m_watcher->removePaths(m_watcher->directories()); - is_watching = false; + m_isWatching = false; } QStringList IconList::mimeTypes() const @@ -242,7 +282,7 @@ bool IconList::dropMimeData(const QMimeData* data, if (data->hasUrls()) { auto urls = data->urls(); QStringList iconFiles; - for (auto url : urls) { + for (const auto& url : urls) { // only local files may be dropped... if (!url.isLocalFile()) continue; @@ -263,33 +303,33 @@ Qt::ItemFlags IconList::flags(const QModelIndex& index) const QVariant IconList::data(const QModelIndex& index, int role) const { if (!index.isValid()) - return QVariant(); + return {}; int row = index.row(); - if (row < 0 || row >= icons.size()) - return QVariant(); + if (row < 0 || row >= m_icons.size()) + return {}; switch (role) { case Qt::DecorationRole: - return icons[row].icon(); + return m_icons[row].icon(); case Qt::DisplayRole: - return icons[row].name(); + return m_icons[row].name(); case Qt::UserRole: - return icons[row].m_key; + return m_icons[row].m_key; default: - return QVariant(); + return {}; } } int IconList::rowCount(const QModelIndex& parent) const { - return parent.isValid() ? 0 : icons.size(); + return parent.isValid() ? 0 : m_icons.size(); } void IconList::installIcons(const QStringList& iconFiles) { - for (QString file : iconFiles) + for (const QString& file : iconFiles) installIcon(file, {}); } @@ -312,17 +352,18 @@ bool IconList::iconFileExists(const QString& key) const return iconEntry && iconEntry->has(IconType::FileBased); } +/// Returns the icon with the given key or nullptr if it doesn't exist. const MMCIcon* IconList::icon(const QString& key) const { int iconIdx = getIconIndex(key); if (iconIdx == -1) return nullptr; - return &icons[iconIdx]; + return &m_icons[iconIdx]; } 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) @@ -332,22 +373,22 @@ bool IconList::trashIcon(const QString& key) bool IconList::addThemeIcon(const QString& key) { - auto iter = name_index.find(key); - if (iter != name_index.end()) { - auto& oldOne = icons[*iter]; + auto iter = m_nameIndex.find(key); + if (iter != m_nameIndex.end()) { + auto& oldOne = m_icons[*iter]; oldOne.replace(Builtin, key); dataChanged(index(*iter), index(*iter)); return true; } // add a new icon - beginInsertRows(QModelIndex(), icons.size(), icons.size()); + beginInsertRows(QModelIndex(), m_icons.size(), m_icons.size()); { MMCIcon mmc_icon; mmc_icon.m_name = key; mmc_icon.m_key = key; mmc_icon.replace(Builtin, key); - icons.push_back(mmc_icon); - name_index[key] = icons.size() - 1; + m_icons.push_back(mmc_icon); + m_nameIndex[key] = m_icons.size() - 1; } endInsertRows(); return true; @@ -359,22 +400,22 @@ bool IconList::addIcon(const QString& key, const QString& name, const QString& p QIcon icon(path); if (icon.isNull()) return false; - auto iter = name_index.find(key); - if (iter != name_index.end()) { - auto& oldOne = icons[*iter]; + auto iter = m_nameIndex.find(key); + if (iter != m_nameIndex.end()) { + auto& oldOne = m_icons[*iter]; oldOne.replace(type, icon, path); dataChanged(index(*iter), index(*iter)); return true; } // add a new icon - beginInsertRows(QModelIndex(), icons.size(), icons.size()); + beginInsertRows(QModelIndex(), m_icons.size(), m_icons.size()); { MMCIcon mmc_icon; mmc_icon.m_name = name; mmc_icon.m_key = key; mmc_icon.replace(type, icon, path); - icons.push_back(mmc_icon); - name_index[key] = icons.size() - 1; + m_icons.push_back(mmc_icon); + m_nameIndex[key] = m_icons.size() - 1; } endInsertRows(); return true; @@ -389,33 +430,32 @@ void IconList::saveIcon(const QString& key, const QString& path, const char* for void IconList::reindex() { - name_index.clear(); - int i = 0; - for (auto& iter : icons) { - name_index[iter.m_key] = i; - i++; + m_nameIndex.clear(); + for (int i = 0; i < m_icons.size(); i++) { + m_nameIndex[m_icons[i].m_key] = i; + emit iconUpdated(m_icons[i].m_key); // prevents incorrect indices with proxy model } } QIcon IconList::getIcon(const QString& key) const { - int icon_index = getIconIndex(key); + int iconIndex = getIconIndex(key); - if (icon_index != -1) - return icons[icon_index].icon(); + if (iconIndex != -1) + return m_icons[iconIndex].icon(); - // Fallback for icons that don't exist. - icon_index = getIconIndex("grass"); + // Fallback for icons that don't exist.b + iconIndex = getIconIndex("grass"); - if (icon_index != -1) - return icons[icon_index].icon(); - return QIcon(); + if (iconIndex != -1) + return m_icons[iconIndex].icon(); + return {}; } int IconList::getIconIndex(const QString& key) const { - auto iter = name_index.find(key == "default" ? "grass" : key); - if (iter != name_index.end()) + auto iter = m_nameIndex.find(key == "default" ? "grass" : key); + if (iter != m_nameIndex.end()) return *iter; return -1; @@ -425,3 +465,15 @@ QString IconList::getDirectory() const { return m_dir.absolutePath(); } + +/// Returns the directory of the icon with the given key or the default directory if it's a builtin icon. +QString IconList::iconDirectory(const QString& key) const +{ + for (const auto& mmcIcon : m_icons) { + if (mmcIcon.m_key == key && mmcIcon.has(IconType::FileBased)) { + QFileInfo iconFile(mmcIcon.getFilePath()); + return iconFile.dir().path(); + } + } + return getDirectory(); +} diff --git a/launcher/icons/IconList.h b/launcher/icons/IconList.h index c51826057..8936195c3 100644 --- a/launcher/icons/IconList.h +++ b/launcher/icons/IconList.h @@ -51,8 +51,8 @@ class QFileSystemWatcher; class IconList : public QAbstractListModel { Q_OBJECT public: - explicit IconList(const QStringList& builtinPaths, QString path, QObject* parent = 0); - virtual ~IconList(){}; + explicit IconList(const QStringList& builtinPaths, const QString& path, QObject* parent = 0); + virtual ~IconList() {}; QIcon getIcon(const QString& key) const; int getIconIndex(const QString& key) const; @@ -72,6 +72,7 @@ class IconList : public QAbstractListModel { bool deleteIcon(const QString& key); bool trashIcon(const QString& key); bool iconFileExists(const QString& key) const; + QString iconDirectory(const QString& key) const; void installIcons(const QStringList& iconFiles); void installIcon(const QString& file, const QString& name); @@ -91,18 +92,20 @@ class IconList : public QAbstractListModel { IconList& operator=(const IconList&) = delete; void reindex(); void sortIconList(); + bool addPathRecursively(const QString& path); + QStringList getIconFilePaths() const; public slots: void directoryChanged(const QString& path); protected slots: void fileChanged(const QString& path); - void SettingChanged(const Setting& setting, QVariant value); + void SettingChanged(const Setting& setting, const QVariant& value); private: shared_qobject_ptr m_watcher; - bool is_watching; - QMap name_index; - QVector icons; + bool m_isWatching; + QMap m_nameIndex; + QVector m_icons; QDir m_dir; }; 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 20caba189..07b5d7b40 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) + : Task(), 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(); @@ -55,6 +56,9 @@ void JavaChecker::performCheck() qDebug() << "Java checker library could not be found. Please check your installation."; return; } +#ifdef Q_OS_WIN + checkerJar = FS::getPathNameInLocal8bit(checkerJar); +#endif QStringList args; @@ -69,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); } @@ -112,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; @@ -124,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; } @@ -158,17 +162,18 @@ 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; } auto os_arch = results["os.arch"]; auto java_version = results["java.version"]; auto java_vendor = results["java.vendor"]; - bool is_64 = os_arch == "x86_64" || os_arch == "amd64" || os_arch == "aarch64" || os_arch == "arm64"; + bool is_64 = os_arch == "x86_64" || os_arch == "amd64" || os_arch == "aarch64" || os_arch == "arm64" || os_arch == "riscv64"; - 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; @@ -176,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) @@ -187,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..a04b68170 100644 --- a/launcher/java/JavaChecker.h +++ b/launcher/java/JavaChecker.h @@ -1,51 +1,52 @@ #pragma once #include #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); 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..aa7fab8a0 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("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); + 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 074bf54df..072cb1d16 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); @@ -104,6 +102,8 @@ QProcessEnvironment CleanEnviroment() QString newValue = stripVariableEntries(key, value, rawenv.value("LAUNCHER_" + key)); qDebug() << "Env: stripped" << key << value << "to" << newValue; + + value = newValue; } #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) // Strip IBus @@ -184,56 +184,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 +285,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 +308,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 +317,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 +326,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 +335,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 +349,7 @@ QList JavaUtils::FindJavaPaths() } candidates.append(getMinecraftJavaBundle()); + candidates.append(getPrismJavaBundle()); candidates = addJavasFromEnv(candidates); candidates.removeDuplicates(); return candidates; @@ -362,23 +375,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 = [&javas]( + 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")); @@ -387,34 +424,56 @@ QList JavaUtils::FindJavaPaths() }; // java installed in a snap is installed in the standard directory, but underneath $SNAP auto snap = qEnvironmentVariable("SNAP"); - auto scanJavaDirs = [&](const QString& dirPath) { + auto scanJavaDirs = [scanJavaDir, snap](const QString& dirPath) { scanJavaDir(dirPath); if (!snap.isNull()) { 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-"); + }; + // AOSC OS's locations for openjdk + auto aoscFilter = [](const QFileInfo& info) { + QString fileName = info.fileName(); + return fileName == "java" || fileName.startsWith("java-"); + }; + scanJavaDir("/usr/lib64", gentooFilter); + scanJavaDir("/usr/lib", gentooFilter); + scanJavaDir("/opt", gentooFilter); + scanJavaDir("/usr/lib", aoscFilter); // 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 scanJavaDirs(FS::PathCombine(home, ".jdks")); // javas downloaded by sdkman scanJavaDirs(FS::PathCombine(home, ".sdkman/candidates/java")); + // javas downloaded by gradle (toolchains) + scanJavaDirs(FS::PathCombine(home, ".gradle/jdks")); javas.append(getMinecraftJavaBundle()); + javas.append(getPrismJavaBundle()); javas = addJavasFromEnv(javas); javas.removeDuplicates(); return javas; @@ -428,6 +487,8 @@ QList JavaUtils::FindJavaPaths() javas.append(this->GetDefaultJava()->path); javas.append(getMinecraftJavaBundle()); + javas.append(getPrismJavaBundle()); + javas.removeDuplicates(); return addJavasFromEnv(javas); } #endif @@ -439,26 +500,23 @@ QString JavaUtils::getJavaCheckPath() QStringList getMinecraftJavaBundle() { - QString partialPath; - QString executable = "java"; QStringList processpaths; #if defined(Q_OS_OSX) - partialPath = FS::PathCombine(QDir::homePath(), "Library/Application Support"); + processpaths << FS::PathCombine(QDir::homePath(), FS::PathCombine("Library", "Application Support", "minecraft", "runtime")); #elif defined(Q_OS_WIN32) - partialPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", ""); - executable += "w.exe"; + + auto appDataPath = QProcessEnvironment::systemEnvironment().value("APPDATA", ""); + processpaths << FS::PathCombine(QFileInfo(appDataPath).absoluteFilePath(), ".minecraft", "runtime"); // add the microsoft store version of the launcher to the search. the current path is: // C:\Users\USERNAME\AppData\Local\Packages\Microsoft.4297127D64EC6_8wekyb3d8bbwe\LocalCache\Local\runtime + auto localAppDataPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", ""); auto minecraftMSStorePath = - FS::PathCombine(QFileInfo(partialPath).absolutePath(), "Local", "Packages", "Microsoft.4297127D64EC6_8wekyb3d8bbwe"); - minecraftMSStorePath = FS::PathCombine(minecraftMSStorePath, "LocalCache", "Local", "runtime"); - processpaths << minecraftMSStorePath; + FS::PathCombine(QFileInfo(localAppDataPath).absoluteFilePath(), "Packages", "Microsoft.4297127D64EC6_8wekyb3d8bbwe"); + processpaths << FS::PathCombine(minecraftMSStorePath, "LocalCache", "Local", "runtime"); #else - partialPath = QDir::homePath(); + processpaths << FS::PathCombine(QDir::homePath(), ".minecraft", "runtime"); #endif - auto minecraftDataPath = FS::PathCombine(partialPath, ".minecraft", "runtime"); - processpaths << minecraftDataPath; QStringList javas; while (!processpaths.isEmpty()) { @@ -470,7 +528,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; } @@ -483,3 +541,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 = [&javas](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 = [scanDir](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..bca50f2c9 100644 --- a/launcher/java/JavaVersion.cpp +++ b/launcher/java/JavaVersion.cpp @@ -43,12 +43,18 @@ 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::defaultsToUtf8() const +{ + // starting from Java 18, UTF-8 is the default charset: https://openjdk.org/jeps/400 + return m_parseable && m_major >= 18; +} + +bool JavaVersion::isModular() const { return m_parseable && m_major >= 9; } @@ -59,12 +65,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 +109,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..c070bdeec 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 isModular(); + bool requiresPermGen() const; + bool defaultsToUtf8() const; + 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..bb7cc568d --- /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..20b39e751 --- /dev/null +++ b/launcher/java/download/ManifestDownloadTask.cpp @@ -0,0 +1,137 @@ +// 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 *nix only ! + auto path = Json::ensureString(meta, "target"); + if (!path.isEmpty()) { + QFile::link(path, 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..0b352ea9f 100644 --- a/launcher/launch/LaunchStep.cpp +++ b/launcher/launch/LaunchStep.cpp @@ -16,9 +16,8 @@ #include "LaunchStep.h" #include "LaunchTask.h" -void LaunchStep::bind(LaunchTask* parent) +LaunchStep::LaunchStep(LaunchTask* parent) : Task(), m_parent(parent) { - m_parent = parent; connect(this, &LaunchStep::readyForLaunch, parent, &LaunchTask::onReadyForLaunch); connect(this, &LaunchStep::logLine, parent, &LaunchTask::onLogLine); connect(this, &LaunchStep::logLines, parent, &LaunchTask::onLogLines); diff --git a/launcher/launch/LaunchStep.h b/launcher/launch/LaunchStep.h index 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..4b93d2077 100644 --- a/launcher/launch/LaunchTask.cpp +++ b/launcher/launch/LaunchTask.cpp @@ -44,7 +44,6 @@ #include #include #include "MessageLevel.h" -#include "java/JavaChecker.h" #include "tasks/Task.h" void LaunchTask::init() @@ -52,14 +51,14 @@ void LaunchTask::init() m_instance->setRunning(true); } -shared_qobject_ptr LaunchTask::create(InstancePtr inst) +shared_qobject_ptr LaunchTask::create(MinecraftInstancePtr inst) { shared_qobject_ptr proc(new LaunchTask(inst)); proc->init(); return proc; } -LaunchTask::LaunchTask(InstancePtr instance) : m_instance(instance) {} +LaunchTask::LaunchTask(MinecraftInstancePtr instance) : m_instance(instance) {} void LaunchTask::appendStep(shared_qobject_ptr step) { @@ -255,20 +254,60 @@ void LaunchTask::emitFailed(QString reason) Task::emitFailed(reason); } -void LaunchTask::substituteVariables(QStringList& args) const +QString expandVariables(const QString& input, QProcessEnvironment dict) { - auto env = m_instance->createEnvironment(); + QString result = input; - for (auto key : env.keys()) { - args.replaceInStrings("$" + key, env.value(key)); + enum { base, maybeBrace, variable, brace } state = base; + int startIdx = -1; + for (int i = 0; i < result.length();) { + QChar c = result.at(i++); + switch (state) { + case base: + if (c == '$') + state = maybeBrace; + break; + case maybeBrace: + if (c == '{') { + state = brace; + startIdx = i; + } else if (c.isLetterOrNumber() || c == '_') { + state = variable; + startIdx = i - 1; + } else { + state = base; + } + break; + case brace: + if (c == '}') { + const auto res = dict.value(result.mid(startIdx, i - 1 - startIdx), ""); + if (!res.isEmpty()) { + result.replace(startIdx - 2, i - startIdx + 2, res); + i = startIdx - 2 + res.length(); + } + state = base; + } + break; + case variable: + if (!c.isLetterOrNumber() && c != '_') { + const auto res = dict.value(result.mid(startIdx, i - startIdx - 1), ""); + if (!res.isEmpty()) { + result.replace(startIdx - 1, i - startIdx, res); + i = startIdx - 1 + res.length(); + } + state = base; + } + break; + } } + if (state == variable) { + if (const auto res = dict.value(result.mid(startIdx), ""); !res.isEmpty()) + result.replace(startIdx - 1, result.length() - startIdx + 1, res); + } + return result; } -void LaunchTask::substituteVariables(QString& cmd) const +QString LaunchTask::substituteVariables(QString& cmd, bool isLaunch) const { - auto env = m_instance->createEnvironment(); - - for (auto key : env.keys()) { - cmd.replace("$" + key, env.value(key)); - } + return expandVariables(cmd, isLaunch ? m_instance->createLaunchEnvironment() : m_instance->createEnvironment()); } diff --git a/launcher/launch/LaunchTask.h b/launcher/launch/LaunchTask.h index e79c43557..2e87ece95 100644 --- a/launcher/launch/LaunchTask.h +++ b/launcher/launch/LaunchTask.h @@ -37,31 +37,31 @@ #pragma once #include +#include #include #include "BaseInstance.h" #include "LaunchStep.h" #include "LogModel.h" -#include "LoggedProcess.h" #include "MessageLevel.h" class LaunchTask : public Task { Q_OBJECT protected: - explicit LaunchTask(InstancePtr instance); + explicit LaunchTask(MinecraftInstancePtr instance); void init(); public: enum State { NotStarted, Running, Waiting, Failed, Aborted, Finished }; public: /* methods */ - static shared_qobject_ptr create(InstancePtr inst); - virtual ~LaunchTask(){}; + static shared_qobject_ptr create(MinecraftInstancePtr inst); + virtual ~LaunchTask() = default; void appendStep(shared_qobject_ptr step); void prependStep(shared_qobject_ptr step); void setCensorFilter(QMap filter); - InstancePtr instance() { return m_instance; } + MinecraftInstancePtr instance() { return m_instance; } void setPid(qint64 pid) { m_pid = pid; } @@ -87,8 +87,7 @@ class LaunchTask : public Task { shared_qobject_ptr getLogModel(); public: - void substituteVariables(QStringList& args) const; - void substituteVariables(QString& cmd) const; + QString substituteVariables(QString& cmd, bool isLaunch = false) const; QString censorPrivateInfo(QString in); protected: /* methods */ @@ -116,7 +115,7 @@ class LaunchTask : public Task { void finalizeSteps(bool successful, const QString& error); protected: /* data */ - InstancePtr m_instance; + MinecraftInstancePtr m_instance; shared_qobject_ptr m_logModel; QList> m_steps; QMap m_censorFilter; diff --git a/launcher/launch/LogModel.h b/launcher/launch/LogModel.h index 18e51d7e3..167f74190 100644 --- a/launcher/launch/LogModel.h +++ b/launcher/launch/LogModel.h @@ -32,7 +32,7 @@ class LogModel : public QAbstractListModel { private /* types */: struct entry { - MessageLevel::Enum level; + MessageLevel::Enum level = MessageLevel::Enum::Unknown; QString line; }; 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..0f8d27e94 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)); 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.cpp b/launcher/launch/steps/PostLaunchCommand.cpp index 725101224..5d893c71f 100644 --- a/launcher/launch/steps/PostLaunchCommand.cpp +++ b/launcher/launch/steps/PostLaunchCommand.cpp @@ -47,25 +47,21 @@ PostLaunchCommand::PostLaunchCommand(LaunchTask* parent) : LaunchStep(parent) void PostLaunchCommand::executeTask() { - // FIXME: where to put this? + auto cmd = m_parent->substituteVariables(m_command); + emit logLine(tr("Running Post-Launch command: %1").arg(cmd), MessageLevel::Launcher); #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - auto args = QProcess::splitCommand(m_command); - m_parent->substituteVariables(args); + auto args = QProcess::splitCommand(cmd); - emit logLine(tr("Running Post-Launch command: %1").arg(args.join(' ')), MessageLevel::Launcher); const QString program = args.takeFirst(); m_process.start(program, args); #else - m_parent->substituteVariables(m_command); - - emit logLine(tr("Running Post-Launch command: %1").arg(m_command), MessageLevel::Launcher); - m_process.start(m_command); + m_process.start(cmd); #endif } void PostLaunchCommand::on_state(LoggedProcess::State state) { - auto getError = [&]() { return tr("Post-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); }; + auto getError = [this]() { return tr("Post-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); }; switch (state) { case LoggedProcess::Aborted: case LoggedProcess::Crashed: 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.cpp b/launcher/launch/steps/PreLaunchCommand.cpp index 6d071a66e..318237e99 100644 --- a/launcher/launch/steps/PreLaunchCommand.cpp +++ b/launcher/launch/steps/PreLaunchCommand.cpp @@ -47,25 +47,20 @@ PreLaunchCommand::PreLaunchCommand(LaunchTask* parent) : LaunchStep(parent) void PreLaunchCommand::executeTask() { - // FIXME: where to put this? + auto cmd = m_parent->substituteVariables(m_command); + emit logLine(tr("Running Pre-Launch command: %1").arg(cmd), MessageLevel::Launcher); #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - auto args = QProcess::splitCommand(m_command); - m_parent->substituteVariables(args); - - emit logLine(tr("Running Pre-Launch command: %1").arg(args.join(' ')), MessageLevel::Launcher); + auto args = QProcess::splitCommand(cmd); const QString program = args.takeFirst(); m_process.start(program, args); #else - m_parent->substituteVariables(m_command); - - emit logLine(tr("Running Pre-Launch command: %1").arg(m_command), MessageLevel::Launcher); - m_process.start(m_command); + m_process.start(cmd); #endif } void PreLaunchCommand::on_state(LoggedProcess::State state) { - auto getError = [&]() { return tr("Pre-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); }; + auto getError = [this]() { return tr("Pre-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); }; switch (state) { case LoggedProcess::Aborted: case LoggedProcess::Crashed: 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..1707854be 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(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..1de4e7f36 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,10 @@ VersionList::VersionList(const QString& uid, QObject* parent) : BaseVersionList( Task::Ptr VersionList::getLoadTask() { - load(Net::Mode::Online); - return getCurrentTask(); + auto loadTask = makeShared(tr("Load meta for %1", "This is for the task name that loads the meta index.").arg(m_uid)); + loadTask->addTask(APPLICATION->metadataIndex()->loadTask(Net::Mode::Online)); + loadTask->addTask(this->loadTask(Net::Mode::Online)); + return loadTask; } bool VersionList::isLoaded() @@ -91,7 +99,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 +116,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,14 +150,16 @@ 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; } bool VersionList::hasVersion(QString version) const { - auto ver = - std::find_if(m_versions.constBegin(), m_versions.constEnd(), [&](Meta::Version::Ptr const& a) { return a->version() == version; }); + auto ver = std::find_if(m_versions.constBegin(), m_versions.constEnd(), + [version](Meta::Version::Ptr const& a) { return a->version() == version; }); return (ver != m_versions.constEnd()); } @@ -171,6 +192,16 @@ void VersionList::parse(const QJsonObject& obj) parseVersionList(obj, this); } +void VersionList::addExternalRecommends(const QStringList& recommends) +{ + m_externalRecommendsVersions.append(recommends); +} + +void VersionList::clearExternalRecommends() +{ + m_externalRecommendsVersions.clear(); +} + // FIXME: this is dumb, we have 'recommended' as part of the metadata already... static const Meta::Version::Ptr& getBetterVersion(const Meta::Version::Ptr& a, const Meta::Version::Ptr& b) { @@ -191,6 +222,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 +232,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 +260,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 +276,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..ad7ef545c 100644 --- a/launcher/minecraft/Component.cpp +++ b/launcher/minecraft/Component.cpp @@ -44,10 +44,19 @@ #include "OneSixVersionFormat.h" #include "VersionFile.h" #include "meta/Version.h" +#include "minecraft/Component.h" #include "minecraft/PackProfile.h" #include +const QMap Component::KNOWN_MODLOADERS = { + { "net.neoforged", { ModPlatform::NeoForge, { "net.minecraftforge", "net.fabricmc.fabric-loader", "org.quiltmc.quilt-loader" } } }, + { "net.minecraftforge", { ModPlatform::Forge, { "net.neoforged", "net.fabricmc.fabric-loader", "org.quiltmc.quilt-loader" } } }, + { "net.fabricmc.fabric-loader", { ModPlatform::Fabric, { "net.minecraftforge", "net.neoforged", "org.quiltmc.quilt-loader" } } }, + { "org.quiltmc.quilt-loader", { ModPlatform::Quilt, { "net.minecraftforge", "net.neoforged", "net.fabricmc.fabric-loader" } } }, + { "com.mumfrey.liteloader", { ModPlatform::LiteLoader, {} } } +}; + Component::Component(PackProfile* parent, const QString& uid) { assert(parent); @@ -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,40 @@ 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() + +bool Component::isVersionChangeable(bool wait) { auto list = getVersionList(); if (list) { - if (!list->isLoaded()) { - list->load(Net::Mode::Online); - } + if (wait) + list->waitToLoad(); return list->count() != 0; } return false; } +bool Component::isKnownModloader() +{ + auto iter = KNOWN_MODLOADERS.find(m_uid); + return iter != KNOWN_MODLOADERS.cend(); +} + +QStringList Component::knownConflictingComponents() +{ + auto iter = KNOWN_MODLOADERS.find(m_uid); + if (iter != KNOWN_MODLOADERS.cend()) { + return (*iter).knownConflictingComponents; + } else { + return {}; + } +} + void Component::setImportant(bool state) { if (m_important != state) { @@ -247,7 +261,8 @@ ProblemSeverity Component::getProblemSeverity() const { auto file = getVersionFile(); if (file) { - return file->getProblemSeverity(); + auto severity = file->getProblemSeverity(); + return m_componentProblemSeverity > severity ? m_componentProblemSeverity : severity; } return ProblemSeverity::Error; } @@ -256,11 +271,31 @@ const QList Component::getProblems() const { auto file = getVersionFile(); if (file) { - return file->getProblems(); + auto problems = file->getProblems(); + problems.append(m_componentProblems); + return problems; } return { { ProblemSeverity::Error, QObject::tr("Patch is not loaded yet.") } }; } +void Component::addComponentProblem(ProblemSeverity severity, const QString& description) +{ + if (severity > m_componentProblemSeverity) { + m_componentProblemSeverity = severity; + } + m_componentProblems.append({ severity, description }); + + emit dataChanged(); +} + +void Component::resetComponentProblems() +{ + m_componentProblems.clear(); + m_componentProblemSeverity = ProblemSeverity::None; + + emit dataChanged(); +} + void Component::setVersion(const QString& version) { if (version == m_version) { @@ -336,7 +371,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 +449,36 @@ void Component::updateCachedData() emit dataChanged(); } } + +void Component::waitLoadMeta() +{ + if (!m_loaded) { + if (!m_metaVersion || !m_metaVersion->isLoaded()) { + // wait for the loaded version from meta + m_metaVersion = APPLICATION->metadataIndex()->getLoadedVersion(m_uid, m_version); + } + m_loaded = true; + updateCachedData(); + } +} + +void Component::setUpdateAction(UpdateAction action) +{ + m_updateAction = action; +} + +UpdateAction Component::getUpdateAction() +{ + return m_updateAction; +} + +void Component::clearUpdateAction() +{ + m_updateAction = UpdateAction{ UpdateActionNone{} }; +} + +QDebug operator<<(QDebug d, const Component& comp) +{ + d << "Component(" << comp.m_uid << " : " << comp.m_cachedVersion << ")"; + return d; +} diff --git a/launcher/minecraft/Component.h b/launcher/minecraft/Component.h index fdb61c45e..203cc2241 100644 --- a/launcher/minecraft/Component.h +++ b/launcher/minecraft/Component.h @@ -4,9 +4,12 @@ #include #include #include +#include +#include #include "ProblemProvider.h" #include "QObjectPtr.h" #include "meta/JsonFormat.h" +#include "modplatform/ModIndex.h" class PackProfile; class LaunchProfile; @@ -16,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(); @@ -38,7 +72,9 @@ class Component : public QObject, public ProblemProvider { bool isRevertible(); bool isRemovable(); bool isCustom(); - bool isVersionChangeable(); + bool isVersionChangeable(bool wait = true); + bool isKnownModloader(); + QStringList knownConflictingComponents(); // DEPRECATED: explicit numeric order values, used for loading old non-component config. TODO: refactor and move to migration code void setOrder(int order); @@ -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..36a07ee72 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 @@ -32,10 +38,10 @@ * If the component list changes, start over. */ -ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list, QObject* parent) : Task(parent) +ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list) : Task() { d.reset(new ComponentUpdateTaskData); - d->m_list = list; + d->m_profile = list; d->mode = mode; d->netmode = netmode; } @@ -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..64c55877b 100644 --- a/launcher/minecraft/ComponentUpdateTask.h +++ b/launcher/minecraft/ComponentUpdateTask.h @@ -1,5 +1,6 @@ #pragma once +#include "minecraft/Component.h" #include "net/Mode.h" #include "tasks/Task.h" @@ -13,7 +14,7 @@ class ComponentUpdateTask : public Task { enum class Mode { Launch, Resolution }; public: - explicit ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list, QObject* parent = 0); + explicit ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list); virtual ~ComponentUpdateTask(); protected: @@ -21,7 +22,11 @@ class ComponentUpdateTask : public Task { private: void loadComponents(); + /// collects components that are dependent on or dependencies of the component + QList collectTreeLinked(const QString& uid); void resolveDependencies(bool checkOnly); + void performUpdateActions(); + void finalizeComponents(); void remoteLoadSucceeded(size_t index); void remoteLoadFailed(size_t index, const QString& msg); diff --git a/launcher/minecraft/ComponentUpdateTask_p.h b/launcher/minecraft/ComponentUpdateTask_p.h index 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..0bc462474 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(); - auto actualPath = [&](QString relPath) { + // Lambda function to get the absolute file path + auto actualPath = [this, local, overridePath](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,16 +92,30 @@ 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(); - auto check_local_file = [&](QString storage) { + // Lambda function to check if a local file exists + auto check_local_file = [overridePath, &failedLocalFiles](QString storage) { QFileInfo fileinfo(storage); QString fileName = fileinfo.fileName(); auto fullPath = FS::PathCombine(overridePath, fileName); @@ -95,7 +127,8 @@ QList Library::getDownloads(const RuntimeContext& runtimeContext return true; }; - auto add_download = [&](QString storage, QString url, QString sha1) { + // Lambda function to add a download request + auto add_download = [this, local, check_local_file, cache, stale, &out](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 { @@ -166,7 +198,7 @@ QList Library::getDownloads(const RuntimeContext& runtimeContext } } } else { - auto raw_dl = [&]() { + auto raw_dl = [this, raw_storage]() { if (!m_absoluteURL.isEmpty()) { return m_absoluteURL; } @@ -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 4229f73eb..d1780d497 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" @@ -90,13 +91,56 @@ #include "tools/BaseProfiler.h" #include +#include +#include +#include #ifdef Q_OS_LINUX #include "MangoHud.h" #endif +#ifdef WITH_QTDBUS +#include +#endif + #define IBUS "@im=ibus" +static bool switcherooSetupGPU(QProcessEnvironment& env) +{ +#ifdef WITH_QTDBUS + if (!QDBusConnection::systemBus().isConnected()) + return false; + + QDBusInterface switcheroo("net.hadess.SwitcherooControl", "/net/hadess/SwitcherooControl", "org.freedesktop.DBus.Properties", + QDBusConnection::systemBus()); + + if (!switcheroo.isValid()) + return false; + + QDBusReply reply = + switcheroo.call(QStringLiteral("Get"), QStringLiteral("net.hadess.SwitcherooControl"), QStringLiteral("GPUs")); + if (!reply.isValid()) + return false; + + QDBusArgument arg = qvariant_cast(reply.value().variant()); + QList gpus; + arg >> gpus; + + for (const auto& gpu : gpus) { + QString name = qvariant_cast(gpu[QStringLiteral("Name")]); + bool defaultGpu = qvariant_cast(gpu[QStringLiteral("Default")]); + if (!defaultGpu) { + QStringList envList = qvariant_cast(gpu[QStringLiteral("Environment")]); + for (int i = 0; i + 1 < envList.size(); i += 2) { + env.insert(envList[i], envList[i + 1]); + } + return true; + } + } +#endif + return false; +} + // all of this because keeping things compatible with deprecated old settings // if either of the settings {a, b} is true, this also resolves to true class OrSetting : public Setting { @@ -134,25 +178,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); @@ -173,11 +213,12 @@ void MinecraftInstance::loadSpecificSettings() m_settings->registerOverride(global_settings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride); m_settings->registerOverride(global_settings->getSetting("CustomGLFWPath"), nativeLibraryWorkaroundsOverride); - // Peformance related options + // Performance related options auto performanceOverride = m_settings->registerSetting("OverridePerformance", false); m_settings->registerOverride(global_settings->getSetting("EnableFeralGamemode"), performanceOverride); m_settings->registerOverride(global_settings->getSetting("EnableMangoHud"), performanceOverride); m_settings->registerOverride(global_settings->getSetting("UseDiscreteGpu"), performanceOverride); + m_settings->registerOverride(global_settings->getSetting("UseZink"), performanceOverride); // Miscellaneous auto miscellaneousOverride = m_settings->registerSetting("OverrideMiscellaneous", false); @@ -197,6 +238,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); @@ -218,6 +260,7 @@ void MinecraftInstance::loadSpecificSettings() void MinecraftInstance::updateRuntimeContext() { m_runtimeContext.updateFromInstanceSettings(m_settings); + m_components->invalidateLaunchProfile(); } QString MinecraftInstance::typeName() const @@ -292,10 +335,10 @@ QString MinecraftInstance::gameRoot() const QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft")); QFileInfo dotMCDir(FS::PathCombine(instanceRoot(), ".minecraft")); - if (mcDir.exists() && !dotMCDir.exists()) - return mcDir.filePath(); - else + if (dotMCDir.exists() && !mcDir.exists()) return dotMCDir.filePath(); + else + return mcDir.filePath(); } QString MinecraftInstance::binRoot() const @@ -522,8 +565,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; } @@ -531,7 +573,7 @@ QStringList MinecraftInstance::javaArguments() QString MinecraftInstance::getLauncher() { // use legacy launcher if the traits are set - if (traits().contains("legacyLaunch") || traits().contains("alphaLaunch")) + if (isLegacy()) return "legacy"; return "standard"; @@ -552,6 +594,13 @@ QMap MinecraftInstance::getVariables() out.insert("INST_JAVA", settings()->get("JavaPath").toString()); out.insert("INST_JAVA_ARGS", javaArguments().join(' ')); out.insert("NO_COLOR", "1"); +#ifdef Q_OS_MACOS + // get library for Steam overlay support + QString steamDyldInsertLibraries = qEnvironmentVariable("STEAM_DYLD_INSERT_LIBRARIES"); + if (!steamDyldInsertLibraries.isEmpty()) { + out.insert("DYLD_INSERT_LIBRARIES", steamDyldInsertLibraries); + } +#endif return out; } @@ -594,9 +643,6 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment() QStringList preloadList; if (auto value = env.value("LD_PRELOAD"); !value.isEmpty()) preloadList = value.split(QLatin1String(":")); - QStringList libPaths; - if (auto value = env.value("LD_LIBRARY_PATH"); !value.isEmpty()) - libPaths = value.split(QLatin1String(":")); auto mangoHudLibString = MangoHud::getLibraryString(); if (!mangoHudLibString.isEmpty()) { @@ -604,28 +650,36 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment() QString libPath = mangoHudLib.absolutePath(); auto appendLib = [libPath, &preloadList](QString fileName) { if (QFileInfo(FS::PathCombine(libPath, fileName)).exists()) - preloadList << fileName; + preloadList << FS::PathCombine(libPath, fileName); }; // 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()); - libPaths << libPath; + appendLib("libMangoHud_shim.so"); + preloadList << mangoHudLibString; } env.insert("LD_PRELOAD", preloadList.join(QLatin1String(":"))); - env.insert("LD_LIBRARY_PATH", libPaths.join(QLatin1String(":"))); env.insert("MANGOHUD", "1"); } if (settings()->get("UseDiscreteGpu").toBool()) { - // Open Source Drivers - env.insert("DRI_PRIME", "1"); - // Proprietary Nvidia Drivers - env.insert("__NV_PRIME_RENDER_OFFLOAD", "1"); - env.insert("__VK_LAYER_NV_optimus", "NVIDIA_only"); - env.insert("__GLX_VENDOR_LIBRARY_NAME", "nvidia"); + if (!switcherooSetupGPU(env)) { + // Open Source Drivers + env.insert("DRI_PRIME", "1"); + // Proprietary Nvidia Drivers + env.insert("__NV_PRIME_RENDER_OFFLOAD", "1"); + env.insert("__VK_LAYER_NV_optimus", "NVIDIA_only"); + env.insert("__GLX_VENDOR_LIBRARY_NAME", "nvidia"); + } + } + + if (settings()->get("UseZink").toBool()) { + // taken from https://wiki.archlinux.org/title/OpenGL#OpenGL_over_Vulkan_(Zink) + env.insert("__GLX_VENDOR_LIBRARY_NAME", "mesa"); + env.insert("MESA_LOADER_DRIVER_OVERRIDE", "zink"); + env.insert("GALLIUM_DRIVER", "zink"); } #endif return env; @@ -653,7 +707,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(); @@ -661,9 +715,17 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, Mine args_pattern += " --tweakClass " + tweaker; } - if (serverToJoin && !serverToJoin->address.isEmpty()) { - 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; + } } QMap token_mapping; @@ -706,7 +768,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; @@ -725,9 +787,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 @@ -739,11 +805,34 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftS // window size, title and state, legacy { QString windowParams; - if (settings()->get("LaunchMaximized").toBool()) - windowParams = "maximized"; - else + if (settings()->get("LaunchMaximized").toBool()) { + // FIXME doesn't support maximisation + if (!isLegacy()) { + auto screen = QGuiApplication::primaryScreen(); + auto screenGeometry = screen->availableSize(); + + // small hack to get the widow decorations + for (auto w : QApplication::topLevelWidgets()) { + auto mainWindow = qobject_cast(w); + if (mainWindow) { + auto m = mainWindow->windowHandle()->frameMargins(); +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + screenGeometry = screenGeometry.shrunkBy(m); +#else + screenGeometry = { screenGeometry.width() - m.left() - m.right(), screenGeometry.height() - m.top() - m.bottom() }; +#endif + break; + } + } + + windowParams = QString("%1x%2").arg(screenGeometry.width()).arg(screenGeometry.height()); + } else { + windowParams = "maximized"; + } + } else { windowParams = QString("%1x%2").arg(settings()->get("MinecraftWinWidth").toInt()).arg(settings()->get("MinecraftWinHeight").toInt()); + } launchScript += "windowTitle " + windowTitle() + "\n"; launchScript += "windowParams " + windowParams + "\n"; } @@ -780,16 +869,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:"; @@ -799,6 +887,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(); @@ -815,7 +904,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr out << "Libraries:"; QStringList jars, nativeJars; profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot()); - auto printLibFile = [&](const QString& path) { + auto printLibFile = [&out](const QString& path) { QFileInfo info(path); if (info.exists()) { out << " " + path; @@ -834,7 +923,8 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr out << ""; } - auto printModList = [&](const QString& label, ModFolderModel& model) { + // mods and core mods + auto printModList = [&out](const QString& label, ModFolderModel& model) { if (model.size()) { out << QString("%1:").arg(label); auto modList = model.allMods(); @@ -862,6 +952,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:"; @@ -877,11 +968,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)"; @@ -993,7 +1086,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") @@ -1013,21 +1106,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 ... @@ -1041,29 +1134,43 @@ 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); } + // load meta + { + auto mode = session->status != AuthSession::PlayableOffline ? Net::Mode::Online : Net::Mode::Offline; + process->appendStep(makeShared(pptr, makeShared(this, mode))); + } + + // check java + { + process->appendStep(makeShared(pptr)); + process->appendStep(makeShared(pptr)); + } + // run pre-launch command if that's needed if (getPreLaunchCommand().size()) { auto step = makeShared(pptr); @@ -1076,9 +1183,9 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt 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 @@ -1093,7 +1200,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 @@ -1116,7 +1223,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); } @@ -1146,7 +1253,7 @@ std::shared_ptr MinecraftInstance::loaderModList() { if (!m_loader_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); - m_loader_mod_list.reset(new ModFolderModel(modsRoot(), this, is_indexed)); + m_loader_mod_list.reset(new ModFolderModel(modsRoot(), this, is_indexed, true)); } return m_loader_mod_list; } @@ -1155,7 +1262,7 @@ std::shared_ptr MinecraftInstance::coreModList() { if (!m_core_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); - m_core_mod_list.reset(new ModFolderModel(coreModsDir(), this, is_indexed)); + m_core_mod_list.reset(new ModFolderModel(coreModsDir(), this, is_indexed, true)); } return m_core_mod_list; } @@ -1172,7 +1279,8 @@ std::shared_ptr MinecraftInstance::nilModList() std::shared_ptr MinecraftInstance::resourcePackList() { if (!m_resource_pack_list) { - m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir(), this)); + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir(), this, is_indexed, true)); } return m_resource_pack_list; } @@ -1180,7 +1288,8 @@ std::shared_ptr MinecraftInstance::resourcePackList() std::shared_ptr MinecraftInstance::texturePackList() { if (!m_texture_pack_list) { - m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir(), this)); + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir(), this, is_indexed, true)); } return m_texture_pack_list; } @@ -1188,11 +1297,17 @@ std::shared_ptr MinecraftInstance::texturePackList() std::shared_ptr MinecraftInstance::shaderPackList() { if (!m_shader_pack_list) { - m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir(), this)); + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir(), this, is_indexed, true)); } return m_shader_pack_list; } +QList> MinecraftInstance::resourceLists() +{ + return { loaderModList(), coreModList(), nilModList(), resourcePackList(), texturePackList(), shaderPackList() }; +} + std::shared_ptr MinecraftInstance::worldList() { if (!m_world_list) { diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index b1f305201..5d9bb45ef 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; @@ -116,16 +116,17 @@ class MinecraftInstance : public BaseInstance { std::shared_ptr resourcePackList(); std::shared_ptr texturePackList(); std::shared_ptr shaderPackList(); + QList> resourceLists(); std::shared_ptr worldList(); 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 +156,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..c0a82e61e 100644 --- a/launcher/minecraft/MinecraftLoadAndCheck.cpp +++ b/launcher/minecraft/MinecraftLoadAndCheck.cpp @@ -2,41 +2,45 @@ #include "MinecraftInstance.h" #include "PackProfile.h" -MinecraftLoadAndCheck::MinecraftLoadAndCheck(MinecraftInstance* inst, QObject* parent) : Task(parent), m_inst(inst) {} +MinecraftLoadAndCheck::MinecraftLoadAndCheck(MinecraftInstance* inst, Net::Mode netmode) : m_inst(inst), m_netmode(netmode) {} void MinecraftLoadAndCheck::executeTask() { // add offline metadata load task auto components = m_inst->getPackProfile(); - components->reload(Net::Mode::Offline); + if (auto result = components->reload(m_netmode); !result) { + emitFailed(result.error); + return; + } 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..c05698bca 100644 --- a/launcher/minecraft/MinecraftLoadAndCheck.h +++ b/launcher/minecraft/MinecraftLoadAndCheck.h @@ -15,32 +15,24 @@ #pragma once -#include -#include -#include - -#include +#include "net/Mode.h" #include "tasks/Task.h" -#include "QObjectPtr.h" - -class MinecraftVersion; class MinecraftInstance; class MinecraftLoadAndCheck : public Task { Q_OBJECT public: - explicit MinecraftLoadAndCheck(MinecraftInstance* inst, QObject* parent = 0); - virtual ~MinecraftLoadAndCheck(){}; + explicit MinecraftLoadAndCheck(MinecraftInstance* inst, Net::Mode netmode); + virtual ~MinecraftLoadAndCheck() = default; void executeTask() override; - private slots: - void subtaskSucceeded(); - void subtaskFailed(QString error); + bool canAbort() const override; + public slots: + bool abort() override; private: MinecraftInstance* m_inst = nullptr; Task::Ptr m_task; - QString m_preFailure; - QString m_fail_reason; + Net::Mode m_netmode; }; diff --git a/launcher/minecraft/MinecraftUpdate.cpp b/launcher/minecraft/MinecraftUpdate.cpp deleted file mode 100644 index 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 6ee10ec15..d17a3a21f 100644 --- a/launcher/minecraft/MojangVersionFormat.cpp +++ b/launcher/minecraft/MojangVersionFormat.cpp @@ -157,20 +157,6 @@ void MojangVersionFormat::readVersionProperties(const QJsonObject& in, VersionFi Bits::readString(in, "id", out->minecraftVersion); Bits::readString(in, "mainClass", out->mainClass); Bits::readString(in, "minecraftArguments", out->minecraftArguments); - if (out->minecraftArguments.isEmpty()) { - QString processArguments; - Bits::readString(in, "processArguments", processArguments); - QString toCompare = processArguments.toLower(); - if (toCompare == "legacy") { - out->minecraftArguments = " ${auth_player_name} ${auth_session}"; - } else if (toCompare == "username_session") { - out->minecraftArguments = "--username ${auth_player_name} --session ${auth_session}"; - } else if (toCompare == "username_session_version") { - out->minecraftArguments = "--username ${auth_player_name} --session ${auth_session} --version ${profile_name}"; - } else if (!toCompare.isEmpty()) { - out->addProblem(ProblemSeverity::Error, QObject::tr("processArguments is set to unknown value '%1'").arg(processArguments)); - } - } Bits::readString(in, "type", out->type); Bits::readString(in, "assets", out->assets); @@ -199,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"); @@ -273,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..684869c8d 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" @@ -174,7 +176,7 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument& doc } } - auto readLibs = [&](const char* which, QList& outList) { + auto readLibs = [&root, &out, &filename](const char* which, QList& outList) { for (auto libVal : requireArray(root.value(which))) { QJsonObject libObj = requireObject(libVal); // parse the library @@ -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..d6534b910 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,44 +158,47 @@ 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; } // Read the given file into component containers -static bool loadPackProfile(PackProfile* parent, - const QString& filename, - const QString& componentJsonPattern, - ComponentContainer& container) +static PackProfile::Result loadPackProfile(PackProfile* parent, + const QString& filename, + const QString& componentJsonPattern, + ComponentContainer& container) { QFile componentsFile(filename); if (!componentsFile.exists()) { - qWarning() << "Components file doesn't exist. This should never happen."; - return false; + auto message = QObject::tr("Components file %1 doesn't exist. This should never happen.").arg(filename); + qCWarning(instanceProfileC) << message; + return PackProfile::Result::Error(message); } if (!componentsFile.open(QFile::ReadOnly)) { - qCritical() << "Couldn't open" << componentsFile.fileName() << " for reading:" << componentsFile.errorString(); - qWarning() << "Ignoring overriden order"; - return false; + auto message = QObject::tr("Couldn't open %1 for reading: %2").arg(componentsFile.fileName(), componentsFile.errorString()); + qCCritical(instanceProfileC) << message; + qCWarning(instanceProfileC) << "Ignoring overridden order"; + return PackProfile::Result::Error(message); } // and it's valid JSON 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"; - return false; + auto message = QObject::tr("Couldn't parse %1 as json: %2").arg(componentsFile.fileName(), error.errorString()); + qCCritical(instanceProfileC) << message; + qCWarning(instanceProfileC) << "Ignoring overridden order"; + return PackProfile::Result::Error(message); } // and then read it and process it if all above is true. @@ -208,11 +215,13 @@ 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"; + auto message = QObject::tr("Couldn't parse %1 : bad file format").arg(componentsFile.fileName()); + qCCritical(instanceProfileC) << message; + qCWarning(instanceProfileC) << "error:" << err.what(); container.clear(); - return false; + return PackProfile::Result::Error(message); } - return true; + return PackProfile::Result::Success(); } // END: component file format @@ -241,12 +250,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,50 +282,49 @@ 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; } -bool PackProfile::load() +PackProfile::Result PackProfile::load() { auto filename = componentsFilePath(); // 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(); - return false; - } else { - // FIXME: actually use fine-grained updates, not this... - beginResetModel(); - // disconnect all the old components - for (auto component : d->components) { - disconnect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); - } - d->components.clear(); - d->componentIndex.clear(); - for (auto component : newComponents) { - if (d->componentIndex.contains(component->m_uid)) { - qWarning() << "Ignoring duplicate component entry" << component->m_uid; - continue; - } - connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); - d->components.append(component); - d->componentIndex[component->m_uid] = component; - } - endResetModel(); - d->loaded = true; - return true; + if (auto result = loadPackProfile(this, filename, patchesPattern(), newComponents); !result) { + qCritical() << d->m_instance->name() << "|" << "Failed to load the component config"; + return result; } + // FIXME: actually use fine-grained updates, not this... + beginResetModel(); + // disconnect all the old components + for (auto component : d->components) { + disconnect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); + } + d->components.clear(); + d->componentIndex.clear(); + for (auto component : newComponents) { + if (d->componentIndex.contains(component->m_uid)) { + qWarning() << d->m_instance->name() << "|" << "Ignoring duplicate component entry" << component->m_uid; + continue; + } + connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); + d->components.append(component); + d->componentIndex[component->m_uid] = component; + } + endResetModel(); + d->loaded = true; + return Result::Success(); } -void PackProfile::reload(Net::Mode netmode) +PackProfile::Result PackProfile::reload(Net::Mode netmode) { // Do not reload when the update/resolve task is running. It is in control. if (d->m_updateTask) { - return; + return Result::Success(); } // flush any scheduled saves to not lose state @@ -325,9 +333,11 @@ void PackProfile::reload(Net::Mode netmode) // FIXME: differentiate when a reapply is required by propagating state from components invalidateLaunchProfile(); - if (load()) { - resolve(netmode); + if (auto result = load(); !result) { + return result; } + resolve(netmode); + return Result::Success(); } Task::Ptr PackProfile::getCurrentTask() @@ -347,14 +357,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 +380,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 +399,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 +415,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 +457,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 +473,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 +690,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 +724,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,13 +745,14 @@ 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; } } // FIXME: we need a generic way of removing local resources, not just jar mods... - auto preRemoveJarMod = [&](LibraryPtr jarMod) -> bool { + auto preRemoveJarMod = [this](LibraryPtr jarMod) -> bool { if (!jarMod->isLocal()) { return true; } @@ -748,7 +762,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 +820,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 +855,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 +875,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 +931,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 +954,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 +973,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 +1021,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 +1049,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..d812dfa48 100644 --- a/launcher/minecraft/PackProfile.h +++ b/launcher/minecraft/PackProfile.h @@ -62,6 +62,19 @@ class PackProfile : public QAbstractListModel { public: enum Columns { NameColumn = 0, VersionColumn, NUM_COLUMNS }; + struct Result { + bool success; + QString error; + + // Implicit conversion to bool + operator bool() const { return success; } + + // Factory methods for convenience + static Result Success() { return { true, "" }; } + + static Result Error(const QString& errorMessage) { return { false, errorMessage }; } + }; + explicit PackProfile(MinecraftInstance* instance); virtual ~PackProfile(); @@ -102,7 +115,7 @@ class PackProfile : public QAbstractListModel { bool revertToBase(int index); /// reload the list, reload all components, resolve dependencies - void reload(Net::Mode netmode); + Result reload(Net::Mode netmode); // reload all components, resolve dependencies void resolve(Net::Mode netmode); @@ -146,14 +159,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); @@ -168,7 +182,7 @@ class PackProfile : public QAbstractListModel { void disableInteraction(bool disable); private: - bool load(); + Result load(); bool installJarMods_internal(QStringList filepaths); bool installCustomJar_internal(QString filepath); bool installAgents_internal(QStringList filepaths); 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..bd28f9e9a 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -38,7 +38,6 @@ #include #include #include -#include #include #include @@ -57,6 +56,7 @@ #include #include "FileSystem.h" +#include "PSaveFile.h" using std::nullopt; using std::optional; @@ -183,7 +183,7 @@ bool putLevelDatDataToFS(const QFileInfo& file, QByteArray& data) if (fullFilePath.isNull()) { return false; } - QSaveFile f(fullFilePath); + PSaveFile f(fullFilePath); if (!f.open(QIODevice::WriteOnly)) { return false; } @@ -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 c3c09003c..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 @@ -52,8 +52,6 @@ #include #include -#include - enum AccountListVersion { MojangMSA = 3 }; AccountList::AccountList(QObject* parent) : QAbstractListModel(parent) @@ -641,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..19fbe15dd --- /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) : Task(), 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..bff4c04e4 --- /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); + 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..1ed39b5ca 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)); 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)); 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..3f39e6e38 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 true; } 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..f9d89baa2 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; } @@ -180,6 +180,7 @@ bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output) if (!getString(skinObj.value("url"), skinOut.url)) { continue; } + skinOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); if (!getString(skinObj.value("variant"), skinOut.variant)) { continue; } @@ -213,7 +214,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; } @@ -221,9 +222,9 @@ namespace { // these skin URLs are for the MHF_Steve and MHF_Alex accounts (made by a Mojang employee) // they are needed because the session server doesn't return skin urls for default skins static const QString SKIN_URL_STEVE = - "http://textures.minecraft.net/texture/1a4af718455d4aab528e7a61f86fa25e6a369d1768dcb13f7df319a713eb810b"; + "https://textures.minecraft.net/texture/1a4af718455d4aab528e7a61f86fa25e6a369d1768dcb13f7df319a713eb810b"; static const QString SKIN_URL_ALEX = - "http://textures.minecraft.net/texture/83cee5ca6afcdb171285aa00e8049c297b2dbeba0efb8ff970a5677a1b644032"; + "https://textures.minecraft.net/texture/83cee5ca6afcdb171285aa00e8049c297b2dbeba0efb8ff970a5677a1b644032"; bool isDefaultModelSteve(QString uuid) { @@ -347,7 +348,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 +389,7 @@ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output) output.currentCape = capeOut.alias; } - output.validity = Katabasis::Validity::Certain; + output.validity = Validity::Certain; return true; } @@ -422,7 +423,7 @@ bool parseMinecraftEntitlements(QByteArray& data, MinecraftEntitlement& output) output.ownsMinecraft = true; } } - output.validity = Katabasis::Validity::Certain; + output.validity = Validity::Certain; return true; } @@ -456,7 +457,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 +489,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..38ff90a47 --- /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::deviceAuthorizationFinished); + + m_task->start(); +} + +struct DeviceAuthorizationResponse { + QString device_code; + QString user_code; + QString verification_uri; + int expires_in; + int interval; + + QString error; + QString error_description; +}; + +DeviceAuthorizationResponse parseDeviceAuthorizationResponse(const QByteArray& data) +{ + QJsonParseError err; + QJsonDocument doc = QJsonDocument::fromJson(data, &err); + if (err.error != QJsonParseError::NoError) { + qWarning() << "Failed to parse device authorization response due to err:" << err.errorString(); + return {}; + } + + if (!doc.isObject()) { + qWarning() << "Device authorization 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::deviceAuthorizationFinished() +{ + auto rsp = parseDeviceAuthorizationResponse(*m_response); + if (!rsp.error.isEmpty() || !rsp.error_description.isEmpty()) { + qWarning() << "Device authorization failed:" << rsp.error; + emit finished(AccountTaskState::STATE_FAILED_HARD, + 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 authorization response due to err:" << err.errorString(); + return {}; + } + + if (!doc.isObject()) { + qWarning() << "Device authorization 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")); +} diff --git a/launcher/minecraft/auth/steps/MSADeviceCodeStep.h b/launcher/minecraft/auth/steps/MSADeviceCodeStep.h new file mode 100644 index 000000000..7f755563f --- /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 deviceAuthorizationFinished(); + 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..151ee4e68 100644 --- a/launcher/minecraft/auth/steps/MSAStep.cpp +++ b/launcher/minecraft/auth/steps/MSAStep.cpp @@ -35,123 +35,151 @@ #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_") || APPLICATION->isPortable() || !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 c33d7e629..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, @@ -126,7 +128,35 @@ bool XboxAuthorizationStep::processSTSError(QNetworkReply::NetworkError error, Q emit finished( AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account is underaged and is not linked to a family.\n\nPlease set up your account according to %1.") - .arg("help.minecraft.net")); + .arg("help.minecraft.net")); + return true; + } + // the following codes where copied from: https://github.com/PrismarineJS/prismarine-auth/pull/44 + case 2148916236: { + emit finished(AccountTaskState::STATE_FAILED_SOFT, + tr("This Microsoft account requires proof of age to play. Please login to %1 to provide proof of age.") + .arg("login.live.com")); + return true; + } + case 2148916237: + emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account has reached its limit for playtime. This " + "Microsoft account has been blocked from logging in.")); + return true; + case 2148916227: { + emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account was banned by Xbox for violating one or more " + "Community Standards for Xbox and is unable to be used.")); + return true; + } + case 2148916229: { + emit finished(AccountTaskState::STATE_FAILED_SOFT, + tr("This Microsoft account is currently restricted and your guardian has not given you permission to play " + "online. Login to %1 and have your guardian change your permissions.") + .arg("account.microsoft.com")); + return true; + } + case 2148916234: { + emit finished(AccountTaskState::STATE_FAILED_SOFT, + tr("This Microsoft account has not accepted Xbox's Terms of Service. Please login and accept them.")); return true; } default: { 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..854590dd2 --- /dev/null +++ b/launcher/minecraft/launch/AutoInstallJava.cpp @@ -0,0 +1,249 @@ +// 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(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::Launcher); + } + 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::Launcher); + 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(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.cpp b/launcher/minecraft/launch/CreateGameFolders.cpp index 36f5e6407..07bdbb600 100644 --- a/launcher/minecraft/launch/CreateGameFolders.cpp +++ b/launcher/minecraft/launch/CreateGameFolders.cpp @@ -8,16 +8,15 @@ CreateGameFolders::CreateGameFolders(LaunchTask* parent) : LaunchStep(parent) {} void CreateGameFolders::executeTask() { auto instance = m_parent->instance(); - std::shared_ptr minecraftInstance = std::dynamic_pointer_cast(instance); - if (!FS::ensureFolderPathExists(minecraftInstance->gameRoot())) { + if (!FS::ensureFolderPathExists(instance->gameRoot())) { emit logLine("Couldn't create the main game folder", MessageLevel::Error); emitFailed(tr("Couldn't create the main game folder")); return; } // HACK: this is a workaround for MCL-3732 - 'server-resource-packs' folder is created. - if (!FS::ensureFolderPathExists(FS::PathCombine(minecraftInstance->gameRoot(), "server-resource-packs"))) { + if (!FS::ensureFolderPathExists(FS::PathCombine(instance->gameRoot(), "server-resource-packs"))) { emit logLine("Couldn't create the 'server-resource-packs' folder", MessageLevel::Error); } emitSucceeded(); diff --git a/launcher/minecraft/launch/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.cpp b/launcher/minecraft/launch/ExtractNatives.cpp index 8f3cac4d1..afe091758 100644 --- a/launcher/minecraft/launch/ExtractNatives.cpp +++ b/launcher/minecraft/launch/ExtractNatives.cpp @@ -70,16 +70,16 @@ static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibH void ExtractNatives::executeTask() { auto instance = m_parent->instance(); - std::shared_ptr minecraftInstance = std::dynamic_pointer_cast(instance); - auto toExtract = minecraftInstance->getNativeJars(); + auto toExtract = instance->getNativeJars(); if (toExtract.isEmpty()) { emitSucceeded(); return; } - auto settings = minecraftInstance->settings(); + auto settings = instance->settings(); - auto outputPath = minecraftInstance->getNativePath(); - auto javaVersion = minecraftInstance->getJavaVersion(); + auto outputPath = instance->getNativePath(); + FS::ensureFolderPathExists(outputPath); + auto javaVersion = instance->getJavaVersion(); bool jniHackEnabled = javaVersion.major() >= 8; for (const auto& source : toExtract) { if (!unzipNatives(source, outputPath, jniHackEnabled)) { diff --git a/launcher/minecraft/launch/ExtractNatives.h b/launcher/minecraft/launch/ExtractNatives.h index 2ab8816bd..1ad9a416e 100644 --- a/launcher/minecraft/launch/ExtractNatives.h +++ b/launcher/minecraft/launch/ExtractNatives.h @@ -16,15 +16,13 @@ #pragma once #include -#include -#include "minecraft/auth/AuthSession.h" // FIXME: temporary wrapper for existing task. 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 aa94edb5d..49d91e433 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -48,50 +48,26 @@ #include "gamemode_client.h" #endif -LauncherPartLaunch::LauncherPartLaunch(LaunchTask* parent) : LaunchStep(parent) +LauncherPartLaunch::LauncherPartLaunch(LaunchTask* parent) + : LaunchStep(parent) + , m_process(parent->instance()->getJavaVersion().defaultsToUtf8() ? QTextCodec::codecForName("UTF-8") : QTextCodec::codecForLocale()) { - auto instance = parent->instance(); - if (instance->settings()->get("CloseAfterLaunch").toBool()) { + if (parent->instance()->settings()->get("CloseAfterLaunch").toBool()) { std::shared_ptr connection{ new QMetaObject::Connection }; - *connection = connect(&m_process, &LoggedProcess::log, this, [=](QStringList lines, [[maybe_unused]] MessageLevel::Enum level) { - qDebug() << lines; - if (lines.filter(QRegularExpression(".*Setting user.+", QRegularExpression::CaseInsensitiveOption)).length() != 0) { - APPLICATION->closeAllWindows(); - disconnect(*connection); - } - }); + *connection = connect( + &m_process, &LoggedProcess::log, this, [connection](const QStringList& lines, [[maybe_unused]] MessageLevel::Enum level) { + qDebug() << lines; + if (lines.filter(QRegularExpression(".*Setting user.+", QRegularExpression::CaseInsensitiveOption)).length() != 0) { + APPLICATION->closeAllWindows(); + disconnect(*connection); + } + }); } connect(&m_process, &LoggedProcess::log, this, &LauncherPartLaunch::logLines); connect(&m_process, &LoggedProcess::stateChanged, this, &LauncherPartLaunch::on_state); } -#ifdef Q_OS_WIN -// returns 8.3 file format from long path -#include -QString shortPathName(const QString& file) -{ - auto input = file.toStdWString(); - std::wstring output; - long length = GetShortPathNameW(input.c_str(), NULL, 0); - // NOTE: this resizing might seem weird... - // when GetShortPathNameW fails, it returns length including null character - // when it succeeds, it returns length excluding null character - // See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa364989(v=vs.85).aspx - output.resize(length); - GetShortPathNameW(input.c_str(), (LPWSTR)output.c_str(), length); - output.resize(length - 1); - QString ret = QString::fromStdWString(output); - return ret; -} -#endif - -// if the string survives roundtrip through local 8bit encoding... -bool fitsInLocal8bit(const QString& string) -{ - return string == QString::fromLocal8Bit(string.toLocal8Bit()); -} - void LauncherPartLaunch::executeTask() { QString jarPath = APPLICATION->getJarPath("NewLaunch.jar"); @@ -103,10 +79,9 @@ void LauncherPartLaunch::executeTask() } auto instance = m_parent->instance(); - std::shared_ptr minecraftInstance = std::dynamic_pointer_cast(instance); QString legacyJarPath; - if (minecraftInstance->getLauncher() == "legacy" || minecraftInstance->shouldApplyOnlineFixes()) { + if (instance->getLauncher() == "legacy" || instance->shouldApplyOnlineFixes()) { legacyJarPath = APPLICATION->getJarPath("NewLaunchLegacy.jar"); if (legacyJarPath.isEmpty()) { const char* reason = QT_TR_NOOP("Legacy launcher library could not be found. Please check your installation."); @@ -116,8 +91,8 @@ void LauncherPartLaunch::executeTask() } } - m_launchScript = minecraftInstance->createLaunchScript(m_session, m_serverToJoin); - QStringList args = minecraftInstance->javaArguments(); + m_launchScript = instance->createLaunchScript(m_session, m_targetToJoin); + QStringList args = instance->javaArguments(); QString allArgs = args.join(", "); emit logLine("Java Arguments:\n[" + m_parent->censorPrivateInfo(allArgs) + "]\n\n", MessageLevel::Launcher); @@ -128,32 +103,23 @@ void LauncherPartLaunch::executeTask() // make detachable - this will keep the process running even if the object is destroyed m_process.setDetachable(true); - auto classPath = minecraftInstance->getClassPath(); + auto classPath = instance->getClassPath(); classPath.prepend(jarPath); if (!legacyJarPath.isEmpty()) classPath.prepend(legacyJarPath); - auto natPath = minecraftInstance->getNativePath(); + auto natPath = instance->getNativePath(); #ifdef Q_OS_WIN - if (!fitsInLocal8bit(natPath)) { - args << "-Djava.library.path=" + shortPathName(natPath); - } else { - args << "-Djava.library.path=" + natPath; - } -#else - args << "-Djava.library.path=" + natPath; + natPath = FS::getPathNameInLocal8bit(natPath); #endif + args << "-Djava.library.path=" + natPath; args << "-cp"; #ifdef Q_OS_WIN QStringList processed; for (auto& item : classPath) { - if (!fitsInLocal8bit(item)) { - processed << shortPathName(item); - } else { - processed << item; - } + processed << FS::getPathNameInLocal8bit(item); } args << processed.join(';'); #else @@ -165,6 +131,7 @@ void LauncherPartLaunch::executeTask() QString wrapperCommandStr = instance->getWrapperCommand().trimmed(); if (!wrapperCommandStr.isEmpty()) { + wrapperCommandStr = m_parent->substituteVariables(wrapperCommandStr); auto wrapperArgs = Commandline::splitArgs(wrapperCommandStr); auto wrapperCommand = wrapperArgs.takeFirst(); auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand); @@ -204,6 +171,7 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state) case LoggedProcess::Aborted: case LoggedProcess::Crashed: { m_parent->setPid(-1); + m_parent->instance()->setMinecraftRunning(false); emitFailed(tr("Game crashed.")); return; } @@ -213,6 +181,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) { @@ -228,7 +197,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()); @@ -248,6 +216,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.cpp b/launcher/minecraft/launch/ModMinecraftJar.cpp index 6e73333b1..e06080ba7 100644 --- a/launcher/minecraft/launch/ModMinecraftJar.cpp +++ b/launcher/minecraft/launch/ModMinecraftJar.cpp @@ -42,7 +42,7 @@ void ModMinecraftJar::executeTask() { - auto m_inst = std::dynamic_pointer_cast(m_parent->instance()); + auto m_inst = m_parent->instance(); if (!m_inst->getJarMods().size()) { emitSucceeded(); @@ -82,7 +82,7 @@ void ModMinecraftJar::finalize() bool ModMinecraftJar::removeJar() { - auto m_inst = std::dynamic_pointer_cast(m_parent->instance()); + auto m_inst = m_parent->instance(); auto finalJarPath = QDir(m_inst->binRoot()).absoluteFilePath("minecraft.jar"); QFile finalJar(finalJarPath); if (finalJar.exists()) { diff --git a/launcher/minecraft/launch/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.cpp b/launcher/minecraft/launch/ReconstructAssets.cpp index 843ccc554..21ae395f0 100644 --- a/launcher/minecraft/launch/ReconstructAssets.cpp +++ b/launcher/minecraft/launch/ReconstructAssets.cpp @@ -22,12 +22,11 @@ void ReconstructAssets::executeTask() { auto instance = m_parent->instance(); - std::shared_ptr minecraftInstance = std::dynamic_pointer_cast(instance); - auto components = minecraftInstance->getPackProfile(); + auto components = instance->getPackProfile(); auto profile = components->getProfile(); auto assets = profile->getMinecraftAssets(); - if (!AssetsUtils::reconstructAssets(assets->id, minecraftInstance->resourcesDir())) { + if (!AssetsUtils::reconstructAssets(assets->id, instance->resourcesDir())) { emit logLine("Failed to reconstruct Minecraft assets.", MessageLevel::Error); } diff --git a/launcher/minecraft/launch/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.cpp b/launcher/minecraft/launch/ScanModFolders.cpp index 7e08a4e36..1a2ddf194 100644 --- a/launcher/minecraft/launch/ScanModFolders.cpp +++ b/launcher/minecraft/launch/ScanModFolders.cpp @@ -42,7 +42,7 @@ void ScanModFolders::executeTask() { - auto m_inst = std::dynamic_pointer_cast(m_parent->instance()); + auto m_inst = m_parent->instance(); auto loaders = m_inst->loaderModList(); connect(loaders.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::modsDone); diff --git a/launcher/minecraft/launch/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..bc950d673 100644 --- a/launcher/minecraft/launch/VerifyJavaInstall.cpp +++ b/launcher/minecraft/launch/VerifyJavaInstall.cpp @@ -34,18 +34,32 @@ */ #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" void VerifyJavaInstall::executeTask() { - auto instance = std::dynamic_pointer_cast(m_parent->instance()); + auto instance = m_parent->instance(); auto packProfile = instance->getPackProfile(); auto settings = instance->settings(); auto storedVersion = settings->get("JavaVersion").toString(); 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 3496da2a0..0b8cb124d 100644 --- a/launcher/minecraft/mod/MetadataHandler.h +++ b/launcher/minecraft/mod/MetadataHandler.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 @@ -25,31 +26,48 @@ // launcher/minecraft/mod/Mod.h class Mod; -/* Abstraction file for easily changing the way metadata is stored / handled - * Needs to be a class because of -Wunused-function and no C++17 [[maybe_unused]] - * */ -class Metadata { - public: - using ModStruct = Packwiz::V1::Mod; - using ModSide = Packwiz::V1::Side; +namespace Metadata { +using ModStruct = Packwiz::V1::Mod; +using ModSide = Packwiz::V1::Side; - static auto create(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> ModStruct - { - return Packwiz::V1::createModFormat(index_dir, mod_pack, mod_version); - } +inline auto create(const QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> ModStruct +{ + return Packwiz::V1::createModFormat(index_dir, mod_pack, mod_version); +} - static auto create(QDir& index_dir, Mod& internal_mod, QString mod_slug) -> ModStruct - { - return Packwiz::V1::createModFormat(index_dir, internal_mod, mod_slug); - } +inline auto create(const QDir& index_dir, Mod& internal_mod, QString mod_slug) -> ModStruct +{ + return Packwiz::V1::createModFormat(index_dir, internal_mod, std::move(mod_slug)); +} - static void update(QDir& index_dir, ModStruct& mod) { Packwiz::V1::updateModIndex(index_dir, mod); } +inline void update(const QDir& index_dir, ModStruct& mod) +{ + Packwiz::V1::updateModIndex(index_dir, mod); +} - static void remove(QDir& index_dir, QString mod_slug) { Packwiz::V1::deleteModIndex(index_dir, mod_slug); } +inline void remove(const QDir& index_dir, QString mod_slug) +{ + Packwiz::V1::deleteModIndex(index_dir, mod_slug); +} - static void remove(QDir& index_dir, QVariant& mod_id) { Packwiz::V1::deleteModIndex(index_dir, mod_id); } +inline void remove(const QDir& index_dir, QVariant& mod_id) +{ + Packwiz::V1::deleteModIndex(index_dir, mod_id); +} - static auto get(QDir& index_dir, QString mod_slug) -> ModStruct { return Packwiz::V1::getIndexForMod(index_dir, mod_slug); } +inline auto get(const QDir& index_dir, QString mod_slug) -> ModStruct +{ + return Packwiz::V1::getIndexForMod(index_dir, std::move(mod_slug)); +} - static auto get(QDir& index_dir, QVariant& mod_id) -> ModStruct { return Packwiz::V1::getIndexForMod(index_dir, mod_id); } -}; +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 310946379..50fb45d77 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -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 @@ -35,49 +36,31 @@ */ #include "Mod.h" +#include -#include #include #include #include #include "MTPixmapCache.h" #include "MetadataHandler.h" +#include "Resource.h" #include "Version.h" #include "minecraft/mod/ModDetails.h" #include "minecraft/mod/tasks/LocalModParseTask.h" - -static ModPlatform::ProviderCapabilities ProviderCaps; +#include "modplatform/ModIndex.h" Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details() { m_enabled = (file.suffix() != "disabled"); } -Mod::Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata) : Mod(mods_dir.absoluteFilePath(metadata.filename)) -{ - m_name = metadata.name; - m_local_details.metadata = std::make_shared(std::move(metadata)); -} - -void Mod::setStatus(ModStatus status) -{ - m_local_details.status = status; -} -void Mod::setMetadata(std::shared_ptr&& metadata) -{ - if (status() == ModStatus::NoMetadata) - setStatus(ModStatus::Installed); - - m_local_details.metadata = metadata; -} - 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) @@ -87,30 +70,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().value_or("Unknown"), cast_other->provider().value_or("Unknown"), 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 @@ -127,28 +124,6 @@ bool Mod::applyFilter(QRegularExpression filter) const return Resource::applyFilter(filter); } -auto Mod::destroy(QDir& index_dir, bool preserve_metadata, bool attempt_trash) -> bool -{ - if (!preserve_metadata) { - qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name()); - - destroyMetadata(index_dir); - } - - return Resource::destroy(attempt_trash); -} - -void Mod::destroyMetadata(QDir& index_dir) -{ - if (metadata()) { - Metadata::remove(index_dir, metadata()->slug); - } else { - auto n = name(); - Metadata::remove(index_dir, n); - } - m_local_details.metadata = nullptr; -} - auto Mod::details() const -> const ModDetails& { return m_local_details; @@ -160,10 +135,7 @@ auto Mod::name() const -> QString if (!d_name.isEmpty()) return d_name; - if (metadata()) - return metadata()->name; - - return m_name; + return Resource::name(); } auto Mod::version() const -> QString @@ -171,16 +143,52 @@ auto Mod::version() const -> QString return details().version; } -auto Mod::homeurl() const -> QString +auto Mod::homepage() const -> QString { - return details().homeurl; + QString metaUrl = Resource::homepage(); + + if (metaUrl.isEmpty()) + return details().homeurl; + else + return metaUrl; } -auto Mod::metaurl() const -> QString +auto Mod::loaders() const -> QString { - if (metadata() == nullptr) - return homeurl(); - return ModPlatform::getMetaURL(metadata()->provider, metadata()->project_id); + if (metadata()) { + QStringList loaders; + auto modLoaders = metadata()->loaders; + for (auto loader : ModPlatform::modLoaderTypesToList(modLoaders)) { + 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 @@ -193,45 +201,17 @@ auto Mod::authors() const -> QStringList return details().authors; } -auto Mod::status() const -> ModStatus -{ - return details().status; -} - -auto Mod::metadata() -> std::shared_ptr -{ - return m_local_details.metadata; -} - -auto Mod::metadata() const -> const std::shared_ptr -{ - return m_local_details.metadata; -} - void Mod::finishResolvingWithDetails(ModDetails&& details) { m_is_resolving = false; m_is_resolved = true; - std::shared_ptr metadata = details.metadata; - if (details.status == ModStatus::Unknown) - details.status = m_local_details.status; - m_local_details = std::move(details); - if (metadata) - setMetadata(std::move(metadata)); if (!iconPath().isEmpty()) { - m_pack_image_cache_key.was_read_attempt = false; + m_packImageCacheKey.wasReadAttempt = false; } } -auto Mod::provider() const -> std::optional -{ - if (metadata()) - return ProviderCaps.readableName(metadata()->provider); - return {}; -} - auto Mod::licenses() const -> const QList& { return details().licenses; @@ -242,45 +222,53 @@ auto Mod::issueTracker() const -> QString return details().issue_tracker; } -void Mod::setIcon(QImage new_image) const +QPixmap Mod::setIcon(QImage new_image) const { QMutexLocker locker(&m_data_lock); Q_ASSERT(!new_image.isNull()); - if (m_pack_image_cache_key.key.isValid()) - PixmapCache::remove(m_pack_image_cache_key.key); + if (m_packImageCacheKey.key.isValid()) + PixmapCache::remove(m_packImageCacheKey.key); // scale the image to avoid flooding the pixmapcache auto pixmap = QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); - m_pack_image_cache_key.key = PixmapCache::insert(pixmap); - m_pack_image_cache_key.was_ever_used = true; - m_pack_image_cache_key.was_read_attempt = true; + m_packImageCacheKey.key = PixmapCache::insert(pixmap); + m_packImageCacheKey.wasEverUsed = true; + m_packImageCacheKey.wasReadAttempt = true; + return pixmap; } QPixmap Mod::icon(QSize size, Qt::AspectRatioMode mode) const { - QPixmap cached_image; - if (PixmapCache::find(m_pack_image_cache_key.key, &cached_image)) { + auto pixmap_transform = [&size, &mode](QPixmap pixmap) { if (size.isNull()) - return cached_image; - return cached_image.scaled(size, mode, Qt::SmoothTransformation); + return pixmap; + return pixmap.scaled(size, mode, Qt::SmoothTransformation); + }; + + QPixmap cached_image; + if (PixmapCache::find(m_packImageCacheKey.key, &cached_image)) { + return pixmap_transform(cached_image); } // No valid image we can get - if ((!m_pack_image_cache_key.was_ever_used && m_pack_image_cache_key.was_read_attempt) || iconPath().isEmpty()) + if ((!m_packImageCacheKey.wasEverUsed && m_packImageCacheKey.wasReadAttempt) || iconPath().isEmpty()) return {}; - if (m_pack_image_cache_key.was_ever_used) { - qDebug() << "Mod" << name() << "Had it's icon evicted form the cache. reloading..."; + if (m_packImageCacheKey.wasEverUsed) { + qDebug() << "Mod" << name() << "Had it's icon evicted from the cache. reloading..."; PixmapCache::markCacheMissByEviciton(); } // Image got evicted from the cache or an attempt to load it has not been made. load it and retry. - m_pack_image_cache_key.was_read_attempt = true; - ModUtils::loadIconFile(*this); - return icon(size); + m_packImageCacheKey.wasReadAttempt = true; + if (ModUtils::loadIconFile(*this, &cached_image)) { + return pixmap_transform(cached_image); + } + // Image failed to load + return {}; } bool Mod::valid() const diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index e97ee9d3b..8a352c66c 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.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 @@ -56,39 +57,33 @@ class Mod : public Resource { Mod() = default; Mod(const QFileInfo& file); - Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata); Mod(QString file_path) : Mod(QFileInfo(file_path)) {} auto details() const -> const ModDetails&; auto name() const -> QString override; auto version() const -> QString; - auto homeurl() const -> QString; + auto homepage() const -> QString override; auto description() const -> QString; auto authors() const -> QStringList; - auto status() const -> ModStatus; - auto provider() const -> std::optional; auto licenses() const -> const QList&; auto issueTracker() const -> QString; - auto metaurl() const -> QString; + 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; } /** Gets the icon of the mod, converted to a QPixmap for drawing, and scaled to size. */ [[nodiscard]] QPixmap icon(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; /** Thread-safe. */ - void setIcon(QImage new_image) const; + QPixmap setIcon(QImage new_image) const; - auto metadata() -> std::shared_ptr; - auto metadata() const -> const std::shared_ptr; - - void setStatus(ModStatus status); - void setMetadata(std::shared_ptr&& metadata); - void setMetadata(const Metadata::ModStruct& metadata) { setMetadata(std::make_shared(metadata)); } 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 @@ -105,7 +100,7 @@ class Mod : public Resource { struct { QPixmapCache::Key key; - bool was_ever_used = false; - bool was_read_attempt = false; - } mutable m_pack_image_cache_key; + bool wasEverUsed = false; + bool wasReadAttempt = false; + } mutable m_packImageCacheKey; }; diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h index a00d5a24b..9195c0368 100644 --- a/launcher/minecraft/mod/ModDetails.h +++ b/launcher/minecraft/mod/ModDetails.h @@ -43,13 +43,6 @@ #include "minecraft/mod/MetadataHandler.h" -enum class ModStatus { - Installed, // Both JAR and Metadata are present - NotInstalled, // Only the Metadata is present - NoMetadata, // Only the JAR is present - Unknown, // Default status -}; - struct ModLicense { QString name = {}; QString id = {}; @@ -149,12 +142,6 @@ struct ModDetails { /* Path of mod logo */ QString icon_file = {}; - /* Installation status of the mod */ - ModStatus status = ModStatus::Unknown; - - /* Metadata information, if any */ - std::shared_ptr metadata = nullptr; - ModDetails() = default; /** Metadata should be handled manually to properly set the mod status. */ @@ -169,40 +156,9 @@ struct ModDetails { , issue_tracker(other.issue_tracker) , licenses(other.licenses) , icon_file(other.icon_file) - , status(other.status) {} - ModDetails& operator=(const ModDetails& other) - { - this->mod_id = other.mod_id; - this->name = other.name; - this->version = other.version; - this->mcversion = other.mcversion; - this->homeurl = other.homeurl; - this->description = other.description; - this->authors = other.authors; - this->issue_tracker = other.issue_tracker; - this->licenses = other.licenses; - this->icon_file = other.icon_file; - this->status = other.status; + ModDetails& operator=(const ModDetails& other) = default; - return *this; - } - - ModDetails& operator=(const ModDetails&& other) - { - this->mod_id = other.mod_id; - this->name = other.name; - this->version = other.version; - this->mcversion = other.mcversion; - this->homeurl = other.homeurl; - this->description = other.description; - this->authors = other.authors; - this->issue_tracker = other.issue_tracker; - this->licenses = other.licenses; - this->icon_file = other.icon_file; - this->status = other.status; - - return *this; - } + ModDetails& operator=(ModDetails&& other) = default; }; diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index fc543202f..027f3d4ca 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -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 @@ -47,27 +48,26 @@ #include #include #include -#include #include "Application.h" -#include "Json.h" #include "minecraft/mod/tasks/LocalModParseTask.h" -#include "minecraft/mod/tasks/LocalModUpdateTask.h" -#include "minecraft/mod/tasks/ModFolderLoadTask.h" -#include "modplatform/ModIndex.h" -#include "modplatform/flame/FlameAPI.h" -#include "modplatform/flame/FlameModIndex.h" -ModFolderModel::ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir) - : ResourceFolderModel(QDir(dir), instance, nullptr, create_dir), m_is_indexed(is_indexed) +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 @@ -84,7 +84,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,42 +92,50 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const default: break; } - return at(row)->version(); + return at(row).version(); } case DateColumn: - return m_resources[row]->dateTimeChanged(); + return at(row).dateTimeChanged(); case ProviderColumn: { - auto provider = at(row)->provider(); - if (!provider.has_value()) { - //: Unknown mod provider (i.e. not Modrinth, CurseForge, etc...) - return tr("Unknown"); - } - - return provider.value(); + 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 (at(row)->isSymLinkUnder(instDirPath())) { + if (column == NameColumn) { + if (at(row).isSymLinkUnder(instDirPath())) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." "\nCanonical Path: %1") - .arg(at(row)->fileinfo().canonicalFilePath()); + .arg(at(row).fileinfo().canonicalFilePath()); } - if (at(row)->isMoreThanOneHardLink()) { + if (at(row).isMoreThanOneHardLink()) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); } } return m_resources[row]->internal_id(); case Qt::DecorationRole: { - if (column == 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); + return at(row).icon({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } return {}; } @@ -139,7 +147,7 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const case Qt::CheckStateRole: switch (column) { case ActiveColumn: - return at(row)->enabled() ? Qt::Checked : Qt::Unchecked; + return at(row).enabled() ? Qt::Checked : Qt::Unchecked; default: return QVariant(); } @@ -159,6 +167,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(); @@ -175,7 +188,17 @@ QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientatio case DateColumn: return tr("The date and time this mod was last changed (or added)."); case ProviderColumn: - return tr("Where the mod was downloaded from."); + 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(); } @@ -190,133 +213,16 @@ int ModFolderModel::columnCount(const QModelIndex& parent) const return parent.isValid() ? 0 : NUM_COLUMNS; } -Task* ModFolderModel::createUpdateTask() -{ - auto index_dir = indexDir(); - auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load); - m_first_folder_load = false; - return task; -} - Task* ModFolderModel::createParseTask(Resource& resource) { return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo()); } -bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata) -{ - for (auto mod : allMods()) { - if (mod->fileinfo().fileName() == filename) { - auto index_dir = indexDir(); - mod->destroy(index_dir, preserve_metadata, false); - - update(); - - return true; - } - } - - return false; -} - -bool ModFolderModel::deleteMods(const QModelIndexList& indexes) -{ - if (indexes.isEmpty()) - return true; - - for (auto i : indexes) { - if (i.column() != 0) { - continue; - } - auto m = at(i.row()); - auto index_dir = indexDir(); - m->destroy(index_dir); - } - - update(); - - return true; -} - -bool ModFolderModel::deleteModsMetadata(const QModelIndexList& indexes) -{ - if (indexes.isEmpty()) - return true; - - for (auto i : indexes) { - if (i.column() != 0) { - continue; - } - auto m = at(i.row()); - auto index_dir = indexDir(); - m->destroyMetadata(index_dir); - } - - update(); - - return true; -} - bool ModFolderModel::isValid() { return m_dir.exists() && m_dir.isReadable(); } -bool ModFolderModel::startWatching() -{ - // Remove orphaned metadata next time - m_first_folder_load = true; - return ResourceFolderModel::startWatching({ m_dir.absolutePath(), indexDir().absolutePath() }); -} - -bool ModFolderModel::stopWatching() -{ - return ResourceFolderModel::stopWatching({ m_dir.absolutePath(), indexDir().absolutePath() }); -} - -auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList -{ - QList selected_resources; - for (auto i : indexes) { - if (i.column() != 0) - continue; - - selected_resources.push_back(at(i.row())); - } - return selected_resources; -} - -auto ModFolderModel::allMods() -> QList -{ - QList mods; - - for (auto& res : qAsConst(m_resources)) { - mods.append(static_cast(res.get())); - } - - return mods; -} - -void ModFolderModel::onUpdateSucceeded() -{ - auto update_results = static_cast(m_current_update_task.get())->result(); - - auto& new_mods = update_results->mods; - -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - auto current_list = m_resources_index.keys(); - QSet current_set(current_list.begin(), current_list.end()); - - auto new_list = new_mods.keys(); - QSet new_set(new_list.begin(), new_list.end()); -#else - QSet current_set(m_resources_index.keys().toSet()); - QSet new_set(new_mods.keys().toSet()); -#endif - - applyUpdates(current_set, new_set, new_mods); -} - void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) { auto iter = m_active_parse_tasks.constFind(ticket); @@ -334,51 +240,7 @@ void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) auto result = cast_task->result(); if (result && resource) - resource->finishResolvingWithDetails(std::move(result->details)); + static_cast(resource.get())->finishResolvingWithDetails(std::move(result->details)); emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } - -static const FlameAPI flameAPI; -bool ModFolderModel::installMod(QString file_path, ModPlatform::IndexedVersion& vers) -{ - if (vers.addonId.isValid()) { - ModPlatform::IndexedPack pack{ - vers.addonId, - ModPlatform::ResourceProvider::FLAME, - }; - - QEventLoop loop; - - auto response = std::make_shared(); - auto job = flameAPI.getProject(vers.addonId.toString(), response); - - QObject::connect(job.get(), &Task::failed, [&loop] { loop.quit(); }); - QObject::connect(job.get(), &Task::aborted, &loop, &QEventLoop::quit); - QObject::connect(job.get(), &Task::succeeded, [response, this, &vers, &loop, &pack] { - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qDebug() << *response; - return; - } - try { - auto obj = Json::requireObject(Json::requireObject(doc), "data"); - FlameMod::loadIndexedPack(pack, obj); - } catch (const JSONValidationError& e) { - qDebug() << doc; - qWarning() << "Error while reading mod info: " << e.cause(); - } - LocalModUpdateTask update_metadata(indexDir(), pack, vers); - QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit); - update_metadata.start(); - }); - - job->start(); - - loop.exec(); - } - return ResourceFolderModel::installResource(file_path); -} diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 61d840f9b..42868dc91 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 @@ -46,11 +47,6 @@ #include "Mod.h" #include "ResourceFolderModel.h" -#include "minecraft/mod/tasks/LocalModParseTask.h" -#include "minecraft/mod/tasks/ModFolderLoadTask.h" -#include "modplatform/ModIndex.h" - -class LegacyInstance; class BaseInstance; class QFileSystemWatcher; @@ -61,9 +57,21 @@ class QFileSystemWatcher; class ModFolderModel : public ResourceFolderModel { Q_OBJECT public: - enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, VersionColumn, DateColumn, ProviderColumn, NUM_COLUMNS }; - enum ModStatusAction { Disable, Enable, Toggle }; - ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed = false, bool create_dir = true); + enum Columns { + ActiveColumn = 0, + ImageColumn, + NameColumn, + VersionColumn, + DateColumn, + ProviderColumn, + SizeColumn, + SideColumn, + LoadersColumn, + McVersionsColumn, + ReleaseTypeColumn, + NUM_COLUMNS + }; + ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); virtual QString id() const override { return "mods"; } @@ -72,34 +80,13 @@ class ModFolderModel : public ResourceFolderModel { QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; int columnCount(const QModelIndex& parent) const override; - [[nodiscard]] Task* createUpdateTask() override; + [[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new Mod(file); } [[nodiscard]] Task* createParseTask(Resource&) override; - bool installMod(QString file_path) { return ResourceFolderModel::installResource(file_path); } - bool installMod(QString file_path, ModPlatform::IndexedVersion& vers); - bool uninstallMod(const QString& filename, bool preserve_metadata = false); - - /// Deletes all the selected mods - bool deleteMods(const QModelIndexList& indexes); - bool deleteModsMetadata(const QModelIndexList& indexes); - bool isValid(); - bool startWatching() override; - bool stopWatching() override; - - QDir indexDir() { return { QString("%1/.index").arg(dir().absolutePath()) }; } - - auto selectedMods(QModelIndexList& indexes) -> QList; - auto allMods() -> QList; - RESOURCE_HELPERS(Mod) private slots: - void onUpdateSucceeded() override; void onParseSucceeded(int ticket, QString resource_id) override; - - protected: - bool m_is_indexed; - bool m_first_folder_load = true; }; diff --git a/launcher/minecraft/mod/Resource.cpp b/launcher/minecraft/mod/Resource.cpp index da806f0f4..d1a7b8f9c 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; @@ -54,6 +72,14 @@ void Resource::parseFile() m_changed_date_time = m_file_info.lastModified(); } +auto Resource::name() const -> QString +{ + if (metadata()) + return metadata()->name; + + return m_name; +} + static void removeThePrefix(QString& string) { QRegularExpression regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption); @@ -61,37 +87,79 @@ static void removeThePrefix(QString& string) string = string.trimmed(); } -std::pair Resource::compare(const Resource& other, SortType type) const +auto Resource::provider() const -> QString +{ + if (metadata()) + return ModPlatform::ProviderCapabilities::readableName(metadata()->provider); + + return tr("Unknown"); +} + +auto Resource::homepage() const -> QString +{ + if (metadata()) + return ModPlatform::getMetaURL(metadata()->provider, metadata()->project_id); + + return {}; +} + +void Resource::setMetadata(std::shared_ptr&& metadata) +{ + if (status() == ResourceStatus::NO_METADATA) + setStatus(ResourceStatus::INSTALLED); + + m_metadata = metadata; +} + +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 @@ -130,15 +198,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)); @@ -146,10 +213,27 @@ bool Resource::enable(EnableAction action) return true; } -bool Resource::destroy(bool attemptTrash) +auto Resource::destroy(const QDir& index_dir, bool preserve_metadata, bool attempt_trash) -> bool { m_type = ResourceType::UNKNOWN; - return (attemptTrash && FS::trash(m_file_info.filePath())) || FS::deletePath(m_file_info.filePath()); + + if (!preserve_metadata) { + qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name()); + destroyMetadata(index_dir); + } + + return (attempt_trash && FS::trash(m_file_info.filePath())) || FS::deletePath(m_file_info.filePath()); +} + +auto Resource::destroyMetadata(const QDir& index_dir) -> void +{ + if (metadata()) { + Metadata::remove(index_dir, metadata()->slug); + } else { + auto n = name(); + Metadata::remove(index_dir, n); + } + m_metadata = nullptr; } bool Resource::isSymLinkUnder(const QString& instPath) const @@ -169,3 +253,11 @@ 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 c1ed49461..42463fe8f 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 @@ -5,6 +40,7 @@ #include #include +#include "MetadataHandler.h" #include "QObjectPtr.h" enum class ResourceType { @@ -15,7 +51,14 @@ enum class ResourceType { LITEMOD, //!< The resource is a litemod }; -enum class SortType { NAME, DATE, VERSION, ENABLED, PACK_FORMAT, PROVIDER }; +enum class ResourceStatus { + INSTALLED, // Both JAR and Metadata are present + NOT_INSTALLED, // Only the Metadata is present + NO_METADATA, // Only the JAR is present + UNKNOWN, // Default status +}; + +enum class SortType { NAME, DATE, VERSION, ENABLED, PACK_FORMAT, PROVIDER, SIZE, SIDE, MC_VERSIONS, LOADERS, RELEASE_TYPE }; enum class EnableAction { ENABLE, DISABLE, TOGGLE }; @@ -45,18 +88,29 @@ 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 { return m_name; } + [[nodiscard]] virtual auto name() const -> QString; [[nodiscard]] virtual bool valid() const { return m_type != ResourceType::UNKNOWN; } + [[nodiscard]] auto status() const -> ResourceStatus { return m_status; }; + [[nodiscard]] auto metadata() -> std::shared_ptr { return m_metadata; } + [[nodiscard]] auto metadata() const -> std::shared_ptr { return m_metadata; } + [[nodiscard]] auto provider() const -> QString; + [[nodiscard]] virtual auto homepage() const -> QString; + + void setStatus(ResourceStatus status) { m_status = status; } + void setMetadata(std::shared_ptr&& metadata); + void setMetadata(const Metadata::ModStruct& metadata) { setMetadata(std::make_shared(metadata)); } + /** Compares two Resources, for sorting purposes, considering a ascending order, returning: * > 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). @@ -81,7 +135,9 @@ class Resource : public QObject { } // Delete all files of this resource. - bool destroy(bool attemptTrash = true); + auto destroy(const QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool; + // Delete the metadata only. + auto destroyMetadata(const QDir& index_dir) -> void; [[nodiscard]] auto isSymLink() const -> bool { return m_file_info.isSymLink(); } @@ -110,6 +166,11 @@ class Resource : public QObject { /* The type of file we're dealing with. */ ResourceType m_type = ResourceType::UNKNOWN; + /* Installation status of the resource. */ + ResourceStatus m_status = ResourceStatus::UNKNOWN; + + std::shared_ptr m_metadata = nullptr; + /* Whether the resource is enabled (e.g. shows up in the game) or not. */ bool m_enabled = true; @@ -117,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 9157f35f0..adeb2e422 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -11,19 +11,23 @@ #include #include #include +#include #include "Application.h" #include "FileSystem.h" -#include "QVariantUtils.h" -#include "minecraft/mod/tasks/BasicFolderLoadTask.h" +#include "minecraft/mod/tasks/ResourceFolderLoadTask.h" +#include "Json.h" +#include "minecraft/mod/tasks/LocalResourceUpdateTask.h" +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/flame/FlameModIndex.h" #include "settings/Setting.h" #include "tasks/Task.h" #include "ui/dialogs/CustomMessageBox.h" -ResourceFolderModel::ResourceFolderModel(QDir dir, BaseInstance* instance, QObject* parent, bool create_dir) - : QAbstractListModel(parent), m_dir(dir), m_instance(instance), m_watcher(this) +ResourceFolderModel::ResourceFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) + : QAbstractListModel(parent), m_dir(dir), m_instance(instance), m_watcher(this), m_is_indexed(is_indexed) { if (create_dir) { FS::ensureFolderPathExists(m_dir.absolutePath()); @@ -34,10 +38,9 @@ ResourceFolderModel::ResourceFolderModel(QDir dir, BaseInstance* instance, QObje connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ResourceFolderModel::directoryChanged); connect(&m_helper_thread_task, &ConcurrentTask::finished, this, [this] { m_helper_thread_task.clear(); }); -#ifndef LAUNCHER_TEST - // in tests the application macro doesn't work - m_helper_thread_task.setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); -#endif + if (APPLICATION_DYN) { // in tests the application macro doesn't work + m_helper_thread_task.setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); + } } ResourceFolderModel::~ResourceFolderModel() @@ -48,6 +51,9 @@ ResourceFolderModel::~ResourceFolderModel() bool ResourceFolderModel::startWatching(const QStringList& paths) { + // Remove orphaned metadata next time + m_first_folder_load = true; + if (m_is_watching) return false; @@ -111,7 +117,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; } @@ -158,11 +164,55 @@ bool ResourceFolderModel::installResource(QString original_path) return false; } -bool ResourceFolderModel::uninstallResource(QString file_name) +bool ResourceFolderModel::installResourceWithFlameMetadata(QString path, ModPlatform::IndexedVersion& vers) +{ + if (vers.addonId.isValid()) { + ModPlatform::IndexedPack pack{ + vers.addonId, + ModPlatform::ResourceProvider::FLAME, + }; + + QEventLoop loop; + + auto response = std::make_shared(); + auto job = FlameAPI().getProject(vers.addonId.toString(), response); + + QObject::connect(job.get(), &Task::failed, [&loop] { loop.quit(); }); + QObject::connect(job.get(), &Task::aborted, &loop, &QEventLoop::quit); + QObject::connect(job.get(), &Task::succeeded, [response, this, &vers, &loop, &pack] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qDebug() << *response; + return; + } + try { + auto obj = Json::requireObject(Json::requireObject(doc), "data"); + FlameMod::loadIndexedPack(pack, obj); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading mod info: " << e.cause(); + } + LocalResourceUpdateTask update_metadata(indexDir(), pack, vers); + QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit); + update_metadata.start(); + }); + + job->start(); + + loop.exec(); + } + + return installResource(std::move(path)); +} + +bool ResourceFolderModel::uninstallResource(QString file_name, bool preserve_metadata) { for (auto& resource : m_resources) { if (resource->fileinfo().fileName() == file_name) { - auto res = resource->destroy(false); + auto res = resource->destroy(indexDir(), preserve_metadata, false); update(); @@ -178,13 +228,11 @@ bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes) return true; for (auto i : indexes) { - if (i.column() != 0) { + if (i.column() != 0) continue; - } auto& resource = m_resources.at(i.row()); - - resource->destroy(); + resource->destroy(indexDir()); } update(); @@ -192,6 +240,22 @@ bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes) return true; } +void ResourceFolderModel::deleteMetadata(const QModelIndexList& indexes) +{ + if (indexes.isEmpty()) + return; + + for (auto i : indexes) { + if (i.column() != 0) + continue; + + auto& resource = m_resources.at(i.row()); + resource->destroyMetadata(indexDir()); + } + + update(); +} + bool ResourceFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAction action) { if (indexes.isEmpty()) @@ -214,9 +278,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; @@ -248,7 +309,7 @@ bool ResourceFolderModel::update() connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection); connect( m_current_update_task.get(), &Task::finished, this, - [=] { + [this] { m_current_update_task.reset(); if (m_scheduled_update) { m_scheduled_update = false; @@ -264,7 +325,7 @@ bool ResourceFolderModel::update() return true; } -void ResourceFolderModel::resolveResource(Resource* res) +void ResourceFolderModel::resolveResource(Resource::Ptr res) { if (!res->shouldResolve()) { return; @@ -280,11 +341,18 @@ void ResourceFolderModel::resolveResource(Resource* res) m_active_parse_tasks.insert(ticket, task); connect( - task.get(), &Task::succeeded, this, [=] { onParseSucceeded(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); + task.get(), &Task::succeeded, this, [this, ticket, res] { onParseSucceeded(ticket, res->internal_id()); }, + Qt::ConnectionType::QueuedConnection); connect( - task.get(), &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); + task.get(), &Task::failed, this, [this, ticket, res] { 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, + [this, ticket] { + m_active_parse_tasks.remove(ticket); + emit parseFinished(); + }, + Qt::ConnectionType::QueuedConnection); m_helper_thread_task.addTask(task); @@ -295,7 +363,7 @@ void ResourceFolderModel::resolveResource(Resource* res) void ResourceFolderModel::onUpdateSucceeded() { - auto update_results = static_cast(m_current_update_task.get())->result(); + auto update_results = static_cast(m_current_update_task.get())->result(); auto& new_resources = update_results->resources; @@ -316,7 +384,7 @@ void ResourceFolderModel::onUpdateSucceeded() void ResourceFolderModel::onParseSucceeded(int ticket, QString resource_id) { auto iter = m_active_parse_tasks.constFind(ticket); - if (iter == m_active_parse_tasks.constEnd()) + if (iter == m_active_parse_tasks.constEnd() || !m_resources_index.contains(resource_id)) return; int row = m_resources_index[resource_id]; @@ -325,7 +393,11 @@ void ResourceFolderModel::onParseSucceeded(int ticket, QString resource_id) Task* ResourceFolderModel::createUpdateTask() { - return new BasicFolderLoadTask(m_dir); + auto index_dir = indexDir(); + auto task = new ResourceFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load, + [this](const QFileInfo& file) { return createResource(file); }); + m_first_folder_load = false; + return task; } bool ResourceFolderModel::hasPendingParseTasks() const @@ -411,15 +483,19 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const switch (role) { case Qt::DisplayRole: switch (column) { - case NAME_COLUMN: + case NameColumn: return m_resources[row]->name(); - case DATE_COLUMN: + case DateColumn: return m_resources[row]->dateTimeChanged(); + case ProviderColumn: + return m_resources[row]->provider(); + case SizeColumn: + return m_resources[row]->sizeStr(); default: return {}; } 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." @@ -435,14 +511,14 @@ QVariant ResourceFolderModel::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"); return {}; } case Qt::CheckStateRole: switch (column) { - case ACTIVE_COLUMN: + case ActiveColumn: return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked; default: return {}; @@ -481,24 +557,28 @@ QVariant ResourceFolderModel::headerData(int section, [[maybe_unused]] Qt::Orien switch (role) { case Qt::DisplayRole: switch (section) { - case ACTIVE_COLUMN: - case NAME_COLUMN: - case DATE_COLUMN: + case ActiveColumn: + case NameColumn: + case DateColumn: + case ProviderColumn: + case SizeColumn: return columnNames().at(section); default: return {}; } case Qt::ToolTipRole: { + //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. switch (section) { - case ACTIVE_COLUMN: - //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. + case ActiveColumn: return tr("Is the resource enabled?"); - case NAME_COLUMN: - //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. + case NameColumn: return tr("The name of the resource."); - case DATE_COLUMN: - //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. + case DateColumn: return tr("The date and time this resource was last changed (or added)."); + case ProviderColumn: + return tr("The source provider of the resource."); + case SizeColumn: + return tr("The size of the resource."); default: return {}; } @@ -528,6 +608,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); @@ -605,15 +689,159 @@ 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() || !m_resources_index.contains(resource_id)) + 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(); +} + +void ResourceFolderModel::applyUpdates(QSet& current_set, QSet& new_set, QMap& new_resources) +{ + // see if the kept resources changed in some way + { + QSet kept_set = current_set; + kept_set.intersect(new_set); + + for (auto const& kept : kept_set) { + auto row_it = m_resources_index.constFind(kept); + Q_ASSERT(row_it != m_resources_index.constEnd()); + auto row = row_it.value(); + + auto& new_resource = new_resources[kept]; + auto const& current_resource = m_resources.at(row); + + if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) { + // no significant change, ignore... + continue; + } + + // If the resource is resolving, but something about it changed, we don't want to + // continue the resolving. + if (current_resource->isResolving()) { + auto ticket = current_resource->resolutionTicket(); + if (m_active_parse_tasks.contains(ticket)) { + auto task = (*m_active_parse_tasks.find(ticket)).get(); + task->abort(); + } + } + + m_resources[row].reset(new_resource); + resolveResource(m_resources.at(row)); + emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); + } + } + + // remove resources no longer present + { + QSet removed_set = current_set; + removed_set.subtract(new_set); + + QList removed_rows; + for (auto& removed : removed_set) + removed_rows.append(m_resources_index[removed]); + + std::sort(removed_rows.begin(), removed_rows.end(), std::greater()); + + for (auto& removed_index : removed_rows) { + auto removed_it = m_resources.begin() + removed_index; + + Q_ASSERT(removed_it != m_resources.end()); + + if ((*removed_it)->isResolving()) { + auto ticket = (*removed_it)->resolutionTicket(); + if (m_active_parse_tasks.contains(ticket)) { + auto task = (*m_active_parse_tasks.find(ticket)).get(); + task->abort(); + } + } + + beginRemoveRows(QModelIndex(), removed_index, removed_index); + m_resources.erase(removed_it); + endRemoveRows(); + } + } + + // add new resources to the end + { + QSet added_set = new_set; + added_set.subtract(current_set); + + // When you have a Qt build with assertions turned on, proceeding here will abort the application + if (added_set.size() > 0) { + beginInsertRows(QModelIndex(), static_cast(m_resources.size()), + static_cast(m_resources.size() + added_set.size() - 1)); + + for (auto& added : added_set) { + auto res = new_resources[added]; + m_resources.append(res); + resolveResource(m_resources.last()); + } + + endInsertRows(); + } + } + + // update index + { + m_resources_index.clear(); + int idx = 0; + for (auto const& mod : qAsConst(m_resources)) { + m_resources_index[mod->internal_id()] = idx; + idx++; + } + } +} +Resource::Ptr ResourceFolderModel::find(QString id) +{ + auto iter = + std::find_if(m_resources.constBegin(), m_resources.constEnd(), [&](Resource::Ptr const& r) { return r->internal_id() == id; }); + if (iter == m_resources.constEnd()) + return nullptr; + return *iter; +} +QList ResourceFolderModel::allResources() +{ + QList result; + result.reserve(m_resources.size()); + for (const Resource ::Ptr& resource : m_resources) + result.append((resource.get())); + return result; +} +QList ResourceFolderModel::selectedResources(const QModelIndexList& indexes) +{ + QList result; + for (const QModelIndex& index : indexes) { + if (index.column() != 0) + continue; + result.append(&at(index.row())); + } + return result; +} diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index d764280b6..ee26a74bc 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -19,6 +19,38 @@ class QSortFilterProxyModel; +/* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */ +#define RESOURCE_HELPERS(T) \ + [[nodiscard]] T& at(int index) \ + { \ + return *static_cast(m_resources[index].get()); \ + } \ + [[nodiscard]] const T& at(int index) const \ + { \ + return *static_cast(m_resources.at(index).get()); \ + } \ + QList selected##T##s(const QModelIndexList& indexes) \ + { \ + QList result; \ + for (const QModelIndex& index : indexes) { \ + if (index.column() != 0) \ + continue; \ + \ + result.append(&at(index.row())); \ + } \ + return result; \ + } \ + QList all##T##s() \ + { \ + QList result; \ + result.reserve(m_resources.size()); \ + \ + for (const Resource::Ptr& resource : m_resources) \ + result.append(static_cast(resource.get())); \ + \ + return result; \ + } + /** A basic model for external resources. * * This model manages a list of resources. As such, external users of such resources do not own them, @@ -29,7 +61,7 @@ class QSortFilterProxyModel; class ResourceFolderModel : public QAbstractListModel { Q_OBJECT public: - ResourceFolderModel(QDir, BaseInstance* instance, QObject* parent = nullptr, bool create_dir = true); + ResourceFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); ~ResourceFolderModel() override; virtual QString id() const { return "resource"; } @@ -49,8 +81,10 @@ class ResourceFolderModel : public QAbstractListModel { bool stopWatching(const QStringList& paths); /* Helper methods for subclasses, using a predetermined list of paths. */ - virtual bool startWatching() { return startWatching({ m_dir.absolutePath() }); } - virtual bool stopWatching() { return stopWatching({ m_dir.absolutePath() }); } + virtual bool startWatching() { return startWatching({ indexDir().absolutePath(), m_dir.absolutePath() }); } + virtual bool stopWatching() { return stopWatching({ indexDir().absolutePath(), m_dir.absolutePath() }); } + + QDir indexDir() { return { QString("%1/.index").arg(dir().absolutePath()) }; } /** Given a path in the system, install that resource, moving it to its place in the * instance file hierarchy. @@ -59,12 +93,15 @@ class ResourceFolderModel : public QAbstractListModel { */ virtual bool installResource(QString path); + virtual bool installResourceWithFlameMetadata(QString path, ModPlatform::IndexedVersion& vers); + /** Uninstall (i.e. remove all data about it) a resource, given its file name. * * Returns whether the removal was successful. */ - virtual bool uninstallResource(QString file_name); + virtual bool uninstallResource(QString file_name, bool preserve_metadata = false); virtual bool deleteResources(const QModelIndexList&); + virtual void deleteMetadata(const QModelIndexList&); /** Applies the given 'action' to the resources in 'indexes'. * @@ -76,13 +113,17 @@ class ResourceFolderModel : public QAbstractListModel { virtual bool update(); /** Creates a new parse task, if needed, for 'res' and start it.*/ - virtual void resolveResource(Resource* res); + virtual void resolveResource(Resource::Ptr res); [[nodiscard]] qsizetype size() const { return m_resources.size(); } [[nodiscard]] bool empty() const { return size() == 0; } - [[nodiscard]] Resource& at(int index) { return *m_resources.at(index); } - [[nodiscard]] Resource const& at(int index) const { return *m_resources.at(index); } - [[nodiscard]] QList const& all() const { return m_resources; } + + [[nodiscard]] Resource& at(int index) { return *m_resources[index].get(); } + [[nodiscard]] const Resource& at(int index) const { return *m_resources.at(index).get(); } + QList selectedResources(const QModelIndexList& indexes); + QList allResources(); + + [[nodiscard]] Resource::Ptr find(QString id); [[nodiscard]] QDir const& dir() const { return m_dir; } @@ -96,7 +137,8 @@ class ResourceFolderModel : public QAbstractListModel { /* Qt behavior */ /* Basic columns */ - enum Columns { ACTIVE_COLUMN = 0, NAME_COLUMN, DATE_COLUMN, NUM_COLUMNS }; + enum Columns { ActiveColumn = 0, NameColumn, DateColumn, ProviderColumn, SizeColumn, 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()); } @@ -143,6 +185,7 @@ class ResourceFolderModel : public QAbstractListModel { signals: void updateFinished(); + void parseFinished(); protected: /** This creates a new update task to be executed by update(). @@ -152,7 +195,9 @@ class ResourceFolderModel : public QAbstractListModel { * This Task is normally executed when opening a page, so it shouldn't contain much heavy work. * If such work is needed, try using it in the Task create by createParseTask() instead! */ - [[nodiscard]] virtual Task* createUpdateTask(); + [[nodiscard]] Task* createUpdateTask(); + + [[nodiscard]] virtual Resource* createResource(const QFileInfo& info) { return new Resource(info); } /** This creates a new parse task to be executed by onUpdateSucceeded(). * @@ -166,10 +211,8 @@ class ResourceFolderModel : public QAbstractListModel { * It uses set operations to find differences between the current state and the updated state, * to act only on those disparities. * - * The implementation is at the end of this header. */ - template - void applyUpdates(QSet& current_set, QSet& new_set, QMap& new_resources); + void applyUpdates(QSet& current_set, QSet& new_set, QMap& new_resources); protected slots: void directoryChanged(QString); @@ -189,26 +232,27 @@ 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 }; - QStringList m_column_names = { "Enable", "Name", "Last Modified" }; - QStringList m_column_names_translated = { tr("Enable"), tr("Name"), tr("Last Modified") }; - QList m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive }; - QList m_columnsHideable = { false, false, 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, QHeaderView::Interactive }; + QList m_columnsHideable = { false, false, true, true, true }; + QList m_columnsHiddenByDefault = { false, false, false, false, true }; QDir m_dir; BaseInstance* m_instance; QFileSystemWatcher m_watcher; bool m_is_watching = false; + bool m_is_indexed; + bool m_first_folder_load = true; + Task::Ptr m_current_update_task = nullptr; bool m_scheduled_update = false; @@ -221,134 +265,3 @@ class ResourceFolderModel : public QAbstractListModel { QMap m_active_parse_tasks; std::atomic m_next_resolution_ticket = 0; }; - -/* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */ -#define RESOURCE_HELPERS(T) \ - [[nodiscard]] T* operator[](int index) \ - { \ - return static_cast(m_resources[index].get()); \ - } \ - [[nodiscard]] T* at(int index) \ - { \ - return static_cast(m_resources[index].get()); \ - } \ - [[nodiscard]] const T* at(int index) const \ - { \ - return static_cast(m_resources.at(index).get()); \ - } \ - [[nodiscard]] T* first() \ - { \ - return static_cast(m_resources.first().get()); \ - } \ - [[nodiscard]] T* last() \ - { \ - return static_cast(m_resources.last().get()); \ - } \ - [[nodiscard]] T* find(QString id) \ - { \ - auto iter = std::find_if(m_resources.constBegin(), m_resources.constEnd(), \ - [&](Resource::Ptr const& r) { return r->internal_id() == id; }); \ - if (iter == m_resources.constEnd()) \ - return nullptr; \ - return static_cast((*iter).get()); \ - } - -/* Template definition to avoid some code duplication */ -template -void ResourceFolderModel::applyUpdates(QSet& current_set, QSet& new_set, QMap& new_resources) -{ - // see if the kept resources changed in some way - { - QSet kept_set = current_set; - kept_set.intersect(new_set); - - for (auto const& kept : kept_set) { - auto row_it = m_resources_index.constFind(kept); - Q_ASSERT(row_it != m_resources_index.constEnd()); - auto row = row_it.value(); - - auto& new_resource = new_resources[kept]; - auto const& current_resource = m_resources.at(row); - - if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) { - // no significant change, ignore... - continue; - } - - // If the resource is resolving, but something about it changed, we don't want to - // continue the resolving. - if (current_resource->isResolving()) { - auto ticket = current_resource->resolutionTicket(); - if (m_active_parse_tasks.contains(ticket)) { - auto task = (*m_active_parse_tasks.find(ticket)).get(); - task->abort(); - } - } - - m_resources[row].reset(new_resource); - resolveResource(m_resources.at(row).get()); - emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); - } - } - - // remove resources no longer present - { - QSet removed_set = current_set; - removed_set.subtract(new_set); - - QList removed_rows; - for (auto& removed : removed_set) - removed_rows.append(m_resources_index[removed]); - - std::sort(removed_rows.begin(), removed_rows.end(), std::greater()); - - for (auto& removed_index : removed_rows) { - auto removed_it = m_resources.begin() + removed_index; - - Q_ASSERT(removed_it != m_resources.end()); - Q_ASSERT(removed_set.contains(removed_it->get()->internal_id())); - - if ((*removed_it)->isResolving()) { - auto ticket = (*removed_it)->resolutionTicket(); - if (m_active_parse_tasks.contains(ticket)) { - auto task = (*m_active_parse_tasks.find(ticket)).get(); - task->abort(); - } - } - - beginRemoveRows(QModelIndex(), removed_index, removed_index); - m_resources.erase(removed_it); - endRemoveRows(); - } - } - - // add new resources to the end - { - QSet added_set = new_set; - added_set.subtract(current_set); - - // When you have a Qt build with assertions turned on, proceeding here will abort the application - if (added_set.size() > 0) { - beginInsertRows(QModelIndex(), static_cast(m_resources.size()), - static_cast(m_resources.size() + added_set.size() - 1)); - - for (auto& added : added_set) { - auto res = new_resources[added]; - m_resources.append(res); - resolveResource(m_resources.last().get()); - } - - endInsertRows(); - } - } - - // update index - { - m_resources_index.clear(); - int idx = 0; - for (auto const& mod : qAsConst(m_resources)) { - m_resources_index[mod->internal_id()] = idx; - idx++; - } - } -} 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 693b8af05..e106a2be9 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -44,17 +44,19 @@ #include "Application.h" #include "Version.h" -#include "minecraft/mod/tasks/BasicFolderLoadTask.h" #include "minecraft/mod/tasks/LocalResourcePackParseTask.h" -ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance) +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" }); - m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified") }); - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE }; - m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, - QHeaderView::Interactive }; - m_columnsHideable = { false, true, false, true, true }; + 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"), 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, QHeaderView::Interactive }; + m_columnsHideable = { false, true, false, true, true, true, true }; } QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const @@ -71,12 +73,12 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const case NameColumn: return m_resources[row]->name(); case PackFormatColumn: { - auto resource = at(row); - auto pack_format = resource->packFormat(); + auto& resource = at(row); + auto pack_format = resource.packFormat(); if (pack_format == 0) return tr("Unrecognized"); - auto version_bounds = resource->compatibleVersions(); + auto version_bounds = resource.compatibleVersions(); if (version_bounds.first.toString().isEmpty()) return QString::number(pack_format); @@ -85,15 +87,18 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const } case DateColumn: return m_resources[row]->dateTimeChanged(); - + case ProviderColumn: + return m_resources[row]->provider(); + case SizeColumn: + return m_resources[row]->sizeStr(); default: return {}; } case Qt::DecorationRole: { - if (column == NameColumn && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink())) + if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) return APPLICATION->getThemedIcon("status-yellow"); if (column == ImageColumn) { - return at(row)->image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); + return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } return {}; } @@ -103,14 +108,14 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); } if (column == NameColumn) { - if (at(row)->isSymLinkUnder(instDirPath())) { + if (at(row).isSymLinkUnder(instDirPath())) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." "\nCanonical Path: %1") - .arg(at(row)->fileinfo().canonicalFilePath()); + .arg(at(row).fileinfo().canonicalFilePath()); ; } - if (at(row)->isMoreThanOneHardLink()) { + if (at(row).isMoreThanOneHardLink()) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); } @@ -125,7 +130,7 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const case Qt::CheckStateRole: switch (column) { case ActiveColumn: - return at(row)->enabled() ? Qt::Checked : Qt::Unchecked; + return at(row).enabled() ? Qt::Checked : Qt::Unchecked; default: return {}; } @@ -144,6 +149,8 @@ QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::O case PackFormatColumn: case DateColumn: case ImageColumn: + case ProviderColumn: + case SizeColumn: return columnNames().at(section); default: return {}; @@ -152,7 +159,7 @@ QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::O case Qt::ToolTipRole: switch (section) { case ActiveColumn: - return tr("Is the resource pack enabled? (Only valid for ZIPs)"); + return tr("Is the resource pack enabled?"); case NameColumn: return tr("The name of the resource pack."); case PackFormatColumn: @@ -160,6 +167,10 @@ QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::O return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); case DateColumn: 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 {}; } @@ -178,11 +189,6 @@ int ResourcePackFolderModel::columnCount(const QModelIndex& parent) const return parent.isValid() ? 0 : NUM_COLUMNS; } -Task* ResourcePackFolderModel::createUpdateTask() -{ - return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return makeShared(entry); }); -} - Task* ResourcePackFolderModel::createParseTask(Resource& resource) { return new LocalResourcePackParseTask(m_next_resolution_ticket, static_cast(resource)); diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.h b/launcher/minecraft/mod/ResourcePackFolderModel.h index 29c2c5995..9dbf41b85 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.h +++ b/launcher/minecraft/mod/ResourcePackFolderModel.h @@ -7,18 +7,18 @@ class ResourcePackFolderModel : public ResourceFolderModel { Q_OBJECT public: - enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, PackFormatColumn, DateColumn, NUM_COLUMNS }; + enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, PackFormatColumn, DateColumn, ProviderColumn, SizeColumn, NUM_COLUMNS }; - explicit ResourcePackFolderModel(const QString& dir, BaseInstance* instance); + explicit ResourcePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); - virtual QString id() const override { return "resourcepacks"; } + QString id() const override { return "resourcepacks"; } [[nodiscard]] QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; [[nodiscard]] int columnCount(const QModelIndex& parent) const override; - [[nodiscard]] Task* createUpdateTask() override; + [[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new ResourcePack(file); } [[nodiscard]] Task* createParseTask(Resource&) override; RESOURCE_HELPERS(ResourcePack) 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/ShaderPackFolderModel.h b/launcher/minecraft/mod/ShaderPackFolderModel.h index 186d02139..cd01f6226 100644 --- a/launcher/minecraft/mod/ShaderPackFolderModel.h +++ b/launcher/minecraft/mod/ShaderPackFolderModel.h @@ -2,24 +2,24 @@ #include "ResourceFolderModel.h" #include "minecraft/mod/ShaderPack.h" -#include "minecraft/mod/tasks/BasicFolderLoadTask.h" #include "minecraft/mod/tasks/LocalShaderPackParseTask.h" class ShaderPackFolderModel : public ResourceFolderModel { Q_OBJECT public: - explicit ShaderPackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance) {} + explicit ShaderPackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr) + : ResourceFolderModel(dir, instance, is_indexed, create_dir, parent) + {} virtual QString id() const override { return "shaderpacks"; } - [[nodiscard]] Task* createUpdateTask() override - { - return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return makeShared(entry); }); - } + [[nodiscard]] Resource* createResource(const QFileInfo& info) override { return new ShaderPack(info); } [[nodiscard]] Task* createParseTask(Resource& resource) override { return new LocalShaderPackParseTask(m_next_resolution_ticket, static_cast(resource)); } + + RESOURCE_HELPERS(ShaderPack); }; diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index f210501c7..073ea7ca7 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -39,21 +39,18 @@ #include "TexturePackFolderModel.h" -#include "minecraft/mod/tasks/BasicFolderLoadTask.h" #include "minecraft/mod/tasks/LocalTexturePackParseTask.h" +#include "minecraft/mod/tasks/ResourceFolderLoadTask.h" -TexturePackFolderModel::TexturePackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance) +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" }); - m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified") }); - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE }; - m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive }; - m_columnsHideable = { false, true, false, true }; -} - -Task* TexturePackFolderModel::createUpdateTask() -{ - return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return makeShared(entry); }); + 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::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 }; + m_columnsHiddenByDefault = { false, false, false, false, false, true }; } Task* TexturePackFolderModel::createParseTask(Resource& resource) @@ -76,19 +73,23 @@ QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const return m_resources[row]->name(); case DateColumn: return m_resources[row]->dateTimeChanged(); + case ProviderColumn: + return m_resources[row]->provider(); + case SizeColumn: + return m_resources[row]->sizeStr(); default: return {}; } case Qt::ToolTipRole: if (column == NameColumn) { - if (at(row)->isSymLinkUnder(instDirPath())) { + if (at(row).isSymLinkUnder(instDirPath())) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." "\nCanonical Path: %1") - .arg(at(row)->fileinfo().canonicalFilePath()); + .arg(at(row).fileinfo().canonicalFilePath()); ; } - if (at(row)->isMoreThanOneHardLink()) { + if (at(row).isMoreThanOneHardLink()) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); } @@ -96,10 +97,10 @@ QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const return m_resources[row]->internal_id(); case Qt::DecorationRole: { - if (column == NameColumn && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink())) + if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) return APPLICATION->getThemedIcon("status-yellow"); if (column == ImageColumn) { - return at(row)->image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); + return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } return {}; } @@ -127,6 +128,8 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or case NameColumn: case DateColumn: case ImageColumn: + case ProviderColumn: + case SizeColumn: return columnNames().at(section); default: return {}; @@ -134,14 +137,15 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or case Qt::ToolTipRole: { switch (section) { case ActiveColumn: - //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. - return tr("Is the resource enabled?"); + return tr("Is the texture pack enabled?"); case NameColumn: - //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. - return tr("The name of the resource."); + return tr("The name of the texture pack."); case DateColumn: - //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. - return tr("The date and time this resource was last changed (or added)."); + 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 b975d8641..7a9264e8f 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.h +++ b/launcher/minecraft/mod/TexturePackFolderModel.h @@ -44,9 +44,9 @@ class TexturePackFolderModel : public ResourceFolderModel { Q_OBJECT public: - enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, DateColumn, NUM_COLUMNS }; + enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, DateColumn, ProviderColumn, SizeColumn, NUM_COLUMNS }; - explicit TexturePackFolderModel(const QString& dir, std::shared_ptr instance); + explicit TexturePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); virtual QString id() const override { return "texturepacks"; } @@ -55,8 +55,7 @@ class TexturePackFolderModel : public ResourceFolderModel { [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; [[nodiscard]] int columnCount(const QModelIndex& parent) const override; - explicit TexturePackFolderModel(const QString& dir, BaseInstance* instance); - [[nodiscard]] Task* createUpdateTask() override; + [[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new TexturePack(file); } [[nodiscard]] Task* createParseTask(Resource&) override; RESOURCE_HELPERS(TexturePack) diff --git a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h deleted file mode 100644 index 23a2b649a..000000000 --- a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h +++ /dev/null @@ -1,74 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include - -#include "minecraft/mod/Resource.h" - -#include "tasks/Task.h" - -/** Very simple task that just loads a folder's contents directly. - */ -class BasicFolderLoadTask : public Task { - Q_OBJECT - public: - struct Result { - QMap resources; - }; - using ResultPtr = std::shared_ptr; - - [[nodiscard]] ResultPtr result() const { return m_result; } - - public: - BasicFolderLoadTask(QDir dir) : Task(nullptr, false), m_dir(dir), m_result(new Result), m_thread_to_spawn_into(thread()) - { - m_create_func = [](QFileInfo const& entry) -> Resource::Ptr { return makeShared(entry); }; - } - BasicFolderLoadTask(QDir dir, std::function create_function) - : Task(nullptr, false) - , m_dir(dir) - , m_result(new Result) - , m_create_func(std::move(create_function)) - , m_thread_to_spawn_into(thread()) - {} - - [[nodiscard]] bool canAbort() const override { return true; } - bool abort() override - { - m_aborted.store(true); - return true; - } - - void executeTask() override - { - if (thread() != m_thread_to_spawn_into) - connect(this, &Task::finished, this->thread(), &QThread::quit); - - m_dir.refresh(); - for (auto entry : m_dir.entryInfoList()) { - auto resource = m_create_func(entry); - resource->moveToThread(m_thread_to_spawn_into); - m_result->resources.insert(resource->internal_id(), resource); - } - - if (m_aborted) - emit finished(); - else - emitSucceeded(); - } - - private: - QDir m_dir; - ResultPtr m_result; - - std::atomic m_aborted = false; - - std::function m_create_func; - - /** This is the thread in which we should put new mod objects */ - QThread* m_thread_to_spawn_into; -}; diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp index efc17298e..b63d36361 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,11 +44,18 @@ static ModPlatform::ModLoaderTypes mcLoaders(BaseInstance* inst) return static_cast(inst)->getPackProfile()->getSupportedModLoaders().value(); } -GetModDependenciesTask::GetModDependenciesTask(QObject* parent, - BaseInstance* instance, +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(BaseInstance* instance, ModFolderModel* folder, QList> selected) - : SequentialTask(parent, tr("Get dependencies")) + : SequentialTask(tr("Get dependencies")) , m_selected(selected) , m_flame_provider{ ModPlatform::ResourceProvider::FLAME, std::make_shared(*instance), std::make_shared() } @@ -57,18 +64,21 @@ GetModDependenciesTask::GetModDependenciesTask(QObject* parent, , m_version(mcVersion(instance)) , m_loaderType(mcLoaders(instance)) { - for (auto mod : folder->allMods()) + for (auto mod : folder->allMods()) { + m_mods_file_names << mod->fileinfo().fileName(); if (auto meta = mod->metadata(); meta) m_mods.append(meta); + } prepare(); } void GetModDependenciesTask::prepare() { for (auto sel : m_selected) { - for (auto dep : getDependenciesForVersion(sel->version, sel->pack->provider)) { - addTask(prepareDependencyTask(dep, sel->pack->provider, 20)); - } + if (checkDependencies(sel, m_version, m_loaderType)) + for (auto dep : getDependenciesForVersion(sel->version, sel->pack->provider)) { + addTask(prepareDependencyTask(dep, sel->pack->provider, 20)); + } } } @@ -174,7 +184,7 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen auto provider = providerName == m_flame_provider.name ? m_flame_provider : m_modrinth_provider; auto tasks = makeShared( - this, QString("DependencyInfo: %1").arg(dep.addonId.toString().isEmpty() ? dep.version : dep.addonId.toString())); + QString("DependencyInfo: %1").arg(dep.addonId.toString().isEmpty() ? dep.version : dep.addonId.toString())); if (!dep.addonId.toString().isEmpty()) { tasks->addTask(getProjectInfoTask(pDep)); @@ -182,7 +192,9 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen ResourceAPI::DependencySearchArgs args = { dep, m_version, m_loaderType }; ResourceAPI::DependencySearchCallbacks callbacks; - + callbacks.on_fail = [](QString reason, int) { + qCritical() << tr("A network error occurred. Could not load project dependencies:%1").arg(reason); + }; callbacks.on_succeed = [dep, provider, pDep, level, this](auto& doc, [[maybe_unused]] auto& pack) { try { QJsonArray arr; @@ -229,8 +241,13 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen if (dep_.addonId != pDep->version.addonId) { removePack(pDep->version.addonId); addTask(prepareDependencyTask(dep_, provider.name, level)); - } else + } else { addTask(getProjectInfoTask(pDep)); + } + } + if (isLocalyInstalled(pDep)) { + removePack(pDep->version.addonId); + return; } for (auto dep_ : getDependenciesForVersion(pDep->version, provider.name)) { addTask(prepareDependencyTask(dep_, provider.name, level - 1)); @@ -256,9 +273,9 @@ void GetModDependenciesTask::removePack(const QVariant& addonId) #endif } -QHash GetModDependenciesTask::getRequiredBy() +auto GetModDependenciesTask::getExtraInfo() -> QHash { - QHash rby; + QHash rby; auto fullList = m_selected + m_pack_dependencies; for (auto& mod : fullList) { auto addonId = mod->pack->addonId; @@ -280,7 +297,61 @@ QHash GetModDependenciesTask::getRequiredBy() req.append(smod->pack->name); } } - rby[addonId.toString()] = req; + rby[addonId.toString()] = { maybeInstalled(mod), req }; } return rby; -} \ No newline at end of file +} + +// super lax compare (but not fuzzy) +// convert to lowercase +// convert all speratores to whitespace +// simplify sequence of internal whitespace to a single space +// efectivly compare two strings ignoring all separators and case +auto laxCompare = [](QString fsfilename, QString metadataFilename, bool excludeDigits = false) { + // allowed character seperators + QList allowedSeperators = { '-', '+', '.', '_' }; + if (excludeDigits) + allowedSeperators.append({ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }); + + // copy in lowercase + auto fsName = fsfilename.toLower(); + auto metaName = metadataFilename.toLower(); + + // replace all potential allowed seperatores with whitespace + for (auto sep : allowedSeperators) { + fsName = fsName.replace(sep, ' '); + metaName = metaName.replace(sep, ' '); + } + + // remove extraneous whitespace + fsName = fsName.simplified(); + metaName = metaName.simplified(); + + return fsName.compare(metaName) == 0; +}; + +bool GetModDependenciesTask::isLocalyInstalled(std::shared_ptr pDep) +{ + return pDep->version.fileName.isEmpty() || + + std::find_if(m_selected.begin(), m_selected.end(), + [pDep](std::shared_ptr i) { + return !i->version.fileName.isEmpty() && laxCompare(i->version.fileName, pDep->version.fileName); + }) != m_selected.end() || // check the selected versions + + std::find_if(m_mods_file_names.begin(), m_mods_file_names.end(), + [pDep](QString i) { return !i.isEmpty() && laxCompare(i, pDep->version.fileName); }) != + m_mods_file_names.end() || // check the existing mods + + std::find_if(m_pack_dependencies.begin(), m_pack_dependencies.end(), [pDep](std::shared_ptr i) { + return pDep->pack->addonId != i->pack->addonId && !i->version.fileName.isEmpty() && + laxCompare(pDep->version.fileName, i->version.fileName); + }) != m_pack_dependencies.end(); // check loaded dependencies +} + +bool GetModDependenciesTask::maybeInstalled(std::shared_ptr pDep) +{ + return std::find_if(m_mods_file_names.begin(), m_mods_file_names.end(), [pDep](QString i) { + return !i.isEmpty() && laxCompare(i, pDep->version.fileName, true); + }) != m_mods_file_names.end(); // check the existing mods +} diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.h b/launcher/minecraft/mod/tasks/GetModDependenciesTask.h index e88204bdc..29c77f9fe 100644 --- a/launcher/minecraft/mod/tasks/GetModDependenciesTask.h +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.h @@ -50,19 +50,21 @@ class GetModDependenciesTask : public SequentialTask { } }; + struct PackDependencyExtraInfo { + bool maybe_installed; + QStringList required_by; + }; + struct Provider { ModPlatform::ResourceProvider name; std::shared_ptr mod; std::shared_ptr api; }; - explicit GetModDependenciesTask(QObject* parent, - BaseInstance* instance, - ModFolderModel* folder, - QList> selected); + explicit GetModDependenciesTask(BaseInstance* instance, ModFolderModel* folder, QList> selected); auto getDependecies() const -> QList> { return m_pack_dependencies; } - QHash getRequiredBy(); + QHash getExtraInfo(); protected slots: Task::Ptr prepareDependencyTask(const ModPlatform::Dependency&, ModPlatform::ResourceProvider, int); @@ -73,10 +75,14 @@ class GetModDependenciesTask : public SequentialTask { ModPlatform::Dependency getOverride(const ModPlatform::Dependency&, ModPlatform::ResourceProvider providerName); void removePack(const QVariant& addonId); + bool isLocalyInstalled(std::shared_ptr pDep); + bool maybeInstalled(std::shared_ptr pDep); + private: QList> m_pack_dependencies; QList> m_mods; QList> m_selected; + QStringList m_mods_file_names; Provider m_flame_provider; Provider m_modrinth_provider; diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp index 82f6b9df9..19b15709d 100644 --- a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp @@ -157,7 +157,7 @@ bool validate(QFileInfo file) } // namespace DataPackUtils -LocalDataPackParseTask::LocalDataPackParseTask(int token, DataPack& dp) : Task(nullptr, false), m_token(token), m_data_pack(dp) {} +LocalDataPackParseTask::LocalDataPackParseTask(int token, DataPack& dp) : Task(false), m_token(token), m_data_pack(dp) {} bool LocalDataPackParseTask::abort() { diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index e9e12d86a..7d101668e 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "FileSystem.h" @@ -15,6 +16,8 @@ #include "minecraft/mod/ModDetails.h" #include "settings/INIFile.h" +static QRegularExpression newlineRegex("\r\n|\n|\r"); + namespace ModUtils { // NEW format @@ -24,7 +27,7 @@ namespace ModUtils { // https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc ModDetails ReadMCModInfo(QByteArray contents) { - auto getInfoFromArray = [&](QJsonArray arr) -> ModDetails { + auto getInfoFromArray = [](QJsonArray arr) -> ModDetails { if (!arr.at(0).isObject()) { return {}; } @@ -469,7 +472,7 @@ bool processZIP(Mod& mod, [[maybe_unused]] ProcessingLevel level) QuaZipFile file(&zip); - if (zip.setCurrentFile("META-INF/mods.toml")) { + if (zip.setCurrentFile("META-INF/mods.toml") || zip.setCurrentFile("META-INF/neoforge.mods.toml")) { if (!file.open(QIODevice::ReadOnly)) { zip.close(); return false; @@ -487,11 +490,11 @@ bool processZIP(Mod& mod, [[maybe_unused]] ProcessingLevel level) } // quick and dirty line-by-line parser - auto manifestLines = file.readAll().split('\n'); + auto manifestLines = QString(file.readAll()).split(newlineRegex); QString manifestVersion = ""; for (auto& line : manifestLines) { - if (QString(line).startsWith("Implementation-Version: ")) { - manifestVersion = QString(line).remove("Implementation-Version: "); + if (line.startsWith("Implementation-Version: ", Qt::CaseInsensitive)) { + manifestVersion = line.remove("Implementation-Version: ", Qt::CaseInsensitive); break; } } @@ -647,11 +650,11 @@ bool validate(QFileInfo file) return ModUtils::process(mod, ProcessingLevel::BasicInfoOnly) && mod.valid(); } -bool processIconPNG(const Mod& mod, QByteArray&& raw_data) +bool processIconPNG(const Mod& mod, QByteArray&& raw_data, QPixmap* pixmap) { auto img = QImage::fromData(raw_data); if (!img.isNull()) { - mod.setIcon(img); + *pixmap = mod.setIcon(img); } else { qWarning() << "Failed to parse mod logo:" << mod.iconPath() << "from" << mod.name(); return false; @@ -659,15 +662,15 @@ bool processIconPNG(const Mod& mod, QByteArray&& raw_data) return true; } -bool loadIconFile(const Mod& mod) +bool loadIconFile(const Mod& mod, QPixmap* pixmap) { if (mod.iconPath().isEmpty()) { qWarning() << "No Iconfile set, be sure to parse the mod first"; return false; } - auto png_invalid = [&mod]() { - qWarning() << "Mod at" << mod.fileinfo().filePath() << "does not have a valid icon"; + auto png_invalid = [&mod](const QString& reason) { + qWarning() << "Mod at" << mod.fileinfo().filePath() << "does not have a valid icon:" << reason; return false; }; @@ -676,24 +679,26 @@ bool loadIconFile(const Mod& mod) QFileInfo icon_info(FS::PathCombine(mod.fileinfo().filePath(), mod.iconPath())); if (icon_info.exists() && icon_info.isFile()) { QFile icon(icon_info.filePath()); - if (!icon.open(QIODevice::ReadOnly)) - return false; + if (!icon.open(QIODevice::ReadOnly)) { + return png_invalid("failed to open file " + icon_info.filePath()); + } auto data = icon.readAll(); - bool icon_result = ModUtils::processIconPNG(mod, std::move(data)); + bool icon_result = ModUtils::processIconPNG(mod, std::move(data), pixmap); icon.close(); if (!icon_result) { - return png_invalid(); // icon invalid + return png_invalid("invalid png image"); // icon invalid } + return true; } - return false; + return png_invalid("file '" + icon_info.filePath() + "' does not exists or is not a file"); } case ResourceType::ZIPFILE: { QuaZip zip(mod.fileinfo().filePath()); if (!zip.open(QuaZip::mdUnzip)) - return false; + return png_invalid("failed to open '" + mod.fileinfo().filePath() + "' as a zip archive"); QuaZipFile file(&zip); @@ -701,35 +706,34 @@ bool loadIconFile(const Mod& mod) if (!file.open(QIODevice::ReadOnly)) { qCritical() << "Failed to open file in zip."; zip.close(); - return png_invalid(); + return png_invalid("Failed to open '" + mod.iconPath() + "' in zip archive"); } auto data = file.readAll(); - bool icon_result = ModUtils::processIconPNG(mod, std::move(data)); + bool icon_result = ModUtils::processIconPNG(mod, std::move(data), pixmap); file.close(); if (!icon_result) { - return png_invalid(); // icon png invalid + return png_invalid("invalid png image"); // icon png invalid } - } else { - return png_invalid(); // could not set icon as current file. + return true; } - return false; + return png_invalid("Failed to set '" + mod.iconPath() + + "' as current file in zip archive"); // could not set icon as current file. } case ResourceType::LITEMOD: { - return false; // can lightmods even have icons? + return png_invalid("litemods do not have icons"); // can lightmods even have icons? } default: - qWarning() << "Invalid type for mod, can not load icon."; - return false; + return png_invalid("Invalid type for mod, can not load icon."); } } } // namespace ModUtils LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile) - : Task(nullptr, false), m_token(token), m_type(type), m_modFile(modFile), m_result(new Result()) + : Task(false), m_token(token), m_type(type), m_modFile(modFile), m_result(new Result()) {} bool LocalModParseTask::abort() @@ -746,7 +750,7 @@ void LocalModParseTask::executeTask() m_result->details = mod.details(); if (m_aborted) - emit finished(); + emitAborted(); else emitSucceeded(); } diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h index a03217093..7ce5a84d2 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.h @@ -26,8 +26,8 @@ bool processLitemod(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); /** Checks whether a file is valid as a mod or not. */ bool validate(QFileInfo file); -bool processIconPNG(const Mod& mod, QByteArray&& raw_data); -bool loadIconFile(const Mod& mod); +bool processIconPNG(const Mod& mod, QByteArray&& raw_data, QPixmap* pixmap); +bool loadIconFile(const Mod& mod, QPixmap* pixmap); } // namespace ModUtils class LocalModParseTask : public Task { @@ -47,11 +47,6 @@ class LocalModParseTask : public Task { [[nodiscard]] int token() const { return m_token; } - private: - void processAsZip(); - void processAsFolder(); - void processAsLitemod(); - private: int m_token; ResourceType m_type; diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp index 26bc07637..0b80db82d 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; @@ -274,9 +358,7 @@ bool validate(QFileInfo file) } // namespace ResourcePackUtils -LocalResourcePackParseTask::LocalResourcePackParseTask(int token, ResourcePack& rp) - : Task(nullptr, false), m_token(token), m_resource_pack(rp) -{} +LocalResourcePackParseTask::LocalResourcePackParseTask(int token, ResourcePack& rp) : Task(false), m_token(token), m_resource_pack(rp) {} bool LocalResourcePackParseTask::abort() { @@ -286,8 +368,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/LocalModUpdateTask.cpp b/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.cpp similarity index 63% rename from launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp rename to launcher/minecraft/mod/tasks/LocalResourceUpdateTask.cpp index 4352fad91..c8fe1050a 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.cpp @@ -17,7 +17,7 @@ * along with this program. If not, see . */ -#include "LocalModUpdateTask.h" +#include "LocalResourceUpdateTask.h" #include "FileSystem.h" #include "minecraft/mod/MetadataHandler.h" @@ -26,12 +26,12 @@ #include #endif -LocalModUpdateTask::LocalModUpdateTask(QDir index_dir, ModPlatform::IndexedPack& mod, ModPlatform::IndexedVersion& mod_version) - : m_index_dir(index_dir), m_mod(mod), m_mod_version(mod_version) +LocalResourceUpdateTask::LocalResourceUpdateTask(QDir index_dir, ModPlatform::IndexedPack& project, ModPlatform::IndexedVersion& version) + : m_index_dir(index_dir), m_project(project), m_version(version) { // Ensure a '.index' folder exists in the mods folder, and create it if it does not if (!FS::ensureFolderPathExists(index_dir.path())) { - emitFailed(QString("Unable to create index for mod %1!").arg(m_mod.name)); + emitFailed(QString("Unable to create index directory at %1!").arg(index_dir.absolutePath())); } #ifdef Q_OS_WIN32 @@ -39,28 +39,28 @@ LocalModUpdateTask::LocalModUpdateTask(QDir index_dir, ModPlatform::IndexedPack& #endif } -void LocalModUpdateTask::executeTask() +void LocalResourceUpdateTask::executeTask() { - setStatus(tr("Updating index for mod:\n%1").arg(m_mod.name)); + setStatus(tr("Updating index for resource:\n%1").arg(m_project.name)); - auto old_metadata = Metadata::get(m_index_dir, m_mod.addonId); + auto old_metadata = Metadata::get(m_index_dir, m_project.addonId); if (old_metadata.isValid()) { - emit hasOldMod(old_metadata.name, old_metadata.filename); - if (m_mod.slug.isEmpty()) - m_mod.slug = old_metadata.slug; + emit hasOldResource(old_metadata.name, old_metadata.filename); + if (m_project.slug.isEmpty()) + m_project.slug = old_metadata.slug; } - auto pw_mod = Metadata::create(m_index_dir, m_mod, m_mod_version); + auto pw_mod = Metadata::create(m_index_dir, m_project, m_version); if (pw_mod.isValid()) { Metadata::update(m_index_dir, pw_mod); emitSucceeded(); } else { - qCritical() << "Tried to update an invalid mod!"; + qCritical() << "Tried to update an invalid resource!"; emitFailed(tr("Invalid metadata")); } } -auto LocalModUpdateTask::abort() -> bool +auto LocalResourceUpdateTask::abort() -> bool { emitAborted(); return true; diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.h b/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.h similarity index 74% rename from launcher/minecraft/mod/tasks/LocalModUpdateTask.h rename to launcher/minecraft/mod/tasks/LocalResourceUpdateTask.h index 080999294..f8869258e 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.h +++ b/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.h @@ -23,12 +23,12 @@ #include "modplatform/ModIndex.h" #include "tasks/Task.h" -class LocalModUpdateTask : public Task { +class LocalResourceUpdateTask : public Task { Q_OBJECT public: - using Ptr = shared_qobject_ptr; + using Ptr = shared_qobject_ptr; - explicit LocalModUpdateTask(QDir index_dir, ModPlatform::IndexedPack& mod, ModPlatform::IndexedVersion& mod_version); + explicit LocalResourceUpdateTask(QDir index_dir, ModPlatform::IndexedPack& project, ModPlatform::IndexedVersion& version); auto canAbort() const -> bool override { return true; } auto abort() -> bool override; @@ -38,10 +38,10 @@ class LocalModUpdateTask : public Task { void executeTask() override; signals: - void hasOldMod(QString name, QString filename); + void hasOldResource(QString name, QString filename); private: QDir m_index_dir; - ModPlatform::IndexedPack& m_mod; - ModPlatform::IndexedVersion& m_mod_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..a6ecc5353 100644 --- a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp @@ -93,7 +93,7 @@ bool validate(QFileInfo file) } // namespace ShaderPackUtils -LocalShaderPackParseTask::LocalShaderPackParseTask(int token, ShaderPack& sp) : Task(nullptr, false), m_token(token), m_shader_pack(sp) {} +LocalShaderPackParseTask::LocalShaderPackParseTask(int token, ShaderPack& sp) : Task(false), m_token(token), m_shader_pack(sp) {} bool LocalShaderPackParseTask::abort() { @@ -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..18020808a 100644 --- a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp @@ -230,8 +230,7 @@ bool validate(QFileInfo file) } // namespace TexturePackUtils -LocalTexturePackParseTask::LocalTexturePackParseTask(int token, TexturePack& rp) : Task(nullptr, false), m_token(token), m_texture_pack(rp) -{} +LocalTexturePackParseTask::LocalTexturePackParseTask(int token, TexturePack& rp) : Task(false), m_token(token), m_texture_pack(rp) {} bool LocalTexturePackParseTask::abort() { @@ -241,8 +240,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/LocalWorldSaveParseTask.cpp b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp index 9d564ddb3..74d8d8d60 100644 --- a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp @@ -170,7 +170,7 @@ bool validate(QFileInfo file) } // namespace WorldSaveUtils -LocalWorldSaveParseTask::LocalWorldSaveParseTask(int token, WorldSave& save) : Task(nullptr, false), m_token(token), m_save(save) {} +LocalWorldSaveParseTask::LocalWorldSaveParseTask(int token, WorldSave& save) : Task(false), m_token(token), m_save(save) {} bool LocalWorldSaveParseTask::abort() { diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp similarity index 50% rename from launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp rename to launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp index 2094df4fc..98dab9abb 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp @@ -34,23 +34,30 @@ * limitations under the License. */ -#include "ModFolderLoadTask.h" +#include "ResourceFolderLoadTask.h" +#include "Application.h" +#include "FileSystem.h" #include "minecraft/mod/MetadataHandler.h" #include -ModFolderLoadTask::ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan) - : Task(nullptr, false) - , m_mods_dir(mods_dir) +ResourceFolderLoadTask::ResourceFolderLoadTask(const QDir& resource_dir, + const QDir& index_dir, + bool is_indexed, + bool clean_orphan, + std::function create_function) + : Task(false) + , m_resource_dir(resource_dir) , m_index_dir(index_dir) , m_is_indexed(is_indexed) , m_clean_orphan(clean_orphan) + , m_create_func(create_function) , m_result(new Result()) , m_thread_to_spawn_into(thread()) {} -void ModFolderLoadTask::executeTask() +void ResourceFolderLoadTask::executeTask() { if (thread() != m_thread_to_spawn_into) connect(this, &Task::finished, this->thread(), &QThread::quit); @@ -61,34 +68,44 @@ void ModFolderLoadTask::executeTask() } // Read JAR files that don't have metadata - m_mods_dir.refresh(); - for (auto entry : m_mods_dir.entryInfoList()) { - Mod* mod(new Mod(entry)); + m_resource_dir.refresh(); + for (auto entry : m_resource_dir.entryInfoList()) { + auto filePath = entry.absoluteFilePath(); + if (auto app = APPLICATION_DYN; app && app->checkQSavePath(filePath)) { + continue; + } + auto newFilePath = FS::getUniqueResourceName(filePath); + if (newFilePath != filePath) { + FS::move(filePath, newFilePath); + entry = QFileInfo(newFilePath); + } - if (mod->enabled()) { - if (m_result->mods.contains(mod->internal_id())) { - m_result->mods[mod->internal_id()]->setStatus(ModStatus::Installed); + Resource* resource = m_create_func(entry); + + if (resource->enabled()) { + if (m_result->resources.contains(resource->internal_id())) { + m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::INSTALLED); // Delete the object we just created, since a valid one is already in the mods list. - delete mod; + delete resource; } else { - m_result->mods[mod->internal_id()].reset(std::move(mod)); - m_result->mods[mod->internal_id()]->setStatus(ModStatus::NoMetadata); + m_result->resources[resource->internal_id()].reset(resource); + m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::NO_METADATA); } } else { - QString chopped_id = mod->internal_id().chopped(9); - if (m_result->mods.contains(chopped_id)) { - m_result->mods[mod->internal_id()].reset(std::move(mod)); + QString chopped_id = resource->internal_id().chopped(9); + if (m_result->resources.contains(chopped_id)) { + m_result->resources[resource->internal_id()].reset(resource); - auto metadata = m_result->mods[chopped_id]->metadata(); + auto metadata = m_result->resources[chopped_id]->metadata(); if (metadata) { - mod->setMetadata(*metadata); + resource->setMetadata(*metadata); - m_result->mods[mod->internal_id()]->setStatus(ModStatus::Installed); - m_result->mods.remove(chopped_id); + m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::INSTALLED); + m_result->resources.remove(chopped_id); } } else { - m_result->mods[mod->internal_id()].reset(std::move(mod)); - m_result->mods[mod->internal_id()]->setStatus(ModStatus::NoMetadata); + m_result->resources[resource->internal_id()].reset(resource); + m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::NO_METADATA); } } } @@ -96,17 +113,17 @@ void ModFolderLoadTask::executeTask() // Remove orphan metadata to prevent issues // See https://github.com/PolyMC/PolyMC/issues/996 if (m_clean_orphan) { - QMutableMapIterator iter(m_result->mods); + QMutableMapIterator iter(m_result->resources); while (iter.hasNext()) { - auto mod = iter.next().value(); - if (mod->status() == ModStatus::NotInstalled) { - mod->destroy(m_index_dir, false, false); + auto resource = iter.next().value(); + if (resource->status() == ResourceStatus::NOT_INSTALLED) { + resource->destroy(m_index_dir, false, false); iter.remove(); } } } - for (auto mod : m_result->mods) + for (auto mod : m_result->resources) mod->moveToThread(m_thread_to_spawn_into); if (m_aborted) @@ -115,18 +132,18 @@ void ModFolderLoadTask::executeTask() emitSucceeded(); } -void ModFolderLoadTask::getFromMetadata() +void ResourceFolderLoadTask::getFromMetadata() { m_index_dir.refresh(); for (auto entry : m_index_dir.entryList(QDir::Files)) { auto metadata = Metadata::get(m_index_dir, entry); - if (!metadata.isValid()) { + if (!metadata.isValid()) continue; - } - auto* mod = new Mod(m_mods_dir, metadata); - mod->setStatus(ModStatus::NotInstalled); - m_result->mods[mod->internal_id()].reset(std::move(mod)); + auto* resource = m_create_func(QFileInfo(m_resource_dir.filePath(metadata.filename))); + resource->setMetadata(metadata); + resource->setStatus(ResourceStatus::NOT_INSTALLED); + m_result->resources[resource->internal_id()].reset(resource); } } diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h similarity index 83% rename from launcher/minecraft/mod/tasks/ModFolderLoadTask.h rename to launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h index 4200ef6d9..9950345ef 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h +++ b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h @@ -44,17 +44,21 @@ #include "minecraft/mod/Mod.h" #include "tasks/Task.h" -class ModFolderLoadTask : public Task { +class ResourceFolderLoadTask : public Task { Q_OBJECT public: struct Result { - QMap mods; + QMap resources; }; using ResultPtr = std::shared_ptr; ResultPtr result() const { return m_result; } public: - ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan = false); + ResourceFolderLoadTask(const QDir& resource_dir, + const QDir& index_dir, + bool is_indexed, + bool clean_orphan, + std::function create_function); [[nodiscard]] bool canAbort() const override { return true; } bool abort() override @@ -69,9 +73,10 @@ class ModFolderLoadTask : public Task { void getFromMetadata(); private: - QDir m_mods_dir, m_index_dir; + QDir m_resource_dir, m_index_dir; bool m_is_indexed; bool m_clean_orphan; + std::function m_create_func; ResultPtr m_result; std::atomic m_aborted = false; 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/ui/pages/global/CustomCommandsPage.cpp b/launcher/minecraft/skins/CapeChange.cpp similarity index 51% rename from launcher/ui/pages/global/CustomCommandsPage.cpp rename to launcher/minecraft/skins/CapeChange.cpp index cc8518c2f..abbaa0b67 100644 --- a/launcher/ui/pages/global/CustomCommandsPage.cpp +++ b/launcher/minecraft/skins/CapeChange.cpp @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (c) 2022 Jamie Mansfield * 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 @@ -34,51 +34,37 @@ * limitations under the License. */ -#include "CustomCommandsPage.h" -#include -#include -#include +#include "CapeChange.h" -CustomCommandsPage::CustomCommandsPage(QWidget* parent) : QWidget(parent) +#include + +#include "net/ByteArraySink.h" +#include "net/RawHeaderProxy.h" + +CapeChange::CapeChange(QString cape) : NetRequest(), m_capeId(cape) { - auto verticalLayout = new QVBoxLayout(this); - verticalLayout->setObjectName(QStringLiteral("verticalLayout")); - verticalLayout->setContentsMargins(0, 0, 0, 0); - - auto tabWidget = new QTabWidget(this); - tabWidget->setObjectName(QStringLiteral("tabWidget")); - commands = new CustomCommands(this); - commands->setContentsMargins(6, 6, 6, 6); - tabWidget->addTab(commands, "Foo"); - tabWidget->tabBar()->hide(); - verticalLayout->addWidget(tabWidget); - loadSettings(); + logCat = taskMCSkinsLogC; } -CustomCommandsPage::~CustomCommandsPage() {} - -bool CustomCommandsPage::apply() +QNetworkReply* CapeChange::getReply(QNetworkRequest& request) { - applySettings(); - return true; + 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()); + } } -void CustomCommandsPage::applySettings() +CapeChange::Ptr CapeChange::make(QString token, QString capeId) { - auto s = APPLICATION->settings(); - s->set("PreLaunchCommand", commands->prelaunchCommand()); - s->set("WrapperCommand", commands->wrapperCommand()); - s->set("PostExitCommand", commands->postexitCommand()); -} - -void CustomCommandsPage::loadSettings() -{ - auto s = APPLICATION->settings(); - commands->initialize(false, true, s->get("PreLaunchCommand").toString(), s->get("WrapperCommand").toString(), - s->get("PostExitCommand").toString()); -} - -void CustomCommandsPage::retranslate() -{ - commands->retranslate(); + 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/ui/pages/global/EnvironmentVariablesPage.h b/launcher/minecraft/skins/SkinUpload.h similarity index 57% rename from launcher/ui/pages/global/EnvironmentVariablesPage.h rename to launcher/minecraft/skins/SkinUpload.h index 6e80775ec..c1a4930b7 100644 --- a/launcher/ui/pages/global/EnvironmentVariablesPage.h +++ b/launcher/minecraft/skins/SkinUpload.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2023 TheKodeToad + * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,25 +18,23 @@ #pragma once -#include +#include "net/NetRequest.h" -#include "ui/pages/BasePage.h" -#include "ui/widgets/EnvironmentVariables.h" - -class EnvironmentVariablesPage : public QWidget, public BasePage { +class SkinUpload : public Net::NetRequest { Q_OBJECT - public: - explicit EnvironmentVariablesPage(QWidget* parent = nullptr); + using Ptr = shared_qobject_ptr; - QString displayName() const override; - QIcon icon() const override; - QString id() const override; - QString helpPage() const override; + // Note this class takes ownership of the file. + SkinUpload(QString path, QString variant); + virtual ~SkinUpload() = default; - bool apply() override; - void retranslate() override; + static SkinUpload::Ptr make(QString token, QString path, QString variant); + + protected: + virtual QNetworkReply* getReply(QNetworkRequest&) override; private: - EnvironmentVariables* variables; + 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.cpp b/launcher/minecraft/update/LibrariesTask.cpp index 1581b32ee..e64691d51 100644 --- a/launcher/minecraft/update/LibrariesTask.cpp +++ b/launcher/minecraft/update/LibrariesTask.cpp @@ -25,7 +25,7 @@ void LibrariesTask::executeTask() auto metacache = APPLICATION->metacache(); - auto processArtifactPool = [&](const QList& pool, QStringList& errors, const QString& localPath) { + auto processArtifactPool = [this, inst, metacache](const QList& pool, QStringList& errors, const QString& localPath) { for (auto lib : pool) { if (!lib) { emitFailed(tr("Null jar is specified in the metadata, aborting.")); 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 8bd83d988..1ee820a63 100644 --- a/launcher/modplatform/CheckUpdateTask.h +++ b/launcher/modplatform/CheckUpdateTask.h @@ -13,13 +13,18 @@ class CheckUpdateTask : public Task { Q_OBJECT public: - CheckUpdateTask(QList& mods, + CheckUpdateTask(QList& resources, std::list& mcVersions, - std::optional loaders, - std::shared_ptr mods_folder) - : Task(nullptr), m_mods(mods), m_game_versions(mcVersions), m_loaders(loaders), m_mods_folder(mods_folder){}; + QList loadersList, + std::shared_ptr resourceModel) + : Task() + , m_resources(resources) + , m_game_versions(mcVersions) + , m_loaders_list(std::move(loadersList)) + , m_resource_model(std::move(resourceModel)) + {} - struct UpdatableMod { + struct Update { QString name; QString old_hash; QString old_version; @@ -28,28 +33,31 @@ class CheckUpdateTask : public Task { QString changelog; ModPlatform::ResourceProvider provider; shared_qobject_ptr download; + bool enabled = true; public: - UpdatableMod(QString name, - QString old_h, - QString old_v, - QString new_v, - std::optional new_v_type, - QString changelog, - ModPlatform::ResourceProvider p, - shared_qobject_ptr t) - : name(name) - , old_hash(old_h) - , old_version(old_v) - , new_version(new_v) - , new_version_type(new_v_type) - , changelog(changelog) + Update(QString name, + QString old_h, + QString old_v, + QString new_v, + std::optional new_v_type, + QString changelog, + ModPlatform::ResourceProvider p, + shared_qobject_ptr t, + bool enabled = true) + : name(std::move(name)) + , old_hash(std::move(old_h)) + , old_version(std::move(old_v)) + , new_version(std::move(new_v)) + , new_version_type(std::move(new_v_type)) + , changelog(std::move(changelog)) , provider(p) - , download(t) + , download(std::move(t)) + , enabled(enabled) {} }; - auto getUpdatable() -> std::vector&& { return std::move(m_updatable); } + auto getUpdates() -> std::vector&& { return std::move(m_updates); } auto getDependencies() -> QList>&& { return std::move(m_deps); } public slots: @@ -59,14 +67,14 @@ class CheckUpdateTask : public Task { void executeTask() override = 0; signals: - void checkFailed(Mod* failed, QString reason, QUrl recover_url = {}); + void checkFailed(Resource* failed, QString reason, QUrl recover_url = {}); protected: - QList& m_mods; + QList& m_resources; std::list& m_game_versions; - std::optional m_loaders; - std::shared_ptr m_mods_folder; + QList m_loaders_list; + std::shared_ptr m_resource_model; - std::vector m_updatable; + std::vector m_updates; QList> m_deps; }; diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp index a9ad22581..8e910e984 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -7,7 +7,7 @@ #include "Json.h" #include "minecraft/mod/Mod.h" -#include "minecraft/mod/tasks/LocalModUpdateTask.h" +#include "minecraft/mod/tasks/LocalResourceUpdateTask.h" #include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameModIndex.h" @@ -15,57 +15,60 @@ #include "modplatform/modrinth/ModrinthAPI.h" #include "modplatform/modrinth/ModrinthPackIndex.h" -static ModPlatform::ProviderCapabilities ProviderCaps; - static ModrinthAPI modrinth_api; static FlameAPI flame_api; -EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::ResourceProvider prov) - : Task(nullptr), m_index_dir(dir), m_provider(prov), m_hashing_task(nullptr), m_current_task(nullptr) +EnsureMetadataTask::EnsureMetadataTask(Resource* resource, QDir dir, ModPlatform::ResourceProvider prov) + : Task(), m_index_dir(dir), m_provider(prov), m_hashingTask(nullptr), m_current_task(nullptr) { - auto hash_task = createNewHash(mod); - if (!hash_task) + auto hashTask = createNewHash(resource); + if (!hashTask) return; - connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) { m_mods.insert(hash, mod); }); - connect(hash_task.get(), &Task::failed, [this, mod] { emitFail(mod, "", RemoveFromList::No); }); - hash_task->start(); + connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_resources.insert(hash, resource); }); + connect(hashTask.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); }); + m_hashingTask = hashTask; } -EnsureMetadataTask::EnsureMetadataTask(QList& mods, QDir dir, ModPlatform::ResourceProvider prov) - : Task(nullptr), m_index_dir(dir), m_provider(prov), m_current_task(nullptr) +EnsureMetadataTask::EnsureMetadataTask(QList& resources, QDir dir, ModPlatform::ResourceProvider prov) + : Task(), m_index_dir(dir), m_provider(prov), m_current_task(nullptr) { - m_hashing_task.reset(new ConcurrentTask(this, "MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); - for (auto* mod : mods) { - auto hash_task = createNewHash(mod); + auto hashTask = makeShared("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); + m_hashingTask = hashTask; + for (auto* resource : resources) { + auto hash_task = createNewHash(resource); if (!hash_task) continue; - connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) { m_mods.insert(hash, mod); }); - connect(hash_task.get(), &Task::failed, [this, mod] { emitFail(mod, "", RemoveFromList::No); }); - m_hashing_task->addTask(hash_task); + connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_resources.insert(hash, resource); }); + connect(hash_task.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); }); + hashTask->addTask(hash_task); } } -Hashing::Hasher::Ptr EnsureMetadataTask::createNewHash(Mod* mod) +EnsureMetadataTask::EnsureMetadataTask(QHash& resources, QDir dir, ModPlatform::ResourceProvider prov) + : Task(), m_resources(resources), m_index_dir(dir), m_provider(prov), m_current_task(nullptr) +{} + +Hashing::Hasher::Ptr EnsureMetadataTask::createNewHash(Resource* resource) { - if (!mod || !mod->valid() || mod->type() == ResourceType::FOLDER) + if (!resource || !resource->valid() || resource->type() == ResourceType::FOLDER) return nullptr; - return Hashing::createHasher(mod->fileinfo().absoluteFilePath(), m_provider); + return Hashing::createHasher(resource->fileinfo().absoluteFilePath(), m_provider); } -QString EnsureMetadataTask::getExistingHash(Mod* mod) +QString EnsureMetadataTask::getExistingHash(Resource* resource) { // Check for already computed hashes // (linear on the number of mods vs. linear on the size of the mod's JAR) - auto it = m_mods.keyValueBegin(); - while (it != m_mods.keyValueEnd()) { - if ((*it).second == mod) + auto it = m_resources.keyValueBegin(); + while (it != m_resources.keyValueEnd()) { + if ((*it).second == resource) break; it++; } // We already have the hash computed - if (it != m_mods.keyValueEnd()) { + if (it != m_resources.keyValueEnd()) { return (*it).first; } @@ -85,25 +88,25 @@ bool EnsureMetadataTask::abort() void EnsureMetadataTask::executeTask() { - setStatus(tr("Checking if mods have metadata...")); + setStatus(tr("Checking if resources have metadata...")); - for (auto* mod : m_mods) { - if (!mod->valid()) { - qDebug() << "Mod" << mod->name() << "is invalid!"; - emitFail(mod); + for (auto* resource : m_resources) { + if (!resource->valid()) { + qDebug() << "Resource" << resource->name() << "is invalid!"; + emitFail(resource); continue; } // They already have the right metadata :o - if (mod->status() != ModStatus::NoMetadata && mod->metadata() && mod->metadata()->provider == m_provider) { - qDebug() << "Mod" << mod->name() << "already has metadata!"; - emitReady(mod); + if (resource->status() != ResourceStatus::NO_METADATA && resource->metadata() && resource->metadata()->provider == m_provider) { + qDebug() << "Resource" << resource->name() << "already has metadata!"; + emitReady(resource); continue; } // Folders don't have metadata - if (mod->type() == ResourceType::FOLDER) { - emitReady(mod); + if (resource->type() == ResourceType::FOLDER) { + emitReady(resource); } } @@ -119,9 +122,9 @@ void EnsureMetadataTask::executeTask() } auto invalidade_leftover = [this] { - for (auto mod = m_mods.constBegin(); mod != m_mods.constEnd(); mod++) - emitFail(mod.value(), mod.key(), RemoveFromList::No); - m_mods.clear(); + for (auto resource = m_resources.constBegin(); resource != m_resources.constEnd(); resource++) + emitFail(resource.value(), resource.key(), RemoveFromList::No); + m_resources.clear(); emitSucceeded(); }; @@ -143,70 +146,71 @@ void EnsureMetadataTask::executeTask() return; } - connect(project_task.get(), &Task::finished, this, [=] { + connect(project_task.get(), &Task::finished, this, [this, invalidade_leftover, project_task] { invalidade_leftover(); project_task->deleteLater(); if (m_current_task) m_current_task.reset(); }); + connect(project_task.get(), &Task::failed, this, &EnsureMetadataTask::emitFailed); m_current_task = project_task; project_task->start(); }); - connect(version_task.get(), &Task::finished, [=] { + connect(version_task.get(), &Task::finished, [this, version_task] { version_task->deleteLater(); if (m_current_task) m_current_task.reset(); }); - if (m_mods.size() > 1) - setStatus(tr("Requesting metadata information from %1...").arg(ProviderCaps.readableName(m_provider))); - else if (!m_mods.empty()) + if (m_resources.size() > 1) + setStatus(tr("Requesting metadata information from %1...").arg(ModPlatform::ProviderCapabilities::readableName(m_provider))); + else if (!m_resources.empty()) setStatus(tr("Requesting metadata information from %1 for '%2'...") - .arg(ProviderCaps.readableName(m_provider), m_mods.begin().value()->name())); + .arg(ModPlatform::ProviderCapabilities::readableName(m_provider), m_resources.begin().value()->name())); m_current_task = version_task; version_task->start(); } -void EnsureMetadataTask::emitReady(Mod* m, QString key, RemoveFromList remove) +void EnsureMetadataTask::emitReady(Resource* resource, QString key, RemoveFromList remove) { - if (!m) { - qCritical() << "Tried to mark a null mod as ready."; + if (!resource) { + qCritical() << "Tried to mark a null resource as ready."; if (!key.isEmpty()) - m_mods.remove(key); + m_resources.remove(key); return; } - qDebug() << QString("Generated metadata for %1").arg(m->name()); - emit metadataReady(m); + qDebug() << QString("Generated metadata for %1").arg(resource->name()); + emit metadataReady(resource); if (remove == RemoveFromList::Yes) { if (key.isEmpty()) - key = getExistingHash(m); - m_mods.remove(key); + key = getExistingHash(resource); + m_resources.remove(key); } } -void EnsureMetadataTask::emitFail(Mod* m, QString key, RemoveFromList remove) +void EnsureMetadataTask::emitFail(Resource* resource, QString key, RemoveFromList remove) { - if (!m) { - qCritical() << "Tried to mark a null mod as failed."; + if (!resource) { + qCritical() << "Tried to mark a null resource as failed."; if (!key.isEmpty()) - m_mods.remove(key); + m_resources.remove(key); return; } - qDebug() << QString("Failed to generate metadata for %1").arg(m->name()); - emit metadataFailed(m); + qDebug() << QString("Failed to generate metadata for %1").arg(resource->name()); + emit metadataFailed(resource); if (remove == RemoveFromList::Yes) { if (key.isEmpty()) - key = getExistingHash(m); - m_mods.remove(key); + key = getExistingHash(resource); + m_resources.remove(key); } } @@ -214,10 +218,10 @@ void EnsureMetadataTask::emitFail(Mod* m, QString key, RemoveFromList remove) Task::Ptr EnsureMetadataTask::modrinthVersionsTask() { - auto hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first(); + auto hash_type = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first(); auto response = std::make_shared(); - auto ver_task = modrinth_api.currentVersions(m_mods.keys(), hash_type, response); + auto ver_task = modrinth_api.currentVersions(m_resources.keys(), hash_type, response); // Prevents unfortunate timings when aborting the task if (!ver_task) @@ -237,20 +241,20 @@ Task::Ptr EnsureMetadataTask::modrinthVersionsTask() try { auto entries = Json::requireObject(doc); - for (auto& hash : m_mods.keys()) { - auto mod = m_mods.find(hash).value(); + for (auto& hash : m_resources.keys()) { + auto resource = m_resources.find(hash).value(); try { auto entry = Json::requireObject(entries, hash); - setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod->name())); - qDebug() << "Getting version for" << mod->name() << "from Modrinth"; + setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(resource->name())); + qDebug() << "Getting version for" << resource->name() << "from Modrinth"; m_temp_versions.insert(hash, Modrinth::loadIndexedPackVersion(entry)); } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << entries; - emitFail(mod); + emitFail(resource); } } } catch (Json::JsonException& e) { @@ -322,23 +326,23 @@ Task::Ptr EnsureMetadataTask::modrinthProjectsTask() auto hash = addonIds.find(pack.addonId.toString()).value(); - auto mod_iter = m_mods.find(hash); - if (mod_iter == m_mods.end()) { + auto resource_iter = m_resources.find(hash); + if (resource_iter == m_resources.end()) { qWarning() << "Invalid project id from the API response."; continue; } - auto* mod = mod_iter.value(); + auto* resource = resource_iter.value(); try { - setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod->name())); + setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(resource->name())); - modrinthCallback(pack, m_temp_versions.find(hash).value(), mod); + modrinthCallback(pack, m_temp_versions.find(hash).value(), resource); } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << entries; - emitFail(mod); + emitFail(resource); } } }); @@ -352,7 +356,7 @@ Task::Ptr EnsureMetadataTask::flameVersionsTask() auto response = std::make_shared(); QList fingerprints; - for (auto& murmur : m_mods.keys()) { + for (auto& murmur : m_resources.keys()) { fingerprints.push_back(murmur.toUInt()); } @@ -392,13 +396,13 @@ Task::Ptr EnsureMetadataTask::flameVersionsTask() } auto fingerprint = QString::number(Json::ensureVariant(file_obj, "fileFingerprint").toUInt()); - auto mod = m_mods.find(fingerprint); - if (mod == m_mods.end()) { + auto resource = m_resources.find(fingerprint); + if (resource == m_resources.end()) { qWarning() << "Invalid fingerprint from the API response."; continue; } - setStatus(tr("Parsing API response from CurseForge for '%1'...").arg((*mod)->name())); + setStatus(tr("Parsing API response from CurseForge for '%1'...").arg((*resource)->name())); m_temp_versions.insert(fingerprint, FlameMod::loadIndexedPackVersion(file_obj)); } @@ -415,7 +419,7 @@ Task::Ptr EnsureMetadataTask::flameVersionsTask() Task::Ptr EnsureMetadataTask::flameProjectsTask() { QHash addonIds; - for (auto const& hash : m_mods.keys()) { + for (auto const& hash : m_resources.keys()) { if (m_temp_versions.contains(hash)) { auto data = m_temp_versions.find(hash).value(); @@ -462,20 +466,20 @@ Task::Ptr EnsureMetadataTask::flameProjectsTask() auto id = QString::number(Json::requireInteger(entry_obj, "id")); auto hash = addonIds.find(id).value(); - auto mod = m_mods.find(hash).value(); + auto resource = m_resources.find(hash).value(); try { - setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name())); + setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(resource->name())); ModPlatform::IndexedPack pack; FlameMod::loadIndexedPack(pack, entry_obj); - flameCallback(pack, m_temp_versions.find(hash).value(), mod); + flameCallback(pack, m_temp_versions.find(hash).value(), resource); } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << entries; - emitFail(mod); + emitFail(resource); } } } catch (Json::JsonException& e) { @@ -487,17 +491,17 @@ Task::Ptr EnsureMetadataTask::flameProjectsTask() return proj_task; } -void EnsureMetadataTask::modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod* mod) +void EnsureMetadataTask::modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Resource* resource) { // Prevent file name mismatch - ver.fileName = mod->fileinfo().fileName(); + ver.fileName = resource->fileinfo().fileName(); if (ver.fileName.endsWith(".disabled")) ver.fileName.chop(9); QDir tmp_index_dir(m_index_dir); { - LocalModUpdateTask update_metadata(m_index_dir, pack, ver); + LocalResourceUpdateTask update_metadata(m_index_dir, pack, ver); QEventLoop loop; QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit); @@ -511,27 +515,27 @@ void EnsureMetadataTask::modrinthCallback(ModPlatform::IndexedPack& pack, ModPla auto metadata = Metadata::get(tmp_index_dir, pack.slug); if (!metadata.isValid()) { qCritical() << "Failed to generate metadata at last step!"; - emitFail(mod); + emitFail(resource); return; } - mod->setMetadata(metadata); + resource->setMetadata(metadata); - emitReady(mod); + emitReady(resource); } -void EnsureMetadataTask::flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod* mod) +void EnsureMetadataTask::flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Resource* resource) { try { // Prevent file name mismatch - ver.fileName = mod->fileinfo().fileName(); + ver.fileName = resource->fileinfo().fileName(); if (ver.fileName.endsWith(".disabled")) ver.fileName.chop(9); QDir tmp_index_dir(m_index_dir); { - LocalModUpdateTask update_metadata(m_index_dir, pack, ver); + LocalResourceUpdateTask update_metadata(m_index_dir, pack, ver); QEventLoop loop; QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit); @@ -545,16 +549,16 @@ void EnsureMetadataTask::flameCallback(ModPlatform::IndexedPack& pack, ModPlatfo auto metadata = Metadata::get(tmp_index_dir, pack.slug); if (!metadata.isValid()) { qCritical() << "Failed to generate metadata at last step!"; - emitFail(mod); + emitFail(resource); return; } - mod->setMetadata(metadata); + resource->setMetadata(metadata); - emitReady(mod); + emitReady(resource); } catch (Json::JsonException& e) { qDebug() << e.cause(); - emitFail(mod); + emitFail(resource); } } diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h index 2f276e5a0..4e5787841 100644 --- a/launcher/modplatform/EnsureMetadataTask.h +++ b/launcher/modplatform/EnsureMetadataTask.h @@ -5,6 +5,7 @@ #include "modplatform/helpers/HashUtils.h" +#include "minecraft/mod/Resource.h" #include "tasks/ConcurrentTask.h" class Mod; @@ -14,12 +15,13 @@ class EnsureMetadataTask : public Task { Q_OBJECT public: - EnsureMetadataTask(Mod*, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); - EnsureMetadataTask(QList&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); + 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; - Task::Ptr getHashingTask() { return m_hashing_task; } + Task::Ptr getHashingTask() { return m_hashingTask; } public slots: bool abort() override; @@ -36,27 +38,27 @@ class EnsureMetadataTask : public Task { // Helpers enum class RemoveFromList { Yes, No }; - void emitReady(Mod*, QString key = {}, RemoveFromList = RemoveFromList::Yes); - void emitFail(Mod*, QString key = {}, RemoveFromList = RemoveFromList::Yes); + void emitReady(Resource*, QString key = {}, RemoveFromList = RemoveFromList::Yes); + void emitFail(Resource*, QString key = {}, RemoveFromList = RemoveFromList::Yes); // Hashes and stuff - auto createNewHash(Mod*) -> Hashing::Hasher::Ptr; - auto getExistingHash(Mod*) -> QString; + auto createNewHash(Resource*) -> Hashing::Hasher::Ptr; + auto getExistingHash(Resource*) -> QString; private slots: - void modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod*); - void flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod*); + void modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Resource*); + void flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Resource*); signals: - void metadataReady(Mod*); - void metadataFailed(Mod*); + void metadataReady(Resource*); + void metadataFailed(Resource*); private: - QHash m_mods; + QHash m_resources; QDir m_index_dir; ModPlatform::ResourceProvider m_provider; QHash m_temp_versions; - ConcurrentTask::Ptr m_hashing_task; + Task::Ptr m_hashingTask; Task::Ptr m_current_task; }; diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index fc79dff15..c3ecccf8e 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 @@ -30,6 +31,19 @@ static const QMap s_indexed_version_ty { "alpha", IndexedVersionType::VersionType::Alpha } }; +static const QList loaderList = { NeoForge, Forge, Cauldron, LiteLoader, Quilt, Fabric }; + +QList modLoaderTypesToList(ModLoaderTypes flags) +{ + QList flagList; + for (auto flag : loaderList) { + if (flags.testFlag(flag)) { + flagList.append(flag); + } + } + return flagList; +} + IndexedVersionType::IndexedVersionType(const QString& type) : IndexedVersionType(enumFromString(type)) {} IndexedVersionType::IndexedVersionType(const IndexedVersionType::VersionType& type) @@ -58,7 +72,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 +82,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 +93,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 +106,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 +133,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 72294c399..8fae1bf6c 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -25,7 +25,6 @@ #include #include #include -#include class QIODevice; @@ -33,20 +32,19 @@ namespace ModPlatform { enum ModLoaderType { NeoForge = 1 << 0, Forge = 1 << 1, Cauldron = 1 << 2, LiteLoader = 1 << 3, Fabric = 1 << 4, Quilt = 1 << 5 }; Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) +QList modLoaderTypesToList(ModLoaderTypes flags); enum class ResourceProvider { MODRINTH, FLAME }; -enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK }; +enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK, MODPACK }; enum class DependencyType { REQUIRED, OPTIONAL, INCOMPATIBLE, EMBEDDED, TOOL, INCLUDE, UNKNOWN }; -class ProviderCapabilities { - public: - auto name(ResourceProvider) -> const char*; - auto readableName(ResourceProvider) -> QString; - auto hashType(ResourceProvider) -> QStringList; - auto hash(ResourceProvider, QIODevice*, QString type = "") -> QString; -}; +namespace ProviderCapabilities { +const char* name(ResourceProvider); +QString readableName(ResourceProvider); +QStringList hashType(ResourceProvider); +} // namespace ProviderCapabilities struct ModpackAuthor { QString name; @@ -109,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; @@ -122,6 +121,8 @@ struct ExtraPackData { QString wikiUrl; QString discordUrl; + QString status; + QString body; }; @@ -181,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 { @@ -189,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 3b1959384..62a1ff199 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 @@ -72,7 +73,10 @@ class ResourceAPI { std::optional search; std::optional sorting; std::optional loaders; - std::optional > versions; + std::optional> versions; + std::optional side; + std::optional categoryIds; + bool openSource; }; struct SearchCallbacks { std::function on_succeed; @@ -83,7 +87,7 @@ class ResourceAPI { struct VersionSearchArgs { ModPlatform::IndexedPack pack; - std::optional > mcVersions; + std::optional> mcVersions; std::optional loaders; VersionSearchArgs(VersionSearchArgs const&) = default; @@ -96,6 +100,7 @@ class ResourceAPI { }; struct VersionSearchCallbacks { std::function on_succeed; + std::function on_fail; }; struct ProjectInfoArgs { @@ -118,6 +123,7 @@ class ResourceAPI { struct DependencySearchCallbacks { std::function on_succeed; + std::function on_fail; }; public: @@ -163,11 +169,23 @@ class ResourceAPI { protected: [[nodiscard]] inline QString debugName() const { return "External resource API"; } - [[nodiscard]] inline auto getGameVersionsString(std::list mcVersions) const -> QString + [[nodiscard]] inline QString mapMCVersionToModrinth(Version v) const + { + static const QString preString = " Pre-Release "; + auto verStr = v.toString(); + + if (verStr.contains(preString)) { + verStr.replace(preString, "-pre"); + } + verStr.replace(" ", "-"); + return verStr; + } + + [[nodiscard]] inline QString getGameVersionsString(std::list mcVersions) const { QString s; for (auto& ver : mcVersions) { - s += QString("\"%1\",").arg(ver.toString()); + s += QString("\"%1\",").arg(mapMCVersionToModrinth(ver)); } s.remove(s.length() - 1, 1); // remove last comma return s; diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 8ae8145de..a0898edbd 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,28 +636,27 @@ 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(); - connect(jobPtr.get(), &NetJob::succeeded, this, [&]() { + connect(jobPtr.get(), &NetJob::succeeded, this, [this]() { abortable = false; jobPtr.reset(); extractConfigs(); }); - connect(jobPtr.get(), &NetJob::failed, [&](QString reason) { + connect(jobPtr.get(), &NetJob::failed, [this](QString reason) { abortable = false; jobPtr.reset(); emitFailed(reason); }); - connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total) { + connect(jobPtr.get(), &NetJob::progress, [this](qint64 current, qint64 total) { abortable = true; setProgress(current, total); }); connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress); - connect(jobPtr.get(), &NetJob::aborted, [&] { + connect(jobPtr.get(), &NetJob::aborted, [this] { abortable = false; jobPtr.reset(); emitAborted(); @@ -688,8 +685,8 @@ void PackInstallTask::extractConfigs() m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/minecraft"); #endif - connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, [&]() { downloadMods(); }); - connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, [&]() { emitAborted(); }); + connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, [this]() { downloadMods(); }); + connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, [this]() { emitAborted(); }); m_extractFutureWatcher.setFuture(m_extractFuture); } @@ -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 5865bee91..7ff38d57e 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -1,86 +1,125 @@ +// 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(); +} + +PackedResourceType getResourceType(int classId) +{ + switch (classId) { + case 17: // Worlds + return PackedResourceType::WorldSave; + case 6: // Mods + return PackedResourceType::Mod; + case 12: // Resource Packs + // return PackedResourceType::ResourcePack; // not really a resourcepack + /* fallthrough */ + case 4546: // Customization + // return PackedResourceType::ShaderPack; // not really a shaderPack + /* fallthrough */ + case 4471: // Modpacks + /* fallthrough */ + case 5: // Bukkit Plugins + /* fallthrough */ + case 4559: // Addons + /* fallthrough */ + default: + return PackedResourceType::UNKNOWN; + } } 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,126 +130,161 @@ 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; + + getFlameProjects(); + return; + } + + try { + auto entries = Json::requireObject(doc); + for (auto& out : m_manifest.files) { + auto url = QUrl(out.version.downloadUrl, QUrl::TolerantMode); + if (!url.isValid() && "sha1" == out.version.hash_type && !out.version.hash.isEmpty()) { + try { + auto entry = Json::requireObject(entries, out.version.hash); + + auto file = Modrinth::loadIndexedPackVersion(entry); + + // If there's more than one mod loader for this version, we can't know for sure + // which file is relative to each loader, so it's best to not use any one and + // let the user download it manually. + if (!file.loaders || hasSingleModLoaderSelected(file.loaders)) { + out.version.downloadUrl = file.downloadUrl; + qDebug() << "Found alternative on modrinth " << out.version.fileName; + } + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << entries; + } + } + } + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << doc; + } + getFlameProjects(); }); - connect(m_checkJob.get(), &NetJob::failed, this, [this, step_progress](QString reason) { + connect(m_task.get(), &Task::failed, this, [this, step_progress](QString reason) { step_progress->state = TaskStepState::Failed; stepProgress(*step_progress); - emitFailed(reason); }); - 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); + file->resourceType = getResourceType(Json::requireInteger(entry_obj, "classId", "modClassId")); + if (file->resourceType == PackedResourceType::WorldSave) { + file->targetFolder = "saves"; + } + } + } 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..699eb792a 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,94 @@ QList FlameAPI::getSortingMethods() const { 7, "Category", QObject::tr("Sort by Category") }, { 8, "GameVersion", QObject::tr("Sort by Game Version") } }; } + +Task::Ptr FlameAPI::getCategories(std::shared_ptr response, ModPlatform::ResourceType type) +{ + auto netJob = makeShared(QString("Flame::GetCategories"), APPLICATION->network()); + netJob->addNetAction(Net::ApiDownload::makeByteArray( + QUrl(QString("https://api.curseforge.com/v1/categories?gameId=432&classId=%1").arg(getClassId(type))), response)); + QObject::connect(netJob.get(), &Task::failed, [](QString msg) { qDebug() << "Flame failed to get categories:" << msg; }); + return netJob; +} + +Task::Ptr FlameAPI::getModCategories(std::shared_ptr response) +{ + return getCategories(response, ModPlatform::ResourceType::MOD); +} + +QList FlameAPI::loadModCategories(std::shared_ptr response) +{ + QList categories; + 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) +{ + static const auto noLoader = ModPlatform::ModLoaderType(0); + QHash bestMatch; + auto checkVersion = [&bestMatch](const ModPlatform::IndexedVersion& version, const ModPlatform::ModLoaderType& loader) { + if (bestMatch.contains(loader)) { + auto best = bestMatch.value(loader); + if (version.date > best.date) { + bestMatch[loader] = version; + } + } else { + bestMatch[loader] = version; + } + }; + for (auto file_tmp : versions) { + auto loaders = ModPlatform::modLoaderTypesToList(file_tmp.loaders); + if (loaders.isEmpty()) { + checkVersion(file_tmp, noLoader); + } else { + for (auto loader : loaders) { + checkVersion(file_tmp, loader); + } + } + } + // edge case: mod has installed for forge but the instance is fabric => fabric version will be prioritizated on update + auto currentLoaders = instanceLoaders + ModPlatform::modLoaderTypesToList(modLoaders); + currentLoaders.append(noLoader); // add a fallback in case the versions do not define a loader + + for (auto loader : currentLoaders) { + if (bestMatch.contains(loader)) { + auto bestForLoader = bestMatch.value(loader); + // awkward case where the mod has only two loaders and one of them is not specified + if (loader != noLoader && bestMatch.contains(noLoader) && bestMatch.size() == 2) { + auto bestForNoLoader = bestMatch.value(noLoader); + if (bestForNoLoader.date > bestForLoader.date) { + return bestForNoLoader; + } + } + return bestForLoader; + } + } + return {}; +} diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index e22d8f0d8..3ca0d5448 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,26 @@ 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 getCategories(std::shared_ptr response, ModPlatform::ResourceType type); + 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); } @@ -40,6 +47,8 @@ class FlameAPI : public NetworkResourceAPI { return 12; case ModPlatform::ResourceType::SHADER_PACK: return 6552; + case ModPlatform::ResourceType::MODPACK: + return 4471; } } @@ -63,7 +72,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,17 +83,11 @@ 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: + public: [[nodiscard]] std::optional getSearchURL(SearchArgs const& args) const override { - auto gameVersionStr = - args.versions.has_value() ? QString("gameVersion=%1").arg(args.versions.value().front().toString()) : QString(); - QStringList get_arguments; get_arguments.append(QString("classId=%1").arg(getClassId(args.type))); get_arguments.append(QString("index=%1").arg(args.offset)); @@ -94,17 +97,22 @@ class FlameAPI : public NetworkResourceAPI { if (args.sorting.has_value()) get_arguments.append(QString("sortField=%1").arg(args.sorting.value().index)); get_arguments.append("sortOrder=desc"); - if (args.loaders.has_value()) + if (args.loaders.has_value() && args.loaders.value() != 0) get_arguments.append(QString("modLoaderTypes=%1").arg(getModLoaderFilters(args.loaders.value()))); - get_arguments.append(gameVersionStr); + if (args.categoryIds.has_value() && !args.categoryIds->empty()) + get_arguments.append(QString("categoryIds=[%1]").arg(args.categoryIds->join(","))); + + if (args.versions.has_value() && !args.versions.value().empty()) + get_arguments.append(QString("gameVersion=%1").arg(args.versions.value().front().toString())); return "https://api.curseforge.com/v1/mods/search?gameId=432&" + get_arguments.join('&'); - }; + } + private: [[nodiscard]] std::optional getInfoURL(QString const& id) const override { return QString("https://api.curseforge.com/v1/mods/%1").arg(id); - }; + } [[nodiscard]] std::optional getVersionsURL(VersionSearchArgs const& args) const override { @@ -119,7 +127,7 @@ class FlameAPI : public NetworkResourceAPI { url += QString("&modLoaderType=%1").arg(mappedModLoader); } return url; - }; + } [[nodiscard]] std::optional getDependencyURL(DependencySearchArgs const& args) const override { @@ -131,5 +139,5 @@ class FlameAPI : public NetworkResourceAPI { url += QString("&modLoaderType=%1").arg(mappedModLoader); } return url; - }; + } }; diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp index c014863a3..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" @@ -24,7 +25,7 @@ bool FlameCheckUpdate::abort() return true; } -ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info) +ModPlatform::IndexedPack FlameCheckUpdate::getProjectInfo(ModPlatform::IndexedVersion& ver_info) { ModPlatform::IndexedPack pack; @@ -57,6 +58,7 @@ ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info) } }); + connect(get_project_job, &NetJob::failed, this, &FlameCheckUpdate::emitFailed); QObject::connect(get_project_job, &NetJob::finished, [&loop, get_project_job] { get_project_job->deleteLater(); loop.quit(); @@ -68,7 +70,7 @@ ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info) return pack; } -ModPlatform::IndexedVersion getFileInfo(int addonId, int fileId) +ModPlatform::IndexedVersion FlameCheckUpdate::getFileInfo(int addonId, int fileId) { ModPlatform::IndexedVersion ver; @@ -100,7 +102,7 @@ ModPlatform::IndexedVersion getFileInfo(int addonId, int fileId) qDebug() << doc; } }); - + connect(get_file_info_job, &NetJob::failed, this, &FlameCheckUpdate::emitFailed); QObject::connect(get_file_info_job, &NetJob::finished, [&loop, get_file_info_job] { get_file_info_job->deleteLater(); loop.quit(); @@ -119,65 +121,67 @@ ModPlatform::IndexedVersion getFileInfo(int addonId, int fileId) * */ void FlameCheckUpdate::executeTask() { - setStatus(tr("Preparing mods for CurseForge...")); + setStatus(tr("Preparing resources for CurseForge...")); int i = 0; - for (auto* mod : m_mods) { - if (!mod->enabled()) { - emit checkFailed(mod, tr("Disabled mods won't be updated, to prevent mod duplication issues!")); - continue; - } + for (auto* resource : m_resources) { + setStatus(tr("Getting API response from CurseForge for '%1'...").arg(resource->name())); + setProgress(i++, m_resources.size()); - setStatus(tr("Getting API response from CurseForge for '%1'...").arg(mod->name())); - setProgress(i++, m_mods.size()); - - auto latest_ver = api.getLatestVersion({ { mod->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(mod->name())); + setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(resource->name())); - if (!latest_ver.addonId.isValid()) { - emit checkFailed(mod, tr("No valid version found for this mod. It's probably unavailable for the current game " - "version / mod loader.")); + if (!latest_ver.has_value() || !latest_ver->addonId.isValid()) { + 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."); + + emit checkFailed(resource, reason); continue; } - if (latest_ver.downloadUrl.isEmpty() && latest_ver.fileId != mod->metadata()->file_id) { - auto pack = getProjectInfo(latest_ver); - auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, latest_ver.fileId.toString()); - emit checkFailed(mod, tr("Mod has a new update available, but is not downloadable using CurseForge."), recover_url); + 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; } // 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->websiteUrl = mod->homeurl(); - for (auto& author : mod->authors()) - pack->authors.append({ author }); - pack->description = mod->description(); + pack->name = resource->name(); + pack->slug = resource->metadata()->slug; + pack->addonId = resource->metadata()->project_id; pack->provider = ModPlatform::ResourceProvider::FLAME; - if (!latest_ver.hash.isEmpty() && (mod->metadata()->hash != latest_ver.hash || mod->status() == ModStatus::NotInstalled)) { - auto old_version = mod->version(); - if (old_version.isEmpty() && mod->status() != ModStatus::NotInstalled) { - auto current_ver = getFileInfo(latest_ver.addonId.toInt(), mod->metadata()->file_id.toInt()); - old_version = current_ver.version; + 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"); + else + old_version = tr("Unknown"); } - auto download_task = makeShared(pack, latest_ver, m_mods_folder); - m_updatable.emplace_back(pack->name, mod->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 05c619a70..6543a0e04 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.h +++ b/launcher/modplatform/flame/FlameCheckUpdate.h @@ -1,6 +1,5 @@ #pragma once -#include "Application.h" #include "modplatform/CheckUpdateTask.h" #include "net/NetJob.h" @@ -8,11 +7,11 @@ class FlameCheckUpdate : public CheckUpdateTask { Q_OBJECT public: - FlameCheckUpdate(QList& mods, + FlameCheckUpdate(QList& resources, std::list& mcVersions, - std::optional loaders, - std::shared_ptr mods_folder) - : CheckUpdateTask(mods, mcVersions, loaders, mods_folder) + QList loadersList, + std::shared_ptr resourceModel) + : CheckUpdateTask(resources, mcVersions, std::move(loadersList), std::move(resourceModel)) {} public slots: @@ -22,6 +21,9 @@ class FlameCheckUpdate : public CheckUpdateTask { void executeTask() override; private: + ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info); + ModPlatform::IndexedVersion getFileInfo(int addonId, int fileId); + NetJob* m_net_job = nullptr; bool m_was_aborted = false; diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index 2a26ce944..6073e90a2 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" @@ -72,12 +75,12 @@ bool FlameCreationTask::abort() return false; m_abort = true; - if (m_process_update_file_info_job) - m_process_update_file_info_job->abort(); - if (m_files_job) - m_files_job->abort(); - if (m_mod_id_resolver) - m_mod_id_resolver->abort(); + if (m_processUpdateFileInfoJob) + m_processUpdateFileInfoJob->abort(); + if (m_filesJob) + m_filesJob->abort(); + if (m_modIdResolver) + m_modIdResolver->abort(); return Task::abort(); } @@ -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,22 +221,23 @@ 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)); } }); + connect(job.get(), &Task::failed, this, [](QString reason) { qCritical() << "Failed to get files: " << reason; }); connect(job.get(), &Task::finished, &loop, &QEventLoop::quit); - m_process_update_file_info_job = job; + m_processUpdateFileInfoJob = job; job->start(); loop.exec(); - m_process_update_file_info_job = nullptr; + m_processUpdateFileInfoJob = nullptr; } else { // We don't have an old index file, so we may duplicate stuff! auto dialog = CustomMessageBox::selectable(m_parent, tr("No index file."), @@ -321,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()); @@ -335,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; } @@ -353,6 +356,8 @@ bool FlameCreationTask::createInstance() auto id = loader.id; if (id.startsWith("neoforge-")) { id.remove("neoforge-"); + if (id.startsWith("1.20.1-")) + id.remove("1.20.1-"); // this is a mess for curseforge loaderType = "neoforge"; loaderUid = "net.neoforged"; } else if (id.startsWith("forge-")) { @@ -425,22 +430,26 @@ bool FlameCreationTask::createInstance() } // Don't add managed info to packs without an ID (most likely imported from ZIP) - if (!m_managed_id.isEmpty()) - instance.setManagedPack("flame", m_managed_id, m_pack.name, m_managed_version_id, m_pack.version); + if (!m_managedId.isEmpty()) + instance.setManagedPack("flame", m_managedId, m_pack.name, m_managedVersionId, m_pack.version); + else + instance.setManagedPack("flame", "", name(), "", ""); + instance.setName(name()); - m_mod_id_resolver.reset(new Flame::FileResolvingTask(APPLICATION->network(), m_pack)); - connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::succeeded, this, [this, &loop] { idResolverSucceeded(loop); }); - connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::failed, [&](QString reason) { - m_mod_id_resolver.reset(); + m_modIdResolver.reset(new Flame::FileResolvingTask(APPLICATION->network(), m_pack)); + connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, this, [this, &loop] { idResolverSucceeded(loop); }); + connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [this, &loop](QString reason) { + m_modIdResolver.reset(); setError(tr("Unable to resolve mod IDs:\n") + reason); loop.quit(); }); - connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress); - connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus); - connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::stepProgress, this, &FlameCreationTask::propagateStepProgress); - connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::details, this, &FlameCreationTask::setDetails); - m_mod_id_resolver->start(); + connect(m_modIdResolver.get(), &Flame::FileResolvingTask::aborted, &loop, &QEventLoop::quit); + connect(m_modIdResolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress); + connect(m_modIdResolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus); + connect(m_modIdResolver.get(), &Flame::FileResolvingTask::stepProgress, this, &FlameCreationTask::propagateStepProgress); + connect(m_modIdResolver.get(), &Flame::FileResolvingTask::details, this, &FlameCreationTask::setDetails); + m_modIdResolver->start(); loop.exec(); @@ -459,21 +468,21 @@ bool FlameCreationTask::createInstance() void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) { - auto results = m_mod_id_resolver->getResults(); + auto results = m_modIdResolver->getResults(); // first check for blocked mods 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.resourceType != PackedResourceType::Mod) { + m_otherResources.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; @@ -498,7 +507,7 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) copyBlockedMods(blocked_mods); setupDownloadJob(loop); } else { - m_mod_id_resolver.reset(); + m_modIdResolver.reset(); setError("Canceled"); loop.quit(); } @@ -509,13 +518,13 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) void FlameCreationTask::setupDownloadJob(QEventLoop& loop) { - m_files_job.reset(new NetJob(tr("Mod Download Flame"), APPLICATION->network())); - auto results = m_mod_id_resolver->getResults().files; + m_filesJob.reset(new NetJob(tr("Mod Download Flame"), APPLICATION->network())); + auto results = m_modIdResolver->getResults().files; QStringList optionalFiles; for (auto& result : results) { if (!result.required) { - optionalFiles << FS::PathCombine(result.targetFolder, result.fileName); + optionalFiles << FS::PathCombine(result.targetFolder, result.version.fileName); } } @@ -531,7 +540,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"; } @@ -539,50 +551,29 @@ 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_filesJob->addNetAction(dl); } } - m_mod_id_resolver.reset(); - connect(m_files_job.get(), &NetJob::succeeded, this, [&]() { - m_files_job.reset(); - validateZIPResources(); + connect(m_filesJob.get(), &NetJob::finished, this, [this, &loop]() { + m_filesJob.reset(); + validateOtherResources(loop); }); - connect(m_files_job.get(), &NetJob::failed, [&](QString reason) { - m_files_job.reset(); + connect(m_filesJob.get(), &NetJob::failed, [this](QString reason) { + m_filesJob.reset(); setError(reason); }); - connect(m_files_job.get(), &NetJob::progress, this, [this](qint64 current, qint64 total) { + connect(m_filesJob.get(), &NetJob::progress, this, [this](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, &FlameCreationTask::propagateStepProgress); - connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit); + connect(m_filesJob.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress); setStatus(tr("Downloading mods...")); - m_files_job->start(); + m_filesJob->start(); } /// @brief copy the matched blocked mods to the instance staging area @@ -606,8 +597,14 @@ void FlameCreationTask::copyBlockedMods(QList const& blocked_mods) qDebug() << "Will try to copy" << mod.localPath << "to" << destPath; - if (!FS::copy(mod.localPath, destPath)()) { - qDebug() << "Copy of" << mod.localPath << "to" << destPath << "Failed"; + if (mod.move) { + if (!FS::move(mod.localPath, destPath)) { + qDebug() << "Move of" << mod.localPath << "to" << destPath << "Failed"; + } + } else { + if (!FS::copy(mod.localPath, destPath)()) { + qDebug() << "Copy of" << mod.localPath << "to" << destPath << "Failed"; + } } i++; @@ -617,10 +614,11 @@ void FlameCreationTask::copyBlockedMods(QList const& blocked_mods) setAbortable(true); } -void FlameCreationTask::validateZIPResources() +void FlameCreationTask::validateOtherResources(QEventLoop& loop) { - qDebug() << "Validating whether resources stored as .zip are in the right place"; - for (auto [fileName, targetFolder] : m_ZIP_resources) { + qDebug() << "Validating whether other resources are in the right place"; + QStringList zipMods; + for (auto [fileName, targetFolder] : m_otherResources) { qDebug() << "Checking" << fileName << "..."; auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName); @@ -659,6 +657,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"); @@ -679,9 +678,23 @@ void FlameCreationTask::validateZIPResources() installWorld(worldPath); break; case PackedResourceType::UNKNOWN: + /* fallthrough */ default: qDebug() << "Can't Identify" << fileName << "at" << localPath << ", leaving it where it is."; break; } } + // TODO make this work with other sorts of resource + auto task = makeShared("CreateModMetadata", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); + auto results = m_modIdResolver->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_processUpdateFileInfoJob = task; + task->start(); } diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.h b/launcher/modplatform/flame/FlameInstanceCreationTask.h index 02ad48f2e..3e586a416 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.h +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.h @@ -57,7 +57,7 @@ class FlameCreationTask final : public InstanceCreationTask { QString id, QString version_id, QString original_instance_id = {}) - : InstanceCreationTask(), m_parent(parent), m_managed_id(std::move(id)), m_managed_version_id(std::move(version_id)) + : InstanceCreationTask(), m_parent(parent), m_managedId(std::move(id)), m_managedVersionId(std::move(version_id)) { setStagingPath(staging_path); setParentSettings(global_settings); @@ -74,22 +74,22 @@ class FlameCreationTask final : public InstanceCreationTask { void idResolverSucceeded(QEventLoop&); void setupDownloadJob(QEventLoop&); void copyBlockedMods(QList const& blocked_mods); - void validateZIPResources(); + void validateOtherResources(QEventLoop& loop); QString getVersionForLoader(QString uid, QString loaderType, QString version, QString mcVersion); private: QWidget* m_parent = nullptr; - shared_qobject_ptr m_mod_id_resolver; + shared_qobject_ptr m_modIdResolver; Flame::Manifest m_pack; // Handle to allow aborting - Task::Ptr m_process_update_file_info_job = nullptr; - NetJob::Ptr m_files_job = nullptr; + Task::Ptr m_processUpdateFileInfoJob = nullptr; + NetJob::Ptr m_filesJob = nullptr; - QString m_managed_id, m_managed_version_id; + QString m_managedId, m_managedVersionId; - QList> m_ZIP_resources; + QList> m_otherResources; std::optional m_instance; }; diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 345883c17..7de05f177 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -1,12 +1,12 @@ #include "FlameModIndex.h" +#include "FileSystem.h" #include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "modplatform/flame/FlameAPI.h" static FlameAPI api; -static ModPlatform::ProviderCapabilities ProviderCaps; void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { @@ -20,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) { @@ -79,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(); @@ -90,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); } @@ -117,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"); @@ -138,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")) { @@ -158,7 +163,7 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> auto hash_list = Json::ensureArray(obj, "hashes"); for (auto h : hash_list) { auto hash_entry = Json::ensureObject(h); - auto hash_types = ProviderCaps.hashType(ModPlatform::ResourceProvider::FLAME); + auto hash_types = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::FLAME); auto hash_algo = enumToString(Json::ensureInteger(hash_entry, "algo", 1, "algorithm")); if (hash_types.contains(hash_algo)) { file.hash = Json::requireString(hash_entry, "value"); diff --git a/launcher/modplatform/flame/FlamePackExportTask.cpp b/launcher/modplatform/flame/FlamePackExportTask.cpp index b5ab7bc73..3405b702f 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.cpp +++ b/launcher/modplatform/flame/FlamePackExportTask.cpp @@ -103,8 +103,7 @@ void FlamePackExportTask::collectHashes() setStatus(tr("Finding file hashes...")); setProgress(1, 5); auto allMods = mcInstance->loaderModList()->allMods(); - ConcurrentTask::Ptr hashingTask( - new ConcurrentTask(this, "MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); + ConcurrentTask::Ptr hashingTask(new ConcurrentTask("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); task.reset(hashingTask); for (const QFileInfo& file : files) { const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath()); @@ -116,7 +115,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 +139,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 +200,7 @@ void FlamePackExportTask::makeApiRequest() << " reason: " << parseError.errorString(); qWarning() << *response; - failed(parseError.errorString()); + emitFailed(parseError.errorString()); return; } @@ -213,6 +212,7 @@ void FlamePackExportTask::makeApiRequest() if (dataArr.isEmpty()) { qWarning() << "No matches found for fingerprint search!"; + getProjectsInfo(); return; } for (auto match : dataArr) { @@ -243,9 +243,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 +279,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; } @@ -323,6 +323,7 @@ void FlamePackExportTask::getProjectsInfo() } buildZip(); }); + connect(projTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed); task.reset(projTask); task->start(); } @@ -332,7 +333,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()); @@ -392,13 +393,17 @@ QByteArray FlamePackExportTask::generateIndex() version["version"] = minecraft->m_version; QString id; if (quilt != nullptr) - id = "quilt-" + quilt->getVersion(); + id = "quilt-" + quilt->m_version; else if (fabric != nullptr) - id = "fabric-" + fabric->getVersion(); + id = "fabric-" + fabric->m_version; else if (forge != nullptr) - id = "forge-" + forge->getVersion(); - else if (neoforge != nullptr) - id = "neoforge-" + neoforge->getVersion(); + id = "forge-" + forge->m_version; + else if (neoforge != nullptr) { + id = "neoforge-"; + if (minecraft->m_version == "1.20.1") + id += "1.20.1-"; + id += neoforge->m_version; + } version["modLoaders"] = QJsonArray(); if (!id.isEmpty()) { QJsonObject loader; diff --git a/launcher/modplatform/flame/FlamePackExportTask.h b/launcher/modplatform/flame/FlamePackExportTask.h index 78b46e91f..b11eb17fa 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.h +++ b/launcher/modplatform/flame/FlamePackExportTask.h @@ -26,6 +26,7 @@ #include "tasks/Task.h" class FlamePackExportTask : public Task { + Q_OBJECT public: FlamePackExportTask(const QString& name, const QString& version, diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp index ca8e0a853..8c25b0482 100644 --- a/launcher/modplatform/flame/FlamePackIndex.cpp +++ b/launcher/modplatform/flame/FlamePackIndex.cpp @@ -3,6 +3,7 @@ #include #include "Json.h" +#include "modplatform/ModIndex.h" void Flame::loadIndexedPack(Flame::IndexedPack& pack, QJsonObject& obj) { @@ -88,8 +89,27 @@ void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr) continue; } + for (auto mcVer : versionArray) { + auto str = mcVer.toString(); + + if (str.contains('.')) + file.mcVersion.append(str); + + if (auto loader = str.toLower(); loader == "neoforge") + file.loaders |= ModPlatform::NeoForge; + else if (loader == "forge") + file.loaders |= ModPlatform::Forge; + else if (loader == "cauldron") + file.loaders |= ModPlatform::Cauldron; + else if (loader == "liteloader") + file.loaders |= ModPlatform::LiteLoader; + else if (loader == "fabric") + file.loaders |= ModPlatform::Fabric; + else if (loader == "quilt") + file.loaders |= ModPlatform::Quilt; + } + // pick the latest version supported - file.mcVersion = versionArray[0].toString(); file.version = Json::requireString(version, "displayName"); ModPlatform::IndexedVersionType::VersionType ver_type; diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h index b2a12a67f..11633deee 100644 --- a/launcher/modplatform/flame/FlamePackIndex.h +++ b/launcher/modplatform/flame/FlamePackIndex.h @@ -18,6 +18,7 @@ struct IndexedVersion { int fileId; QString version; ModPlatform::IndexedVersionType version_type; + ModPlatform::ModLoaderTypes loaders = {}; QString mcVersion; QString downloadUrl; }; diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp index 40a523d31..e576a6a84 100644 --- a/launcher/modplatform/flame/PackManifest.cpp +++ b/launcher/modplatform/flame/PackManifest.cpp @@ -68,35 +68,3 @@ void Flame::loadManifest(Flame::Manifest& m, const QString& filepath) } loadManifestV1(m, obj); } - -bool Flame::File::parseFromObject(const QJsonObject& obj, bool throw_on_blocked) -{ - fileName = Json::requireString(obj, "fileName"); - // This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience - // It is also optional - type = File::Type::SingleFile; - - targetFolder = "mods"; - - // get the hash - hash = QString(); - auto hashes = Json::ensureArray(obj, "hashes"); - for (QJsonValueRef item : hashes) { - auto hobj = Json::requireObject(item); - auto algo = Json::requireInteger(hobj, "algo"); - auto value = Json::requireString(hobj, "value"); - if (algo == 1) { - hash = value; - } - } - - // may throw, if the project is blocked - QString rawUrl = Json::ensureString(obj, "downloadUrl"); - url = QUrl(rawUrl, QUrl::TolerantMode); - if (!url.isValid() && throw_on_blocked) { - throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); - } - - resolved = true; - return true; -} diff --git a/launcher/modplatform/flame/PackManifest.h b/launcher/modplatform/flame/PackManifest.h index 4417c2430..7af3b9d6b 100644 --- a/launcher/modplatform/flame/PackManifest.h +++ b/launcher/modplatform/flame/PackManifest.h @@ -40,26 +40,22 @@ #include #include #include +#include "minecraft/mod/tasks/LocalResourceParse.h" +#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; + PackedResourceType resourceType; }; struct Modloader { diff --git a/launcher/modplatform/helpers/ExportToModList.cpp b/launcher/modplatform/helpers/ExportToModList.cpp index 1f01c4a89..bddc7e320 100644 --- a/launcher/modplatform/helpers/ExportToModList.cpp +++ b/launcher/modplatform/helpers/ExportToModList.cpp @@ -28,7 +28,7 @@ QString toHTML(QList mods, OptionalData extraData) auto meta = mod->metadata(); auto modName = mod->name().toHtmlEscaped(); if (extraData & Url) { - auto url = mod->metaurl().toHtmlEscaped(); + auto url = mod->homepage().toHtmlEscaped(); if (!url.isEmpty()) modName = QString("%2").arg(url, modName); } @@ -42,32 +42,45 @@ 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(); + auto url = mod->homepage(); if (!url.isEmpty()) modName = QString("[%1](%2)").arg(modName, url); } 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"); @@ -82,7 +95,7 @@ QString toPlainTXT(QList mods, OptionalData extraData) auto line = modName; if (extraData & Url) { - auto url = mod->metaurl(); + auto url = mod->homepage(); if (!url.isEmpty()) line += QString(" (%1)").arg(url); } @@ -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"); @@ -109,7 +124,7 @@ QString toJSON(QList mods, OptionalData extraData) QJsonObject line; line["name"] = modName; if (extraData & Url) { - auto url = mod->metaurl(); + auto url = mod->homepage(); if (!url.isEmpty()) line["url"] = url; } @@ -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; @@ -139,7 +156,7 @@ QString toCSV(QList mods, OptionalData extraData) data << modName; if (extraData & Url) - data << mod->metaurl(); + data << mod->homepage(); if (extraData & Version) { auto ver = mod->version(); if (ver.isEmpty() && meta != nullptr) @@ -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"); @@ -184,16 +203,18 @@ QString exportToModList(QList mods, QString lineTemplate) for (auto mod : mods) { auto meta = mod->metadata(); auto modName = mod->name(); - auto url = mod->metaurl(); + auto url = mod->homepage(); auto ver = mod->version(); 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 6ff1d1710..a3b8d904c 100644 --- a/launcher/modplatform/helpers/HashUtils.cpp +++ b/launcher/modplatform/helpers/HashUtils.cpp @@ -1,144 +1,165 @@ #include "HashUtils.h" +#include #include #include - -#include "FileSystem.h" -#include "StringUtils.h" +#include #include namespace Hashing { -static ModPlatform::ProviderCapabilities ProviderCaps; - Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provider) { 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 = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first(); - m_hash = ProviderCaps.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 = ProviderCaps.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 = ProviderCaps.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 ProviderCaps.hashType(provider); -} - -bool BlockedModHasher::useHashType(QString type) -{ - auto types = ProviderCaps.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 dccccdc21..d0e1bb912 100644 --- a/launcher/modplatform/helpers/NetworkResourceAPI.cpp +++ b/launcher/modplatform/helpers/NetworkResourceAPI.cpp @@ -43,11 +43,16 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks& callbacks.on_succeed(doc); }); - QObject::connect(netJob.get(), &NetJob::failed, [&netJob, callbacks](QString reason) { + // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. + // This prevents the lambda from extending the lifetime of the shared resource, + // as it only temporarily locks the resource when needed. + auto weak = netJob.toWeakRef(); + QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { int network_error_code = -1; - if (auto* failed_action = netJob->getFailedActions().at(0); failed_action && failed_action->m_reply) - network_error_code = failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - + if (auto netJob = weak.lock()) { + if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) + network_error_code = failed_action->replyStatusCode(); + } callbacks.on_fail(reason, network_error_code); }); QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); }); @@ -103,6 +108,19 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi callbacks.on_succeed(doc, args.pack); }); + // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. + // This prevents the lambda from extending the lifetime of the shared resource, + // as it only temporarily locks the resource when needed. + auto weak = netJob.toWeakRef(); + QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { + int network_error_code = -1; + if (auto netJob = weak.lock()) { + if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) + network_error_code = failed_action->replyStatusCode(); + } + callbacks.on_fail(reason, network_error_code); + }); + return netJob; } @@ -134,7 +152,7 @@ Task::Ptr NetworkResourceAPI::getDependencyVersion(DependencySearchArgs&& args, netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response)); - QObject::connect(netJob.get(), &NetJob::succeeded, [=] { + QObject::connect(netJob.get(), &NetJob::succeeded, [response, callbacks, args] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { @@ -147,5 +165,17 @@ Task::Ptr NetworkResourceAPI::getDependencyVersion(DependencySearchArgs&& args, callbacks.on_succeed(doc, args.dependency); }); + // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. + // This prevents the lambda from extending the lifetime of the shared resource, + // as it only temporarily locks the resource when needed. + auto weak = netJob.toWeakRef(); + QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { + int network_error_code = -1; + if (auto netJob = weak.lock()) { + if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) + network_error_code = failed_action->replyStatusCode(); + } + callbacks.on_fail(reason, network_error_code); + }); return netJob; } diff --git a/launcher/modplatform/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/import_ftb/PackHelpers.cpp b/launcher/modplatform/import_ftb/PackHelpers.cpp index ecf973452..e523b9d20 100644 --- a/launcher/modplatform/import_ftb/PackHelpers.cpp +++ b/launcher/modplatform/import_ftb/PackHelpers.cpp @@ -43,6 +43,7 @@ Modpack parseDirectory(QString path) modpack.version = Json::requireString(root, "version", "version"); modpack.mcVersion = Json::requireString(root, "mcVersion", "mcVersion"); modpack.jvmArgs = Json::ensureVariant(root, "jvmArgs", {}, "jvmArgs"); + modpack.totalPlayTime = Json::requireInteger(root, "totalPlayTime", "totalPlayTime"); } catch (const Exception& e) { qDebug() << "Couldn't load ftb instance json: " << e.cause(); return {}; diff --git a/launcher/modplatform/import_ftb/PackHelpers.h b/launcher/modplatform/import_ftb/PackHelpers.h index 221eb5bf6..449ed2546 100644 --- a/launcher/modplatform/import_ftb/PackHelpers.h +++ b/launcher/modplatform/import_ftb/PackHelpers.h @@ -36,6 +36,7 @@ struct Modpack { QString name; QString version; QString mcVersion; + int totalPlayTime; // not needed for instance creation QVariant jvmArgs; diff --git a/launcher/modplatform/import_ftb/PackInstallTask.cpp b/launcher/modplatform/import_ftb/PackInstallTask.cpp index 9a3b2595b..8046300e1 100644 --- a/launcher/modplatform/import_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/import_ftb/PackInstallTask.cpp @@ -37,7 +37,7 @@ void PackInstallTask::executeTask() progress(1, 2); m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { - FS::copy folderCopy(m_pack.path, FS::PathCombine(m_stagingPath, ".minecraft")); + FS::copy folderCopy(m_pack.path, FS::PathCombine(m_stagingPath, "minecraft")); folderCopy.followSymlinks(true); return folderCopy(); }); @@ -55,6 +55,7 @@ void PackInstallTask::copySettings() instanceSettings->suspendSave(); MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); instance.settings()->set("InstanceType", "OneSix"); + instance.settings()->set("totalTimePlayed", m_pack.totalPlayTime / 1000); if (m_pack.jvmArgs.isValid() && !m_pack.jvmArgs.toString().isEmpty()) { instance.settings()->set("OverrideJavaArgs", true); diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp index 8f1a6e2ff..a0beeddcc 100644 --- a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp @@ -74,6 +74,7 @@ void PackFetchTask::fetchPrivate(const QStringList& toFetch) auto data = std::make_shared(); NetJob* job = new NetJob("Fetching private pack", m_network); job->addNetAction(Net::ApiDownload::makeByteArray(privatePackBaseUrl.arg(packCode), data)); + job->setAskRetry(false); QObject::connect(job, &NetJob::succeeded, this, [this, job, data, packCode] { ModpackList packs; diff --git a/launcher/modplatform/legacy_ftb/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 091296751..d6252663f 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -137,8 +137,8 @@ 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")) { - emitFailed(tr("Failed to move unzipped Minecraft!")); + if (!FS::move(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/minecraft")) { + emitFailed(tr("Failed to move unpacked Minecraft!")); return; } } @@ -155,7 +155,7 @@ void PackInstallTask::install() bool fallback = true; // handle different versions - QFile packJson(m_stagingPath + "/.minecraft/pack.json"); + QFile packJson(m_stagingPath + "/minecraft/pack.json"); QDir jarmodDir = QDir(m_stagingPath + "/unzip/instMods"); if (packJson.exists()) { packJson.open(QIODevice::ReadOnly | QIODevice::Text); diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp index 9777c2cfd..bdef1a0e5 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.cpp +++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp @@ -34,7 +34,7 @@ Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_f auto body_raw = body.toJson(); netJob->addNetAction(Net::ApiUpload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files"), response, body_raw)); - + netJob->setAskRetry(false); return netJob; } @@ -54,7 +54,7 @@ Task::Ptr ModrinthAPI::latestVersion(QString hash, if (mcVersions.has_value()) { QStringList game_versions; for (auto& ver : mcVersions.value()) { - game_versions.append(ver.toString()); + game_versions.append(mapMCVersionToModrinth(ver)); } Json::writeStringList(body_obj, "game_versions", game_versions); } @@ -87,7 +87,7 @@ Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes, if (mcVersions.has_value()) { QStringList game_versions; for (auto& ver : mcVersions.value()) { - game_versions.append(ver.toString()); + game_versions.append(mapMCVersionToModrinth(ver)); } Json::writeStringList(body_obj, "game_versions", game_versions); } @@ -120,3 +120,46 @@ 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::loadCategories(std::shared_ptr response, QString projectType) +{ + 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", "") == projectType) + 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; +} + +QList ModrinthAPI::loadModCategories(std::shared_ptr response) +{ + return loadCategories(response, "mod"); +}; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index d0f0811b2..a0e7efb08 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -30,6 +30,10 @@ class ModrinthAPI : public NetworkResourceAPI { Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const override; + static Task::Ptr getModCategories(std::shared_ptr response); + static QList loadCategories(std::shared_ptr response, QString projectType); + static QList loadModCategories(std::shared_ptr response); + public: [[nodiscard]] auto getSortingMethods() const -> QList override; @@ -41,7 +45,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 +60,44 @@ 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()) { + return {}; + } + if (side == "both") + return QString("\"client_side:required\"],[\"server_side:required\""); + if (side == "client") + return QString("\"client_side:required\",\"client_side:optional\"],[\"server_side:optional\",\"server_side:unsupported\""); + if (side == "server") + return QString("\"server_side:required\",\"server_side:optional\"],[\"client_side:optional\",\"client_side:unsupported\""); + return {}; + } + + [[nodiscard]] static inline QString mapMCVersionFromModrinth(QString v) + { + static const QString preString = " Pre-Release "; + bool pre = false; + if (v.contains("-pre")) { + pre = true; + v.replace("-pre", preString); + } + v.replace("-", " "); + if (pre) { + v.replace(" Pre Release ", preString); + } + return v; + } + private: [[nodiscard]] static QString resourceTypeParameter(ModPlatform::ResourceType type) { @@ -66,6 +108,8 @@ class ModrinthAPI : public NetworkResourceAPI { return "resourcepack"; case ModPlatform::ResourceType::SHADER_PACK: return "shader"; + case ModPlatform::ResourceType::MODPACK: + return "modpack"; default: qWarning() << "Invalid resource type for Modrinth API!"; break; @@ -73,14 +117,25 @@ class ModrinthAPI : public NetworkResourceAPI { return ""; } + [[nodiscard]] QString createFacets(SearchArgs const& args) const { QStringList facets_list; - if (args.loaders.has_value()) + if (args.loaders.has_value() && args.loaders.value() != 0) facets_list.append(QString("[%1]").arg(getModLoaderFilters(args.loaders.value()))); - if (args.versions.has_value()) + if (args.versions.has_value() && !args.versions.value().empty()) facets_list.append(QString("[%1]").arg(getGameVersionsArray(args.versions.value()))); + if (args.side.has_value()) { + auto side = getSideFilters(args.side.value()); + 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()))); + if (args.openSource) + facets_list.append("[\"open_source:true\"]"); + facets_list.append(QString("[\"project_type:%1\"]").arg(resourceTypeParameter(args.type))); return QString("[%1]").arg(facets_list.join(',')); @@ -89,7 +144,7 @@ class ModrinthAPI : public NetworkResourceAPI { public: [[nodiscard]] inline auto getSearchURL(SearchArgs const& args) const -> std::optional override { - if (args.loaders.has_value()) { + if (args.loaders.has_value() && args.loaders.value() != 0) { if (!validateModLoaders(args.loaders.value())) { qWarning() << "Modrinth - or our interface - does not support any the provided mod loaders!"; return {}; @@ -130,11 +185,11 @@ class ModrinthAPI : public NetworkResourceAPI { .arg(BuildConfig.MODRINTH_PROD_URL, args.pack.addonId.toString(), get_arguments.isEmpty() ? "" : "?", get_arguments.join('&')); }; - auto getGameVersionsArray(std::list mcVersions) const -> QString + QString getGameVersionsArray(std::list mcVersions) const { QString s; for (auto& ver : mcVersions) { - s += QString("\"versions:%1\",").arg(ver.toString()); + s += QString("\"versions:%1\",").arg(mapMCVersionToModrinth(ver)); } s.remove(s.length() - 1, 1); // remove last comma return s.isEmpty() ? QString() : s; @@ -151,7 +206,7 @@ class ModrinthAPI : public NetworkResourceAPI { : QString("%1/project/%2/version?game_versions=[\"%3\"]&loaders=[\"%4\"]") .arg(BuildConfig.MODRINTH_PROD_URL) .arg(args.dependency.addonId.toString()) - .arg(args.mcVersion.toString()) + .arg(mapMCVersionToModrinth(args.mcVersion)) .arg(getModLoaderStrings(args.loader).join("\",\"")); }; }; diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index 9b7c53854..aa371f280 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -1,24 +1,24 @@ #include "ModrinthCheckUpdate.h" +#include "Application.h" #include "ModrinthAPI.h" #include "ModrinthPackIndex.h" #include "Json.h" +#include "QObjectPtr.h" #include "ResourceDownloadTask.h" +#include "modplatform/ModIndex.h" #include "modplatform/helpers/HashUtils.h" #include "tasks/ConcurrentTask.h" -#include "minecraft/mod/ModFolderModel.h" - static ModrinthAPI api; -static ModPlatform::ProviderCapabilities ProviderCaps; bool ModrinthCheckUpdate::abort() { - if (m_net_job) - return m_net_job->abort(); + if (m_job) + return m_job->abort(); return true; } @@ -29,157 +29,167 @@ bool ModrinthCheckUpdate::abort() * */ void ModrinthCheckUpdate::executeTask() { - setStatus(tr("Preparing mods for Modrinth...")); - setProgress(0, 3); + setStatus(tr("Preparing resources for Modrinth...")); + setProgress(0, (m_loaders_list.isEmpty() ? 1 : m_loaders_list.length()) * 2 + 1); - QHash mappings; - - // Create all hashes - QStringList hashes; - auto best_hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first(); - - ConcurrentTask hashing_task(this, "MakeModrinthHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); - for (auto* mod : m_mods) { - if (!mod->enabled()) { - emit checkFailed(mod, tr("Disabled mods won't be updated, to prevent mod duplication issues!")); - continue; - } - - auto hash = mod->metadata()->hash; + auto hashing_task = + makeShared("MakeModrinthHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); + for (auto* resource : m_resources) { + 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 (mod->metadata()->hash_format != best_hash_type) { - auto hash_task = Hashing::createModrinthHasher(mod->fileinfo().absoluteFilePath()); - connect(hash_task.get(), &Hashing::Hasher::resultsReady, [&hashes, &mappings, mod](QString hash) { - hashes.append(hash); - mappings.insert(hash, mod); - }); + 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, mod); + 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(); +} + +void ModrinthCheckUpdate::getUpdateModsForLoader(std::optional loader) +{ + setStatus(tr("Waiting for the API response from Modrinth...")); + setProgress(m_progress + 1, m_progressTotal); auto response = std::make_shared(); - auto job = api.latestVersions(hashes, best_hash_type, m_game_versions, m_loaders, response); + QStringList hashes = m_mappings.keys(); + auto job = api.latestVersions(hashes, m_hash_type, m_game_versions, loader, response); - QEventLoop lock; + connect(job.get(), &Task::succeeded, this, [this, response, loader] { checkVersionsResponse(response, loader); }); - 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; + connect(job.get(), &Task::failed, this, &ModrinthCheckUpdate::checkNextLoader); - failed(parse_error.errorString()); - return; - } - - setStatus(tr("Parsing the API response from Modrinth...")); - setProgress(2, 3); - - try { - for (auto hash : mappings.keys()) { - auto project_obj = doc[hash].toObject(); - - // 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 " << mappings.find(hash).value()->name() << " got an empty response."; - qDebug() << "Hash: " << hash; - - emit checkFailed( - mappings.find(hash).value(), - tr("No valid version found for this mod. It's probably unavailable for the current game version / mod loader.")); - - 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 }; - 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 mod without download url!"; - qCritical() << project_ver.fileName; - - emit checkFailed(mappings.find(hash).value(), tr("Mod has an empty download URL")); - - continue; - } - - auto mod_iter = mappings.find(hash); - if (mod_iter == mappings.end()) { - qCritical() << "Failed to remap mod from Modrinth!"; - continue; - } - auto mod = *mod_iter; - - auto key = project_ver.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->websiteUrl = mod->homeurl(); - for (auto& author : mod->authors()) - pack->authors.append({ author }); - pack->description = mod->description(); - pack->provider = ModPlatform::ResourceProvider::MODRINTH; - if ((key != hash && project_ver.is_preferred) || (mod->status() == ModStatus::NotInstalled)) { - if (mod->version() == project_ver.version_number) - continue; - - auto download_task = makeShared(pack, project_ver, m_mods_folder); - - m_updatable.emplace_back(pack->name, hash, mod->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) { - failed(e.cause() + " : " + e.what()); - } - }); - - connect(job.get(), &Task::finished, &lock, &QEventLoop::quit); - - setStatus(tr("Waiting for the API response from Modrinth...")); - setProgress(1, 3); - - m_net_job = qSharedPointerObjectCast(job); + m_job = job; job->start(); +} - lock.exec(); +void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr response, std::optional loader) +{ + setStatus(tr("Parsing the API response from Modrinth...")); + setProgress(m_progress + 1, m_progressTotal); + + 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; + } + + try { + auto iter = m_mappings.begin(); + + while (iter != m_mappings.end()) { + const QString hash = iter.key(); + Resource* resource = iter.value(); + + auto project_obj = doc[hash].toObject(); + + // 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; + ++iter; + 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 (loader.has_value()) { + for (auto flag : ModPlatform::modLoaderTypesToList(*loader)) { + loader_filter = ModPlatform::getModLoaderAsString(flag); + break; + } + } + + // Currently, we rely on a couple heuristics to determine whether an update is actually available or not: + // - 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; + ++iter; + 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, resource->enabled()); + } + m_deps.append(std::make_shared(pack, project_ver)); + + iter = m_mappings.erase(iter); + } + } catch (Json::JsonException& e) { + emitFailed(e.cause() + ": " + e.what()); + return; + } + checkNextLoader(); +} + +void ModrinthCheckUpdate::checkNextLoader() +{ + if (m_mappings.isEmpty()) { + emitSucceeded(); + return; + } + + if (m_loaders_list.isEmpty() && m_loader_idx == 0) { + getUpdateModsForLoader({}); + m_loader_idx++; + return; + } + + if (m_loader_idx < m_loaders_list.size()) { + getUpdateModsForLoader(m_loaders_list.at(m_loader_idx)); + m_loader_idx++; + return; + } + + for (auto resource : m_mappings) { + 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."); + + emit checkFailed(resource, reason); + } emitSucceeded(); } diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h index f2f2c7e92..204b24784 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& mods, + ModrinthCheckUpdate(QList& resources, std::list& mcVersions, - std::optional loaders, - std::shared_ptr mods_folder) - : CheckUpdateTask(mods, mcVersions, loaders, mods_folder) + QList loadersList, + std::shared_ptr resourceModel) + : CheckUpdateTask(resources, mcVersions, std::move(loadersList), std::move(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(std::optional loader); + void checkVersionsResponse(std::shared_ptr response, std::optional loader); + void checkNextLoader(); private: - NetJob::Ptr m_net_job = nullptr; + Task::Ptr m_job = nullptr; + QHash m_mappings; + QString m_hash_type; + int m_loader_idx = 0; }; diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index e732ad39c..2cc8767a4 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" @@ -20,6 +24,8 @@ #include "ui/pages/modplatform/OptionalModDialog.h" #include +#include +#include #include bool ModrinthCreationTask::abort() @@ -28,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(); } @@ -58,6 +64,7 @@ bool ModrinthCreationTask::updateInstance() return false; auto version_name = inst->getManagedPackVersionName(); + m_root_path = QFileInfo(inst->gameRoot()).fileName(); auto version_str = !version_name.isEmpty() ? tr(" (version %1)").arg(version_name) : ""; if (shouldConfirmUpdate()) { @@ -129,7 +136,7 @@ bool ModrinthCreationTask::updateInstance() } auto old_client_overrides = Override::readOverrides("client-overrides", old_index_folder); - for (const auto& entry : old_overrides) { + for (const auto& entry : old_client_overrides) { if (entry.isEmpty()) continue; qDebug() << "Scheduling" << entry << "for removal"; @@ -171,9 +178,9 @@ 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, ".minecraft"); + auto mcPath = FS::PathCombine(m_stagingPath, m_root_path); auto override_path = FS::PathCombine(m_stagingPath, "overrides"); if (QFile::exists(override_path)) { @@ -181,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; } @@ -226,36 +233,51 @@ bool ModrinthCreationTask::createInstance() // Don't add managed info to packs without an ID (most likely imported from ZIP) if (!m_managed_id.isEmpty()) instance.setManagedPack("modrinth", m_managed_id, m_managed_name, m_managed_version_id, version()); + else + instance.setManagedPack("modrinth", "", name(), "", ""); + 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, ".minecraft"); + 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; + } + if (file.downloads.empty()) { + setError(tr("The file '%1' is missing a download link. This is invalid in the pack format.").arg(fileName)); return false; } - qDebug() << "Will try to download" << file.downloads.front() << "to" << file_path; auto dl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path); dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); - m_files_job->addNetAction(dl); - + downloadMods->addNetAction(dl); if (!file.downloads.empty()) { // FIXME: This really needs to be put into a ConcurrentTask of // MultipleOptionsTask's , once those exist :) auto param = dl.toWeakRef(); - connect(dl.get(), &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(); }); @@ -264,23 +286,51 @@ bool ModrinthCreationTask::createInstance() bool ended_well = false; - connect(m_files_job.get(), &NetJob::succeeded, this, [&]() { ended_well = true; }); - connect(m_files_job.get(), &NetJob::failed, [&](const QString& reason) { + connect(downloadMods.get(), &NetJob::succeeded, this, [&ended_well]() { ended_well = true; }); + connect(downloadMods.get(), &NetJob::failed, [this, &ended_well](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, [this](qint64 current, qint64 total) { setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); setProgress(current, total); }); - connect(m_files_job.get(), &NetJob::stepProgress, this, &ModrinthCreationTask::propagateStepProgress); + connect(downloadMods.get(), &NetJob::stepProgress, this, &ModrinthCreationTask::propagateStepProgress); setStatus(tr("Downloading mods...")); - m_files_job->start(); + downloadMods->start(); + m_task = downloadMods; loop.exec(); + if (!ended_well) { + for (auto resource : resources) { + delete resource; + } + return ended_well; + } + + 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]() { ended_well = true; }); + connect(ensureMetadataTask.get(), &Task::finished, &ensureMetaLoop, &QEventLoop::quit); + connect(ensureMetadataTask.get(), &Task::progress, [this](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 resource : resources) { + delete resource; + } + resources.clear(); + // Update information of the already installed instance, if any. if (m_instance && ended_well) { setAbortable(false); @@ -289,7 +339,7 @@ bool ModrinthCreationTask::createInstance() // Only change the name if it didn't use a custom name, so that the previous custom name // is preserved, but if we're using the original one, we update the version string. // NOTE: This needs to come before the copyManagedPack call! - if (inst->name().contains(inst->getManagedPackVersionName())) { + if (inst->name().contains(inst->getManagedPackVersionName()) && inst->name() != instance.name()) { if (askForChangingInstanceName(m_parent, inst->name(), instance.name()) == InstanceNameChange::ShouldChange) inst->setName(instance.name()); } @@ -339,23 +389,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 1bd5b7de9..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,9 @@ 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; + + QString m_root_path = "minecraft"; }; diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp index e9e8a3b75..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,23 +116,21 @@ 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; }); modIter != allMods.end()) { const Mod* mod = *modIter; if (mod->metadata() != nullptr) { - QUrl& url = mod->metadata()->url; + const QUrl& url = mod->metadata()->url; // ensure the url is permitted on modrinth.com 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()); @@ -287,16 +286,12 @@ QByteArray ModrinthPackExportTask::generateIndex() env["client"] = "required"; env["server"] = "required"; } - switch (iterator->side) { - case Metadata::ModSide::ClientSide: - env["server"] = "unsupported"; - break; - case Metadata::ModSide::ServerSide: - env["client"] = "unsupported"; - break; - case Metadata::ModSide::UniversalSide: - break; - } + + // a server side mod does not imply that the mod does not work on the client + // however, if a mrpack mod is marked as server-only it will not install on the client + if (iterator->side == Metadata::ModSide::ClientSide) + env["server"] = "unsupported"; + fileOut["env"] = env; fileOut["path"] = path; diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.h b/launcher/modplatform/modrinth/ModrinthPackExportTask.h index 81c2f25bf..ee740a456 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.h +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.h @@ -27,6 +27,7 @@ #include "tasks/Task.h" class ModrinthPackExportTask : public Task { + Q_OBJECT public: ModrinthPackExportTask(const QString& name, const QString& version, diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 7d0893261..72550937c 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" @@ -25,7 +27,6 @@ #include "modplatform/ModIndex.h" static ModrinthAPI api; -static ModPlatform::ProviderCapabilities ProviderCaps; bool shouldDownloadOnSide(QString side) { @@ -104,6 +105,8 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob pack.extraData.donate.append(donate); } + pack.extraData.status = Json::ensureString(obj, "status"); + pack.extraData.body = Json::ensureString(obj, "body").remove("
    "); pack.extraDataLoaded = true; @@ -112,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 { @@ -133,8 +131,7 @@ 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 +ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_type, QString preferred_file_name) { ModPlatform::IndexedVersion file; @@ -146,21 +143,21 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t return {}; } for (auto mcVer : versionArray) { - file.mcVersion.append(mcVer.toString()); + file.mcVersion.append(ModrinthAPI::mapMCVersionFromModrinth(mcVer.toString())); } auto loaders = Json::requireArray(obj, "loaders"); 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"); @@ -224,6 +221,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"); @@ -231,7 +229,7 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t file.hash = Json::requireString(hash_list, preferred_hash_type); file.hash_type = preferred_hash_type; } else { - auto hash_types = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH); + auto hash_types = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH); for (auto& hash_type : hash_types) { if (hash_list.contains(hash_type)) { file.hash = Json::requireString(hash_list, hash_type); @@ -247,8 +245,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 +ModPlatform::IndexedVersion Modrinth::loadDependencyVersions([[maybe_unused]] const ModPlatform::Dependency& m, + QJsonArray& arr, + const BaseInstance* inst) { 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 d86f1c0e5..89ef6e4c4 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -40,9 +40,6 @@ #include "modplatform/modrinth/ModrinthAPI.h" -#include "minecraft/MinecraftInstance.h" -#include "minecraft/PackProfile.h" - #include static ModrinthAPI api; @@ -95,6 +92,8 @@ void loadIndexedInfo(Modpack& pack, QJsonObject& obj) pack.extra.donate.append(donate); } + pack.extra.status = Json::ensureString(obj, "status"); + pack.extraInfoLoaded = true; } @@ -129,6 +128,26 @@ 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.gameVersion = ModrinthAPI::mapMCVersionFromModrinth(file.gameVersion); + } + auto loaders = Json::requireArray(obj, "loaders"); + for (auto loader : loaders) { + if (loader == "neoforge") + file.loaders |= ModPlatform::NeoForge; + else if (loader == "forge") + file.loaders |= ModPlatform::Forge; + else if (loader == "cauldron") + file.loaders |= ModPlatform::Cauldron; + else if (loader == "liteloader") + file.loaders |= ModPlatform::LiteLoader; + else if (loader == "fabric") + file.loaders |= ModPlatform::Fabric; + else if (loader == "quilt") + file.loaders |= ModPlatform::Quilt; + } file.version_type = ModPlatform::IndexedVersionType(Json::requireString(obj, "version_type")); file.changelog = Json::ensureString(obj, "changelog"); diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index 6fc55a043..2e5e2da84 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -77,13 +77,17 @@ struct ModpackExtra { QString discordUrl; QList donate; + + QString status; }; struct ModpackVersion { QString name; QString version; + QString gameVersion; ModPlatform::IndexedVersionType version_type; QString changelog; + ModPlatform::ModLoaderTypes loaders = {}; QString id; QString project_id; diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index e35567f24..a3bb74399 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 @@ -34,7 +35,7 @@ namespace Packwiz { -auto getRealIndexName(QDir& index_dir, QString normalized_fname, bool should_find_match) -> QString +auto getRealIndexName(const QDir& index_dir, QString normalized_fname, bool should_find_match) -> QString { QFile index_file(index_dir.absoluteFilePath(normalized_fname)); @@ -66,14 +67,12 @@ static inline auto indexFileName(QString const& mod_slug) -> QString return QString("%1.pw.toml").arg(mod_slug); } -static ModPlatform::ProviderCapabilities ProviderCaps; - // Helper functions for extracting data from the TOML file auto stringEntry(toml::table table, QString entry_name) -> QString { auto node = table[StringUtils::toStdString(entry_name)]; if (!node) { - qCritical() << "Failed to read str property '" + entry_name + "' in mod metadata."; + qWarning() << "Failed to read str property '" + entry_name + "' in mod metadata."; return {}; } @@ -84,15 +83,16 @@ auto intEntry(toml::table table, QString entry_name) -> int { auto node = table[StringUtils::toStdString(entry_name)]; if (!node) { - qCritical() << "Failed to read int property '" + entry_name + "' in mod metadata."; + qWarning() << "Failed to read int property '" + entry_name + "' in mod metadata."; return {}; } return node.value_or(0); } -auto V1::createModFormat([[maybe_unused]] QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) - -> Mod +auto V1::createModFormat([[maybe_unused]] const QDir& index_dir, + ModPlatform::IndexedPack& mod_pack, + ModPlatform::IndexedVersion& mod_version) -> Mod { Mod mod; @@ -113,12 +113,20 @@ auto V1::createModFormat([[maybe_unused]] QDir& index_dir, ModPlatform::IndexedP 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 version number + mod.version_number = mod_version.version; return mod; } -auto V1::createModFormat(QDir& index_dir, [[maybe_unused]] ::Mod& internal_mod, QString slug) -> Mod +auto V1::createModFormat(const QDir& index_dir, [[maybe_unused]] ::Mod& internal_mod, QString slug) -> Mod { // Try getting metadata if it exists Mod mod{ getIndexForMod(index_dir, slug) }; @@ -130,7 +138,7 @@ auto V1::createModFormat(QDir& index_dir, [[maybe_unused]] ::Mod& internal_mod, return {}; } -void V1::updateModIndex(QDir& index_dir, Mod& mod) +void V1::updateModIndex(const QDir& index_dir, Mod& mod) { if (!mod.isValid()) { qCritical() << QString("Tried to update metadata of an invalid mod!"); @@ -181,6 +189,15 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) break; } + toml::array loaders; + for (auto loader : ModPlatform::modLoaderTypesToList(mod.loaders)) { + 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; @@ -192,6 +209,10 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) auto tbl = toml::table{ { "name", mod.name.toStdString() }, { "filename", mod.filename.toStdString() }, { "side", sideToString(mod.side).toStdString() }, + { "x-prismlauncher-loaders", loaders }, + { "x-prismlauncher-mc-versions", mcVersions }, + { "x-prismlauncher-release-type", mod.releaseType.toString().toStdString() }, + { "x-prismlauncher-version-number", mod.version_number.toStdString() }, { "download", toml::table{ { "mode", mod.mode.toStdString() }, @@ -199,7 +220,7 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) { "hash-format", mod.hash_format.toStdString() }, { "hash", mod.hash.toStdString() }, } }, - { "update", toml::table{ { ProviderCaps.name(mod.provider), update } } } }; + { "update", toml::table{ { ModPlatform::ProviderCapabilities::name(mod.provider), update } } } }; std::stringstream ss; ss << tbl; in_stream << QString::fromStdString(ss.str()); @@ -209,7 +230,7 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) index_file.close(); } -void V1::deleteModIndex(QDir& index_dir, QString& mod_slug) +void V1::deleteModIndex(const QDir& index_dir, QString& mod_slug) { auto normalized_fname = indexFileName(mod_slug); auto real_fname = getRealIndexName(index_dir, normalized_fname); @@ -228,7 +249,7 @@ void V1::deleteModIndex(QDir& index_dir, QString& mod_slug) } } -void V1::deleteModIndex(QDir& index_dir, QVariant& mod_id) +void V1::deleteModIndex(const QDir& index_dir, QVariant& mod_id) { for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) { auto mod = getIndexForMod(index_dir, file_name); @@ -240,7 +261,7 @@ void V1::deleteModIndex(QDir& index_dir, QVariant& mod_id) } } -auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod +auto V1::getIndexForMod(const QDir& index_dir, QString slug) -> Mod { Mod mod; @@ -276,7 +297,27 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod mod.name = stringEntry(table, "name"); mod.filename = stringEntry(table, "filename"); mod.side = stringToSide(stringEntry(table, "side")); + mod.releaseType = ModPlatform::IndexedVersionType(table["x-prismlauncher-release-type"].value_or("")); + if (auto loaders = table["x-prismlauncher-loaders"]; loaders && loaders.is_array()) { + for (auto&& loader : *loaders.as_array()) { + if (loader.is_string()) { + mod.loaders |= ModPlatform::getModLoaderFromString(QString::fromStdString(loader.as_string()->value_or(""))); + } + } + } + if (auto versions = table["x-prismlauncher-mc-versions"]; versions && versions.is_array()) { + for (auto&& version : *versions.as_array()) { + if (version.is_string()) { + auto ver = QString::fromStdString(version.as_string()->value_or("")); + if (!ver.isEmpty()) { + mod.mcVersions << ver; + } + } + } + mod.mcVersions.sort(); + } } + mod.version_number = table["x-prismlauncher-version-number"].value_or(""); { // [download] info auto download_table = table["download"].as_table(); @@ -301,11 +342,11 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod } toml::table* mod_provider_table = nullptr; - if ((mod_provider_table = update_table[ProviderCaps.name(Provider::FLAME)].as_table())) { + if ((mod_provider_table = update_table[ModPlatform::ProviderCapabilities::name(Provider::FLAME)].as_table())) { mod.provider = Provider::FLAME; mod.file_id = intEntry(*mod_provider_table, "file-id"); mod.project_id = intEntry(*mod_provider_table, "project-id"); - } else if ((mod_provider_table = update_table[ProviderCaps.name(Provider::MODRINTH)].as_table())) { + } else if ((mod_provider_table = update_table[ModPlatform::ProviderCapabilities::name(Provider::MODRINTH)].as_table())) { mod.provider = Provider::MODRINTH; mod.mod_id() = stringEntry(*mod_provider_table, "mod-id"); mod.version() = stringEntry(*mod_provider_table, "version"); @@ -318,7 +359,7 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod return mod; } -auto V1::getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod +auto V1::getIndexForMod(const QDir& index_dir, QVariant& mod_id) -> Mod { for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) { auto mod = getIndexForMod(index_dir, file_name); diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index dce198b0e..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 @@ -31,16 +32,21 @@ class Mod; namespace Packwiz { -auto getRealIndexName(QDir& index_dir, QString normalized_index_name, bool should_match = false) -> QString; +auto getRealIndexName(const QDir& index_dir, QString normalized_index_name, bool should_match = false) -> QString; class V1 { public: enum class Side { ClientSide = 1 << 0, ServerSide = 1 << 1, UniversalSide = ClientSide | ServerSide }; + + // can also represent other resources beside loader mods - but this is what packwiz calls it struct Mod { QString slug{}; QString name{}; QString filename{}; Side side{ Side::UniversalSide }; + ModPlatform::ModLoaderTypes loaders; + QStringList mcVersions; + ModPlatform::IndexedVersionType releaseType; // [download] QString mode{}; @@ -52,6 +58,7 @@ class V1 { ModPlatform::ResourceProvider provider{}; QVariant file_id{}; QVariant project_id{}; + QString version_number{}; public: // This is a totally heuristic, but should work for now. @@ -66,33 +73,33 @@ class V1 { /* Generates the object representing the information in a mod.pw.toml file via * its common representation in the launcher, when downloading mods. * */ - static auto createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod; + static auto createModFormat(const QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod; /* Generates the object representing the information in a mod.pw.toml file via * its common representation in the launcher, plus a necessary slug. * */ - static auto createModFormat(QDir& index_dir, ::Mod& internal_mod, QString slug) -> Mod; + static auto createModFormat(const QDir& index_dir, ::Mod& internal_mod, QString slug) -> Mod; /* Updates the mod index for the provided mod. * This creates a new index if one does not exist already * TODO: Ask the user if they want to override, and delete the old mod's files, or keep the old one. * */ - static void updateModIndex(QDir& index_dir, Mod& mod); + static void updateModIndex(const QDir& index_dir, Mod& mod); /* Deletes the metadata for the mod with the given slug. If the metadata doesn't exist, it does nothing. */ - static void deleteModIndex(QDir& index_dir, QString& mod_slug); + static void deleteModIndex(const QDir& index_dir, QString& mod_slug); /* Deletes the metadata for the mod with the given id. If the metadata doesn't exist, it does nothing. */ - static void deleteModIndex(QDir& index_dir, QVariant& mod_id); + static void deleteModIndex(const QDir& index_dir, QVariant& mod_id); /* Gets the metadata for a mod with a particular file name. * If the mod doesn't have a metadata, it simply returns an empty Mod object. * */ - static auto getIndexForMod(QDir& index_dir, QString slug) -> Mod; + static auto getIndexForMod(const QDir& index_dir, QString slug) -> Mod; /* Gets the metadata for a mod with a particular id. * If the mod doesn't have a metadata, it simply returns an empty Mod object. * */ - static auto getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod; + static auto getIndexForMod(const QDir& index_dir, QVariant& mod_id) -> Mod; static auto sideToString(Side side) -> QString; static auto stringToSide(QString side) -> Side; diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.cpp b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp index ad08d72d4..cc9ced10b 100644 --- a/launcher/modplatform/technic/SingleZipPackInstallTask.cpp +++ b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp @@ -62,7 +62,7 @@ void Technic::SingleZipPackInstallTask::downloadSucceeded() m_abortable = false; setStatus(tr("Extracting modpack")); - QDir extractDir(FS::PathCombine(m_stagingPath, ".minecraft")); + QDir extractDir(FS::PathCombine(m_stagingPath, "minecraft")); qDebug() << "Attempting to create instance from" << m_archivePath; // open the zip and find relevant files in it diff --git a/launcher/modplatform/technic/SolderPackInstallTask.cpp b/launcher/modplatform/technic/SolderPackInstallTask.cpp index c162d6253..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); @@ -140,7 +139,7 @@ void Technic::SolderPackInstallTask::downloadSucceeded() m_filesNetJob.reset(); m_extractFuture = QtConcurrent::run([this]() { int i = 0; - QString extractDir = FS::PathCombine(m_stagingPath, ".minecraft"); + QString extractDir = FS::PathCombine(m_stagingPath, "minecraft"); FS::ensureFolderPathExists(extractDir); while (m_modCount > i) { diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp index 3b9424bf8..9050e14d8 100644 --- a/launcher/modplatform/technic/TechnicPackProcessor.cpp +++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp @@ -33,7 +33,7 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const QString& minecraftVersion, [[maybe_unused]] const bool isSolder) { - QString minecraftPath = FS::PathCombine(stagingPath, ".minecraft"); + QString minecraftPath = FS::PathCombine(stagingPath, "minecraft"); QString configPath = FS::PathCombine(stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared(configPath); MinecraftInstance instance(globalSettings, instanceSettings, stagingPath); @@ -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.cpp b/launcher/net/FileSink.cpp index 1ecb21fdf..95c1a8f44 100644 --- a/launcher/net/FileSink.cpp +++ b/launcher/net/FileSink.cpp @@ -55,7 +55,7 @@ Task::State FileSink::init(QNetworkRequest& request) } wroteAnyData = false; - m_output_file.reset(new QSaveFile(m_filename)); + m_output_file.reset(new PSaveFile(m_filename)); if (!m_output_file->open(QIODevice::WriteOnly)) { qCCritical(taskNetLogC) << "Could not open " + m_filename + " for writing"; return Task::State::Failed; diff --git a/launcher/net/FileSink.h b/launcher/net/FileSink.h index 40134b5f4..272f8ddc3 100644 --- a/launcher/net/FileSink.h +++ b/launcher/net/FileSink.h @@ -35,14 +35,13 @@ #pragma once -#include - +#include "PSaveFile.h" #include "Sink.h" namespace Net { class FileSink : public Sink { public: - FileSink(QString filename) : m_filename(filename){}; + FileSink(QString filename) : m_filename(filename) {}; virtual ~FileSink() = default; public: @@ -60,6 +59,6 @@ class FileSink : public Sink { protected: QString m_filename; bool wroteAnyData = false; - std::unique_ptr m_output_file; + std::unique_ptr m_output_file; }; } // namespace Net diff --git a/launcher/net/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 784d81c37..335e360b2 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -36,18 +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(job_name), m_network(network) { #if defined(LAUNCHER_APPLICATION) - setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + if (APPLICATION_DYN && 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); @@ -56,18 +64,18 @@ auto NetJob::addNetAction(NetAction::Ptr action) -> bool return true; } -void NetJob::startNext() +void NetJob::executeNextSubTask() { - if (m_queue.isEmpty() && m_doing.isEmpty()) { - // We're finished, check for failures and retry if we can (up to 3 times) - if (!m_failed.isEmpty() && m_try < 3) { - m_try += 1; - while (!m_failed.isEmpty()) - m_queue.enqueue(m_failed.take(*m_failed.keyBegin())); + // 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()) { + auto task = m_failed.take(*m_failed.keyBegin()); + m_done.remove(task.get()); + m_queue.enqueue(task); } } - - ConcurrentTask::startNext(); + ConcurrentTask::executeNextSubTask(); } auto NetJob::size() const -> int @@ -113,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; } @@ -126,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; } @@ -137,3 +145,46 @@ 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 (APPLICATION_DYN && m_ask_retry && m_manual_try < APPLICATION->settings()->get("NumberOfManualRetries").toInt() && isOnline()) { + m_manual_try++; + auto response = CustomMessageBox::selectable(nullptr, "Confirm retry", + "The tasks failed.\n" + "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 1c4337ec6..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,28 +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; - void startNext() override; - 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/resources/assets/underconstruction.png b/launcher/resources/assets/underconstruction.png index 6ae06476e..5f2fdf9e4 100644 Binary files a/launcher/resources/assets/underconstruction.png and b/launcher/resources/assets/underconstruction.png differ diff --git a/launcher/resources/backgrounds/kitteh-bday.png b/launcher/resources/backgrounds/kitteh-bday.png index 09a365669..f4a7bbc1f 100644 Binary files a/launcher/resources/backgrounds/kitteh-bday.png and b/launcher/resources/backgrounds/kitteh-bday.png differ diff --git a/launcher/resources/backgrounds/kitteh-spooky.png b/launcher/resources/backgrounds/kitteh-spooky.png index deb0bebbe..bb3765f92 100644 Binary files a/launcher/resources/backgrounds/kitteh-spooky.png and b/launcher/resources/backgrounds/kitteh-spooky.png differ diff --git a/launcher/resources/backgrounds/kitteh-xmas.png b/launcher/resources/backgrounds/kitteh-xmas.png index 8bdb1d5c8..1e92e9081 100644 Binary files a/launcher/resources/backgrounds/kitteh-xmas.png and b/launcher/resources/backgrounds/kitteh-xmas.png differ diff --git a/launcher/resources/backgrounds/kitteh.png b/launcher/resources/backgrounds/kitteh.png index e9de7f27c..fa3d52548 100644 Binary files a/launcher/resources/backgrounds/kitteh.png and b/launcher/resources/backgrounds/kitteh.png differ diff --git a/launcher/resources/backgrounds/rory-bday.png b/launcher/resources/backgrounds/rory-bday.png index 66b880948..8c796927c 100644 Binary files a/launcher/resources/backgrounds/rory-bday.png and b/launcher/resources/backgrounds/rory-bday.png differ diff --git a/launcher/resources/backgrounds/rory-flat-bday.png b/launcher/resources/backgrounds/rory-flat-bday.png index 8a6e366db..94c4509a4 100644 Binary files a/launcher/resources/backgrounds/rory-flat-bday.png and b/launcher/resources/backgrounds/rory-flat-bday.png differ diff --git a/launcher/resources/backgrounds/rory-flat-spooky.png b/launcher/resources/backgrounds/rory-flat-spooky.png index 6360c612f..4a0046c2b 100644 Binary files a/launcher/resources/backgrounds/rory-flat-spooky.png and b/launcher/resources/backgrounds/rory-flat-spooky.png differ diff --git a/launcher/resources/backgrounds/rory-flat-xmas.png b/launcher/resources/backgrounds/rory-flat-xmas.png index 96c3ae381..e6278ed5c 100644 Binary files a/launcher/resources/backgrounds/rory-flat-xmas.png and b/launcher/resources/backgrounds/rory-flat-xmas.png differ diff --git a/launcher/resources/backgrounds/rory-flat.png b/launcher/resources/backgrounds/rory-flat.png index ccec0662b..22fe61887 100644 Binary files a/launcher/resources/backgrounds/rory-flat.png and b/launcher/resources/backgrounds/rory-flat.png differ diff --git a/launcher/resources/backgrounds/rory-spooky.png b/launcher/resources/backgrounds/rory-spooky.png index a727619b4..1aa928671 100644 Binary files a/launcher/resources/backgrounds/rory-spooky.png and b/launcher/resources/backgrounds/rory-spooky.png differ diff --git a/launcher/resources/backgrounds/rory-xmas.png b/launcher/resources/backgrounds/rory-xmas.png index 107feb780..f33e92666 100644 Binary files a/launcher/resources/backgrounds/rory-xmas.png and b/launcher/resources/backgrounds/rory-xmas.png differ diff --git a/launcher/resources/backgrounds/rory.png b/launcher/resources/backgrounds/rory.png index 577f4dce9..5570499c2 100644 Binary files a/launcher/resources/backgrounds/rory.png and b/launcher/resources/backgrounds/rory.png differ diff --git a/launcher/resources/backgrounds/teawie-bday.png b/launcher/resources/backgrounds/teawie-bday.png index f4ecf247c..b4621f9b5 100644 Binary files a/launcher/resources/backgrounds/teawie-bday.png and b/launcher/resources/backgrounds/teawie-bday.png differ diff --git a/launcher/resources/backgrounds/teawie-spooky.png b/launcher/resources/backgrounds/teawie-spooky.png index cefc6c855..194d8ab7c 100644 Binary files a/launcher/resources/backgrounds/teawie-spooky.png and b/launcher/resources/backgrounds/teawie-spooky.png differ diff --git a/launcher/resources/backgrounds/teawie-xmas.png b/launcher/resources/backgrounds/teawie-xmas.png index 55fb7cfc6..54a09ae51 100644 Binary files a/launcher/resources/backgrounds/teawie-xmas.png and b/launcher/resources/backgrounds/teawie-xmas.png differ diff --git a/launcher/resources/backgrounds/teawie.png b/launcher/resources/backgrounds/teawie.png index dc32c51f9..99b60ad1e 100644 Binary files a/launcher/resources/backgrounds/teawie.png and b/launcher/resources/backgrounds/teawie.png differ 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/128x128/instances/chicken_legacy.png b/launcher/resources/multimc/128x128/instances/chicken_legacy.png index 71f6dedc5..b4945d75a 100644 Binary files a/launcher/resources/multimc/128x128/instances/chicken_legacy.png and b/launcher/resources/multimc/128x128/instances/chicken_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/creeper_legacy.png b/launcher/resources/multimc/128x128/instances/creeper_legacy.png index 41b7d07db..92d923132 100644 Binary files a/launcher/resources/multimc/128x128/instances/creeper_legacy.png and b/launcher/resources/multimc/128x128/instances/creeper_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/enderpearl_legacy.png b/launcher/resources/multimc/128x128/instances/enderpearl_legacy.png index 0a5bf91a4..fd910da47 100644 Binary files a/launcher/resources/multimc/128x128/instances/enderpearl_legacy.png and b/launcher/resources/multimc/128x128/instances/enderpearl_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/flame_legacy.png b/launcher/resources/multimc/128x128/instances/flame_legacy.png index 6482975c4..3dd8500c6 100644 Binary files a/launcher/resources/multimc/128x128/instances/flame_legacy.png and b/launcher/resources/multimc/128x128/instances/flame_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/forge.png b/launcher/resources/multimc/128x128/instances/forge.png index d8ff79a53..10c5f8d6b 100644 Binary files a/launcher/resources/multimc/128x128/instances/forge.png and b/launcher/resources/multimc/128x128/instances/forge.png differ diff --git a/launcher/resources/multimc/128x128/instances/ftb_glow.png b/launcher/resources/multimc/128x128/instances/ftb_glow.png index 86632b21d..a8bfbbb96 100644 Binary files a/launcher/resources/multimc/128x128/instances/ftb_glow.png and b/launcher/resources/multimc/128x128/instances/ftb_glow.png differ diff --git a/launcher/resources/multimc/128x128/instances/ftb_logo_legacy.png b/launcher/resources/multimc/128x128/instances/ftb_logo_legacy.png index e725b7fe4..01aa4d517 100644 Binary files a/launcher/resources/multimc/128x128/instances/ftb_logo_legacy.png and b/launcher/resources/multimc/128x128/instances/ftb_logo_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/gear_legacy.png b/launcher/resources/multimc/128x128/instances/gear_legacy.png index 75c68a66f..bb46fe026 100644 Binary files a/launcher/resources/multimc/128x128/instances/gear_legacy.png and b/launcher/resources/multimc/128x128/instances/gear_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/herobrine_legacy.png b/launcher/resources/multimc/128x128/instances/herobrine_legacy.png index 13f1494c4..d25d1b1b1 100644 Binary files a/launcher/resources/multimc/128x128/instances/herobrine_legacy.png and b/launcher/resources/multimc/128x128/instances/herobrine_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/infinity_legacy.png b/launcher/resources/multimc/128x128/instances/infinity_legacy.png index 63e06e5b3..322ab4361 100644 Binary files a/launcher/resources/multimc/128x128/instances/infinity_legacy.png and b/launcher/resources/multimc/128x128/instances/infinity_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/liteloader.png b/launcher/resources/multimc/128x128/instances/liteloader.png index 646217de0..acd977d7e 100644 Binary files a/launcher/resources/multimc/128x128/instances/liteloader.png and b/launcher/resources/multimc/128x128/instances/liteloader.png differ diff --git a/launcher/resources/multimc/128x128/instances/magitech_legacy.png b/launcher/resources/multimc/128x128/instances/magitech_legacy.png index 0f81a1997..c83d0c948 100644 Binary files a/launcher/resources/multimc/128x128/instances/magitech_legacy.png and b/launcher/resources/multimc/128x128/instances/magitech_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/meat_legacy.png b/launcher/resources/multimc/128x128/instances/meat_legacy.png index fefc9bf11..14a50bec0 100644 Binary files a/launcher/resources/multimc/128x128/instances/meat_legacy.png and b/launcher/resources/multimc/128x128/instances/meat_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/netherstar_legacy.png b/launcher/resources/multimc/128x128/instances/netherstar_legacy.png index 132085f02..86cc87b4a 100644 Binary files a/launcher/resources/multimc/128x128/instances/netherstar_legacy.png and b/launcher/resources/multimc/128x128/instances/netherstar_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/skeleton_legacy.png b/launcher/resources/multimc/128x128/instances/skeleton_legacy.png index 55fcf5a99..416ca66e0 100644 Binary files a/launcher/resources/multimc/128x128/instances/skeleton_legacy.png and b/launcher/resources/multimc/128x128/instances/skeleton_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/squarecreeper_legacy.png b/launcher/resources/multimc/128x128/instances/squarecreeper_legacy.png index c82d8406d..b7e2bdc13 100644 Binary files a/launcher/resources/multimc/128x128/instances/squarecreeper_legacy.png and b/launcher/resources/multimc/128x128/instances/squarecreeper_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/steve_legacy.png b/launcher/resources/multimc/128x128/instances/steve_legacy.png index a07cbd2f9..afe8aaf46 100644 Binary files a/launcher/resources/multimc/128x128/instances/steve_legacy.png and b/launcher/resources/multimc/128x128/instances/steve_legacy.png differ diff --git a/launcher/resources/multimc/128x128/shaderpacks.png b/launcher/resources/multimc/128x128/shaderpacks.png index 1de0e9169..d2f1c0328 100644 Binary files a/launcher/resources/multimc/128x128/shaderpacks.png and b/launcher/resources/multimc/128x128/shaderpacks.png differ diff --git a/launcher/resources/multimc/128x128/unknown_server.png b/launcher/resources/multimc/128x128/unknown_server.png index ec98382d4..b9761e08f 100644 Binary files a/launcher/resources/multimc/128x128/unknown_server.png and b/launcher/resources/multimc/128x128/unknown_server.png differ diff --git a/launcher/resources/multimc/16x16/about.png b/launcher/resources/multimc/16x16/about.png index a6a986e19..ed7e56dd6 100644 Binary files a/launcher/resources/multimc/16x16/about.png and b/launcher/resources/multimc/16x16/about.png differ diff --git a/launcher/resources/multimc/16x16/bug.png b/launcher/resources/multimc/16x16/bug.png index 0c5b78b40..57e7d8203 100644 Binary files a/launcher/resources/multimc/16x16/bug.png and b/launcher/resources/multimc/16x16/bug.png differ diff --git a/launcher/resources/multimc/16x16/cat.png b/launcher/resources/multimc/16x16/cat.png index e6e31b44b..73d5fa856 100644 Binary files a/launcher/resources/multimc/16x16/cat.png and b/launcher/resources/multimc/16x16/cat.png differ diff --git a/launcher/resources/multimc/16x16/centralmods.png b/launcher/resources/multimc/16x16/centralmods.png index c1b91c763..0a573fb4e 100644 Binary files a/launcher/resources/multimc/16x16/centralmods.png and b/launcher/resources/multimc/16x16/centralmods.png differ diff --git a/launcher/resources/multimc/16x16/checkupdate.png b/launcher/resources/multimc/16x16/checkupdate.png index f37420588..9d08c56f0 100644 Binary files a/launcher/resources/multimc/16x16/checkupdate.png and b/launcher/resources/multimc/16x16/checkupdate.png differ diff --git a/launcher/resources/multimc/16x16/copy.png b/launcher/resources/multimc/16x16/copy.png index ccaed9e11..24251adcf 100644 Binary files a/launcher/resources/multimc/16x16/copy.png and b/launcher/resources/multimc/16x16/copy.png differ diff --git a/launcher/resources/multimc/16x16/coremods.png b/launcher/resources/multimc/16x16/coremods.png index af0f11667..3d3932dbe 100644 Binary files a/launcher/resources/multimc/16x16/coremods.png and b/launcher/resources/multimc/16x16/coremods.png differ diff --git a/launcher/resources/multimc/16x16/help.png b/launcher/resources/multimc/16x16/help.png index e6edf6ba2..3dee5a3f9 100644 Binary files a/launcher/resources/multimc/16x16/help.png and b/launcher/resources/multimc/16x16/help.png differ diff --git a/launcher/resources/multimc/16x16/instance-settings.png b/launcher/resources/multimc/16x16/instance-settings.png index b916cd245..6c9073b96 100644 Binary files a/launcher/resources/multimc/16x16/instance-settings.png and b/launcher/resources/multimc/16x16/instance-settings.png differ diff --git a/launcher/resources/multimc/16x16/jarmods.png b/launcher/resources/multimc/16x16/jarmods.png index 1a97c9c00..cdcbe788b 100644 Binary files a/launcher/resources/multimc/16x16/jarmods.png and b/launcher/resources/multimc/16x16/jarmods.png differ diff --git a/launcher/resources/multimc/16x16/loadermods.png b/launcher/resources/multimc/16x16/loadermods.png index b5ab3fced..ad0e6237d 100644 Binary files a/launcher/resources/multimc/16x16/loadermods.png and b/launcher/resources/multimc/16x16/loadermods.png differ diff --git a/launcher/resources/multimc/16x16/log.png b/launcher/resources/multimc/16x16/log.png index efa2a0b57..74324047e 100644 Binary files a/launcher/resources/multimc/16x16/log.png and b/launcher/resources/multimc/16x16/log.png differ diff --git a/launcher/resources/multimc/16x16/minecraft.png b/launcher/resources/multimc/16x16/minecraft.png index e9f2f2a5f..3de54f74a 100644 Binary files a/launcher/resources/multimc/16x16/minecraft.png and b/launcher/resources/multimc/16x16/minecraft.png differ diff --git a/launcher/resources/multimc/16x16/new.png b/launcher/resources/multimc/16x16/new.png index 2e56f5893..dfde06f61 100644 Binary files a/launcher/resources/multimc/16x16/new.png and b/launcher/resources/multimc/16x16/new.png differ diff --git a/launcher/resources/multimc/16x16/news.png b/launcher/resources/multimc/16x16/news.png index 872e85dbc..04e016da7 100644 Binary files a/launcher/resources/multimc/16x16/news.png and b/launcher/resources/multimc/16x16/news.png differ diff --git a/launcher/resources/multimc/16x16/noaccount.png b/launcher/resources/multimc/16x16/noaccount.png index b49bcf36a..544d68207 100644 Binary files a/launcher/resources/multimc/16x16/noaccount.png and b/launcher/resources/multimc/16x16/noaccount.png differ diff --git a/launcher/resources/multimc/16x16/patreon.png b/launcher/resources/multimc/16x16/patreon.png index 9150c478f..0c306e7cc 100644 Binary files a/launcher/resources/multimc/16x16/patreon.png and b/launcher/resources/multimc/16x16/patreon.png differ diff --git a/launcher/resources/multimc/16x16/refresh.png b/launcher/resources/multimc/16x16/refresh.png index 86b6f82c1..2e81c9246 100644 Binary files a/launcher/resources/multimc/16x16/refresh.png and b/launcher/resources/multimc/16x16/refresh.png differ diff --git a/launcher/resources/multimc/16x16/resourcepacks.png b/launcher/resources/multimc/16x16/resourcepacks.png index d862f5ca6..ac4c5dc43 100644 Binary files a/launcher/resources/multimc/16x16/resourcepacks.png and b/launcher/resources/multimc/16x16/resourcepacks.png differ diff --git a/launcher/resources/multimc/16x16/screenshots.png b/launcher/resources/multimc/16x16/screenshots.png index 460000d4b..f0e5e439e 100644 Binary files a/launcher/resources/multimc/16x16/screenshots.png and b/launcher/resources/multimc/16x16/screenshots.png differ diff --git a/launcher/resources/multimc/16x16/settings.png b/launcher/resources/multimc/16x16/settings.png index b916cd245..6c9073b96 100644 Binary files a/launcher/resources/multimc/16x16/settings.png and b/launcher/resources/multimc/16x16/settings.png differ diff --git a/launcher/resources/multimc/16x16/star.png b/launcher/resources/multimc/16x16/star.png index 4963e6ec9..20278be0c 100644 Binary files a/launcher/resources/multimc/16x16/star.png and b/launcher/resources/multimc/16x16/star.png differ diff --git a/launcher/resources/multimc/16x16/status-bad.png b/launcher/resources/multimc/16x16/status-bad.png index 5b3f20518..c71142b8a 100644 Binary files a/launcher/resources/multimc/16x16/status-bad.png and b/launcher/resources/multimc/16x16/status-bad.png differ diff --git a/launcher/resources/multimc/16x16/status-good.png b/launcher/resources/multimc/16x16/status-good.png index 5cbdee815..456a67c5b 100644 Binary files a/launcher/resources/multimc/16x16/status-good.png and b/launcher/resources/multimc/16x16/status-good.png differ diff --git a/launcher/resources/multimc/16x16/status-running.png b/launcher/resources/multimc/16x16/status-running.png index a4c42e392..7b7bfec91 100644 Binary files a/launcher/resources/multimc/16x16/status-running.png and b/launcher/resources/multimc/16x16/status-running.png differ diff --git a/launcher/resources/multimc/16x16/status-yellow.png b/launcher/resources/multimc/16x16/status-yellow.png index b25375d18..f652ddae2 100644 Binary files a/launcher/resources/multimc/16x16/status-yellow.png and b/launcher/resources/multimc/16x16/status-yellow.png differ diff --git a/launcher/resources/multimc/16x16/viewfolder.png b/launcher/resources/multimc/16x16/viewfolder.png index 98b8a9448..f5f401427 100644 Binary files a/launcher/resources/multimc/16x16/viewfolder.png and b/launcher/resources/multimc/16x16/viewfolder.png differ diff --git a/launcher/resources/multimc/16x16/worlds.png b/launcher/resources/multimc/16x16/worlds.png index 1a38f38e7..ed4249ec3 100644 Binary files a/launcher/resources/multimc/16x16/worlds.png and b/launcher/resources/multimc/16x16/worlds.png differ diff --git a/launcher/resources/multimc/22x22/about.png b/launcher/resources/multimc/22x22/about.png index 57775e25a..fbf18726f 100644 Binary files a/launcher/resources/multimc/22x22/about.png and b/launcher/resources/multimc/22x22/about.png differ diff --git a/launcher/resources/multimc/22x22/bug.png b/launcher/resources/multimc/22x22/bug.png index 90481bba6..8aeb25d66 100644 Binary files a/launcher/resources/multimc/22x22/bug.png and b/launcher/resources/multimc/22x22/bug.png differ diff --git a/launcher/resources/multimc/22x22/cat.png b/launcher/resources/multimc/22x22/cat.png index 3ea7ba69e..a5795b9b8 100644 Binary files a/launcher/resources/multimc/22x22/cat.png and b/launcher/resources/multimc/22x22/cat.png differ diff --git a/launcher/resources/multimc/22x22/centralmods.png b/launcher/resources/multimc/22x22/centralmods.png index a10f9a2b9..a54fdb0b0 100644 Binary files a/launcher/resources/multimc/22x22/centralmods.png and b/launcher/resources/multimc/22x22/centralmods.png differ diff --git a/launcher/resources/multimc/22x22/checkupdate.png b/launcher/resources/multimc/22x22/checkupdate.png index badb200cf..a44d47fe0 100644 Binary files a/launcher/resources/multimc/22x22/checkupdate.png and b/launcher/resources/multimc/22x22/checkupdate.png differ diff --git a/launcher/resources/multimc/22x22/copy.png b/launcher/resources/multimc/22x22/copy.png index ea236a241..5cdc69dbe 100644 Binary files a/launcher/resources/multimc/22x22/copy.png and b/launcher/resources/multimc/22x22/copy.png differ diff --git a/launcher/resources/multimc/22x22/help.png b/launcher/resources/multimc/22x22/help.png index da79b3e3f..db49f9e31 100644 Binary files a/launcher/resources/multimc/22x22/help.png and b/launcher/resources/multimc/22x22/help.png differ diff --git a/launcher/resources/multimc/22x22/instance-settings.png b/launcher/resources/multimc/22x22/instance-settings.png index daf56aad3..8eb9ee49d 100644 Binary files a/launcher/resources/multimc/22x22/instance-settings.png and b/launcher/resources/multimc/22x22/instance-settings.png differ diff --git a/launcher/resources/multimc/22x22/new.png b/launcher/resources/multimc/22x22/new.png index c707fbbfb..41edf3ef0 100644 Binary files a/launcher/resources/multimc/22x22/new.png and b/launcher/resources/multimc/22x22/new.png differ diff --git a/launcher/resources/multimc/22x22/news.png b/launcher/resources/multimc/22x22/news.png index 1953bf7b2..46eaab869 100644 Binary files a/launcher/resources/multimc/22x22/news.png and b/launcher/resources/multimc/22x22/news.png differ diff --git a/launcher/resources/multimc/22x22/patreon.png b/launcher/resources/multimc/22x22/patreon.png index f2c2076c0..8da8780ec 100644 Binary files a/launcher/resources/multimc/22x22/patreon.png and b/launcher/resources/multimc/22x22/patreon.png differ diff --git a/launcher/resources/multimc/22x22/refresh.png b/launcher/resources/multimc/22x22/refresh.png index 45b5535ce..f517f7ace 100644 Binary files a/launcher/resources/multimc/22x22/refresh.png and b/launcher/resources/multimc/22x22/refresh.png differ diff --git a/launcher/resources/multimc/22x22/screenshots.png b/launcher/resources/multimc/22x22/screenshots.png index 6fb42bbdf..780eb4351 100644 Binary files a/launcher/resources/multimc/22x22/screenshots.png and b/launcher/resources/multimc/22x22/screenshots.png differ diff --git a/launcher/resources/multimc/22x22/settings.png b/launcher/resources/multimc/22x22/settings.png index daf56aad3..8eb9ee49d 100644 Binary files a/launcher/resources/multimc/22x22/settings.png and b/launcher/resources/multimc/22x22/settings.png differ diff --git a/launcher/resources/multimc/22x22/status-bad.png b/launcher/resources/multimc/22x22/status-bad.png index 2707539e1..9d001ccd2 100644 Binary files a/launcher/resources/multimc/22x22/status-bad.png and b/launcher/resources/multimc/22x22/status-bad.png differ diff --git a/launcher/resources/multimc/22x22/status-good.png b/launcher/resources/multimc/22x22/status-good.png index f55debc37..9ac765abe 100644 Binary files a/launcher/resources/multimc/22x22/status-good.png and b/launcher/resources/multimc/22x22/status-good.png differ diff --git a/launcher/resources/multimc/22x22/status-running.png b/launcher/resources/multimc/22x22/status-running.png index 0dffba18f..21caa06b8 100644 Binary files a/launcher/resources/multimc/22x22/status-running.png and b/launcher/resources/multimc/22x22/status-running.png differ diff --git a/launcher/resources/multimc/22x22/status-yellow.png b/launcher/resources/multimc/22x22/status-yellow.png index 481eb7f3f..e125cf098 100644 Binary files a/launcher/resources/multimc/22x22/status-yellow.png and b/launcher/resources/multimc/22x22/status-yellow.png differ diff --git a/launcher/resources/multimc/22x22/viewfolder.png b/launcher/resources/multimc/22x22/viewfolder.png index b645167fc..7065e9ac9 100644 Binary files a/launcher/resources/multimc/22x22/viewfolder.png and b/launcher/resources/multimc/22x22/viewfolder.png differ diff --git a/launcher/resources/multimc/22x22/worlds.png b/launcher/resources/multimc/22x22/worlds.png index e8825bab4..ebb32f10c 100644 Binary files a/launcher/resources/multimc/22x22/worlds.png and b/launcher/resources/multimc/22x22/worlds.png differ diff --git a/launcher/resources/multimc/24x24/cat.png b/launcher/resources/multimc/24x24/cat.png index c93245f65..08b0ab1b0 100644 Binary files a/launcher/resources/multimc/24x24/cat.png and b/launcher/resources/multimc/24x24/cat.png differ diff --git a/launcher/resources/multimc/24x24/coremods.png b/launcher/resources/multimc/24x24/coremods.png index 90603d248..0cbd3f173 100644 Binary files a/launcher/resources/multimc/24x24/coremods.png and b/launcher/resources/multimc/24x24/coremods.png differ diff --git a/launcher/resources/multimc/24x24/jarmods.png b/launcher/resources/multimc/24x24/jarmods.png index 68cb8e9db..a4824c276 100644 Binary files a/launcher/resources/multimc/24x24/jarmods.png and b/launcher/resources/multimc/24x24/jarmods.png differ diff --git a/launcher/resources/multimc/24x24/loadermods.png b/launcher/resources/multimc/24x24/loadermods.png index 250a62609..cd4954d5a 100644 Binary files a/launcher/resources/multimc/24x24/loadermods.png and b/launcher/resources/multimc/24x24/loadermods.png differ diff --git a/launcher/resources/multimc/24x24/log.png b/launcher/resources/multimc/24x24/log.png index fe3020534..7978968c1 100644 Binary files a/launcher/resources/multimc/24x24/log.png and b/launcher/resources/multimc/24x24/log.png differ diff --git a/launcher/resources/multimc/24x24/minecraft.png b/launcher/resources/multimc/24x24/minecraft.png index b31177c9c..8869844cb 100644 Binary files a/launcher/resources/multimc/24x24/minecraft.png and b/launcher/resources/multimc/24x24/minecraft.png differ diff --git a/launcher/resources/multimc/24x24/noaccount.png b/launcher/resources/multimc/24x24/noaccount.png index ac12437cf..05d5cc584 100644 Binary files a/launcher/resources/multimc/24x24/noaccount.png and b/launcher/resources/multimc/24x24/noaccount.png differ diff --git a/launcher/resources/multimc/24x24/patreon.png b/launcher/resources/multimc/24x24/patreon.png index add806686..2e1cc0548 100644 Binary files a/launcher/resources/multimc/24x24/patreon.png and b/launcher/resources/multimc/24x24/patreon.png differ diff --git a/launcher/resources/multimc/24x24/resourcepacks.png b/launcher/resources/multimc/24x24/resourcepacks.png index 68359d395..b434fb124 100644 Binary files a/launcher/resources/multimc/24x24/resourcepacks.png and b/launcher/resources/multimc/24x24/resourcepacks.png differ diff --git a/launcher/resources/multimc/24x24/star.png b/launcher/resources/multimc/24x24/star.png index 7f16618a6..8527f5092 100644 Binary files a/launcher/resources/multimc/24x24/star.png and b/launcher/resources/multimc/24x24/star.png differ diff --git a/launcher/resources/multimc/24x24/status-bad.png b/launcher/resources/multimc/24x24/status-bad.png index d1547a474..eae695286 100644 Binary files a/launcher/resources/multimc/24x24/status-bad.png and b/launcher/resources/multimc/24x24/status-bad.png differ diff --git a/launcher/resources/multimc/24x24/status-good.png b/launcher/resources/multimc/24x24/status-good.png index 3545bc4c5..e315beaf3 100644 Binary files a/launcher/resources/multimc/24x24/status-good.png and b/launcher/resources/multimc/24x24/status-good.png differ diff --git a/launcher/resources/multimc/24x24/status-running.png b/launcher/resources/multimc/24x24/status-running.png index ecd64451f..9c6059462 100644 Binary files a/launcher/resources/multimc/24x24/status-running.png and b/launcher/resources/multimc/24x24/status-running.png differ diff --git a/launcher/resources/multimc/24x24/status-yellow.png b/launcher/resources/multimc/24x24/status-yellow.png index dd5fde67b..118efd890 100644 Binary files a/launcher/resources/multimc/24x24/status-yellow.png and b/launcher/resources/multimc/24x24/status-yellow.png differ diff --git a/launcher/resources/multimc/256x256/minecraft.png b/launcher/resources/multimc/256x256/minecraft.png index 77e3f03e2..0b24f5501 100644 Binary files a/launcher/resources/multimc/256x256/minecraft.png and b/launcher/resources/multimc/256x256/minecraft.png differ diff --git a/launcher/resources/multimc/32x32/about.png b/launcher/resources/multimc/32x32/about.png index 5174c4f19..261d2a44c 100644 Binary files a/launcher/resources/multimc/32x32/about.png and b/launcher/resources/multimc/32x32/about.png differ diff --git a/launcher/resources/multimc/32x32/bug.png b/launcher/resources/multimc/32x32/bug.png index ada466530..3d97be84a 100644 Binary files a/launcher/resources/multimc/32x32/bug.png and b/launcher/resources/multimc/32x32/bug.png differ diff --git a/launcher/resources/multimc/32x32/cat.png b/launcher/resources/multimc/32x32/cat.png index 78ff98e9e..b9b21e663 100644 Binary files a/launcher/resources/multimc/32x32/cat.png and b/launcher/resources/multimc/32x32/cat.png differ diff --git a/launcher/resources/multimc/32x32/centralmods.png b/launcher/resources/multimc/32x32/centralmods.png index cd2b8208e..7225ba08c 100644 Binary files a/launcher/resources/multimc/32x32/centralmods.png and b/launcher/resources/multimc/32x32/centralmods.png differ diff --git a/launcher/resources/multimc/32x32/checkupdate.png b/launcher/resources/multimc/32x32/checkupdate.png index 754005f97..c60f965b2 100644 Binary files a/launcher/resources/multimc/32x32/checkupdate.png and b/launcher/resources/multimc/32x32/checkupdate.png differ diff --git a/launcher/resources/multimc/32x32/copy.png b/launcher/resources/multimc/32x32/copy.png index c137b0f11..ce662604e 100644 Binary files a/launcher/resources/multimc/32x32/copy.png and b/launcher/resources/multimc/32x32/copy.png differ diff --git a/launcher/resources/multimc/32x32/coremods.png b/launcher/resources/multimc/32x32/coremods.png index 770d695eb..9718ec671 100644 Binary files a/launcher/resources/multimc/32x32/coremods.png and b/launcher/resources/multimc/32x32/coremods.png differ diff --git a/launcher/resources/multimc/32x32/help.png b/launcher/resources/multimc/32x32/help.png index b38542784..6e4cdbff6 100644 Binary files a/launcher/resources/multimc/32x32/help.png and b/launcher/resources/multimc/32x32/help.png differ diff --git a/launcher/resources/multimc/32x32/instance-settings.png b/launcher/resources/multimc/32x32/instance-settings.png index a9c0817c9..4be48c1d5 100644 Binary files a/launcher/resources/multimc/32x32/instance-settings.png and b/launcher/resources/multimc/32x32/instance-settings.png differ diff --git a/launcher/resources/multimc/32x32/instances/brick_legacy.png b/launcher/resources/multimc/32x32/instances/brick_legacy.png index c324fda06..7d35f4da5 100644 Binary files a/launcher/resources/multimc/32x32/instances/brick_legacy.png and b/launcher/resources/multimc/32x32/instances/brick_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/chicken_legacy.png b/launcher/resources/multimc/32x32/instances/chicken_legacy.png index f870467a6..7991410e1 100644 Binary files a/launcher/resources/multimc/32x32/instances/chicken_legacy.png and b/launcher/resources/multimc/32x32/instances/chicken_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/creeper_legacy.png b/launcher/resources/multimc/32x32/instances/creeper_legacy.png index a67ecfc35..571d2de19 100644 Binary files a/launcher/resources/multimc/32x32/instances/creeper_legacy.png and b/launcher/resources/multimc/32x32/instances/creeper_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/diamond_legacy.png b/launcher/resources/multimc/32x32/instances/diamond_legacy.png index 1eb264697..3ad9c002f 100644 Binary files a/launcher/resources/multimc/32x32/instances/diamond_legacy.png and b/launcher/resources/multimc/32x32/instances/diamond_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/dirt_legacy.png b/launcher/resources/multimc/32x32/instances/dirt_legacy.png index 9e19eb8fa..719a45ed5 100644 Binary files a/launcher/resources/multimc/32x32/instances/dirt_legacy.png and b/launcher/resources/multimc/32x32/instances/dirt_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/enderpearl_legacy.png b/launcher/resources/multimc/32x32/instances/enderpearl_legacy.png index a818eb8e6..e0262f659 100644 Binary files a/launcher/resources/multimc/32x32/instances/enderpearl_legacy.png and b/launcher/resources/multimc/32x32/instances/enderpearl_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/ftb_glow.png b/launcher/resources/multimc/32x32/instances/ftb_glow.png index c4e6fd5d3..7437b27cc 100644 Binary files a/launcher/resources/multimc/32x32/instances/ftb_glow.png and b/launcher/resources/multimc/32x32/instances/ftb_glow.png differ diff --git a/launcher/resources/multimc/32x32/instances/ftb_logo_legacy.png b/launcher/resources/multimc/32x32/instances/ftb_logo_legacy.png index 20df71710..a70109bbb 100644 Binary files a/launcher/resources/multimc/32x32/instances/ftb_logo_legacy.png and b/launcher/resources/multimc/32x32/instances/ftb_logo_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/gear_legacy.png b/launcher/resources/multimc/32x32/instances/gear_legacy.png index da9ba2f9d..61dc9f500 100644 Binary files a/launcher/resources/multimc/32x32/instances/gear_legacy.png and b/launcher/resources/multimc/32x32/instances/gear_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/gold_legacy.png b/launcher/resources/multimc/32x32/instances/gold_legacy.png index 593410fac..99d91795c 100644 Binary files a/launcher/resources/multimc/32x32/instances/gold_legacy.png and b/launcher/resources/multimc/32x32/instances/gold_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/grass_legacy.png b/launcher/resources/multimc/32x32/instances/grass_legacy.png index f1694547a..400f21067 100644 Binary files a/launcher/resources/multimc/32x32/instances/grass_legacy.png and b/launcher/resources/multimc/32x32/instances/grass_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/herobrine_legacy.png b/launcher/resources/multimc/32x32/instances/herobrine_legacy.png index e5460da31..8ed872a6f 100644 Binary files a/launcher/resources/multimc/32x32/instances/herobrine_legacy.png and b/launcher/resources/multimc/32x32/instances/herobrine_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/infinity_legacy.png b/launcher/resources/multimc/32x32/instances/infinity_legacy.png index bd94a3dc2..62291c782 100644 Binary files a/launcher/resources/multimc/32x32/instances/infinity_legacy.png and b/launcher/resources/multimc/32x32/instances/infinity_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/iron_legacy.png b/launcher/resources/multimc/32x32/instances/iron_legacy.png index 3e811bd63..d05d7c01e 100644 Binary files a/launcher/resources/multimc/32x32/instances/iron_legacy.png and b/launcher/resources/multimc/32x32/instances/iron_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/magitech_legacy.png b/launcher/resources/multimc/32x32/instances/magitech_legacy.png index 6fd8ff604..bd630da8f 100644 Binary files a/launcher/resources/multimc/32x32/instances/magitech_legacy.png and b/launcher/resources/multimc/32x32/instances/magitech_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/meat_legacy.png b/launcher/resources/multimc/32x32/instances/meat_legacy.png index 6694859db..422c88eeb 100644 Binary files a/launcher/resources/multimc/32x32/instances/meat_legacy.png and b/launcher/resources/multimc/32x32/instances/meat_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/netherstar_legacy.png b/launcher/resources/multimc/32x32/instances/netherstar_legacy.png index 43cb51139..6f5c6f22b 100644 Binary files a/launcher/resources/multimc/32x32/instances/netherstar_legacy.png and b/launcher/resources/multimc/32x32/instances/netherstar_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/planks_legacy.png b/launcher/resources/multimc/32x32/instances/planks_legacy.png index a94b75029..0ff6d19b0 100644 Binary files a/launcher/resources/multimc/32x32/instances/planks_legacy.png and b/launcher/resources/multimc/32x32/instances/planks_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/skeleton_legacy.png b/launcher/resources/multimc/32x32/instances/skeleton_legacy.png index 0c8d3505a..2327a036a 100644 Binary files a/launcher/resources/multimc/32x32/instances/skeleton_legacy.png and b/launcher/resources/multimc/32x32/instances/skeleton_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/squarecreeper_legacy.png b/launcher/resources/multimc/32x32/instances/squarecreeper_legacy.png index b78c4ae09..258c9b34d 100644 Binary files a/launcher/resources/multimc/32x32/instances/squarecreeper_legacy.png and b/launcher/resources/multimc/32x32/instances/squarecreeper_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/steve_legacy.png b/launcher/resources/multimc/32x32/instances/steve_legacy.png index 07c6acdee..3467335f0 100644 Binary files a/launcher/resources/multimc/32x32/instances/steve_legacy.png and b/launcher/resources/multimc/32x32/instances/steve_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/stone_legacy.png b/launcher/resources/multimc/32x32/instances/stone_legacy.png index 1b6ef7a43..7a4d88cf0 100644 Binary files a/launcher/resources/multimc/32x32/instances/stone_legacy.png and b/launcher/resources/multimc/32x32/instances/stone_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/tnt_legacy.png b/launcher/resources/multimc/32x32/instances/tnt_legacy.png index e40d404d8..7ab83644f 100644 Binary files a/launcher/resources/multimc/32x32/instances/tnt_legacy.png and b/launcher/resources/multimc/32x32/instances/tnt_legacy.png differ diff --git a/launcher/resources/multimc/32x32/jarmods.png b/launcher/resources/multimc/32x32/jarmods.png index 5cda173a9..848be629f 100644 Binary files a/launcher/resources/multimc/32x32/jarmods.png and b/launcher/resources/multimc/32x32/jarmods.png differ diff --git a/launcher/resources/multimc/32x32/loadermods.png b/launcher/resources/multimc/32x32/loadermods.png index c4ca12e26..73d70a30a 100644 Binary files a/launcher/resources/multimc/32x32/loadermods.png and b/launcher/resources/multimc/32x32/loadermods.png differ diff --git a/launcher/resources/multimc/32x32/log.png b/launcher/resources/multimc/32x32/log.png index d620da122..6c2290f77 100644 Binary files a/launcher/resources/multimc/32x32/log.png and b/launcher/resources/multimc/32x32/log.png differ diff --git a/launcher/resources/multimc/32x32/minecraft.png b/launcher/resources/multimc/32x32/minecraft.png index 816bec98f..6b3642692 100644 Binary files a/launcher/resources/multimc/32x32/minecraft.png and b/launcher/resources/multimc/32x32/minecraft.png differ diff --git a/launcher/resources/multimc/32x32/new.png b/launcher/resources/multimc/32x32/new.png index a3555ba49..8fc4be64a 100644 Binary files a/launcher/resources/multimc/32x32/new.png and b/launcher/resources/multimc/32x32/new.png differ diff --git a/launcher/resources/multimc/32x32/news.png b/launcher/resources/multimc/32x32/news.png index c579fd44d..240803124 100644 Binary files a/launcher/resources/multimc/32x32/news.png and b/launcher/resources/multimc/32x32/news.png differ diff --git a/launcher/resources/multimc/32x32/noaccount.png b/launcher/resources/multimc/32x32/noaccount.png index a73afc946..98ca7130e 100644 Binary files a/launcher/resources/multimc/32x32/noaccount.png and b/launcher/resources/multimc/32x32/noaccount.png differ diff --git a/launcher/resources/multimc/32x32/patreon.png b/launcher/resources/multimc/32x32/patreon.png index 70085aa1c..440195d2e 100644 Binary files a/launcher/resources/multimc/32x32/patreon.png and b/launcher/resources/multimc/32x32/patreon.png differ diff --git a/launcher/resources/multimc/32x32/refresh.png b/launcher/resources/multimc/32x32/refresh.png index afa2a9d77..e67c5fe51 100644 Binary files a/launcher/resources/multimc/32x32/refresh.png and b/launcher/resources/multimc/32x32/refresh.png differ diff --git a/launcher/resources/multimc/32x32/resourcepacks.png b/launcher/resources/multimc/32x32/resourcepacks.png index c14759ef7..8af7fe31f 100644 Binary files a/launcher/resources/multimc/32x32/resourcepacks.png and b/launcher/resources/multimc/32x32/resourcepacks.png differ diff --git a/launcher/resources/multimc/32x32/screenshots.png b/launcher/resources/multimc/32x32/screenshots.png index 4fcd62246..95c8c7e93 100644 Binary files a/launcher/resources/multimc/32x32/screenshots.png and b/launcher/resources/multimc/32x32/screenshots.png differ diff --git a/launcher/resources/multimc/32x32/settings.png b/launcher/resources/multimc/32x32/settings.png index a9c0817c9..4be48c1d5 100644 Binary files a/launcher/resources/multimc/32x32/settings.png and b/launcher/resources/multimc/32x32/settings.png differ diff --git a/launcher/resources/multimc/32x32/star.png b/launcher/resources/multimc/32x32/star.png index b271f0d1b..c797ab34c 100644 Binary files a/launcher/resources/multimc/32x32/star.png and b/launcher/resources/multimc/32x32/star.png differ diff --git a/launcher/resources/multimc/32x32/status-bad.png b/launcher/resources/multimc/32x32/status-bad.png index 8c2c9d4f7..77ac8fe01 100644 Binary files a/launcher/resources/multimc/32x32/status-bad.png and b/launcher/resources/multimc/32x32/status-bad.png differ diff --git a/launcher/resources/multimc/32x32/status-good.png b/launcher/resources/multimc/32x32/status-good.png index 1a805e68b..b8f7095ad 100644 Binary files a/launcher/resources/multimc/32x32/status-good.png and b/launcher/resources/multimc/32x32/status-good.png differ diff --git a/launcher/resources/multimc/32x32/status-running.png b/launcher/resources/multimc/32x32/status-running.png index f561f01ad..8ff17a046 100644 Binary files a/launcher/resources/multimc/32x32/status-running.png and b/launcher/resources/multimc/32x32/status-running.png differ diff --git a/launcher/resources/multimc/32x32/status-yellow.png b/launcher/resources/multimc/32x32/status-yellow.png index 42c685520..36270afb9 100644 Binary files a/launcher/resources/multimc/32x32/status-yellow.png and b/launcher/resources/multimc/32x32/status-yellow.png differ diff --git a/launcher/resources/multimc/32x32/viewfolder.png b/launcher/resources/multimc/32x32/viewfolder.png index 74ab8fa63..32d7b4bae 100644 Binary files a/launcher/resources/multimc/32x32/viewfolder.png and b/launcher/resources/multimc/32x32/viewfolder.png differ diff --git a/launcher/resources/multimc/32x32/worlds.png b/launcher/resources/multimc/32x32/worlds.png index c986596c9..dce4d96b5 100644 Binary files a/launcher/resources/multimc/32x32/worlds.png and b/launcher/resources/multimc/32x32/worlds.png differ diff --git a/launcher/resources/multimc/48x48/about.png b/launcher/resources/multimc/48x48/about.png index b4ac71b8e..f6d4d11cb 100644 Binary files a/launcher/resources/multimc/48x48/about.png and b/launcher/resources/multimc/48x48/about.png differ diff --git a/launcher/resources/multimc/48x48/bug.png b/launcher/resources/multimc/48x48/bug.png index 298f9397c..8de0b0755 100644 Binary files a/launcher/resources/multimc/48x48/bug.png and b/launcher/resources/multimc/48x48/bug.png differ diff --git a/launcher/resources/multimc/48x48/cat.png b/launcher/resources/multimc/48x48/cat.png index 25912a3c0..f84221d7a 100644 Binary files a/launcher/resources/multimc/48x48/cat.png and b/launcher/resources/multimc/48x48/cat.png differ diff --git a/launcher/resources/multimc/48x48/centralmods.png b/launcher/resources/multimc/48x48/centralmods.png index d927e39b4..2425a7c74 100644 Binary files a/launcher/resources/multimc/48x48/centralmods.png and b/launcher/resources/multimc/48x48/centralmods.png differ diff --git a/launcher/resources/multimc/48x48/checkupdate.png b/launcher/resources/multimc/48x48/checkupdate.png index 2e2c7d6be..b181736a8 100644 Binary files a/launcher/resources/multimc/48x48/checkupdate.png and b/launcher/resources/multimc/48x48/checkupdate.png differ diff --git a/launcher/resources/multimc/48x48/copy.png b/launcher/resources/multimc/48x48/copy.png index ea40e34b7..4dc04b080 100644 Binary files a/launcher/resources/multimc/48x48/copy.png and b/launcher/resources/multimc/48x48/copy.png differ diff --git a/launcher/resources/multimc/48x48/help.png b/launcher/resources/multimc/48x48/help.png index 82d828fab..f57c6c896 100644 Binary files a/launcher/resources/multimc/48x48/help.png and b/launcher/resources/multimc/48x48/help.png differ diff --git a/launcher/resources/multimc/48x48/instance-settings.png b/launcher/resources/multimc/48x48/instance-settings.png index 6674eb236..ec298cd62 100644 Binary files a/launcher/resources/multimc/48x48/instance-settings.png and b/launcher/resources/multimc/48x48/instance-settings.png differ diff --git a/launcher/resources/multimc/48x48/log.png b/launcher/resources/multimc/48x48/log.png index 45f60e6b4..dc3eb4e27 100644 Binary files a/launcher/resources/multimc/48x48/log.png and b/launcher/resources/multimc/48x48/log.png differ diff --git a/launcher/resources/multimc/48x48/minecraft.png b/launcher/resources/multimc/48x48/minecraft.png index 38fc9f6cc..4fe522ffb 100644 Binary files a/launcher/resources/multimc/48x48/minecraft.png and b/launcher/resources/multimc/48x48/minecraft.png differ diff --git a/launcher/resources/multimc/48x48/new.png b/launcher/resources/multimc/48x48/new.png index a81753b31..a81ccf1b2 100644 Binary files a/launcher/resources/multimc/48x48/new.png and b/launcher/resources/multimc/48x48/new.png differ diff --git a/launcher/resources/multimc/48x48/news.png b/launcher/resources/multimc/48x48/news.png index 0f82d8577..d2f5d178a 100644 Binary files a/launcher/resources/multimc/48x48/news.png and b/launcher/resources/multimc/48x48/news.png differ diff --git a/launcher/resources/multimc/48x48/noaccount.png b/launcher/resources/multimc/48x48/noaccount.png index 4703796c7..c13e4d6d5 100644 Binary files a/launcher/resources/multimc/48x48/noaccount.png and b/launcher/resources/multimc/48x48/noaccount.png differ diff --git a/launcher/resources/multimc/48x48/patreon.png b/launcher/resources/multimc/48x48/patreon.png index 7aec4d7d3..7e8f25367 100644 Binary files a/launcher/resources/multimc/48x48/patreon.png and b/launcher/resources/multimc/48x48/patreon.png differ diff --git a/launcher/resources/multimc/48x48/refresh.png b/launcher/resources/multimc/48x48/refresh.png index 0b08b2388..87e113583 100644 Binary files a/launcher/resources/multimc/48x48/refresh.png and b/launcher/resources/multimc/48x48/refresh.png differ diff --git a/launcher/resources/multimc/48x48/screenshots.png b/launcher/resources/multimc/48x48/screenshots.png index 03c0059fa..694b96cd9 100644 Binary files a/launcher/resources/multimc/48x48/screenshots.png and b/launcher/resources/multimc/48x48/screenshots.png differ diff --git a/launcher/resources/multimc/48x48/settings.png b/launcher/resources/multimc/48x48/settings.png index 6674eb236..ec298cd62 100644 Binary files a/launcher/resources/multimc/48x48/settings.png and b/launcher/resources/multimc/48x48/settings.png differ diff --git a/launcher/resources/multimc/48x48/star.png b/launcher/resources/multimc/48x48/star.png index d9468e7e3..c5253c334 100644 Binary files a/launcher/resources/multimc/48x48/star.png and b/launcher/resources/multimc/48x48/star.png differ diff --git a/launcher/resources/multimc/48x48/status-bad.png b/launcher/resources/multimc/48x48/status-bad.png index 41c9cf227..083506d28 100644 Binary files a/launcher/resources/multimc/48x48/status-bad.png and b/launcher/resources/multimc/48x48/status-bad.png differ diff --git a/launcher/resources/multimc/48x48/status-good.png b/launcher/resources/multimc/48x48/status-good.png index df7cb59b6..0c3377ad7 100644 Binary files a/launcher/resources/multimc/48x48/status-good.png and b/launcher/resources/multimc/48x48/status-good.png differ diff --git a/launcher/resources/multimc/48x48/status-running.png b/launcher/resources/multimc/48x48/status-running.png index b8c0bf7cd..94598c927 100644 Binary files a/launcher/resources/multimc/48x48/status-running.png and b/launcher/resources/multimc/48x48/status-running.png differ diff --git a/launcher/resources/multimc/48x48/status-yellow.png b/launcher/resources/multimc/48x48/status-yellow.png index 4f3b11d69..bb76fcd69 100644 Binary files a/launcher/resources/multimc/48x48/status-yellow.png and b/launcher/resources/multimc/48x48/status-yellow.png differ diff --git a/launcher/resources/multimc/48x48/viewfolder.png b/launcher/resources/multimc/48x48/viewfolder.png index 0492a736c..2245ba30a 100644 Binary files a/launcher/resources/multimc/48x48/viewfolder.png and b/launcher/resources/multimc/48x48/viewfolder.png differ diff --git a/launcher/resources/multimc/48x48/worlds.png b/launcher/resources/multimc/48x48/worlds.png index 4fc337511..eb44150a3 100644 Binary files a/launcher/resources/multimc/48x48/worlds.png and b/launcher/resources/multimc/48x48/worlds.png differ diff --git a/launcher/resources/multimc/50x50/instances/enderman_legacy.png b/launcher/resources/multimc/50x50/instances/enderman_legacy.png index 9f3a72b3a..36c791eb0 100644 Binary files a/launcher/resources/multimc/50x50/instances/enderman_legacy.png and b/launcher/resources/multimc/50x50/instances/enderman_legacy.png differ diff --git a/launcher/resources/multimc/64x64/about.png b/launcher/resources/multimc/64x64/about.png index b83e92690..b9be9abef 100644 Binary files a/launcher/resources/multimc/64x64/about.png and b/launcher/resources/multimc/64x64/about.png differ diff --git a/launcher/resources/multimc/64x64/bug.png b/launcher/resources/multimc/64x64/bug.png index 156b03159..6c9ac6af2 100644 Binary files a/launcher/resources/multimc/64x64/bug.png and b/launcher/resources/multimc/64x64/bug.png differ diff --git a/launcher/resources/multimc/64x64/cat.png b/launcher/resources/multimc/64x64/cat.png index 2cc21f808..65681e6b8 100644 Binary files a/launcher/resources/multimc/64x64/cat.png and b/launcher/resources/multimc/64x64/cat.png differ diff --git a/launcher/resources/multimc/64x64/centralmods.png b/launcher/resources/multimc/64x64/centralmods.png index 8831f437c..d30735601 100644 Binary files a/launcher/resources/multimc/64x64/centralmods.png and b/launcher/resources/multimc/64x64/centralmods.png differ diff --git a/launcher/resources/multimc/64x64/checkupdate.png b/launcher/resources/multimc/64x64/checkupdate.png index dd1e29ac6..a4002a61e 100644 Binary files a/launcher/resources/multimc/64x64/checkupdate.png and b/launcher/resources/multimc/64x64/checkupdate.png differ diff --git a/launcher/resources/multimc/64x64/copy.png b/launcher/resources/multimc/64x64/copy.png index d12cf9c8a..69fa1c3fb 100644 Binary files a/launcher/resources/multimc/64x64/copy.png and b/launcher/resources/multimc/64x64/copy.png differ diff --git a/launcher/resources/multimc/64x64/coremods.png b/launcher/resources/multimc/64x64/coremods.png index 668be3341..b1b1f8237 100644 Binary files a/launcher/resources/multimc/64x64/coremods.png and b/launcher/resources/multimc/64x64/coremods.png differ diff --git a/launcher/resources/multimc/64x64/help.png b/launcher/resources/multimc/64x64/help.png index 0f3948c2c..e419f8600 100644 Binary files a/launcher/resources/multimc/64x64/help.png and b/launcher/resources/multimc/64x64/help.png differ diff --git a/launcher/resources/multimc/64x64/instance-settings.png b/launcher/resources/multimc/64x64/instance-settings.png index e3ff58faf..9df7fe9bc 100644 Binary files a/launcher/resources/multimc/64x64/instance-settings.png and b/launcher/resources/multimc/64x64/instance-settings.png differ diff --git a/launcher/resources/multimc/64x64/jarmods.png b/launcher/resources/multimc/64x64/jarmods.png index 55d1a42a0..5abd5ecc5 100644 Binary files a/launcher/resources/multimc/64x64/jarmods.png and b/launcher/resources/multimc/64x64/jarmods.png differ diff --git a/launcher/resources/multimc/64x64/loadermods.png b/launcher/resources/multimc/64x64/loadermods.png index 24618fd0b..485aa843a 100644 Binary files a/launcher/resources/multimc/64x64/loadermods.png and b/launcher/resources/multimc/64x64/loadermods.png differ diff --git a/launcher/resources/multimc/64x64/log.png b/launcher/resources/multimc/64x64/log.png index 0f531cdfc..decee34bd 100644 Binary files a/launcher/resources/multimc/64x64/log.png and b/launcher/resources/multimc/64x64/log.png differ diff --git a/launcher/resources/multimc/64x64/new.png b/launcher/resources/multimc/64x64/new.png index c3c6796c4..289a6ad0b 100644 Binary files a/launcher/resources/multimc/64x64/new.png and b/launcher/resources/multimc/64x64/new.png differ diff --git a/launcher/resources/multimc/64x64/news.png b/launcher/resources/multimc/64x64/news.png index e306eed37..a1c28fdd6 100644 Binary files a/launcher/resources/multimc/64x64/news.png and b/launcher/resources/multimc/64x64/news.png differ diff --git a/launcher/resources/multimc/64x64/patreon.png b/launcher/resources/multimc/64x64/patreon.png index ef5d690eb..5c2d88814 100644 Binary files a/launcher/resources/multimc/64x64/patreon.png and b/launcher/resources/multimc/64x64/patreon.png differ diff --git a/launcher/resources/multimc/64x64/refresh.png b/launcher/resources/multimc/64x64/refresh.png index 8373d8198..737bd0581 100644 Binary files a/launcher/resources/multimc/64x64/refresh.png and b/launcher/resources/multimc/64x64/refresh.png differ diff --git a/launcher/resources/multimc/64x64/resourcepacks.png b/launcher/resources/multimc/64x64/resourcepacks.png index fb874e7d3..703fde6b5 100644 Binary files a/launcher/resources/multimc/64x64/resourcepacks.png and b/launcher/resources/multimc/64x64/resourcepacks.png differ diff --git a/launcher/resources/multimc/64x64/screenshots.png b/launcher/resources/multimc/64x64/screenshots.png index af18e39ca..a57bf2772 100644 Binary files a/launcher/resources/multimc/64x64/screenshots.png and b/launcher/resources/multimc/64x64/screenshots.png differ diff --git a/launcher/resources/multimc/64x64/settings.png b/launcher/resources/multimc/64x64/settings.png index e3ff58faf..9df7fe9bc 100644 Binary files a/launcher/resources/multimc/64x64/settings.png and b/launcher/resources/multimc/64x64/settings.png differ diff --git a/launcher/resources/multimc/64x64/star.png b/launcher/resources/multimc/64x64/star.png index 4ed5d978f..24e9d75c7 100644 Binary files a/launcher/resources/multimc/64x64/star.png and b/launcher/resources/multimc/64x64/star.png differ diff --git a/launcher/resources/multimc/64x64/status-bad.png b/launcher/resources/multimc/64x64/status-bad.png index 64060ba09..669d3159d 100644 Binary files a/launcher/resources/multimc/64x64/status-bad.png and b/launcher/resources/multimc/64x64/status-bad.png differ diff --git a/launcher/resources/multimc/64x64/status-good.png b/launcher/resources/multimc/64x64/status-good.png index e862ddcdf..4d256cc04 100644 Binary files a/launcher/resources/multimc/64x64/status-good.png and b/launcher/resources/multimc/64x64/status-good.png differ diff --git a/launcher/resources/multimc/64x64/status-running.png b/launcher/resources/multimc/64x64/status-running.png index 38afda0f9..64d6d0a8d 100644 Binary files a/launcher/resources/multimc/64x64/status-running.png and b/launcher/resources/multimc/64x64/status-running.png differ diff --git a/launcher/resources/multimc/64x64/status-yellow.png b/launcher/resources/multimc/64x64/status-yellow.png index 3d54d320c..98013151b 100644 Binary files a/launcher/resources/multimc/64x64/status-yellow.png and b/launcher/resources/multimc/64x64/status-yellow.png differ diff --git a/launcher/resources/multimc/64x64/viewfolder.png b/launcher/resources/multimc/64x64/viewfolder.png index 7d531f9cc..d16cacc4d 100644 Binary files a/launcher/resources/multimc/64x64/viewfolder.png and b/launcher/resources/multimc/64x64/viewfolder.png differ diff --git a/launcher/resources/multimc/64x64/worlds.png b/launcher/resources/multimc/64x64/worlds.png index 1d40f1df7..25aa1d685 100644 Binary files a/launcher/resources/multimc/64x64/worlds.png and b/launcher/resources/multimc/64x64/worlds.png differ diff --git a/launcher/resources/multimc/8x8/noaccount.png b/launcher/resources/multimc/8x8/noaccount.png index 466e4c076..645ea1bed 100644 Binary files a/launcher/resources/multimc/8x8/noaccount.png and b/launcher/resources/multimc/8x8/noaccount.png differ diff --git a/launcher/resources/multimc/index.theme b/launcher/resources/multimc/index.theme index 4da8072d9..497106d6f 100644 --- a/launcher/resources/multimc/index.theme +++ b/launcher/resources/multimc/index.theme @@ -1,7 +1,6 @@ [Icon Theme] Name=Legacy Comment=Default Icons -Inherits=default Directories=8x8,16x16,22x22,24x24,32x32,32x32/instances,48x48,50x50/instances,64x64,128x128/instances,256x256,scalable,scalable/instances [8x8] diff --git a/launcher/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/atlauncher-placeholder.png b/launcher/resources/multimc/scalable/atlauncher-placeholder.png index f4314c434..8b6dedad5 100644 Binary files a/launcher/resources/multimc/scalable/atlauncher-placeholder.png and b/launcher/resources/multimc/scalable/atlauncher-placeholder.png differ 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/resources/sources/burfcat_hat.png b/launcher/resources/sources/burfcat_hat.png index a378c1fbb..6abf17820 100644 Binary files a/launcher/resources/sources/burfcat_hat.png and b/launcher/resources/sources/burfcat_hat.png differ 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 7ed672eb7..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,48 +47,39 @@ #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()); + auto file = new QFile(m_fileInfo.absoluteFilePath(), this); if (!file->open(QFile::ReadOnly)) { emitFailed(); return nullptr; } - QHttpMultiPart* multipart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + QHttpMultiPart* multipart = new QHttpMultiPart(QHttpMultiPart::FormDataType, this); file->setParent(multipart); 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/INIFile.cpp b/launcher/settings/INIFile.cpp index 4fb11ed35..2c7620e65 100644 --- a/launcher/settings/INIFile.cpp +++ b/launcher/settings/INIFile.cpp @@ -39,7 +39,6 @@ #include #include -#include #include #include #include @@ -54,6 +53,7 @@ bool INIFile::saveFile(QString fileName) insert("ConfigVersion", "1.2"); QSettings _settings_obj{ fileName, QSettings::Format::IniFormat }; _settings_obj.setFallbacksEnabled(false); + _settings_obj.clear(); for (Iterator iter = begin(); iter != end(); iter++) _settings_obj.setValue(iter.key(), iter.value()); 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 6cfd864cc..ad2a14c42 100644 --- a/launcher/tasks/ConcurrentTask.cpp +++ b/launcher/tasks/ConcurrentTask.cpp @@ -35,21 +35,19 @@ */ #include "ConcurrentTask.h" -#include #include #include "tasks/Task.h" -ConcurrentTask::ConcurrentTask(QObject* parent, QString task_name, int max_concurrent) - : Task(parent), m_name(task_name), m_total_max_size(max_concurrent) +ConcurrentTask::ConcurrentTask(QString task_name, int max_concurrent) : Task(), m_total_max_size(max_concurrent) { setObjectName(task_name); } ConcurrentTask::~ConcurrentTask() { - for (auto task : m_queue) { + for (auto task : m_doing) { if (task) - task->deleteLater(); + task->disconnect(this); } } @@ -65,15 +63,13 @@ void ConcurrentTask::addTask(Task::Ptr task) void ConcurrentTask::executeTask() { - // Start one task, startNext handles starting the up to the m_total_max_size - // while tracking the number currently being done - QMetaObject::invokeMethod(this, &ConcurrentTask::startNext, Qt::QueuedConnection); + for (auto i = 0; i < m_total_max_size; i++) + QMetaObject::invokeMethod(this, &ConcurrentTask::executeNextSubTask, Qt::QueuedConnection); } bool ConcurrentTask::abort() { m_queue.clear(); - m_aborted = true; if (m_doing.isEmpty()) { // Don't call emitAborted() here, we want to bypass the 'is the task running' check @@ -107,88 +103,81 @@ void ConcurrentTask::clear() m_done.clear(); m_failed.clear(); m_queue.clear(); - - m_aborted = false; + m_task_progress.clear(); m_progress = 0; - m_stepProgress = 0; } -void ConcurrentTask::startNext() +void ConcurrentTask::executeNextSubTask() { - if (m_aborted || m_doing.count() > m_total_max_size) + if (!isRunning()) { return; - - if (m_queue.isEmpty() && m_doing.isEmpty() && !wasSuccessful()) { - emitSucceeded(); + } + if (m_doing.count() >= m_total_max_size) { + return; + } + if (m_queue.isEmpty()) { + if (m_doing.isEmpty()) { + if (m_failed.isEmpty()) + emitSucceeded(); + else + emitFailed(tr("One or more subtasks failed")); + } return; } - if (m_queue.isEmpty()) - return; - - Task::Ptr next = m_queue.dequeue(); + startSubTask(m_queue.dequeue()); +} +void ConcurrentTask::startSubTask(Task::Ptr next) +{ connect(next.get(), &Task::succeeded, this, [this, next]() { subTaskSucceeded(next); }); connect(next.get(), &Task::failed, this, [this, next](QString msg) { subTaskFailed(next, msg); }); + // this should never happen but if it does, it's better to fail the task than get stuck connect(next.get(), &Task::aborted, this, [this, next] { subTaskFailed(next, "Aborted"); }); 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); }); m_doing.insert(next.get(), next); - qsizetype num_starts = qMin(m_queue.size(), m_total_max_size - m_doing.size()); + auto task_progress = std::make_shared(next->getUid()); m_task_progress.insert(next->getUid(), task_progress); updateState(); - updateStepProgress(*task_progress.get(), Operation::ADDED); - - QCoreApplication::processEvents(); QMetaObject::invokeMethod(next.get(), &Task::start, Qt::QueuedConnection); +} - // Allow going up the number of concurrent tasks in case of tasks being added in the middle of a running task. - for (int i = 0; i < num_starts; i++) - QMetaObject::invokeMethod(this, &ConcurrentTask::startNext, Qt::QueuedConnection); +void ConcurrentTask::subTaskFinished(Task::Ptr task, TaskStepState state) +{ + m_done.insert(task.get(), task); + (state == TaskStepState::Succeeded ? m_succeeded : m_failed).insert(task.get(), task); + + m_doing.remove(task.get()); + + 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); + updateState(); + QMetaObject::invokeMethod(this, &ConcurrentTask::executeNextSubTask, Qt::QueuedConnection); } void ConcurrentTask::subTaskSucceeded(Task::Ptr task) { - m_done.insert(task.get(), task); - m_succeeded.insert(task.get(), task); - - m_doing.remove(task.get()); - auto task_progress = m_task_progress.value(task->getUid()); - task_progress->state = TaskStepState::Succeeded; - - disconnect(task.get(), 0, this, 0); - - emit stepProgress(*task_progress); - updateState(); - updateStepProgress(*task_progress, Operation::REMOVED); - startNext(); + subTaskFinished(task, TaskStepState::Succeeded); } void ConcurrentTask::subTaskFailed(Task::Ptr task, [[maybe_unused]] const QString& msg) { - m_done.insert(task.get(), task); - m_failed.insert(task.get(), task); - - m_doing.remove(task.get()); - - auto task_progress = m_task_progress.value(task->getUid()); - task_progress->state = TaskStepState::Failed; - - disconnect(task.get(), 0, this, 0); - - emit stepProgress(*task_progress); - updateState(); - updateStepProgress(*task_progress, Operation::REMOVED); - startNext(); + subTaskFinished(task, TaskStepState::Failed); } void ConcurrentTask::subTaskStatus(Task::Ptr task, const QString& msg) @@ -224,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) { @@ -232,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) { @@ -285,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 00b1d48d6..d988623b9 100644 --- a/launcher/tasks/ConcurrentTask.h +++ b/launcher/tasks/ConcurrentTask.h @@ -48,7 +48,7 @@ class ConcurrentTask : public Task { public: using Ptr = shared_qobject_ptr; - explicit ConcurrentTask(QObject* parent = nullptr, QString task_name = "", int max_concurrent = 6); + explicit ConcurrentTask(QString task_name = "", int max_concurrent = 6); ~ConcurrentTask() override; // safe to call before starting the task @@ -72,28 +72,24 @@ class ConcurrentTask : public Task { protected slots: void executeTask() override; - virtual void startNext(); + virtual void executeNextSubTask(); void subTaskSucceeded(Task::Ptr); - void subTaskFailed(Task::Ptr, const QString& msg); + virtual void subTaskFailed(Task::Ptr, const QString& msg); + void subTaskFinished(Task::Ptr, TaskStepState); 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(); - protected: - QString m_name; - QString m_step_status; + void startSubTask(Task::Ptr task); + protected: QQueue m_queue; QHash m_doing; @@ -104,9 +100,4 @@ class ConcurrentTask : public Task { QHash> m_task_progress; int m_total_max_size; - - qint64 m_stepProgress = 0; - qint64 m_stepTotalProgress = 100; - - bool m_aborted = false; }; diff --git a/launcher/tasks/MultipleOptionsTask.cpp b/launcher/tasks/MultipleOptionsTask.cpp index 89187a26d..ba0c23542 100644 --- a/launcher/tasks/MultipleOptionsTask.cpp +++ b/launcher/tasks/MultipleOptionsTask.cpp @@ -36,9 +36,9 @@ #include -MultipleOptionsTask::MultipleOptionsTask(QObject* parent, const QString& task_name) : SequentialTask(parent, task_name) {} +MultipleOptionsTask::MultipleOptionsTask(const QString& task_name) : ConcurrentTask(task_name, 1) {} -void MultipleOptionsTask::startNext() +void MultipleOptionsTask::executeNextSubTask() { if (m_done.size() != m_failed.size()) { emitSucceeded(); @@ -51,7 +51,7 @@ void MultipleOptionsTask::startNext() return; } - ConcurrentTask::startNext(); + ConcurrentTask::executeNextSubTask(); } void MultipleOptionsTask::updateState() diff --git a/launcher/tasks/MultipleOptionsTask.h b/launcher/tasks/MultipleOptionsTask.h index a344343ef..7a19ed6ad 100644 --- a/launcher/tasks/MultipleOptionsTask.h +++ b/launcher/tasks/MultipleOptionsTask.h @@ -34,18 +34,18 @@ */ #pragma once -#include "SequentialTask.h" +#include "ConcurrentTask.h" /* This task type will attempt to do run each of it's subtasks in sequence, * until one of them succeeds. When that happens, the remaining tasks will not run. * */ -class MultipleOptionsTask : public SequentialTask { +class MultipleOptionsTask : public ConcurrentTask { Q_OBJECT public: - explicit MultipleOptionsTask(QObject* parent = nullptr, const QString& task_name = ""); + explicit MultipleOptionsTask(const QString& task_name = ""); ~MultipleOptionsTask() override = default; private slots: - void startNext() override; + void executeNextSubTask() override; void updateState() override; }; diff --git a/launcher/tasks/SequentialTask.cpp b/launcher/tasks/SequentialTask.cpp index abf7536b9..2e48414f2 100644 --- a/launcher/tasks/SequentialTask.cpp +++ b/launcher/tasks/SequentialTask.cpp @@ -36,18 +36,15 @@ #include "SequentialTask.h" #include +#include "tasks/ConcurrentTask.h" -SequentialTask::SequentialTask(QObject* parent, QString task_name) : ConcurrentTask(parent, task_name, 1) {} +SequentialTask::SequentialTask(QString task_name) : ConcurrentTask(task_name, 1) {} -void SequentialTask::startNext() +void SequentialTask::subTaskFailed(Task::Ptr task, const QString& msg) { - if (m_failed.size() > 0) { - emitFailed(tr("One of the tasks failed!")); - qWarning() << m_failed.constBegin()->get()->failReason(); - return; - } - - ConcurrentTask::startNext(); + emitFailed(msg); + qWarning() << msg; + ConcurrentTask::subTaskFailed(task, msg); } void SequentialTask::updateState() diff --git a/launcher/tasks/SequentialTask.h b/launcher/tasks/SequentialTask.h index cec3b2be8..77cd4387f 100644 --- a/launcher/tasks/SequentialTask.h +++ b/launcher/tasks/SequentialTask.h @@ -47,10 +47,12 @@ class SequentialTask : public ConcurrentTask { Q_OBJECT public: - explicit SequentialTask(QObject* parent = nullptr, QString task_name = ""); + explicit SequentialTask(QString task_name = ""); ~SequentialTask() override = default; + protected slots: + virtual void subTaskFailed(Task::Ptr, const QString& msg) override; + protected: - void startNext() override; void updateState() override; }; diff --git a/launcher/tasks/Task.cpp b/launcher/tasks/Task.cpp index b17096ca7..1871c5df8 100644 --- a/launcher/tasks/Task.cpp +++ b/launcher/tasks/Task.cpp @@ -40,7 +40,7 @@ Q_LOGGING_CATEGORY(taskLogC, "launcher.task") -Task::Task(QObject* parent, bool show_debug) : QObject(parent), m_show_debug(show_debug) +Task::Task(bool show_debug) : m_show_debug(show_debug) { m_uid = QUuid::createUuid(); setAutoDelete(false); diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index 883408c97..e712700a1 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -87,7 +87,7 @@ class Task : public QObject, public QRunnable { enum class State { Inactive, Running, Succeeded, Failed, AbortedByUser }; public: - explicit Task(QObject* parent = 0, bool show_debug_log = true); + explicit Task(bool show_debug_log = true); virtual ~Task() = default; bool isRunning() const; diff --git a/launcher/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..429ead47d 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -419,7 +419,7 @@ int TranslationsModel::columnCount([[maybe_unused]] const QModelIndex& parent) c QVector::Iterator TranslationsModel::findLanguage(const QString& key) { - return std::find_if(d->m_languages.begin(), d->m_languages.end(), [&](Language& lang) { return lang.key == key; }); + return std::find_if(d->m_languages.begin(), d->m_languages.end(), [key](Language& lang) { return lang.key == key; }); } std::optional TranslationsModel::findLanguageAsOptional(const QString& key) @@ -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/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp index 584a34710..d53ade86d 100644 --- a/launcher/ui/GuiUtil.cpp +++ b/launcher/ui/GuiUtil.cpp @@ -51,11 +51,35 @@ #include #include "Application.h" +constexpr int MaxMclogsLines = 25000; +constexpr int InitialMclogsLines = 10000; +constexpr int FinalMclogsLines = 14900; + +QString truncateLogForMclogs(const QString& logContent) +{ + QStringList lines = logContent.split("\n"); + if (lines.size() > MaxMclogsLines) { + QString truncatedLog = lines.mid(0, InitialMclogsLines).join("\n"); + truncatedLog += + "\n\n\n\n\n\n\n\n\n\n" + "------------------------------------------------------------\n" + "----------------------- Log truncated ----------------------\n" + "------------------------------------------------------------\n" + "----- Middle portion omitted to fit mclo.gs size limits ----\n" + "------------------------------------------------------------\n" + "\n\n\n\n\n\n\n\n\n\n"; + truncatedLog += lines.mid(lines.size() - FinalMclogsLines - 1).join("\n"); + return truncatedLog; + } + return logContent; +} + std::optional GuiUtil::uploadPaste(const QString& name, const QString& text, QWidget* parentWidget) { ProgressDialog dialog(parentWidget); auto pasteTypeSetting = static_cast(APPLICATION->settings()->get("PastebinType").toInt()); auto pasteCustomAPIBaseSetting = APPLICATION->settings()->get("PastebinCustomAPIBase").toString(); + bool shouldTruncate = false; { QUrl baseUrl; @@ -75,10 +99,36 @@ std::optional GuiUtil::uploadPaste(const QString& name, const QString& if (response != QMessageBox::Yes) return {}; + + if (baseUrl.toString() == "https://api.mclo.gs" && text.count("\n") > MaxMclogsLines) { + auto truncateResponse = CustomMessageBox::selectable( + parentWidget, QObject::tr("Confirm Truncation"), + QObject::tr("The log has %1 lines, exceeding mclo.gs' limit of %2.\n" + "The launcher can keep the first %3 and last %4 lines, trimming the middle.\n\n" + "If you choose 'No', mclo.gs will only keep the first %2 lines, cutting off " + "potentially useful info like crashes at the end.\n\n" + "Proceed with truncation?") + .arg(text.count("\n")) + .arg(MaxMclogsLines) + .arg(InitialMclogsLines) + .arg(FinalMclogsLines), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No) + ->exec(); + + if (truncateResponse == QMessageBox::Cancel) { + return {}; + } + shouldTruncate = truncateResponse == QMessageBox::Yes; + } } } - std::unique_ptr paste(new PasteUpload(parentWidget, text, pasteCustomAPIBaseSetting, pasteTypeSetting)); + QString textToUpload = text; + if (shouldTruncate) { + textToUpload = truncateLogForMclogs(text); + } + + std::unique_ptr paste(new PasteUpload(parentWidget, textToUpload, pasteCustomAPIBaseSetting, pasteTypeSetting)); dialog.execWithTask(paste.get()); if (!paste->wasSuccessful()) { @@ -112,7 +162,7 @@ static QStringList BrowseForFileInternal(QString context, QFileDialog w(parentWidget, caption); QSet locations; - auto f = [&](QStandardPaths::StandardLocation l) { + auto f = [&locations](QStandardPaths::StandardLocation l) { QString location = QStandardPaths::writableLocation(l); QFileInfo finfo(location); if (!finfo.exists()) { diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 7b4d1c8a5..ee12eaa2d 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" @@ -156,7 +154,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi // Qt doesn't like vertical moving toolbars, so we have to force them... // See https://github.com/PolyMC/PolyMC/issues/493 connect(ui->instanceToolBar, &QToolBar::orientationChanged, - [=](Qt::Orientation) { ui->instanceToolBar->setOrientation(Qt::Vertical); }); + [this](Qt::Orientation) { ui->instanceToolBar->setOrientation(Qt::Vertical); }); // if you try to add a widget to a toolbar in a .ui file // qt designer will delete it when you save the file >:( @@ -186,6 +184,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi ui->instanceToolBar->addContextMenuAction(ui->newsToolBar->toggleViewAction()); ui->instanceToolBar->addContextMenuAction(ui->instanceToolBar->toggleViewAction()); + ui->instanceToolBar->addContextMenuAction(ui->actionToggleStatusBar); ui->instanceToolBar->addContextMenuAction(ui->actionLockToolbars); } @@ -208,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); } @@ -230,10 +228,13 @@ 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 @@ -319,6 +320,14 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi setCatBackground(cat_enable); } + // Togglable status bar + { + bool statusBarVisible = APPLICATION->settings()->get("StatusBarVisible").toBool(); + ui->actionToggleStatusBar->setChecked(statusBarVisible); + connect(ui->actionToggleStatusBar, &QAction::toggled, this, &MainWindow::setStatusBarVisibility); + setStatusBarVisibility(statusBarVisible); + } + // Lock toolbars { bool toolbarsLocked = APPLICATION->settings()->get("ToolbarsLocked").toBool(); @@ -451,10 +460,16 @@ QMenu* MainWindow::createPopupMenu() QMenu* filteredMenu = QMainWindow::createPopupMenu(); filteredMenu->removeAction(ui->mainToolBar->toggleViewAction()); + filteredMenu->addAction(ui->actionToggleStatusBar); filteredMenu->addAction(ui->actionLockToolbars); return filteredMenu; } +void MainWindow::setStatusBarVisibility(bool state) +{ + statusBar()->setVisible(state); + APPLICATION->settings()->set("StatusBarVisible", state); +} void MainWindow::lockToolbars(bool state) { ui->mainToolBar->setMovable(!state); @@ -855,30 +870,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; @@ -983,6 +974,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; } @@ -1028,6 +1027,14 @@ void MainWindow::processURLs(QList urls) continue; } + if (APPLICATION->instances()->count() <= 0) { + CustomMessageBox::selectable(this, tr("No instance!"), + tr("No instance available to add the resource to.\nPlease create a new instance before " + "attempting to install this resource again."), + QMessageBox::Critical) + ->show(); + continue; + } ImportResourceDialog dlg(localFileName, type, this); if (dlg.exec() != QDialog::Accepted) @@ -1049,7 +1056,7 @@ void MainWindow::processURLs(QList urls) qWarning() << "Importing of Data Packs not supported at this time. Ignoring" << localFileName; break; case PackedResourceType::Mod: - minecraftInst->loaderModList()->installMod(localFileName, version); + minecraftInst->loaderModList()->installResourceWithFlameMetadata(localFileName, version); break; case PackedResourceType::ShaderPack: minecraftInst->shaderPackList()->installResource(localFileName); @@ -1182,43 +1189,53 @@ void MainWindow::undoTrashInstance() void MainWindow::on_actionViewLauncherRootFolder_triggered() { - DesktopServices::openDirectory("."); + DesktopServices::openPath("."); } void MainWindow::on_actionViewInstanceFolder_triggered() { QString str = APPLICATION->settings()->get("InstanceDir").toString(); - DesktopServices::openDirectory(str); + DesktopServices::openPath(str); } void MainWindow::on_actionViewCentralModsFolder_triggered() { - DesktopServices::openDirectory(APPLICATION->settings()->get("CentralModsDir").toString(), true); + 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::openDirectory(APPLICATION->themeManager()->getIconThemesFolder().path(), true); + DesktopServices::openPath(APPLICATION->themeManager()->getIconThemesFolder().path(), true); } void MainWindow::on_actionViewWidgetThemeFolder_triggered() { - DesktopServices::openDirectory(APPLICATION->themeManager()->getApplicationThemesFolder().path(), true); + DesktopServices::openPath(APPLICATION->themeManager()->getApplicationThemesFolder().path(), true); } void MainWindow::on_actionViewCatPackFolder_triggered() { - DesktopServices::openDirectory(APPLICATION->themeManager()->getCatPacksFolder().path(), true); + DesktopServices::openPath(APPLICATION->themeManager()->getCatPacksFolder().path(), true); } void MainWindow::on_actionViewIconsFolder_triggered() { - DesktopServices::openDirectory(APPLICATION->icons()->getDirectory(), true); + DesktopServices::openPath(APPLICATION->icons()->getDirectory(), true); } void MainWindow::on_actionViewLogsFolder_triggered() { - DesktopServices::openDirectory("logs", true); + DesktopServices::openPath("logs", true); +} + +void MainWindow::on_actionViewJavaFolder_triggered() +{ + DesktopServices::openPath(APPLICATION->javaPath(), true); } void MainWindow::refreshInstances() @@ -1400,14 +1417,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) { @@ -1437,7 +1446,7 @@ void MainWindow::on_actionViewSelectedInstFolder_triggered() { if (m_selectedInstance) { QString str = m_selectedInstance->instanceRoot(); - DesktopServices::openDirectory(QDir(str).absolutePath()); + DesktopServices::openPath(QFileInfo(str)); } } @@ -1574,7 +1583,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 d2e154643..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(); @@ -205,6 +206,8 @@ class MainWindow : public QMainWindow { void globalSettingsClosed(); + void setStatusBarVisibility(bool); + void lockToolbars(bool); #ifndef Q_OS_MAC @@ -226,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 1ee3a5632..f20c34206 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -131,7 +131,7 @@ 0 0 800 - 20 + 27 @@ -176,6 +176,7 @@ + @@ -190,6 +191,8 @@ + + @@ -257,6 +260,14 @@ It's a fluffy kitty :3 + + + true + + + Status Bar + + true @@ -482,15 +493,6 @@ CurseForge (zip) - - - - .. - - - Mod List - - @@ -578,6 +580,18 @@ Open the central mods folder in a file browser. + + + + .. + + + &Skins + + + Open the skins folder in a file browser. + + @@ -775,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 cee01e821..a8d60aef1 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 @@ -76,7 +77,7 @@ QString getCreditsHtml() stream << QString("

    d-513 %1

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

    txtsd %1

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

    timoreo %1

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

    Ezekiel Smith (ZekeSmith) %1

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

    ZekeZ %1

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

    cozyGalvinism %1

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

    DioEgizio %1

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

    flowln %1

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

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

    \n"; stream << QString("

    Boba %1

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

    Davi Rafael %1

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

    AutiOne %1

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

    Fulmine %1

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

    ely %1

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

    gon sawa %1

    \n").arg(getGitHub("gonsawa")); @@ -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); @@ -174,8 +175,7 @@ AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent), ui(new Ui::AboutDia QString urlText("

    %1

    "); ui->urlLabel->setText(urlText.arg(BuildConfig.LAUNCHER_GIT)); - QString copyText("© 2022-2023 %1"); - ui->copyLabel->setText(copyText.arg(BuildConfig.LAUNCHER_COPYRIGHT)); + ui->copyLabel->setText(BuildConfig.LAUNCHER_COPYRIGHT); connect(ui->closeButton, SIGNAL(clicked()), SLOT(close())); diff --git a/launcher/ui/dialogs/AboutDialog.h b/launcher/ui/dialogs/AboutDialog.h index 356f005e0..5da686b5f 100644 --- a/launcher/ui/dialogs/AboutDialog.h +++ b/launcher/ui/dialogs/AboutDialog.h @@ -15,7 +15,6 @@ #pragma once -#include #include namespace Ui { @@ -31,7 +30,4 @@ class AboutDialog : public QDialog { private: Ui::AboutDialog* ui; - - NetJob::Ptr netJob; - QByteArray dataSink; }; diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index 7a5a16818..0095f7af9 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -40,16 +40,19 @@ #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) { m_hashing_task = shared_qobject_ptr( - new ConcurrentTask(this, "MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); + new ConcurrentTask("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); connect(m_hashing_task.get(), &Task::finished, this, &BlockedModsDialog::hashTaskFinished); ui->setupUi(this); + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); m_openMissingButton = ui->buttonBox->addButton(tr("Open Missing"), QDialogButtonBox::ActionRole); connect(m_openMissingButton, &QPushButton::clicked, this, [this]() { openAll(true); }); @@ -60,8 +63,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 +166,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 +203,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 +268,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; @@ -275,6 +288,8 @@ void BlockedModsDialog::checkMatchHash(QString hash, QString path) qDebug() << "[Blocked Mods Dialog] Checking for match on hash: " << hash << "| From path:" << path; + auto downloadDir = QFileInfo(APPLICATION->settings()->get("DownloadsDir").toString()).absoluteFilePath(); + auto moveFiles = APPLICATION->settings()->get("MoveModsFromDownloadsDir").toBool(); for (auto& mod : m_mods) { if (mod.matched) { continue; @@ -282,6 +297,9 @@ void BlockedModsDialog::checkMatchHash(QString hash, QString path) if (mod.hash.compare(hash, Qt::CaseInsensitive) == 0) { mod.matched = true; mod.localPath = path; + if (moveFiles) { + mod.move = QFileInfo(path).absoluteFilePath().startsWith(downloadDir); + } match = true; qDebug() << "[Blocked Mods Dialog] Hash match found:" << mod.name << hash << "| From path:" << path; @@ -333,6 +351,8 @@ bool BlockedModsDialog::checkValidPath(QString path) return fsName.compare(metaName) == 0; }; + auto downloadDir = QFileInfo(APPLICATION->settings()->get("DownloadsDir").toString()).absoluteFilePath(); + auto moveFiles = APPLICATION->settings()->get("MoveModsFromDownloadsDir").toBool(); for (auto& mod : m_mods) { if (compare(filename, mod.name)) { // if the mod is not yet matched and doesn't have a hash then @@ -340,6 +360,9 @@ bool BlockedModsDialog::checkValidPath(QString path) if (!mod.matched && mod.hash.isEmpty()) { mod.matched = true; mod.localPath = path; + if (moveFiles) { + mod.move = QFileInfo(path).absoluteFilePath().startsWith(downloadDir); + } return false; } qDebug() << "[Blocked Mods Dialog] Name match found:" << mod.name << "| From path:" << path; diff --git a/launcher/ui/dialogs/BlockedModsDialog.h b/launcher/ui/dialogs/BlockedModsDialog.h index 09722bce9..b2d2c0374 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.h +++ b/launcher/ui/dialogs/BlockedModsDialog.h @@ -42,6 +42,7 @@ struct BlockedMod { bool matched; QString localPath; QString targetFolder; + bool move = false; }; QT_BEGIN_NAMESPACE diff --git a/launcher/ui/dialogs/ChooseProviderDialog.cpp b/launcher/ui/dialogs/ChooseProviderDialog.cpp index 83748e1e2..68457802d 100644 --- a/launcher/ui/dialogs/ChooseProviderDialog.cpp +++ b/launcher/ui/dialogs/ChooseProviderDialog.cpp @@ -6,8 +6,6 @@ #include "modplatform/ModIndex.h" -static ModPlatform::ProviderCapabilities ProviderCaps; - ChooseProviderDialog::ChooseProviderDialog(QWidget* parent, bool single_choice, bool allow_skipping) : QDialog(parent), ui(new Ui::ChooseProviderDialog) { @@ -78,7 +76,7 @@ void ChooseProviderDialog::addProviders() QRadioButton* btn; for (auto& provider : { ModPlatform::ResourceProvider::MODRINTH, ModPlatform::ResourceProvider::FLAME }) { - btn = new QRadioButton(ProviderCaps.readableName(provider), this); + btn = new QRadioButton(ModPlatform::ProviderCapabilities::readableName(provider), this); m_providers.addButton(btn, btn_index++); ui->providersLayout->addWidget(btn); } diff --git a/launcher/ui/dialogs/CopyInstanceDialog.cpp b/launcher/ui/dialogs/CopyInstanceDialog.cpp index 770741a61..e5c2c301b 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.cpp +++ b/launcher/ui/dialogs/CopyInstanceDialog.cpp @@ -109,6 +109,9 @@ CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget* parent) auto HelpButton = ui->buttonBox->button(QDialogButtonBox::Help); connect(HelpButton, &QPushButton::clicked, this, &CopyInstanceDialog::help); + HelpButton->setText(tr("Help")); + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } CopyInstanceDialog::~CopyInstanceDialog() diff --git a/launcher/ui/dialogs/EditAccountDialog.cpp b/launcher/ui/dialogs/EditAccountDialog.cpp deleted file mode 100644 index 58036fd82..000000000 --- a/launcher/ui/dialogs/EditAccountDialog.cpp +++ /dev/null @@ -1,60 +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 "EditAccountDialog.h" -#include -#include -#include "ui_EditAccountDialog.h" - -EditAccountDialog::EditAccountDialog(const QString& text, QWidget* parent, int flags) : QDialog(parent), ui(new Ui::EditAccountDialog) -{ - ui->setupUi(this); - - ui->label->setText(text); - ui->label->setVisible(!text.isEmpty()); - - ui->userTextBox->setEnabled(flags & UsernameField); - ui->passTextBox->setEnabled(flags & PasswordField); -} - -EditAccountDialog::~EditAccountDialog() -{ - delete ui; -} - -void EditAccountDialog::on_label_linkActivated(const QString& link) -{ - DesktopServices::openUrl(QUrl(link)); -} - -void EditAccountDialog::setUsername(const QString& user) const -{ - ui->userTextBox->setText(user); -} - -QString EditAccountDialog::username() const -{ - return ui->userTextBox->text(); -} - -void EditAccountDialog::setPassword(const QString& pass) const -{ - ui->passTextBox->setText(pass); -} - -QString EditAccountDialog::password() const -{ - return ui->passTextBox->text(); -} diff --git a/launcher/ui/dialogs/EditAccountDialog.h b/launcher/ui/dialogs/EditAccountDialog.h deleted file mode 100644 index 7a9ccba79..000000000 --- a/launcher/ui/dialogs/EditAccountDialog.h +++ /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. - */ - -#pragma once - -#include - -namespace Ui { -class EditAccountDialog; -} - -class EditAccountDialog : public QDialog { - Q_OBJECT - - public: - explicit EditAccountDialog(const QString& text = "", QWidget* parent = 0, int flags = UsernameField | PasswordField); - ~EditAccountDialog(); - - void setUsername(const QString& user) const; - void setPassword(const QString& pass) const; - - QString username() const; - QString password() const; - - enum Flags { - NoFlags = 0, - - //! Specifies that the dialog should have a username field. - UsernameField, - - //! Specifies that the dialog should have a password field. - PasswordField, - }; - - private slots: - void on_label_linkActivated(const QString& link); - - private: - Ui::EditAccountDialog* ui; -}; diff --git a/launcher/ui/dialogs/EditAccountDialog.ui b/launcher/ui/dialogs/EditAccountDialog.ui deleted file mode 100644 index e87509bcb..000000000 --- a/launcher/ui/dialogs/EditAccountDialog.ui +++ /dev/null @@ -1,94 +0,0 @@ - - - EditAccountDialog - - - - 0 - 0 - 400 - 148 - - - - Login - - - - - - Message label placeholder. - - - Qt::RichText - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Email - - - - - - - QLineEdit::Password - - - Password - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - buttonBox - accepted() - EditAccountDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - EditAccountDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp index 703736d68..d25cd32b6 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.cpp +++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -51,6 +51,7 @@ #include #include #include +#include #include #include #include @@ -85,6 +86,9 @@ ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget* parent auto headerView = ui->treeView->header(); headerView->setSectionResizeMode(QHeaderView::ResizeToContents); headerView->setSectionResizeMode(0, QHeaderView::Stretch); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ExportInstanceDialog::~ExportInstanceDialog() @@ -146,7 +150,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 5af24b1b7..879d19a53 100644 --- a/launcher/ui/dialogs/ExportPackDialog.cpp +++ b/launcher/ui/dialogs/ExportPackDialog.cpp @@ -47,11 +47,18 @@ ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPla if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { setWindowTitle(tr("Export Modrinth Pack")); - ui->summary->setText(instance->settings()->get("ExportSummary").toString()); + + ui->authorLabel->hide(); + ui->author->hide(); + + ui->summary->setPlainText(instance->settings()->get("ExportSummary").toString()); } else { setWindowTitle(tr("Export CurseForge Pack")); - ui->summaryLabel->setText(tr("&Author")); - ui->summary->setText(instance->settings()->get("ExportAuthor").toString()); + + ui->summaryLabel->hide(); + ui->summary->hide(); + + ui->author->setText(instance->settings()->get("ExportAuthor").toString()); } // ensure a valid pack is generated @@ -81,9 +88,9 @@ ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPla MinecraftInstance* mcInstance = dynamic_cast(instance.get()); if (mcInstance) { - const QDir index = mcInstance->loaderModList()->indexDir(); - if (index.exists()) - proxy->ignoreFilesWithPath().insert(root.relativeFilePath(index.absolutePath())); + for (auto& resourceModel : mcInstance->resourceLists()) + if (resourceModel->indexDir().exists()) + proxy->ignoreFilesWithPath().insert(root.relativeFilePath(resourceModel->indexDir().absolutePath())); } ui->files->setModel(proxy); @@ -96,6 +103,9 @@ ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPla QHeaderView* headerView = ui->files->header(); headerView->setSectionResizeMode(QHeaderView::ResizeToContents); headerView->setSectionResizeMode(0, QHeaderView::Stretch); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ExportPackDialog::~ExportPackDialog() @@ -108,9 +118,13 @@ void ExportPackDialog::done(int result) auto settings = instance->settings(); settings->set("ExportName", ui->name->text()); settings->set("ExportVersion", ui->version->text()); - settings->set(m_provider == ModPlatform::ResourceProvider::FLAME ? "ExportAuthor" : "ExportSummary", ui->summary->text()); settings->set("ExportOptionalFiles", ui->optionalFiles->isChecked()); + if (m_provider == ModPlatform::ResourceProvider::MODRINTH) + settings->set("ExportSummary", ui->summary->toPlainText()); + else + settings->set("ExportAuthor", ui->author->text()); + if (result == Accepted) { const QString name = ui->name->text().isEmpty() ? instance->name() : ui->name->text(); const QString filename = FS::RemoveInvalidFilenameChars(name); @@ -118,14 +132,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")) @@ -134,10 +148,10 @@ void ExportPackDialog::done(int result) Task* task; if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { - task = new ModrinthPackExportTask(name, ui->version->text(), ui->summary->text(), ui->optionalFiles->isChecked(), instance, - output, std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1)); + task = new ModrinthPackExportTask(name, ui->version->text(), ui->summary->toPlainText(), ui->optionalFiles->isChecked(), + instance, output, std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1)); } else { - task = new FlamePackExportTask(name, ui->version->text(), ui->summary->text(), ui->optionalFiles->isChecked(), instance, output, + task = new FlamePackExportTask(name, ui->version->text(), ui->author->text(), ui->optionalFiles->isChecked(), instance, output, std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1)); } diff --git a/launcher/ui/dialogs/ExportPackDialog.ui b/launcher/ui/dialogs/ExportPackDialog.ui index 09dea72a8..a4a174212 100644 --- a/launcher/ui/dialogs/ExportPackDialog.ui +++ b/launcher/ui/dialogs/ExportPackDialog.ui @@ -7,7 +7,7 @@ 0 0 650 - 510 + 532 @@ -19,21 +19,8 @@ &Description - - - - - &Summary - - - summary - - - - - - - + + &Name @@ -43,7 +30,10 @@ - + + + + &Version @@ -53,16 +43,43 @@ - - - - + 1.0.0 + + + + &Summary + + + summary + + + + + + + true + + + + + + + &Author + + + author + + + + + + @@ -124,6 +141,7 @@ name version summary + author files optionalFiles diff --git a/launcher/ui/dialogs/ExportToModListDialog.cpp b/launcher/ui/dialogs/ExportToModListDialog.cpp index a343f555a..c2ba68f7a 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,34 @@ 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(); }); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Save)->setText(tr("Save")); + triggerImp(); } ExportToModListDialog::~ExportToModListDialog() @@ -86,38 +81,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 +121,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 +132,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 +152,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 +160,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 +180,7 @@ void ExportToModListDialog::done(int result) QString ExportToModListDialog::extension() { - switch (format) { + switch (m_format) { case ExportToModList::HTML: return ".html"; case ExportToModList::MARKDOWN: @@ -197,7 +199,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 +211,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 +226,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/IconPickerDialog.cpp b/launcher/ui/dialogs/IconPickerDialog.cpp index faad3ce75..b6e928a3d 100644 --- a/launcher/ui/dialogs/IconPickerDialog.cpp +++ b/launcher/ui/dialogs/IconPickerDialog.cpp @@ -15,7 +15,9 @@ #include #include +#include #include +#include #include "Application.h" @@ -33,6 +35,15 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui ui->setupUi(this); setWindowModality(Qt::WindowModal); + searchBar = new QLineEdit(this); + searchBar->setPlaceholderText(tr("Search...")); + ui->verticalLayout->insertWidget(0, searchBar); + + proxyModel = new QSortFilterProxyModel(this); + proxyModel->setSourceModel(APPLICATION->icons().get()); + proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + ui->iconView->setModel(proxyModel); + auto contentsWidget = ui->iconView; contentsWidget->setViewMode(QListView::IconMode); contentsWidget->setFlow(QListView::LeftToRight); @@ -57,12 +68,15 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui contentsWidget->installEventFilter(this); - contentsWidget->setModel(APPLICATION->icons().get()); + contentsWidget->setModel(proxyModel); // NOTE: ResetRole forces the button to be on the left, while the OK/Cancel ones are on the right. We win. auto buttonAdd = ui->buttonBox->addButton(tr("Add Icon"), QDialogButtonBox::ResetRole); buttonRemove = ui->buttonBox->addButton(tr("Remove Icon"), QDialogButtonBox::ResetRole); + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); + connect(buttonAdd, SIGNAL(clicked(bool)), SLOT(addNewIcon())); connect(buttonRemove, SIGNAL(clicked(bool)), SLOT(removeSelectedIcon())); @@ -73,6 +87,9 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui auto buttonFolder = ui->buttonBox->addButton(tr("Open Folder"), QDialogButtonBox::ResetRole); connect(buttonFolder, &QPushButton::clicked, this, &IconPickerDialog::openFolder); + connect(searchBar, &QLineEdit::textChanged, this, &IconPickerDialog::filterIcons); + // Prevent incorrect indices from e.g. filesystem changes + connect(APPLICATION->icons().get(), &IconList::iconUpdated, this, [this]() { proxyModel->invalidate(); }); } bool IconPickerDialog::eventFilter(QObject* obj, QEvent* evt) @@ -159,5 +176,10 @@ IconPickerDialog::~IconPickerDialog() void IconPickerDialog::openFolder() { - DesktopServices::openDirectory(APPLICATION->icons()->getDirectory(), true); + DesktopServices::openPath(APPLICATION->icons()->iconDirectory(selectedIconKey), true); } + +void IconPickerDialog::filterIcons(const QString& query) +{ + proxyModel->setFilterFixedString(query); +} \ No newline at end of file diff --git a/launcher/ui/dialogs/IconPickerDialog.h b/launcher/ui/dialogs/IconPickerDialog.h index 37e53dcce..db1315338 100644 --- a/launcher/ui/dialogs/IconPickerDialog.h +++ b/launcher/ui/dialogs/IconPickerDialog.h @@ -16,6 +16,8 @@ #pragma once #include #include +#include +#include namespace Ui { class IconPickerDialog; @@ -36,6 +38,8 @@ class IconPickerDialog : public QDialog { private: Ui::IconPickerDialog* ui; QPushButton* buttonRemove; + QLineEdit* searchBar; + QSortFilterProxyModel* proxyModel; private slots: void selectionChanged(QItemSelection, QItemSelection); @@ -44,4 +48,5 @@ class IconPickerDialog : public QDialog { void addNewIcon(); void removeSelectedIcon(); void openFolder(); + void filterIcons(const QString& text); }; diff --git a/launcher/ui/dialogs/ImportResourceDialog.cpp b/launcher/ui/dialogs/ImportResourceDialog.cpp index 84b692730..e3a1e9a6c 100644 --- a/launcher/ui/dialogs/ImportResourceDialog.cpp +++ b/launcher/ui/dialogs/ImportResourceDialog.cpp @@ -45,6 +45,9 @@ ImportResourceDialog::ImportResourceDialog(QString file_path, PackedResourceType ui->label->setText( tr("Choose the instance you would like to import this %1 to.").arg(ResourceUtils::getPackedTypeName(m_resource_type))); ui->label_file_path->setText(tr("File: %1").arg(m_file_path)); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } void ImportResourceDialog::activated(QModelIndex index) diff --git a/launcher/ui/dialogs/InstallLoaderDialog.cpp b/launcher/ui/dialogs/InstallLoaderDialog.cpp index 541119d10..7082125f2 100644 --- a/launcher/ui/dialogs/InstallLoaderDialog.cpp +++ b/launcher/ui/dialogs/InstallLoaderDialog.cpp @@ -31,6 +31,7 @@ #include "ui/widgets/VersionSelectWidget.h" class InstallLoaderPage : public VersionSelectWidget, public BasePage { + Q_OBJECT public: InstallLoaderPage(const QString& id, const QString& iconName, @@ -103,6 +104,8 @@ InstallLoaderDialog::InstallLoaderDialog(std::shared_ptr profile, c buttons->setOrientation(Qt::Horizontal); buttons->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); + buttons->button(QDialogButtonBox::Ok)->setText(tr("Ok")); + buttons->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); buttonLayout->addWidget(buttons); @@ -164,3 +167,4 @@ void InstallLoaderDialog::done(int result) QDialog::done(result); } +#include "InstallLoaderDialog.moc" \ No newline at end of file diff --git a/launcher/ui/dialogs/MSALoginDialog.cpp b/launcher/ui/dialogs/MSALoginDialog.cpp index 7df423412..40d1eff1e 100644 --- a/launcher/ui/dialogs/MSALoginDialog.cpp +++ b/launcher/ui/dialogs/MSALoginDialog.cpp @@ -34,43 +34,67 @@ */ #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()); + } + } + }); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); } 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::onAuthFlowStatus); + connect(m_authflow_task.get(), &AuthFlow::authorizeWithBrowser, this, &MSALoginDialog::authorizeWithBrowser); + connect(m_authflow_task.get(), &AuthFlow::authorizeWithBrowserWithExtra, this, &MSALoginDialog::authorizeWithBrowserWithExtra); + connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, m_authflow_task.get(), &Task::abort); + + m_devicecode_task.reset(new AuthFlow(m_account->accountData(), AuthFlow::Action::DeviceCode)); + 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::onDeviceFlowStatus); + connect(m_devicecode_task.get(), &AuthFlow::authorizeWithBrowser, this, &MSALoginDialog::authorizeWithBrowser); + connect(m_devicecode_task.get(), &AuthFlow::authorizeWithBrowserWithExtra, this, &MSALoginDialog::authorizeWithBrowserWithExtra); + connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, m_devicecode_task.get(), &Task::abort); + 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 +104,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 +119,59 @@ 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->stackedWidget2->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::onDeviceFlowStatus(QString status) { - ui->progressBar->setMaximum(total); - ui->progressBar->setValue(current); + ui->stackedWidget->setCurrentIndex(0); + ui->status->setText(status); +} + +void MSALoginDialog::onAuthFlowStatus(QString status) +{ + ui->stackedWidget2->setCurrentIndex(0); + ui->status2->setText(status); } // Public interface -MinecraftAccountPtr MSALoginDialog::newAccount(QWidget* parent, 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..375ccc57a 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,24 @@ 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 onDeviceFlowStatus(QString status); + void onAuthFlowStatus(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..69cd2e1ab 100644 --- a/launcher/ui/dialogs/MSALoginDialog.ui +++ b/launcher/ui/dialogs/MSALoginDialog.ui @@ -6,69 +6,422 @@ 0 0 - 491 - 143 + 440 + 447 - - - 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 + + + + + + + + - - - 24 - - - false - - - - - + - - - Open page and copy code + + + + 0 + 0 + + + + Qt::Horizontal - + + + + 16 + + + + Or + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + Qt::Horizontal - - QDialogButtonBox::Cancel - + + + + 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 + + + + + + + + + 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 + + + + + + + + + + + QDialogButtonBox::Cancel + + + diff --git a/launcher/ui/dialogs/NewComponentDialog.cpp b/launcher/ui/dialogs/NewComponentDialog.cpp index b47b85ff1..b5f8ff889 100644 --- a/launcher/ui/dialogs/NewComponentDialog.cpp +++ b/launcher/ui/dialogs/NewComponentDialog.cpp @@ -68,6 +68,9 @@ NewComponentDialog::NewComponentDialog(const QString& initialName, const QString ui->nameTextBox->setFocus(); + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); + originalPlaceholderText = ui->uidTextBox->placeholderText(); updateDialogState(); } diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp index 6f3f7f7ec..d9ea0aafb 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.cpp +++ b/launcher/ui/dialogs/NewInstanceDialog.cpp @@ -36,6 +36,7 @@ #include "NewInstanceDialog.h" #include "Application.h" +#include "ui/pages/modplatform/ModpackProviderBasePage.h" #include "ui/pages/modplatform/import_ftb/ImportFTBPage.h" #include "ui_NewInstanceDialog.h" @@ -52,6 +53,7 @@ #include #include #include +#include #include #include @@ -63,6 +65,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, @@ -97,22 +100,28 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, ui->verticalLayout->insertWidget(2, m_container); m_container->addButtons(m_buttons); + connect(m_container, &PageContainer::selectedPageChanged, this, [this](BasePage* previous, BasePage* selected) { + m_buttons->button(QDialogButtonBox::Ok)->setEnabled(creationTask && !instName().isEmpty()); + }); // Bonk Qt over its stupid head and make sure it understands which button is the default one... // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button auto OkButton = m_buttons->button(QDialogButtonBox::Ok); OkButton->setDefault(true); OkButton->setAutoDefault(true); + OkButton->setText(tr("OK")); connect(OkButton, &QPushButton::clicked, this, &NewInstanceDialog::accept); auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel); CancelButton->setDefault(false); CancelButton->setAutoDefault(false); + CancelButton->setText(tr("Cancel")); connect(CancelButton, &QPushButton::clicked, this, &NewInstanceDialog::reject); auto HelpButton = m_buttons->button(QDialogButtonBox::Help); HelpButton->setDefault(false); HelpButton->setAutoDefault(false); + HelpButton->setText(tr("Help")); connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help); if (!url.isEmpty()) { @@ -124,7 +133,19 @@ 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)); + } + + connect(m_container, &PageContainer::selectedPageChanged, this, &NewInstanceDialog::selectedPageChanged); } void NewInstanceDialog::reject() @@ -185,7 +206,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; } @@ -201,7 +222,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; } @@ -221,6 +242,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; @@ -298,3 +322,16 @@ void NewInstanceDialog::importIconNow() } APPLICATION->settings()->set("NewInstanceGeometry", saveGeometry().toBase64()); } + +void NewInstanceDialog::selectedPageChanged(BasePage* previous, BasePage* selected) +{ + auto prevPage = dynamic_cast(previous); + if (prevPage) { + m_searchTerm = prevPage->getSerachTerm(); + } + + auto nextPage = dynamic_cast(selected); + if (nextPage) { + nextPage->setSearchTerm(m_searchTerm); + } +} diff --git a/launcher/ui/dialogs/NewInstanceDialog.h b/launcher/ui/dialogs/NewInstanceDialog.h index 923579567..e97c9f543 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.h +++ b/launcher/ui/dialogs/NewInstanceDialog.h @@ -82,6 +82,7 @@ class NewInstanceDialog : public QDialog, public BasePageProvider { private slots: void on_iconButton_clicked(); void on_instNameTextBox_textChanged(const QString& arg1); + void selectedPageChanged(BasePage* previous, BasePage* selected); private: Ui::NewInstanceDialog* ui = nullptr; @@ -98,5 +99,7 @@ class NewInstanceDialog : public QDialog, public BasePageProvider { QString importVersion; + QString m_searchTerm; + void importIconNow(); }; diff --git a/launcher/ui/dialogs/OfflineLoginDialog.cpp b/launcher/ui/dialogs/OfflineLoginDialog.cpp index 137620be4..d8fbc04fd 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) @@ -11,6 +9,9 @@ OfflineLoginDialog::OfflineLoginDialog(QWidget* parent) : QDialog(parent), ui(ne ui->progressBar->setVisible(false); ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); } @@ -28,7 +29,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..95bdf99a9 100644 --- a/launcher/ui/dialogs/ProfileSelectDialog.cpp +++ b/launcher/ui/dialogs/ProfileSelectDialog.cpp @@ -18,9 +18,9 @@ #include #include +#include #include "Application.h" -#include "SkinUtils.h" #include "ui/dialogs/ProgressDialog.h" @@ -71,6 +71,9 @@ ProfileSelectDialog::ProfileSelectDialog(const QString& message, int flags, QWid ui->listView->setCurrentIndex(ui->listView->model()->index(0, 0)); connect(ui->listView, SIGNAL(doubleClicked(QModelIndex)), SLOT(on_buttonBox_accepted())); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ProfileSelectDialog::~ProfileSelectDialog() diff --git a/launcher/ui/dialogs/ProfileSetupDialog.cpp b/launcher/ui/dialogs/ProfileSetupDialog.cpp index 4b0c5b768..a6512784f 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) @@ -69,6 +70,9 @@ ProfileSetupDialog::ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidg connect(&checkStartTimer, &QTimer::timeout, this, &ProfileSetupDialog::startCheck); setNameStatus(NameStatus::NotSet, QString()); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ProfileSetupDialog::~ProfileSetupDialog() @@ -150,28 +154,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 +198,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 +249,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/ProfileSetupDialog.ui b/launcher/ui/dialogs/ProfileSetupDialog.ui index 9dbabb4b3..947110da7 100644 --- a/launcher/ui/dialogs/ProfileSetupDialog.ui +++ b/launcher/ui/dialogs/ProfileSetupDialog.ui @@ -30,6 +30,9 @@ Choose your name carefully: true + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + nameEdit diff --git a/launcher/ui/dialogs/ProgressDialog.cpp b/launcher/ui/dialogs/ProgressDialog.cpp index 0ca3a1bd9..9897687e3 100644 --- a/launcher/ui/dialogs/ProgressDialog.cpp +++ b/launcher/ui/dialogs/ProgressDialog.cpp @@ -90,6 +90,9 @@ void ProgressDialog::on_skipButton_clicked(bool checked) ProgressDialog::~ProgressDialog() { + for (auto conn : this->m_taskConnections) { + disconnect(conn); + } delete ui; } @@ -140,15 +143,15 @@ int ProgressDialog::execWithTask(Task* task) } // Connect signals. - connect(task, &Task::started, this, &ProgressDialog::onTaskStarted); - connect(task, &Task::failed, this, &ProgressDialog::onTaskFailed); - connect(task, &Task::succeeded, this, &ProgressDialog::onTaskSucceeded); - connect(task, &Task::status, this, &ProgressDialog::changeStatus); - connect(task, &Task::details, this, &ProgressDialog::changeStatus); - connect(task, &Task::stepProgress, this, &ProgressDialog::changeStepProgress); - connect(task, &Task::progress, this, &ProgressDialog::changeProgress); - connect(task, &Task::aborted, this, &ProgressDialog::hide); - connect(task, &Task::abortStatusChanged, ui->skipButton, &QPushButton::setEnabled); + this->m_taskConnections.push_back(connect(task, &Task::started, this, &ProgressDialog::onTaskStarted)); + this->m_taskConnections.push_back(connect(task, &Task::failed, this, &ProgressDialog::onTaskFailed)); + this->m_taskConnections.push_back(connect(task, &Task::succeeded, this, &ProgressDialog::onTaskSucceeded)); + this->m_taskConnections.push_back(connect(task, &Task::status, this, &ProgressDialog::changeStatus)); + this->m_taskConnections.push_back(connect(task, &Task::details, this, &ProgressDialog::changeStatus)); + this->m_taskConnections.push_back(connect(task, &Task::stepProgress, this, &ProgressDialog::changeStepProgress)); + this->m_taskConnections.push_back(connect(task, &Task::progress, this, &ProgressDialog::changeProgress)); + this->m_taskConnections.push_back(connect(task, &Task::aborted, this, &ProgressDialog::hide)); + this->m_taskConnections.push_back(connect(task, &Task::abortStatusChanged, ui->skipButton, &QPushButton::setEnabled)); m_is_multi_step = task->isMultiStep(); ui->taskProgressScrollArea->setHidden(!m_is_multi_step); diff --git a/launcher/ui/dialogs/ProgressDialog.h b/launcher/ui/dialogs/ProgressDialog.h index 15eadf4e7..4a696a49d 100644 --- a/launcher/ui/dialogs/ProgressDialog.h +++ b/launcher/ui/dialogs/ProgressDialog.h @@ -93,6 +93,8 @@ class ProgressDialog : public QDialog { Ui::ProgressDialog* ui; Task* m_task; + + QList m_taskConnections; bool m_is_multi_step = false; QHash taskProgress; diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index 1431ea92c..b2c07d490 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()); @@ -125,20 +139,23 @@ void ResourceDownloadDialog::connectButtons() connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help); } -static ModPlatform::ProviderCapabilities ProviderCaps; - void ResourceDownloadDialog::confirm() { auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm %1 to download").arg(resourcesString())); confirm_dialog->retranslateUi(resourcesString()); - QHash getRequiredBy; + 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(); }); + [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); - connect(task.get(), &Task::succeeded, this, [&]() { - QStringList warnings = task->warnings(); + auto weak = task.toWeakRef(); + connect(task.get(), &Task::succeeded, this, [this, weak]() { + QStringList warnings; + if (auto task = weak.lock()) { + warnings = task->warnings(); + } if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec(); } @@ -155,9 +172,11 @@ 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); - getRequiredBy = task->getRequiredBy(); + depNames << dep->pack->name; + } + dependencyExtraInfo = task->getExtraInfo(); } } @@ -166,9 +185,10 @@ void ResourceDownloadDialog::confirm() return QString::compare(a->getName(), b->getName(), Qt::CaseInsensitive) < 0; }); for (auto& task : selected) { + auto extraInfo = dependencyExtraInfo.value(task->getPack()->addonId.toString()); confirm_dialog->appendResource({ task->getName(), task->getFilename(), task->getCustomPath(), - ProviderCaps.name(task->getProvider()), getRequiredBy.value(task->getPack()->addonId.toString()), - task->getVersion().version_type.toString() }); + ModPlatform::ProviderCapabilities::name(task->getProvider()), extraInfo.required_by, + task->getVersion().version_type.toString(), !extraInfo.maybe_installed }); } if (confirm_dialog->exec()) { @@ -180,6 +200,9 @@ void ResourceDownloadDialog::confirm() } this->accept(); + } else { + for (auto name : depNames) + removeResource(name); } } @@ -239,7 +262,9 @@ void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* s } // Same effect as having a global search bar - selectedPage()->setSearchTerm(prev_page->getSearchTerm()); + ResourcePage* result = dynamic_cast(selected); + Q_ASSERT(result != nullptr); + result->setSearchTerm(prev_page->getSearchTerm()); } ModDownloadDialog::ModDownloadDialog(QWidget* parent, const std::shared_ptr& mods, BaseInstance* instance) @@ -277,7 +302,7 @@ GetModDependenciesTask::Ptr ModDownloadDialog::getModDependenciesTask() selectedVers.append(std::make_shared(selected->getPack(), selected->getVersion())); } - return makeShared(this, m_instance, model, selectedVers); + return makeShared(m_instance, model, selectedVers); } } return nullptr; @@ -356,4 +381,20 @@ QList ShaderPackDownloadDialog::getPages() return pages; } +void ResourceDownloadDialog::setResourceMetadata(const 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..0c3e314bc 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.h +++ b/launcher/ui/dialogs/ResourceDownloadDialog.h @@ -69,6 +69,8 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider { const QList getTasks(); [[nodiscard]] const std::shared_ptr getBaseModel() const { return m_base_model; } + void setResourceMetadata(const std::shared_ptr& meta); + public slots: void accept() override; void reject() override; diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ResourceUpdateDialog.cpp similarity index 61% rename from launcher/ui/dialogs/ModUpdateDialog.cpp rename to launcher/ui/dialogs/ResourceUpdateDialog.cpp index cbaaf88b9..7e29e1192 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ResourceUpdateDialog.cpp @@ -1,11 +1,14 @@ -#include "ModUpdateDialog.h" +#include "ResourceUpdateDialog.h" +#include "Application.h" #include "ChooseProviderDialog.h" #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" +#include "tasks/SequentialTask.h" #include "ui_ReviewMessageBox.h" #include "Markdown.h" @@ -24,39 +27,38 @@ #include -static ModPlatform::ProviderCapabilities ProviderCaps; - 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(); } -ModUpdateDialog::ModUpdateDialog(QWidget* parent, - BaseInstance* instance, - const std::shared_ptr mods, - QList& search_for, - bool includeDeps) - : ReviewMessageBox(parent, tr("Confirm mods to update"), "") +ResourceUpdateDialog::ResourceUpdateDialog(QWidget* parent, + BaseInstance* instance, + const std::shared_ptr resource_model, + QList& search_for, + bool include_deps, + bool filter_loaders) + : ReviewMessageBox(parent, tr("Confirm resources to update"), "") , m_parent(parent) - , m_mod_model(mods) + , m_resource_model(resource_model) , m_candidates(search_for) - , m_second_try_metadata( - new ConcurrentTask(nullptr, "Second Metadata Search", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())) + , m_second_try_metadata(new ConcurrentTask("Second Metadata Search", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())) , m_instance(instance) - , m_include_deps(includeDeps) + , m_include_deps(include_deps) + , m_filter_loaders(filter_loaders) { 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 ModUpdateDialog::checkCandidates() +void ResourceUpdateDialog::checkCandidates() { // Ensure mods have valid metadata auto went_well = ensureMetadata(); @@ -75,8 +77,8 @@ void ModUpdateDialog::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,30 +89,32 @@ void ModUpdateDialog::checkCandidates() } auto versions = mcVersions(m_instance); - auto loaders = mcLoaders(m_instance); + auto loadersList = m_filter_loaders ? mcLoadersList(m_instance) : QList(); - SequentialTask check_task(m_parent, tr("Checking for updates")); + SequentialTask check_task(tr("Checking for updates")); if (!m_modrinth_to_update.empty()) { - m_modrinth_check_task.reset(new ModrinthCheckUpdate(m_modrinth_to_update, versions, loaders, m_mod_model)); - connect(m_modrinth_check_task.get(), &CheckUpdateTask::checkFailed, this, [this](Mod* mod, QString reason, QUrl recover_url) { - m_failed_check_update.append({ mod, reason, recover_url }); - }); + 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 }); + }); check_task.addTask(m_modrinth_check_task); } if (!m_flame_to_update.empty()) { - m_flame_check_task.reset(new FlameCheckUpdate(m_flame_to_update, versions, loaders, m_mod_model)); - connect(m_flame_check_task.get(), &CheckUpdateTask::checkFailed, this, [this](Mod* mod, QString reason, QUrl recover_url) { - m_failed_check_update.append({ mod, reason, recover_url }); - }); + 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 }); + }); check_task.addTask(m_flame_check_task); } connect(&check_task, &Task::failed, this, - [&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); - connect(&check_task, &Task::succeeded, this, [&]() { + connect(&check_task, &Task::succeeded, this, [this, &check_task]() { QStringList warnings = check_task.warnings(); if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec(); @@ -134,11 +138,11 @@ void ModUpdateDialog::checkCandidates() // Add found updates for Modrinth if (m_modrinth_check_task) { - auto modrinth_updates = m_modrinth_check_task->getUpdatable(); + auto modrinth_updates = m_modrinth_check_task->getUpdates(); for (auto& updatable : modrinth_updates) { qDebug() << QString("Mod %1 has an update available!").arg(updatable.name); - appendMod(updatable); + appendResource(updatable); m_tasks.insert(updatable.name, updatable.download); } selectedVers.append(m_modrinth_check_task->getDependencies()); @@ -146,11 +150,11 @@ void ModUpdateDialog::checkCandidates() // Add found updated for Flame if (m_flame_check_task) { - auto flame_updates = m_flame_check_task->getUpdatable(); + auto flame_updates = m_flame_check_task->getUpdates(); for (auto& updatable : flame_updates) { qDebug() << QString("Mod %1 has an update available!").arg(updatable.name); - appendMod(updatable); + appendResource(updatable); m_tasks.insert(updatable.name, updatable.download); } selectedVers.append(m_flame_check_task->getDependencies()); @@ -177,8 +181,8 @@ void ModUpdateDialog::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) { @@ -189,49 +193,58 @@ void ModUpdateDialog::checkCandidates() } if (m_include_deps && !APPLICATION->settings()->get("ModDependenciesDisabled").toBool()) { // dependencies - auto depTask = makeShared(this, m_instance, m_mod_model.get(), selectedVers); + auto* mod_model = dynamic_cast(m_resource_model.get()); - connect(depTask.get(), &Task::failed, this, - [&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + if (mod_model != nullptr) { + auto depTask = makeShared(m_instance, mod_model, selectedVers); - connect(depTask.get(), &Task::succeeded, this, [&]() { - QStringList warnings = depTask->warnings(); - if (warnings.count()) { - CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec(); + connect(depTask.get(), &Task::failed, this, [this](const QString& reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); + }); + auto weak = depTask.toWeakRef(); + connect(depTask.get(), &Task::succeeded, this, [this, weak]() { + QStringList warnings; + if (auto depTask = weak.lock()) { + warnings = depTask->warnings(); + } + if (warnings.count()) { + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec(); + } + }); + + ProgressDialog progress_dialog_deps(m_parent); + progress_dialog_deps.setSkipButton(true, tr("Abort")); + progress_dialog_deps.setWindowTitle(tr("Checking for dependencies...")); + auto dret = progress_dialog_deps.execWithTask(depTask.get()); + + // If the dialog was skipped / some download error happened + if (dret == QDialog::DialogCode::Rejected) { + m_aborted = true; + QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); + return; } - }); + static FlameAPI api; - ProgressDialog progress_dialog_deps(m_parent); - progress_dialog_deps.setSkipButton(true, tr("Abort")); - progress_dialog_deps.setWindowTitle(tr("Checking for dependencies...")); - auto dret = progress_dialog_deps.execWithTask(depTask.get()); + auto dependencyExtraInfo = depTask->getExtraInfo(); - // If the dialog was skipped / some download error happened - if (dret == QDialog::DialogCode::Rejected) { - m_aborted = true; - QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); - return; - } - static FlameAPI api; + for (const auto& dep : depTask->getDependecies()) { + auto changelog = dep->version.changelog; + if (dep->pack->provider == ModPlatform::ResourceProvider::FLAME) + changelog = api.getModFileChangelog(dep->version.addonId.toInt(), dep->version.fileId.toInt()); + auto download_task = makeShared(dep->pack, dep->version, m_resource_model); + auto extraInfo = dependencyExtraInfo.value(dep->version.addonId.toString()); + CheckUpdateTask::Update updatable = { + dep->pack->name, dep->version.hash, tr("Not installed"), dep->version.version, dep->version.version_type, + changelog, dep->pack->provider, download_task, !extraInfo.maybe_installed + }; - auto getRequiredBy = depTask->getRequiredBy(); - - for (auto dep : depTask->getDependecies()) { - auto changelog = dep->version.changelog; - if (dep->pack->provider == ModPlatform::ResourceProvider::FLAME) - changelog = api.getModFileChangelog(dep->version.addonId.toInt(), dep->version.fileId.toInt()); - auto download_task = makeShared(dep->pack, dep->version, m_mod_model); - CheckUpdateTask::UpdatableMod updatable = { - dep->pack->name, dep->version.hash, "", dep->version.version, dep->version.version_type, - changelog, dep->pack->provider, download_task - }; - - appendMod(updatable, getRequiredBy.value(dep->version.addonId.toString())); - m_tasks.insert(updatable.name, updatable.download); + appendResource(updatable, extraInfo.required_by); + m_tasks.insert(updatable.name, updatable.download); + } } } - // If there's no mod to be updated + // If there's no resource to be updated if (ui->modTreeWidget->topLevelItemCount() == 0) { m_no_updates = true; } else { @@ -253,35 +266,35 @@ void ModUpdateDialog::checkCandidates() } // Part 1: Ensure we have a valid metadata -auto ModUpdateDialog::ensureMetadata() -> bool +auto ResourceUpdateDialog::ensureMetadata() -> bool { auto index_dir = indexDir(); - SequentialTask seq(m_parent, tr("Looking for metadata")); + SequentialTask seq(tr("Looking for metadata")); // A better use of data structures here could remove the need for this QHash QHash should_try_others; - QList modrinth_tmp; - QList flame_tmp; + QList modrinth_tmp; + QList flame_tmp; bool confirm_rest = false; bool try_others_rest = false; bool skip_rest = false; ModPlatform::ResourceProvider provider_rest = ModPlatform::ResourceProvider::MODRINTH; - auto addToTmp = [&](Mod* m, ModPlatform::ResourceProvider p) { + auto addToTmp = [&modrinth_tmp, &flame_tmp](Resource* resource, ModPlatform::ResourceProvider p) { switch (p) { case ModPlatform::ResourceProvider::MODRINTH: - modrinth_tmp.push_back(m); + modrinth_tmp.push_back(resource); break; case ModPlatform::ResourceProvider::FLAME: - flame_tmp.push_back(m); + flame_tmp.push_back(resource); break; } }; for (auto candidate : m_candidates) { - if (candidate->status() != ModStatus::NoMetadata) { + if (candidate->status() != ResourceStatus::NO_METADATA) { onMetadataEnsured(candidate); continue; } @@ -300,7 +313,7 @@ auto ModUpdateDialog::ensureMetadata() -> bool } ChooseProviderDialog chooser(this); - chooser.setDescription(tr("The mod '%1' does not have a metadata yet. We need to generate it in order to track relevant " + chooser.setDescription(tr("The resource '%1' does not have a metadata yet. We need to generate it in order to track relevant " "information on how to update this mod. " "To do this, please select a mod provider which we can use to check for updates for this mod.") .arg(candidate->name())); @@ -324,10 +337,12 @@ auto ModUpdateDialog::ensureMetadata() -> bool if (!modrinth_tmp.empty()) { auto modrinth_task = makeShared(modrinth_tmp, index_dir, ModPlatform::ResourceProvider::MODRINTH); - connect(modrinth_task.get(), &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); - connect(modrinth_task.get(), &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) { + connect(modrinth_task.get(), &EnsureMetadataTask::metadataReady, [this](Resource* candidate) { onMetadataEnsured(candidate); }); + connect(modrinth_task.get(), &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Resource* candidate) { onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::ResourceProvider::MODRINTH); }); + connect(modrinth_task.get(), &EnsureMetadataTask::failed, + [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); if (modrinth_task->getHashingTask()) seq.addTask(modrinth_task->getHashingTask()); @@ -337,10 +352,12 @@ auto ModUpdateDialog::ensureMetadata() -> bool if (!flame_tmp.empty()) { auto flame_task = makeShared(flame_tmp, index_dir, ModPlatform::ResourceProvider::FLAME); - connect(flame_task.get(), &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); - connect(flame_task.get(), &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) { + connect(flame_task.get(), &EnsureMetadataTask::metadataReady, [this](Resource* candidate) { onMetadataEnsured(candidate); }); + connect(flame_task.get(), &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Resource* candidate) { onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::ResourceProvider::FLAME); }); + connect(flame_task.get(), &EnsureMetadataTask::failed, + [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); if (flame_task->getHashingTask()) seq.addTask(flame_task->getHashingTask()); @@ -358,18 +375,18 @@ auto ModUpdateDialog::ensureMetadata() -> bool return (ret_metadata != QDialog::DialogCode::Rejected); } -void ModUpdateDialog::onMetadataEnsured(Mod* mod) +void ResourceUpdateDialog::onMetadataEnsured(Resource* resource) { // When the mod is a folder, for instance - if (!mod->metadata()) + if (!resource->metadata()) return; - switch (mod->metadata()->provider) { + switch (resource->metadata()->provider) { case ModPlatform::ResourceProvider::MODRINTH: - m_modrinth_to_update.push_back(mod); + m_modrinth_to_update.push_back(resource); break; case ModPlatform::ResourceProvider::FLAME: - m_flame_to_update.push_back(mod); + m_flame_to_update.push_back(resource); break; } } @@ -386,35 +403,46 @@ ModPlatform::ResourceProvider next(ModPlatform::ResourceProvider p) return ModPlatform::ResourceProvider::FLAME; } -void ModUpdateDialog::onMetadataFailed(Mod* mod, bool try_others, ModPlatform::ResourceProvider first_choice) +void ResourceUpdateDialog::onMetadataFailed(Resource* resource, bool try_others, ModPlatform::ResourceProvider first_choice) { if (try_others) { auto index_dir = indexDir(); - auto task = makeShared(mod, index_dir, next(first_choice)); - connect(task.get(), &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); - connect(task.get(), &EnsureMetadataTask::metadataFailed, [this](Mod* candidate) { onMetadataFailed(candidate, false); }); - - m_second_try_metadata->addTask(task); + auto task = makeShared(resource, index_dir, next(first_choice)); + connect(task.get(), &EnsureMetadataTask::metadataReady, [this](Resource* candidate) { onMetadataEnsured(candidate); }); + connect(task.get(), &EnsureMetadataTask::metadataFailed, [this](Resource* candidate) { onMetadataFailed(candidate, false); }); + connect(task.get(), &EnsureMetadataTask::failed, + [this](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + if (task->getHashingTask()) { + auto seq = makeShared(); + seq->addTask(task->getHashingTask()); + seq->addTask(task); + m_second_try_metadata->addTask(seq); + } else { + m_second_try_metadata->addTask(task); + } } else { QString reason{ tr("Couldn't find a valid version on the selected mod provider(s)") }; - m_failed_metadata.append({ mod, reason }); + m_failed_metadata.append({ resource, reason }); } } -void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStringList requiredBy) +void ResourceUpdateDialog::appendResource(CheckUpdateTask::Update const& info, QStringList requiredBy) { auto item_top = new QTreeWidgetItem(ui->modTreeWidget); - item_top->setCheckState(0, Qt::CheckState::Checked); + item_top->setCheckState(0, info.enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); + if (!info.enabled) { + item_top->setToolTip(0, tr("Mod was disabled as it may be already installed.")); + } item_top->setText(0, info.name); item_top->setExpanded(true); auto provider_item = new QTreeWidgetItem(item_top); - provider_item->setText(0, tr("Provider: %1").arg(ProviderCaps.readableName(info.provider))); + provider_item->setText(0, tr("Provider: %1").arg(ModPlatform::ProviderCapabilities::readableName(info.provider))); auto old_version_item = new QTreeWidgetItem(item_top); - old_version_item->setText(0, tr("Old version: %1").arg(info.old_version.isEmpty() ? tr("Not installed") : info.old_version)); + old_version_item->setText(0, tr("Old version: %1").arg(info.old_version)); auto new_version_item = new QTreeWidgetItem(item_top); new_version_item->setText(0, tr("New version: %1").arg(info.new_version)); @@ -437,6 +465,9 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStri reqItem->insertChildren(i++, { reqItem }); } } + + ui->toggleDepsButton->show(); + m_deps << item_top; } auto changelog_item = new QTreeWidgetItem(item_top); @@ -455,7 +486,7 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStri 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); @@ -465,7 +496,7 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStri ui->modTreeWidget->addTopLevelItem(item_top); } -auto ModUpdateDialog::getTasks() -> const QList +auto ResourceUpdateDialog::getTasks() -> const QList { QList list; diff --git a/launcher/ui/dialogs/ModUpdateDialog.h b/launcher/ui/dialogs/ResourceUpdateDialog.h similarity index 52% rename from launcher/ui/dialogs/ModUpdateDialog.h rename to launcher/ui/dialogs/ResourceUpdateDialog.h index de5ab46a5..de1d845d2 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.h +++ b/launcher/ui/dialogs/ResourceUpdateDialog.h @@ -13,22 +13,22 @@ class ModrinthCheckUpdate; class FlameCheckUpdate; class ConcurrentTask; -class ModUpdateDialog final : public ReviewMessageBox { +class ResourceUpdateDialog final : public ReviewMessageBox { Q_OBJECT public: - explicit ModUpdateDialog(QWidget* parent, BaseInstance* instance, std::shared_ptr mod_model, QList& search_for); - explicit ModUpdateDialog(QWidget* parent, - BaseInstance* instance, - std::shared_ptr mod_model, - QList& search_for, - bool includeDeps); + explicit ResourceUpdateDialog(QWidget* parent, + BaseInstance* instance, + std::shared_ptr resource_model, + QList& search_for, + bool include_deps, + bool filter_loaders); void checkCandidates(); - void appendMod(const CheckUpdateTask::UpdatableMod& info, QStringList requiredBy = {}); + void appendResource(const CheckUpdateTask::Update& info, QStringList requiredBy = {}); const QList getTasks(); - auto indexDir() const -> QDir { return m_mod_model->indexDir(); } + auto indexDir() const -> QDir { return m_resource_model->indexDir(); } auto noUpdates() const -> bool { return m_no_updates; }; auto aborted() const -> bool { return m_aborted; }; @@ -37,8 +37,8 @@ class ModUpdateDialog final : public ReviewMessageBox { auto ensureMetadata() -> bool; private slots: - void onMetadataEnsured(Mod*); - void onMetadataFailed(Mod*, + void onMetadataEnsured(Resource* resource); + void onMetadataFailed(Resource* resource, bool try_others = false, ModPlatform::ResourceProvider first_choice = ModPlatform::ResourceProvider::MODRINTH); @@ -48,15 +48,15 @@ class ModUpdateDialog final : public ReviewMessageBox { shared_qobject_ptr m_modrinth_check_task; shared_qobject_ptr m_flame_check_task; - const std::shared_ptr m_mod_model; + const std::shared_ptr m_resource_model; - QList& m_candidates; - QList m_modrinth_to_update; - QList m_flame_to_update; + QList& m_candidates; + QList m_modrinth_to_update; + QList m_flame_to_update; ConcurrentTask::Ptr m_second_try_metadata; - QList> m_failed_metadata; - QList> m_failed_check_update; + QList> m_failed_metadata; + QList> m_failed_check_update; QHash m_tasks; BaseInstance* m_instance; @@ -64,4 +64,5 @@ class ModUpdateDialog final : public ReviewMessageBox { bool m_no_updates = false; bool m_aborted = false; bool m_include_deps = false; + bool m_filter_loaders = false; }; diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp index aa668f8c2..96cc8149f 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.cpp +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -13,12 +13,16 @@ ReviewMessageBox::ReviewMessageBox(QWidget* parent, [[maybe_unused]] QString con auto back_button = ui->buttonBox->button(QDialogButtonBox::Cancel); back_button->setText(tr("Back")); + ui->toggleDepsButton->hide(); ui->modTreeWidget->header()->setSectionResizeMode(0, QHeaderView::Stretch); ui->modTreeWidget->header()->setStretchLastSection(false); ui->modTreeWidget->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &ReviewMessageBox::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &ReviewMessageBox::reject); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ReviewMessageBox::~ReviewMessageBox() @@ -34,8 +38,11 @@ auto ReviewMessageBox::create(QWidget* parent, QString&& title, QString&& icon) void ReviewMessageBox::appendResource(ResourceInformation&& info) { auto itemTop = new QTreeWidgetItem(ui->modTreeWidget); - itemTop->setCheckState(0, Qt::CheckState::Checked); + itemTop->setCheckState(0, info.enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); itemTop->setText(0, info.name); + if (!info.enabled) { + itemTop->setToolTip(0, tr("Mod was disabled as it may be already installed.")); + } auto filenameItem = new QTreeWidgetItem(itemTop); filenameItem->setText(0, tr("Filename: %1").arg(info.filename)); @@ -75,6 +82,8 @@ void ReviewMessageBox::appendResource(ResourceInformation&& info) } itemTop->insertChildren(childIndx++, { requiredByItem }); + ui->toggleDepsButton->show(); + m_deps << itemTop; } auto versionTypeItem = new QTreeWidgetItem(itemTop); @@ -108,3 +117,10 @@ void ReviewMessageBox::retranslateUi(QString resources_name) ui->explainLabel->setText(tr("You're about to download the following %1:").arg(resources_name)); ui->onlyCheckedLabel->setText(tr("Only %1 with a check will be downloaded!").arg(resources_name)); } +void ReviewMessageBox::on_toggleDepsButton_clicked() +{ + m_deps_checked = !m_deps_checked; + auto state = m_deps_checked ? Qt::Checked : Qt::Unchecked; + for (auto dep : m_deps) + dep->setCheckState(0, state); +}; \ No newline at end of file diff --git a/launcher/ui/dialogs/ReviewMessageBox.h b/launcher/ui/dialogs/ReviewMessageBox.h index 596f39c8e..82a43bc11 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.h +++ b/launcher/ui/dialogs/ReviewMessageBox.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace Ui { class ReviewMessageBox; @@ -19,6 +20,7 @@ class ReviewMessageBox : public QDialog { QString provider; QStringList required_by; QString version_type; + bool enabled = true; }; void appendResource(ResourceInformation&& info); @@ -28,8 +30,14 @@ class ReviewMessageBox : public QDialog { ~ReviewMessageBox() override; + protected slots: + void on_toggleDepsButton_clicked(); + protected: ReviewMessageBox(QWidget* parent, const QString& title, const QString& icon); Ui::ReviewMessageBox* ui; + + QList m_deps; + bool m_deps_checked = true; }; diff --git a/launcher/ui/dialogs/ReviewMessageBox.ui b/launcher/ui/dialogs/ReviewMessageBox.ui index bf53ae80b..dbe351019 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.ui +++ b/launcher/ui/dialogs/ReviewMessageBox.ui @@ -44,15 +44,20 @@ - - + - + + + Toggle Dependencies + + + + diff --git a/launcher/ui/dialogs/ScrollMessageBox.cpp b/launcher/ui/dialogs/ScrollMessageBox.cpp index c04d87842..1cfb848f4 100644 --- a/launcher/ui/dialogs/ScrollMessageBox.cpp +++ b/launcher/ui/dialogs/ScrollMessageBox.cpp @@ -1,4 +1,5 @@ #include "ScrollMessageBox.h" +#include #include "ui_ScrollMessageBox.h" ScrollMessageBox::ScrollMessageBox(QWidget* parent, const QString& title, const QString& text, const QString& body) @@ -8,6 +9,9 @@ ScrollMessageBox::ScrollMessageBox(QWidget* parent, const QString& title, const this->setWindowTitle(title); ui->label->setText(text); ui->textBrowser->setText(body); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ScrollMessageBox::~ScrollMessageBox() diff --git a/launcher/ui/dialogs/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..30377288b 100644 --- a/launcher/ui/dialogs/VersionSelectDialog.cpp +++ b/launcher/ui/dialogs/VersionSelectDialog.cpp @@ -68,6 +68,9 @@ VersionSelectDialog::VersionSelectDialog(BaseVersionList* vlist, QString title, m_buttonBox->setObjectName(QStringLiteral("buttonBox")); m_buttonBox->setOrientation(Qt::Horizontal); m_buttonBox->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); + + m_buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Ok")); + m_buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); m_horizontalLayout->addWidget(m_buttonBox); m_verticalLayout->addLayout(m_horizontalLayout); @@ -121,7 +124,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..f112e1acf --- /dev/null +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -0,0 +1,526 @@ +// 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())); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); +} + +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); + if (raw_path.isNull()) { + return; + } + auto message = m_list.installSkin(raw_path, {}); + if (!message.isEmpty()) { + CustomMessageBox::selectable(this, tr("Selected file is not a valid skin"), message, QMessageBox::Critical)->show(); + 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 e665097f4..c677f3951 100644 --- a/launcher/ui/instanceview/InstanceView.cpp +++ b/launcher/ui/instanceview/InstanceView.cpp @@ -458,16 +458,18 @@ void InstanceView::paintEvent([[maybe_unused]] QPaintEvent* event) QPainter painter(this->viewport()); if (m_catVisible) { + painter.setOpacity(APPLICATION->settings()->get("CatOpacity").toFloat() / 100); int widWidth = this->viewport()->width(); int widHeight = this->viewport()->height(); if (m_catPixmap.width() < widWidth) widWidth = m_catPixmap.width(); if (m_catPixmap.height() < widHeight) widHeight = m_catPixmap.height(); - auto pixmap = m_catPixmap.scaled(widWidth, widHeight, Qt::KeepAspectRatio); + auto pixmap = m_catPixmap.scaled(widWidth, widHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation); QRect rectOfPixmap = pixmap.rect(); rectOfPixmap.moveBottomRight(this->viewport()->rect().bottomRight()); painter.drawPixmap(rectOfPixmap.topLeft(), pixmap); + painter.setOpacity(1.0); } #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) @@ -480,32 +482,42 @@ void InstanceView::paintEvent([[maybe_unused]] QPaintEvent* event) if (model()->rowCount() == 0) { painter.save(); - const QString line1 = tr("Welcome!"); - const QString line2 = tr("Click \"Add Instance\" to get started."); - auto rect = this->viewport()->rect(); - auto font = option.font; - font.setPointSize(37); - painter.setFont(font); - auto fm = painter.fontMetrics(); + QString emptyString = tr("Welcome!") + "\n" + tr("Click \"Add Instance\" to get started."); - if (rect.height() <= (fm.height() * 5) || rect.width() <= fm.horizontalAdvance(line2)) { - auto s = rect.height() / (5. * fm.height()); - auto sx = rect.width() * 1. / fm.horizontalAdvance(line2); - if (s >= sx) - s = sx; - auto ps = font.pointSize() * s; - if (ps <= 0) - ps = 1; - font.setPointSize(ps); - painter.setFont(font); - fm = painter.fontMetrics(); + // calculate the rect for the overlay + painter.setRenderHint(QPainter::Antialiasing, true); + QFont font("sans", 20); + font.setBold(true); + + QRect bounds = viewport()->geometry(); + bounds.moveTop(0); + auto innerBounds = bounds; + innerBounds.adjust(10, 10, -10, -10); + + QColor background = QApplication::palette().color(QPalette::WindowText); + QColor foreground = QApplication::palette().color(QPalette::Base); + foreground.setAlpha(190); + painter.setFont(font); + auto fontMetrics = painter.fontMetrics(); + auto textRect = fontMetrics.boundingRect(innerBounds, Qt::AlignHCenter | Qt::TextWordWrap, emptyString); + textRect.moveCenter(bounds.center()); + + auto wrapRect = textRect; + wrapRect.adjust(-10, -10, 10, 10); + + // check if we are allowed to draw in our area + if (!event->rect().intersects(wrapRect)) { + return; } - // text - rect.setTop(rect.top() + fm.height() * 1.5); - painter.drawText(rect, Qt::AlignHCenter, line1); - rect.setTop(rect.top() + fm.height()); - painter.drawText(rect, Qt::AlignHCenter, line2); + painter.setBrush(QBrush(background)); + painter.setPen(foreground); + painter.drawRoundedRect(wrapRect, 5.0, 5.0); + + painter.setPen(foreground); + painter.setFont(font); + painter.drawText(textRect, Qt::AlignHCenter | Qt::TextWordWrap, emptyString); + painter.restore(); return; } @@ -836,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()) { @@ -853,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) { @@ -913,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: { @@ -935,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 aaf31941d..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; @@ -158,13 +161,14 @@ void VisualGroup::drawHeader(QPainter* painter, const QStyleOptionViewItem& opti painter->setRenderHint(QPainter::Antialiasing); // sizes and offsets, to keep things consistent below - int arrowOffsetLeft = fontMetrics.height() / 2 + 7; - int textOffsetLeft = arrowOffsetLeft * 2; - int arrowSize = 6; - int centerHeight = optRect.top() + fontMetrics.height() / 2; + const int arrowOffsetLeft = fontMetrics.height() / 2 + 7; + const int textOffsetLeft = arrowOffsetLeft * 2; + const int centerHeight = optRect.top() + fontMetrics.height() / 2; + const QString& textToDraw = text.isEmpty() ? QObject::tr("Ungrouped") : text; // BEGIN: arrow { + constexpr int arrowSize = 6; QPolygon arrowPolygon; if (collapsed) { arrowPolygon << QPoint(arrowOffsetLeft - arrowSize / 2, centerHeight - arrowSize) @@ -188,9 +192,26 @@ void VisualGroup::drawHeader(QPainter* painter, const QStyleOptionViewItem& opti textRect.setHeight(fontMetrics.height()); textRect.setRight(textRect.right() - 7); - painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, !text.isEmpty() ? text : QObject::tr("Ungrouped")); + painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, textToDraw); } // END: text + + // BEGIN: horizontal line + { + penColor.setAlphaF(0.05); + pen.setColor(penColor); + painter->setPen(pen); + // startPoint is left + arrow + text + space + const int startPoint = + optRect.left() + fontMetrics.height() + fontMetrics.size(Qt::AlignLeft | Qt::AlignVCenter, textToDraw).width() + 20; + painter->setRenderHint(QPainter::Antialiasing, false); + QPolygon polygon; + // for some reason the height (yPos) doesn't look centered, so we are adding 1 to the center height + const int lineHeight = centerHeight + 1; + polygon << QPoint(startPoint, lineHeight) << QPoint(optRect.right() - 3, lineHeight); + painter->drawPolyline(polygon); + } + // END: horizontal line } int VisualGroup::totalHeight() const diff --git a/launcher/ui/java/InstallJavaDialog.cpp b/launcher/ui/java/InstallJavaDialog.cpp new file mode 100644 index 000000000..5f69b9d46 --- /dev/null +++ b/launcher/ui/java/InstallJavaDialog.cpp @@ -0,0 +1,347 @@ +// 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 setRecommend(bool recommend) + { + m_recommend = recommend; + 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)->setRecommend(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")); + buttons->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + 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->setRecommend(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(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" 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/pagedialog/PageDialog.cpp b/launcher/ui/pagedialog/PageDialog.cpp index 6514217cd..d211cb4d3 100644 --- a/launcher/ui/pagedialog/PageDialog.cpp +++ b/launcher/ui/pagedialog/PageDialog.cpp @@ -39,6 +39,8 @@ PageDialog::PageDialog(BasePageProvider* pageProvider, QString defaultId, QWidge QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Close); buttons->button(QDialogButtonBox::Close)->setDefault(true); + buttons->button(QDialogButtonBox::Close)->setText(tr("Close")); + buttons->button(QDialogButtonBox::Help)->setText(tr("Help")); buttons->setContentsMargins(6, 0, 6, 0); m_container->addButtons(buttons); diff --git a/launcher/ui/pages/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.cpp b/launcher/ui/pages/global/APIPage.cpp index 82aa76a4f..a137c4cde 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -143,6 +143,7 @@ void APIPage::loadSettings() ui->modrinthToken->setText(modrinthToken); QString customUserAgent = s->get("UserAgentOverride").toString(); ui->userAgentLineEdit->setText(customUserAgent); + ui->technicClientID->setText(s->get("TechnicClientID").toString()); } void APIPage::applySettings() @@ -172,6 +173,7 @@ void APIPage::applySettings() QString modrinthToken = ui->modrinthToken->text(); s->set("ModrinthToken", modrinthToken); s->set("UserAgentOverride", ui->userAgentLineEdit->text()); + s->set("TechnicClientID", ui->technicClientID->text()); } bool APIPage::apply() diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 93591e440..05c256bb2 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -6,8 +6,8 @@ 0 0 - 800 - 600 + 841 + 620 @@ -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/api/#authentication">documentation</a> for more information.</p></body></html> true @@ -288,6 +288,36 @@ + + + + Technic Client ID + + + + + + <html><head/><body><p>Note: you only need to set this to access private data.</p></body></html> + + + + + + + (None) + + + + + + + Enter a custom GUID client ID for Technic here. + + + + + + diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index abd8fa228..90eb6becf 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) { @@ -147,14 +141,6 @@ void AccountListPage::on_actionAddMicrosoft_triggered() void AccountListPage::on_actionAddOffline_triggered() { - if (!m_accounts->anyAccountIsValid()) { - QMessageBox::warning(this, tr("Error"), - tr("You must add a Microsoft account that owns Minecraft before you can add an offline account." - "

    " - "If you have lost your account you can contact Microsoft for support.")); - return; - } - MinecraftAccountPtr account = OfflineLoginDialog::newAccount(this, tr("Please enter your desired username to add your offline account.")); @@ -221,8 +207,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 +220,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/EnvironmentVariablesPage.cpp b/launcher/ui/pages/global/EnvironmentVariablesPage.cpp deleted file mode 100644 index 2d44ed624..000000000 --- a/launcher/ui/pages/global/EnvironmentVariablesPage.cpp +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2023 TheKodeToad - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * 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 -#include -#include - -#include "EnvironmentVariablesPage.h" - -EnvironmentVariablesPage::EnvironmentVariablesPage(QWidget* parent) : QWidget(parent) -{ - auto verticalLayout = new QVBoxLayout(this); - verticalLayout->setObjectName(QStringLiteral("verticalLayout")); - verticalLayout->setContentsMargins(0, 0, 0, 0); - - auto tabWidget = new QTabWidget(this); - tabWidget->setObjectName(QStringLiteral("tabWidget")); - variables = new EnvironmentVariables(this); - variables->setContentsMargins(6, 6, 6, 6); - tabWidget->addTab(variables, "Foo"); - tabWidget->tabBar()->hide(); - verticalLayout->addWidget(tabWidget); - - variables->initialize(false, false, APPLICATION->settings()->get("Env").toMap()); -} - -QString EnvironmentVariablesPage::displayName() const -{ - return tr("Environment Variables"); -} - -QIcon EnvironmentVariablesPage::icon() const -{ - return APPLICATION->getThemedIcon("environment-variables"); -} - -QString EnvironmentVariablesPage::id() const -{ - return "environment-variables"; -} - -QString EnvironmentVariablesPage::helpPage() const -{ - return "Environment-variables"; -} - -bool EnvironmentVariablesPage::apply() -{ - APPLICATION->settings()->set("Env", variables->value()); - return true; -} - -void EnvironmentVariablesPage::retranslate() -{ - variables->retranslate(); -} diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index ac50319ec..b99d0c63e 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,10 +62,15 @@ JavaPage::JavaPage(QWidget* parent) : QWidget(parent), ui(new Ui::JavaPage) { ui->setupUi(this); - ui->tabWidget->tabBar()->hide(); - - loadSettings(); - updateThresholds(); + + 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!")); + } else + ui->tabWidget->tabBar()->hide(); } JavaPage::~JavaPage() @@ -67,146 +78,53 @@ JavaPage::~JavaPage() delete ui; } -bool JavaPage::apply() -{ - applySettings(); - return true; -} - -void JavaPage::applySettings() -{ - auto s = APPLICATION->settings(); - - // Memory - int min = ui->minMemSpinBox->value(); - int max = ui->maxMemSpinBox->value(); - if (min < max) { - s->set("MinMemAlloc", min); - s->set("MaxMemAlloc", max); - } else { - s->set("MinMemAlloc", max); - s->set("MaxMemAlloc", min); - } - s->set("PermGen", ui->permGenSpinBox->value()); - - // Java Settings - s->set("JavaPath", ui->javaPathTextBox->text()); - s->set("JvmArgs", ui->jvmArgsTextBox->toPlainText().replace("\n", " ")); - s->set("IgnoreJavaCompatibility", ui->skipCompatibilityCheckbox->isChecked()); - s->set("IgnoreJavaWizard", ui->skipJavaWizardCheckbox->isChecked()); - JavaCommon::checkJVMArgs(s->get("JvmArgs").toString(), this->parentWidget()); -} -void JavaPage::loadSettings() -{ - auto s = APPLICATION->settings(); - // Memory - int min = s->get("MinMemAlloc").toInt(); - int max = s->get("MaxMemAlloc").toInt(); - if (min < max) { - ui->minMemSpinBox->setValue(min); - ui->maxMemSpinBox->setValue(max); - } else { - ui->minMemSpinBox->setValue(max); - ui->maxMemSpinBox->setValue(min); - } - ui->permGenSpinBox->setValue(s->get("PermGen").toInt()); - - // Java Settings - ui->javaPathTextBox->setText(s->get("JavaPath").toString()); - ui->jvmArgsTextBox->setPlainText(s->get("JvmArgs").toString()); - ui->skipCompatibilityCheckbox->setChecked(s->get("IgnoreJavaCompatibility").toBool()); - ui->skipJavaWizardCheckbox->setChecked(s->get("IgnoreJavaWizard").toBool()); -} - -void JavaPage::on_javaDetectBtn_clicked() -{ - if (JavaUtils::getJavaCheckPath().isEmpty()) { - JavaCommon::javaCheckNotFound(this); - return; - } - - JavaInstallPtr java; - - VersionSelectDialog vselect(APPLICATION->javalist().get(), tr("Select a Java version"), this, true); - vselect.setResizeOn(2); - vselect.exec(); - - if (vselect.result() == QDialog::Accepted && vselect.selectedVersion()) { - java = std::dynamic_pointer_cast(vselect.selectedVersion()); - ui->javaPathTextBox->setText(java->path); - } -} - -void JavaPage::on_javaBrowseBtn_clicked() -{ - QString raw_path = QFileDialog::getOpenFileName(this, tr("Find Java executable")); - - // do not allow current dir - it's dirty. Do not allow dirs that don't exist - if (raw_path.isEmpty()) { - return; - } - - QString cooked_path = FS::NormalizePath(raw_path); - QFileInfo javaInfo(cooked_path); - ; - if (!javaInfo.exists() || !javaInfo.isExecutable()) { - return; - } - ui->javaPathTextBox->setText(cooked_path); -} - -void JavaPage::on_javaTestBtn_clicked() -{ - if (checker) { - return; - } - checker.reset(new JavaCommon::TestCheck(this, ui->javaPathTextBox->text(), ui->jvmArgsTextBox->toPlainText().replace("\n", " "), - ui->minMemSpinBox->value(), ui->maxMemSpinBox->value(), ui->permGenSpinBox->value())); - connect(checker.get(), SIGNAL(finished()), SLOT(checkerFinished())); - checker->run(); -} - -void JavaPage::on_maxMemSpinBox_valueChanged([[maybe_unused]] int i) -{ - updateThresholds(); -} - -void JavaPage::checkerFinished() -{ - checker.reset(); -} - void JavaPage::retranslate() { ui->retranslateUi(this); } -void JavaPage::updateThresholds() +bool JavaPage::apply() { - auto sysMiB = Sys::getSystemRam() / Sys::mebibyte; - unsigned int maxMem = ui->maxMemSpinBox->value(); - unsigned int minMem = ui->minMemSpinBox->value(); + ui->javaSettings->saveSettings(); + JavaCommon::checkJVMArgs(APPLICATION->settings()->get("JvmArgs").toString(), this); + return true; +} - QString iconName; +void JavaPage::on_downloadJavaButton_clicked() +{ + auto jdialog = new Java::InstallDialog({}, nullptr, this); + jdialog->exec(); + ui->managedJavaList->loadList(); +} - if (maxMem >= sysMiB) { - iconName = "status-bad"; - ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation exceeds your system memory capacity.")); - } else if (maxMem > (sysMiB * 0.9)) { - iconName = "status-yellow"; - ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity.")); - } else if (maxMem < minMem) { - iconName = "status-yellow"; - ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation is smaller than the minimum value")); - } else { - iconName = "status-good"; - ui->labelMaxMemIcon->setToolTip(""); +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 height = ui->labelMaxMemIcon->fontInfo().pixelSize(); - QIcon icon = APPLICATION->getThemedIcon(iconName); - QPixmap pix = icon.pixmap(height, height); - ui->labelMaxMemIcon->setPixmap(pix); + 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..ea7724c1d 100644 --- a/launcher/ui/pages/global/JavaPage.h +++ b/launcher/ui/pages/global/JavaPage.h @@ -37,8 +37,9 @@ #include #include +#include "ui/widgets/JavaSettingsWidget.h" #include -#include +#include #include "JavaCommon.h" #include "ui/pages/BasePage.h" @@ -59,23 +60,15 @@ class JavaPage : public QWidget, public BasePage { QIcon icon() const override { return APPLICATION->getThemedIcon("java"); } QString id() const override { return "java-settings"; } QString helpPage() const override { return "Java-settings"; } - bool apply() override; void retranslate() override; - void updateThresholds(); - - private: - void applySettings(); - void loadSettings(); + bool apply() override; private slots: - void on_javaDetectBtn_clicked(); - void on_javaTestBtn_clicked(); - void on_javaBrowseBtn_clicked(); - void on_maxMemSpinBox_valueChanged(int i); - void checkerFinished(); + void on_downloadJavaButton_clicked(); + void on_removeJavaButton_clicked(); + void on_refreshJavaButton_clicked(); private: Ui::JavaPage* ui; - unique_qobject_ptr checker; }; diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index fd16572d3..a4b2ac203 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,264 +34,99 @@ 0 - + - Tab 1 + General - - - Memory + + + true - - - - - Ma&ximum memory allocation: - - - maxMemSpinBox - - - - - - - &PermGen: - - - permGenSpinBox - - - - - - - &Minimum memory allocation: - - - minMemSpinBox - - - - - - - The amount of memory Minecraft is started with. - - - MiB - - - 8 - - - 1048576 - - - 128 - - - 256 - - - - - - - The maximum amount of memory Minecraft is allowed to use. - - - MiB - - - 8 - - - 1048576 - - - 128 - - - 1024 - - - - - - - The amount of memory available to store loaded Java classes. - - - MiB - - - 4 - - - 999999999 - - - 8 - - - 64 - - - - - - - - - - maxMemSpinBox - - - - + + + + 0 + 0 + 535 + 610 + + + + + + + + + + + + + Management + + - + - Java Runtime + Downloaded Java Versions - - - - - true - + + + - + 0 0 - - - 16777215 - 100 - - - - - - - 0 - 0 - - - - If enabled, the launcher will not check if an instance is compatible with the selected Java version. - - - &Skip Java compatibility checks - - - - - + + - - - - 0 - 0 - - + - &Auto-detect... + Download - - - - 0 - 0 - - + - &Test + Remove + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Refresh - - - - - 0 - 0 - - - - JVM arguments: - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - - - 0 - 0 - - - - &Java path: - - - javaPathTextBox - - - - - - - - - - - 0 - 0 - - - - Browse - - - - - - - - - If enabled, the launcher will not prompt you to choose a Java version if one isn't found. - - - Skip Java &Wizard - - - - + Qt::Vertical @@ -309,14 +144,20 @@ - - minMemSpinBox - maxMemSpinBox - permGenSpinBox - javaBrowseBtn - javaPathTextBox - tabWidget - + + + VersionSelectWidget + QWidget +
    ui/widgets/VersionSelectWidget.h
    + 1 +
    + + JavaSettingsWidget + QWidget +
    ui/widgets/JavaSettingsWidget.h
    + 1 +
    +
    diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index d15883db5..04ee01b00 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,17 +206,17 @@ 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()); - s->set("AutoCloseConsole", ui->autoCloseConsoleCheck->isChecked()); - s->set("ShowConsoleOnError", ui->showConsoleErrorCheck->isChecked()); QString consoleFontFamily = ui->consoleFont->currentFont().family(); s->set("ConsoleFont", consoleFontFamily); s->set("ConsoleFontSize", ui->fontSizeBox->value()); @@ -208,7 +229,10 @@ 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()); + s->set("MoveModsFromDownloadsDir", ui->downloadsDirMoveCheckBox->isChecked()); auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId(); switch (sortMode) { @@ -221,9 +245,13 @@ void LauncherPage::applySettings() break; } + // Cat + s->set("CatOpacity", ui->catOpacitySpinBox->value()); + // Mods s->set("ModMetadataDisabled", ui->metadataDisableBtn->isChecked()); s->set("ModDependenciesDisabled", ui->dependenciesDisableBtn->isChecked()); + s->set("SkipModpackUpdatePrompt", ui->skipModpackUpdatePromptBtn->isChecked()); } void LauncherPage::loadSettings() { @@ -231,6 +259,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) @@ -242,11 +271,10 @@ 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()); - ui->autoCloseConsoleCheck->setChecked(s->get("AutoCloseConsole").toBool()); - ui->showConsoleErrorCheck->setChecked(s->get("ShowConsoleOnError").toBool()); QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString(); QFont consoleFont(fontFamily); ui->consoleFont->setCurrentFont(consoleFont); @@ -266,7 +294,10 @@ 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()); + ui->downloadsDirMoveCheckBox->setChecked(s->get("MoveModsFromDownloadsDir").toBool()); QString sortMode = s->get("InstSortMode").toString(); @@ -276,45 +307,59 @@ void LauncherPage::loadSettings() ui->sortByNameBtn->setChecked(true); } + // Cat + ui->catOpacitySpinBox->setValue(s->get("CatOpacity").toInt()); + // Mods 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 83897f7a7..31c878f3e 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -7,7 +7,7 @@ 0 0 511 - 629 + 726
    @@ -46,212 +46,356 @@ - - - Update Settings + + + Qt::ScrollBarAsNeeded - - - - - Check for updates automatically - - - - - - - - - - Folders + + true - - - - - &Downloads: - - - downloadsDirTextBox - - - - - - - I&nstances: - - - instDirTextBox - - - - - - - - - - - - - - - - Browse - - - - - - - - - - Browse - - - - - - - &Mods: - - - modsDirTextBox - - - - - - - Browse - - - - - - - Browse - - - - - - - &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 - - - - - - - - - - Mods - - - - - - Disable using metadata provided by mod providers (like Modrinth or CurseForge) for mods. - - - Disable using metadata for mods - - - - - - - <html><head/><body><p><span style=" font-weight:600; color:#f5c211;">Warning</span><span style=" color:#f5c211;">: Disabling mod metadata may also disable some QoL features, such as mod updating!</span></p></body></html> - - - true - - - + + + + 0 + 0 + 473 + 770 + + + - - - Disable the automatic detection, installation, and updating of mod dependencies. - - - Disable automatic mod dependency management - - - - + + + Update Settings + + + + + + Check for updates automatically + + + + + + + + + Update interval + + + + + + + Set it to 0 to only check on launch + + + h + + + 0 + + + 99999999 + + + + + + + + + + + + Folders + + + + + + &Downloads: + + + downloadsDirTextBox + + + + + + + Browse + + + + + + + + + + + + + &Skins: + + + skinsDirTextBox + + + + + + + &Icons: + + + iconsDirTextBox + + + + + + + + + When enabled, in addition to the downloads folder, its sub folders will also be searched when looking for resources (e.g. when looking for blocked mods on CurseForge). + + + Check downloads folder recursively + + + + + + + When enabled, it will move blocked resources instead of copying them. + + + Move blocked resources + + + + + + + + + + + + &Java: + + + javaDirTextBox + + + + + + + &Mods: + + + modsDirTextBox + + + + + + + + + + + + + + + + Browse + + + + + + + Browse + + + + + + + Browse + + + + + + + I&nstances: + + + instDirTextBox + + + + + + + Browse + + + + + + + Browse + + + + + + + + + + Mods + + + + + + Disable using metadata provided by mod providers (like Modrinth or CurseForge) for mods. + + + Disable using metadata for mods + + + + + + + <html><head/><body><p><span style=" font-weight:600; color:#f5c211;">Warning</span><span style=" color:#f5c211;">: Disabling mod metadata may also disable some QoL features, such as mod updating!</span></p></body></html> + + + true + + + + + + + Disable the automatic detection, installation, and updating of mod dependencies. + + + Disable automatic mod dependency management + + + + + + + When creating a new modpack instance, do not suggest updating existing instances instead. + + + Skip modpack update prompt + + + + + + + + + + Miscellaneous + + + + + + 1 + + + + + + + Number of concurrent tasks + + + + + + + 1 + + + + + + + Number of concurrent downloads + + + + + + + Number of manual retries + + + + + + + 0 + + + + + + + Seconds to wait until the requests are terminated + + + Timeout for HTTP requests + + + + + + + s + + + + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + - - - - Miscellaneous - - - - - - Number of concurrent tasks - - - - - - - 1 - - - - - - - Number of concurrent downloads - - - - - - - 1 - - - - - - - - - - Qt::Vertical - - - - 0 - 0 - - - - - + User Interface @@ -300,6 +444,54 @@ + + + + Cat + + + + + + Set the cat's opacity. 0% is fully transparent and 100% is fully opaque. + + + Opacity + + + + + + + + + + % + + + 100 + + + 0 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + @@ -326,7 +518,7 @@ - + Qt::Vertical @@ -345,36 +537,6 @@ Console - - - - Console Settings - - - - - - Show console while the game is &running - - - - - - - &Automatically close console when the game quits - - - - - - - Show console when the game &crashes - - - - - - @@ -437,7 +599,7 @@ - Qt::ScrollBarAlwaysOff + Qt::ScrollBarAsNeeded false @@ -489,18 +651,33 @@
    tabWidget + scrollArea autoUpdateCheckBox + updateIntervalSpinBox instDirTextBox instDirBrowseBtn modsDirTextBox modsDirBrowseBtn iconsDirTextBox iconsDirBrowseBtn + javaDirTextBox + javaDirBrowseBtn + skinsDirTextBox + skinsDirBrowseBtn + downloadsDirTextBox + downloadsDirBrowseBtn + downloadsDirWatchRecursiveCheckBox + metadataDisableBtn + dependenciesDisableBtn + skipModpackUpdatePromptBtn + numberOfConcurrentTasksSpinBox + numberOfConcurrentDownloadsSpinBox + numberOfManualRetriesSpinBox + timeoutSecondsSpinBox sortLastLaunchedBtn sortByNameBtn - showConsoleCheck - autoCloseConsoleCheck - showConsoleErrorCheck + catOpacitySpinBox + preferMenuBarCheckBox lineLimitSpinBox checkStopLogging consoleFont @@ -512,4 +689,4 @@ - + \ No newline at end of file diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp deleted file mode 100644 index a9530effc..000000000 --- a/launcher/ui/pages/global/MinecraftPage.cpp +++ /dev/null @@ -1,183 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2022 Jamie Mansfield - * - * 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 "MinecraftPage.h" -#include "BuildConfig.h" -#include "ui_MinecraftPage.h" - -#include -#include -#include - -#include "Application.h" -#include "settings/SettingsObject.h" - -#ifdef Q_OS_LINUX -#include "MangoHud.h" -#endif - -MinecraftPage::MinecraftPage(QWidget* parent) : QWidget(parent), ui(new Ui::MinecraftPage) -{ - ui->setupUi(this); - connect(ui->useNativeGLFWCheck, &QAbstractButton::toggled, this, &MinecraftPage::onUseNativeGLFWChanged); - connect(ui->useNativeOpenALCheck, &QAbstractButton::toggled, this, &MinecraftPage::onUseNativeOpenALChanged); - loadSettings(); - updateCheckboxStuff(); -} - -MinecraftPage::~MinecraftPage() -{ - delete ui; -} - -bool MinecraftPage::apply() -{ - applySettings(); - return true; -} - -void MinecraftPage::updateCheckboxStuff() -{ - ui->windowWidthSpinBox->setEnabled(!ui->maximizedCheckBox->isChecked()); - ui->windowHeightSpinBox->setEnabled(!ui->maximizedCheckBox->isChecked()); -} - -void MinecraftPage::on_maximizedCheckBox_clicked(bool checked) -{ - Q_UNUSED(checked); - updateCheckboxStuff(); -} - -void MinecraftPage::onUseNativeGLFWChanged(bool checked) -{ - ui->lineEditGLFWPath->setEnabled(checked); -} - -void MinecraftPage::onUseNativeOpenALChanged(bool checked) -{ - ui->lineEditOpenALPath->setEnabled(checked); -} - -void MinecraftPage::applySettings() -{ - auto s = APPLICATION->settings(); - - // Window Size - s->set("LaunchMaximized", ui->maximizedCheckBox->isChecked()); - s->set("MinecraftWinWidth", ui->windowWidthSpinBox->value()); - s->set("MinecraftWinHeight", ui->windowHeightSpinBox->value()); - - // Native library workarounds - s->set("UseNativeGLFW", ui->useNativeGLFWCheck->isChecked()); - s->set("CustomGLFWPath", ui->lineEditGLFWPath->text()); - s->set("UseNativeOpenAL", ui->useNativeOpenALCheck->isChecked()); - s->set("CustomOpenALPath", ui->lineEditOpenALPath->text()); - - // Peformance related options - s->set("EnableFeralGamemode", ui->enableFeralGamemodeCheck->isChecked()); - s->set("EnableMangoHud", ui->enableMangoHud->isChecked()); - s->set("UseDiscreteGpu", ui->useDiscreteGpuCheck->isChecked()); - - // Game time - s->set("ShowGameTime", ui->showGameTime->isChecked()); - s->set("ShowGlobalGameTime", ui->showGlobalGameTime->isChecked()); - s->set("RecordGameTime", ui->recordGameTime->isChecked()); - s->set("ShowGameTimeWithoutDays", ui->showGameTimeWithoutDays->isChecked()); - - // Miscellaneous - s->set("CloseAfterLaunch", ui->closeAfterLaunchCheck->isChecked()); - s->set("QuitAfterGameStop", ui->quitAfterGameStopCheck->isChecked()); - - // Legacy settings - s->set("OnlineFixes", ui->onlineFixes->isChecked()); -} - -void MinecraftPage::loadSettings() -{ - auto s = APPLICATION->settings(); - - // Window Size - ui->maximizedCheckBox->setChecked(s->get("LaunchMaximized").toBool()); - ui->windowWidthSpinBox->setValue(s->get("MinecraftWinWidth").toInt()); - ui->windowHeightSpinBox->setValue(s->get("MinecraftWinHeight").toInt()); - - ui->useNativeGLFWCheck->setChecked(s->get("UseNativeGLFW").toBool()); - ui->lineEditGLFWPath->setText(s->get("CustomGLFWPath").toString()); - ui->lineEditGLFWPath->setPlaceholderText(tr("Path to %1 library file").arg(BuildConfig.GLFW_LIBRARY_NAME)); -#ifdef Q_OS_LINUX - if (!APPLICATION->m_detectedGLFWPath.isEmpty()) - ui->lineEditGLFWPath->setPlaceholderText(tr("Auto detected path: %1").arg(APPLICATION->m_detectedGLFWPath)); -#endif - ui->useNativeOpenALCheck->setChecked(s->get("UseNativeOpenAL").toBool()); - ui->lineEditOpenALPath->setText(s->get("CustomOpenALPath").toString()); - ui->lineEditOpenALPath->setPlaceholderText(tr("Path to %1 library file").arg(BuildConfig.OPENAL_LIBRARY_NAME)); -#ifdef Q_OS_LINUX - if (!APPLICATION->m_detectedOpenALPath.isEmpty()) - ui->lineEditOpenALPath->setPlaceholderText(tr("Auto detected path: %1").arg(APPLICATION->m_detectedOpenALPath)); -#endif - - ui->enableFeralGamemodeCheck->setChecked(s->get("EnableFeralGamemode").toBool()); - ui->enableMangoHud->setChecked(s->get("EnableMangoHud").toBool()); - ui->useDiscreteGpuCheck->setChecked(s->get("UseDiscreteGpu").toBool()); - -#if !defined(Q_OS_LINUX) - ui->perfomanceGroupBox->setVisible(false); -#endif - - if (!(APPLICATION->capabilities() & Application::SupportsGameMode)) { - ui->enableFeralGamemodeCheck->setDisabled(true); - ui->enableFeralGamemodeCheck->setToolTip(tr("Feral Interactive's GameMode could not be found on your system.")); - } - - if (!(APPLICATION->capabilities() & Application::SupportsMangoHud)) { - ui->enableMangoHud->setDisabled(true); - ui->enableMangoHud->setToolTip(tr("MangoHud could not be found on your system.")); - } - - ui->showGameTime->setChecked(s->get("ShowGameTime").toBool()); - ui->showGlobalGameTime->setChecked(s->get("ShowGlobalGameTime").toBool()); - ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool()); - ui->showGameTimeWithoutDays->setChecked(s->get("ShowGameTimeWithoutDays").toBool()); - - ui->closeAfterLaunchCheck->setChecked(s->get("CloseAfterLaunch").toBool()); - ui->quitAfterGameStopCheck->setChecked(s->get("QuitAfterGameStop").toBool()); - - ui->onlineFixes->setChecked(s->get("OnlineFixes").toBool()); -} - -void MinecraftPage::retranslate() -{ - ui->retranslateUi(this); -} diff --git a/launcher/ui/pages/global/MinecraftPage.h b/launcher/ui/pages/global/MinecraftPage.h index 5facfbb3f..b21862536 100644 --- a/launcher/ui/pages/global/MinecraftPage.h +++ b/launcher/ui/pages/global/MinecraftPage.h @@ -38,41 +38,27 @@ #include #include -#include +#include "Application.h" #include "java/JavaChecker.h" #include "ui/pages/BasePage.h" +#include "ui/widgets/MinecraftSettingsWidget.h" class SettingsObject; -namespace Ui { -class MinecraftPage; -} - -class MinecraftPage : public QWidget, public BasePage { +class MinecraftPage : public MinecraftSettingsWidget, public BasePage { Q_OBJECT public: - explicit MinecraftPage(QWidget* parent = 0); - ~MinecraftPage(); + explicit MinecraftPage(QWidget* parent = nullptr) : MinecraftSettingsWidget(nullptr, parent) {} + ~MinecraftPage() override {} QString displayName() const override { return tr("Minecraft"); } QIcon icon() const override { return APPLICATION->getThemedIcon("minecraft"); } QString id() const override { return "minecraft-settings"; } QString helpPage() const override { return "Minecraft-settings"; } - bool apply() override; - void retranslate() override; - - private: - void updateCheckboxStuff(); - void applySettings(); - void loadSettings(); - - private slots: - void on_maximizedCheckBox_clicked(bool checked); - - void onUseNativeGLFWChanged(bool checked); - void onUseNativeOpenALChanged(bool checked); - - private: - Ui::MinecraftPage* ui; + bool apply() override + { + saveSettings(); + return true; + } }; diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui deleted file mode 100644 index 2a3c0d96d..000000000 --- a/launcher/ui/pages/global/MinecraftPage.ui +++ /dev/null @@ -1,341 +0,0 @@ - - - MinecraftPage - - - - 0 - 0 - 936 - 541 - - - - - 0 - 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - QTabWidget::Rounded - - - 0 - - - - General - - - - - - Window Size - - - - - - Start Minecraft &maximized - - - - - - - - - Window &height: - - - windowHeightSpinBox - - - - - - - Window &width: - - - windowWidthSpinBox - - - - - - - 1 - - - 65536 - - - 1 - - - 854 - - - - - - - 1 - - - 65536 - - - 480 - - - - - - - - - - - - Game time - - - - - - Show time spent &playing instances - - - - - - - Show time spent playing across &all instances - - - - - - - &Record time spent playing instances - - - - - - - Show time spent playing in hours - - - - - - - - - - Miscellaneous - - - - - - <html><head/><body><p>The launcher will automatically reopen when the game crashes or exits.</p></body></html> - - - &Close the launcher after game window opens - - - - - - - <html><head/><body><p>The launcher will automatically quit after the game exits or crashes.</p></body></html> - - - &Quit the launcher after game window closes - - - - - - - - - - Qt::Vertical - - - - 0 - 0 - - - - - - - - - Tweaks - - - - - - Legacy settings - - - - - - <html><head/><body><p>Emulates usages of old online services which are no longer operating.</p><p>This currently allows modern skins to be used.</p></body></html> - - - Enable online fixes (experimental) - - - - - - - - - - Native library workarounds - - - - - - Use system installation of &GLFW - - - - - - - &GLFW library path - - - lineEditGLFWPath - - - - - - - Use system installation of &OpenAL - - - - - - - &OpenAL library path - - - lineEditOpenALPath - - - - - - - false - - - - - - - false - - - - - - - - - - Performance - - - - - - <html><head/><body><p>Enable Feral Interactive's GameMode, to potentially improve gaming performance.</p></body></html> - - - Enable Feral GameMode - - - - - - - <html><head/><body><p>Enable MangoHud's advanced performance overlay.</p></body></html> - - - Enable MangoHud - - - - - - - <html><head/><body><p>Use the discrete GPU instead of the primary GPU.</p></body></html> - - - Use discrete GPU - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - maximizedCheckBox - windowWidthSpinBox - windowHeightSpinBox - - - - diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index 48a71b809..50217f982 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -74,6 +74,7 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared connect(ui->actionRemoveItem, &QAction::triggered, this, &ExternalResourcesPage::removeItem); connect(ui->actionEnableItem, &QAction::triggered, this, &ExternalResourcesPage::enableItem); connect(ui->actionDisableItem, &QAction::triggered, this, &ExternalResourcesPage::disableItem); + connect(ui->actionViewHomepage, &QAction::triggered, this, &ExternalResourcesPage::viewHomepage); connect(ui->actionViewConfigs, &QAction::triggered, this, &ExternalResourcesPage::viewConfigs); connect(ui->actionViewFolder, &QAction::triggered, this, &ExternalResourcesPage::viewFolder); @@ -81,15 +82,28 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared connect(ui->treeView, &ModListView::activated, this, &ExternalResourcesPage::itemActivated); auto selection_model = ui->treeView->selectionModel(); - connect(selection_model, &QItemSelectionModel::currentChanged, this, &ExternalResourcesPage::current); + + connect(selection_model, &QItemSelectionModel::currentChanged, this, [this](const QModelIndex& current, const QModelIndex& previous) { + if (!current.isValid()) { + ui->frame->clear(); + return; + } + + updateFrame(current, previous); + }); + auto updateExtra = [this]() { if (updateExtraInfo) updateExtraInfo(id(), extraHeaderInfoString()); }; + connect(selection_model, &QItemSelectionModel::selectionChanged, this, updateExtra); connect(model.get(), &ResourceFolderModel::updateFinished, this, updateExtra); + connect(model.get(), &ResourceFolderModel::parseFinished, this, updateExtra); - connect(ui->filterEdit, &QLineEdit::textChanged, this, &ExternalResourcesPage::filterTextChanged); + connect(selection_model, &QItemSelectionModel::selectionChanged, this, [this] { updateActions(); }); + connect(m_model.get(), &ResourceFolderModel::rowsInserted, this, [this] { updateActions(); }); + connect(m_model.get(), &ResourceFolderModel::rowsRemoved, this, [this] { updateActions(); }); auto viewHeader = ui->treeView->header(); viewHeader->setContextMenuPolicy(Qt::CustomContextMenu); @@ -98,6 +112,7 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared m_model->loadColumns(ui->treeView); connect(ui->treeView->header(), &QHeaderView::sectionResized, this, [this] { m_model->saveColumns(ui->treeView); }); + connect(ui->filterEdit, &QLineEdit::textChanged, this, &ExternalResourcesPage::filterTextChanged); } ExternalResourcesPage::~ExternalResourcesPage() @@ -288,33 +303,52 @@ void ExternalResourcesPage::disableItem() m_model->setResourceEnabled(selection.indexes(), EnableAction::DISABLE); } +void ExternalResourcesPage::viewHomepage() +{ + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + for (auto resource : m_model->selectedResources(selection)) { + auto url = resource->homepage(); + if (!url.isEmpty()) + DesktopServices::openUrl(url); + } +} + void ExternalResourcesPage::viewConfigs() { - DesktopServices::openDirectory(m_instance->instanceConfigFolder(), true); + DesktopServices::openPath(m_instance->instanceConfigFolder(), true); } void ExternalResourcesPage::viewFolder() { - DesktopServices::openDirectory(m_model->dir().absolutePath(), true); + DesktopServices::openPath(m_model->dir().absolutePath(), true); } -bool ExternalResourcesPage::current(const QModelIndex& current, const QModelIndex& previous) +void ExternalResourcesPage::updateActions() { - if (!current.isValid()) { - ui->frame->clear(); - return false; - } + const bool hasSelection = ui->treeView->selectionModel()->hasSelection(); + const QModelIndexList selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + const QList selectedResources = m_model->selectedResources(selection); - return onSelectionChanged(current, previous); + ui->actionUpdateItem->setEnabled(!m_model->empty()); + ui->actionResetItemMetadata->setEnabled(hasSelection); + + ui->actionChangeVersion->setEnabled(selectedResources.size() == 1 && selectedResources[0]->metadata() != nullptr); + + ui->actionRemoveItem->setEnabled(hasSelection); + ui->actionEnableItem->setEnabled(hasSelection); + ui->actionDisableItem->setEnabled(hasSelection); + + ui->actionViewHomepage->setEnabled(hasSelection && std::any_of(selectedResources.begin(), selectedResources.end(), + [](Resource* resource) { return !resource->homepage().isEmpty(); })); + ui->actionExportMetadata->setEnabled(!m_model->empty()); } -bool ExternalResourcesPage::onSelectionChanged(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) +void ExternalResourcesPage::updateFrame(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); Resource const& resource = m_model->at(row); ui->frame->updateWithResource(resource); - return true; } QString ExternalResourcesPage::extraHeaderInfoString() diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.h b/launcher/ui/pages/instance/ExternalResourcesPage.h index d29be0fc3..00bb5d17d 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.h +++ b/launcher/ui/pages/instance/ExternalResourcesPage.h @@ -42,9 +42,8 @@ class ExternalResourcesPage : public QMainWindow, public BasePage { QMenu* createPopupMenu() override; public slots: - bool current(const QModelIndex& current, const QModelIndex& previous); - - virtual bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous); + virtual void updateActions(); + virtual void updateFrame(const QModelIndex& current, const QModelIndex& previous); protected slots: void itemActivated(const QModelIndex& index); @@ -57,6 +56,8 @@ class ExternalResourcesPage : public QMainWindow, public BasePage { virtual void enableItem(); virtual void disableItem(); + virtual void viewHomepage(); + virtual void viewFolder(); virtual void viewConfigs(); diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui index ff08e12d2..5df8aafa2 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.ui +++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui @@ -70,14 +70,14 @@ - - true - Actions - Qt::ToolButtonTextOnly + Qt::ToolButtonIconOnly + + + true RightToolBarArea @@ -90,39 +90,50 @@ + + - &Add + &Add File - Add + Add a locally downloaded file. + + false + &Remove - Remove selected item + Remove all selected items. + + false + &Enable - Enable selected item + Enable all selected items. + + false + &Disable - Disable selected item + Disable all selected items. @@ -137,6 +148,9 @@ View &Folder + + Open the folder in the system file manager. + @@ -146,29 +160,70 @@ &Download - Download a new resource - - - - - false - - - Visit mod's page - - - Go to mods home page + Download resources from online mod platforms. - true + false Check for &Updates - Try to check or update all selected resources (all resources if none are selected) + Try to check or update all selected resources (all resources if none are selected). + + + + + Reset Update Metadata + + + QAction::NoRole + + + + + Verify Dependencies + + + QAction::NoRole + + + + + false + + + Export List + + + Export resource's metadata to text. + + + + + false + + + Change Version + + + Change a resource's version. + + + QAction::NoRole + + + + + false + + + View Homepage + + + View the homepages of all selected items. diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp deleted file mode 100644 index 74785f97e..000000000 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ /dev/null @@ -1,532 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2022 Jamie Mansfield - * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (C) 2022 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 "InstanceSettingsPage.h" -#include "ui_InstanceSettingsPage.h" - -#include -#include -#include - -#include - -#include "ui/dialogs/VersionSelectDialog.h" -#include "ui/widgets/CustomCommands.h" - -#include "Application.h" -#include "BuildConfig.h" -#include "JavaCommon.h" -#include "minecraft/auth/AccountList.h" - -#include "FileSystem.h" -#include "java/JavaInstallList.h" -#include "java/JavaUtils.h" - -InstanceSettingsPage::InstanceSettingsPage(BaseInstance* inst, QWidget* parent) - : QWidget(parent), ui(new Ui::InstanceSettingsPage), m_instance(inst) -{ - m_settings = inst->settings(); - ui->setupUi(this); - - connect(ui->openGlobalJavaSettingsButton, &QCommandLinkButton::clicked, this, &InstanceSettingsPage::globalSettingsButtonClicked); - connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::applySettings); - connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings); - connect(ui->instanceAccountSelector, QOverload::of(&QComboBox::currentIndexChanged), this, - &InstanceSettingsPage::changeInstanceAccount); - - connect(ui->useNativeGLFWCheck, &QAbstractButton::toggled, this, &InstanceSettingsPage::onUseNativeGLFWChanged); - connect(ui->useNativeOpenALCheck, &QAbstractButton::toggled, this, &InstanceSettingsPage::onUseNativeOpenALChanged); - - loadSettings(); - - updateThresholds(); -} - -InstanceSettingsPage::~InstanceSettingsPage() -{ - delete ui; -} - -void InstanceSettingsPage::globalSettingsButtonClicked(bool) -{ - switch (ui->settingsTabs->currentIndex()) { - case 0: - APPLICATION->ShowGlobalSettings(this, "java-settings"); - return; - case 2: - APPLICATION->ShowGlobalSettings(this, "custom-commands"); - return; - case 3: - APPLICATION->ShowGlobalSettings(this, "environment-variables"); - return; - default: - APPLICATION->ShowGlobalSettings(this, "minecraft-settings"); - return; - } -} - -bool InstanceSettingsPage::apply() -{ - applySettings(); - return true; -} - -void InstanceSettingsPage::applySettings() -{ - SettingsObject::Lock lock(m_settings); - - // Miscellaneous - bool miscellaneous = ui->miscellaneousSettingsBox->isChecked(); - m_settings->set("OverrideMiscellaneous", miscellaneous); - if (miscellaneous) { - m_settings->set("CloseAfterLaunch", ui->closeAfterLaunchCheck->isChecked()); - m_settings->set("QuitAfterGameStop", ui->quitAfterGameStopCheck->isChecked()); - } else { - m_settings->reset("CloseAfterLaunch"); - m_settings->reset("QuitAfterGameStop"); - } - - // Console - bool console = ui->consoleSettingsBox->isChecked(); - m_settings->set("OverrideConsole", console); - if (console) { - m_settings->set("ShowConsole", ui->showConsoleCheck->isChecked()); - m_settings->set("AutoCloseConsole", ui->autoCloseConsoleCheck->isChecked()); - m_settings->set("ShowConsoleOnError", ui->showConsoleErrorCheck->isChecked()); - } else { - m_settings->reset("ShowConsole"); - m_settings->reset("AutoCloseConsole"); - m_settings->reset("ShowConsoleOnError"); - } - - // Window Size - bool window = ui->windowSizeGroupBox->isChecked(); - m_settings->set("OverrideWindow", window); - if (window) { - m_settings->set("LaunchMaximized", ui->maximizedCheckBox->isChecked()); - m_settings->set("MinecraftWinWidth", ui->windowWidthSpinBox->value()); - m_settings->set("MinecraftWinHeight", ui->windowHeightSpinBox->value()); - } else { - m_settings->reset("LaunchMaximized"); - m_settings->reset("MinecraftWinWidth"); - m_settings->reset("MinecraftWinHeight"); - } - - // Memory - bool memory = ui->memoryGroupBox->isChecked(); - m_settings->set("OverrideMemory", memory); - if (memory) { - int min = ui->minMemSpinBox->value(); - int max = ui->maxMemSpinBox->value(); - if (min < max) { - m_settings->set("MinMemAlloc", min); - m_settings->set("MaxMemAlloc", max); - } else { - m_settings->set("MinMemAlloc", max); - m_settings->set("MaxMemAlloc", min); - } - m_settings->set("PermGen", ui->permGenSpinBox->value()); - } else { - m_settings->reset("MinMemAlloc"); - m_settings->reset("MaxMemAlloc"); - m_settings->reset("PermGen"); - } - - // Java Install Settings - bool javaInstall = ui->javaSettingsGroupBox->isChecked(); - m_settings->set("OverrideJavaLocation", javaInstall); - if (javaInstall) { - m_settings->set("JavaPath", ui->javaPathTextBox->text()); - m_settings->set("IgnoreJavaCompatibility", ui->skipCompatibilityCheckbox->isChecked()); - } else { - m_settings->reset("JavaPath"); - m_settings->reset("IgnoreJavaCompatibility"); - } - - // Java arguments - bool javaArgs = ui->javaArgumentsGroupBox->isChecked(); - m_settings->set("OverrideJavaArgs", javaArgs); - if (javaArgs) { - m_settings->set("JvmArgs", ui->jvmArgsTextBox->toPlainText().replace("\n", " ")); - } else { - 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); - if (custcmd) { - m_settings->set("PreLaunchCommand", ui->customCommands->prelaunchCommand()); - m_settings->set("WrapperCommand", ui->customCommands->wrapperCommand()); - m_settings->set("PostExitCommand", ui->customCommands->postexitCommand()); - } else { - m_settings->reset("PreLaunchCommand"); - m_settings->reset("WrapperCommand"); - m_settings->reset("PostExitCommand"); - } - - // Environment Variables - auto env = ui->environmentVariables->override(); - m_settings->set("OverrideEnv", env); - if (env) - m_settings->set("Env", ui->environmentVariables->value()); - else - m_settings->reset("Env"); - - // Workarounds - bool workarounds = ui->nativeWorkaroundsGroupBox->isChecked(); - m_settings->set("OverrideNativeWorkarounds", workarounds); - if (workarounds) { - m_settings->set("UseNativeGLFW", ui->useNativeGLFWCheck->isChecked()); - m_settings->set("CustomGLFWPath", ui->lineEditGLFWPath->text()); - m_settings->set("UseNativeOpenAL", ui->useNativeOpenALCheck->isChecked()); - m_settings->set("CustomOpenALPath", ui->lineEditOpenALPath->text()); - } else { - m_settings->reset("UseNativeGLFW"); - m_settings->reset("CustomGLFWPath"); - m_settings->reset("UseNativeOpenAL"); - m_settings->reset("CustomOpenALPath"); - } - - // Performance - bool performance = ui->perfomanceGroupBox->isChecked(); - m_settings->set("OverridePerformance", performance); - if (performance) { - m_settings->set("EnableFeralGamemode", ui->enableFeralGamemodeCheck->isChecked()); - m_settings->set("EnableMangoHud", ui->enableMangoHud->isChecked()); - m_settings->set("UseDiscreteGpu", ui->useDiscreteGpuCheck->isChecked()); - } else { - m_settings->reset("EnableFeralGamemode"); - m_settings->reset("EnableMangoHud"); - m_settings->reset("UseDiscreteGpu"); - } - - // Game time - bool gameTime = ui->gameTimeGroupBox->isChecked(); - m_settings->set("OverrideGameTime", gameTime); - if (gameTime) { - m_settings->set("ShowGameTime", ui->showGameTime->isChecked()); - m_settings->set("RecordGameTime", ui->recordGameTime->isChecked()); - } else { - m_settings->reset("ShowGameTime"); - m_settings->reset("RecordGameTime"); - } - - // Join server on launch - bool joinServerOnLaunch = ui->serverJoinGroupBox->isChecked(); - m_settings->set("JoinServerOnLaunch", joinServerOnLaunch); - if (joinServerOnLaunch) { - m_settings->set("JoinServerOnLaunchAddress", ui->serverJoinAddress->text()); - } else { - m_settings->reset("JoinServerOnLaunchAddress"); - } - - // Use an account for this instance - bool useAccountForInstance = ui->instanceAccountGroupBox->isChecked(); - m_settings->set("UseAccountForInstance", useAccountForInstance); - if (!useAccountForInstance) { - m_settings->reset("InstanceAccountId"); - } - - bool overrideLegacySettings = ui->legacySettingsGroupBox->isChecked(); - m_settings->set("OverrideLegacySettings", overrideLegacySettings); - if (overrideLegacySettings) { - m_settings->set("OnlineFixes", ui->onlineFixes->isChecked()); - } else { - m_settings->reset("OnlineFixes"); - } - - // FIXME: This should probably be called by a signal instead - m_instance->updateRuntimeContext(); -} - -void InstanceSettingsPage::loadSettings() -{ - // Miscellaneous - ui->miscellaneousSettingsBox->setChecked(m_settings->get("OverrideMiscellaneous").toBool()); - ui->closeAfterLaunchCheck->setChecked(m_settings->get("CloseAfterLaunch").toBool()); - ui->quitAfterGameStopCheck->setChecked(m_settings->get("QuitAfterGameStop").toBool()); - - // Console - ui->consoleSettingsBox->setChecked(m_settings->get("OverrideConsole").toBool()); - ui->showConsoleCheck->setChecked(m_settings->get("ShowConsole").toBool()); - ui->autoCloseConsoleCheck->setChecked(m_settings->get("AutoCloseConsole").toBool()); - ui->showConsoleErrorCheck->setChecked(m_settings->get("ShowConsoleOnError").toBool()); - - // Window Size - ui->windowSizeGroupBox->setChecked(m_settings->get("OverrideWindow").toBool()); - ui->maximizedCheckBox->setChecked(m_settings->get("LaunchMaximized").toBool()); - ui->windowWidthSpinBox->setValue(m_settings->get("MinecraftWinWidth").toInt()); - ui->windowHeightSpinBox->setValue(m_settings->get("MinecraftWinHeight").toInt()); - - // Memory - ui->memoryGroupBox->setChecked(m_settings->get("OverrideMemory").toBool()); - int min = m_settings->get("MinMemAlloc").toInt(); - int max = m_settings->get("MaxMemAlloc").toInt(); - if (min < max) { - ui->minMemSpinBox->setValue(min); - ui->maxMemSpinBox->setValue(max); - } else { - ui->minMemSpinBox->setValue(max); - ui->maxMemSpinBox->setValue(min); - } - ui->permGenSpinBox->setValue(m_settings->get("PermGen").toInt()); - bool permGenVisible = m_settings->get("PermGenVisible").toBool(); - ui->permGenSpinBox->setVisible(permGenVisible); - ui->labelPermGen->setVisible(permGenVisible); - 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; - - ui->javaSettingsGroupBox->setChecked(overrideLocation); - ui->javaPathTextBox->setText(m_settings->get("JavaPath").toString()); - ui->skipCompatibilityCheckbox->setChecked(m_settings->get("IgnoreJavaCompatibility").toBool()); - - ui->javaArgumentsGroupBox->setChecked(overrideArgs); - ui->jvmArgsTextBox->setPlainText(m_settings->get("JvmArgs").toString()); - - // Custom commands - ui->customCommands->initialize(true, m_settings->get("OverrideCommands").toBool(), m_settings->get("PreLaunchCommand").toString(), - m_settings->get("WrapperCommand").toString(), m_settings->get("PostExitCommand").toString()); - - // Environment variables - ui->environmentVariables->initialize(true, m_settings->get("OverrideEnv").toBool(), m_settings->get("Env").toMap()); - - // Workarounds - ui->nativeWorkaroundsGroupBox->setChecked(m_settings->get("OverrideNativeWorkarounds").toBool()); - ui->useNativeGLFWCheck->setChecked(m_settings->get("UseNativeGLFW").toBool()); - ui->lineEditGLFWPath->setText(m_settings->get("CustomGLFWPath").toString()); -#ifdef Q_OS_LINUX - ui->lineEditGLFWPath->setPlaceholderText(APPLICATION->m_detectedGLFWPath); -#else - ui->lineEditGLFWPath->setPlaceholderText(tr("Path to %1 library file").arg(BuildConfig.GLFW_LIBRARY_NAME)); -#endif - ui->useNativeOpenALCheck->setChecked(m_settings->get("UseNativeOpenAL").toBool()); - ui->lineEditOpenALPath->setText(m_settings->get("CustomOpenALPath").toString()); -#ifdef Q_OS_LINUX - ui->lineEditOpenALPath->setPlaceholderText(APPLICATION->m_detectedOpenALPath); -#else - ui->lineEditGLFWPath->setPlaceholderText(tr("Path to %1 library file").arg(BuildConfig.OPENAL_LIBRARY_NAME)); -#endif - - // Performance - ui->perfomanceGroupBox->setChecked(m_settings->get("OverridePerformance").toBool()); - ui->enableFeralGamemodeCheck->setChecked(m_settings->get("EnableFeralGamemode").toBool()); - ui->enableMangoHud->setChecked(m_settings->get("EnableMangoHud").toBool()); - ui->useDiscreteGpuCheck->setChecked(m_settings->get("UseDiscreteGpu").toBool()); - -#if !defined(Q_OS_LINUX) - ui->settingsTabs->setTabVisible(ui->settingsTabs->indexOf(ui->performancePage), false); -#endif - - if (!(APPLICATION->capabilities() & Application::SupportsGameMode)) { - ui->enableFeralGamemodeCheck->setDisabled(true); - ui->enableFeralGamemodeCheck->setToolTip(tr("Feral Interactive's GameMode could not be found on your system.")); - } - - if (!(APPLICATION->capabilities() & Application::SupportsMangoHud)) { - ui->enableMangoHud->setDisabled(true); - ui->enableMangoHud->setToolTip(tr("MangoHud could not be found on your system.")); - } - - // Miscellanous - ui->gameTimeGroupBox->setChecked(m_settings->get("OverrideGameTime").toBool()); - ui->showGameTime->setChecked(m_settings->get("ShowGameTime").toBool()); - ui->recordGameTime->setChecked(m_settings->get("RecordGameTime").toBool()); - - ui->serverJoinGroupBox->setChecked(m_settings->get("JoinServerOnLaunch").toBool()); - ui->serverJoinAddress->setText(m_settings->get("JoinServerOnLaunchAddress").toString()); - - ui->instanceAccountGroupBox->setChecked(m_settings->get("UseAccountForInstance").toBool()); - updateAccountsMenu(); - - ui->legacySettingsGroupBox->setChecked(m_settings->get("OverrideLegacySettings").toBool()); - ui->onlineFixes->setChecked(m_settings->get("OnlineFixes").toBool()); -} - -void InstanceSettingsPage::on_javaDetectBtn_clicked() -{ - if (JavaUtils::getJavaCheckPath().isEmpty()) { - JavaCommon::javaCheckNotFound(this); - return; - } - - JavaInstallPtr java; - - VersionSelectDialog vselect(APPLICATION->javalist().get(), tr("Select a Java version"), this, true); - vselect.setResizeOn(2); - vselect.exec(); - - if (vselect.result() == QDialog::Accepted && vselect.selectedVersion()) { - java = std::dynamic_pointer_cast(vselect.selectedVersion()); - ui->javaPathTextBox->setText(java->path); - bool visible = java->id.requiresPermGen() && m_settings->get("OverrideMemory").toBool(); - ui->permGenSpinBox->setVisible(visible); - ui->labelPermGen->setVisible(visible); - ui->labelPermgenNote->setVisible(visible); - m_settings->set("PermGenVisible", visible); - } -} - -void InstanceSettingsPage::on_javaBrowseBtn_clicked() -{ - QString raw_path = QFileDialog::getOpenFileName(this, tr("Find Java executable")); - - // do not allow current dir - it's dirty. Do not allow dirs that don't exist - if (raw_path.isEmpty()) { - return; - } - QString cooked_path = FS::NormalizePath(raw_path); - - QFileInfo javaInfo(cooked_path); - if (!javaInfo.exists() || !javaInfo.isExecutable()) { - return; - } - ui->javaPathTextBox->setText(cooked_path); - - // custom Java could be anything... enable perm gen option - ui->permGenSpinBox->setVisible(true); - ui->labelPermGen->setVisible(true); - ui->labelPermgenNote->setVisible(true); - m_settings->set("PermGenVisible", true); -} - -void InstanceSettingsPage::on_javaTestBtn_clicked() -{ - if (checker) { - return; - } - checker.reset(new JavaCommon::TestCheck(this, ui->javaPathTextBox->text(), ui->jvmArgsTextBox->toPlainText().replace("\n", " "), - ui->minMemSpinBox->value(), ui->maxMemSpinBox->value(), ui->permGenSpinBox->value())); - connect(checker.get(), SIGNAL(finished()), SLOT(checkerFinished())); - checker->run(); -} - -void InstanceSettingsPage::onUseNativeGLFWChanged(bool checked) -{ - ui->lineEditGLFWPath->setEnabled(checked); -} - -void InstanceSettingsPage::onUseNativeOpenALChanged(bool checked) -{ - ui->lineEditOpenALPath->setEnabled(checked); -} - -void InstanceSettingsPage::updateAccountsMenu() -{ - ui->instanceAccountSelector->clear(); - auto accounts = APPLICATION->accounts(); - int accountIndex = accounts->findAccountByProfileId(m_settings->get("InstanceAccountId").toString()); - - for (int i = 0; i < accounts->count(); i++) { - MinecraftAccountPtr account = accounts->at(i); - ui->instanceAccountSelector->addItem(getFaceForAccount(account), account->profileName(), i); - if (i == accountIndex) - ui->instanceAccountSelector->setCurrentIndex(i); - } -} - -QIcon InstanceSettingsPage::getFaceForAccount(MinecraftAccountPtr account) -{ - if (auto face = account->getFace(); !face.isNull()) { - return face; - } - - return APPLICATION->getThemedIcon("noaccount"); -} - -void InstanceSettingsPage::changeInstanceAccount(int index) -{ - auto accounts = APPLICATION->accounts(); - if (index != -1 && accounts->at(index) && ui->instanceAccountGroupBox->isChecked()) { - auto account = accounts->at(index); - m_settings->set("InstanceAccountId", account->profileId()); - } -} - -void InstanceSettingsPage::on_maxMemSpinBox_valueChanged([[maybe_unused]] int i) -{ - updateThresholds(); -} - -void InstanceSettingsPage::checkerFinished() -{ - checker.reset(); -} - -void InstanceSettingsPage::retranslate() -{ - ui->retranslateUi(this); - ui->customCommands->retranslate(); // TODO: why is this seperate from the others? - ui->environmentVariables->retranslate(); -} - -void InstanceSettingsPage::updateThresholds() -{ - auto sysMiB = Sys::getSystemRam() / Sys::mebibyte; - unsigned int maxMem = ui->maxMemSpinBox->value(); - unsigned int minMem = ui->minMemSpinBox->value(); - - QString iconName; - - if (maxMem >= sysMiB) { - iconName = "status-bad"; - ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation exceeds your system memory capacity.")); - } else if (maxMem > (sysMiB * 0.9)) { - iconName = "status-yellow"; - ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity.")); - } else if (maxMem < minMem) { - iconName = "status-yellow"; - ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation is smaller than the minimum value")); - } else { - iconName = "status-good"; - ui->labelMaxMemIcon->setToolTip(""); - } - - { - auto height = ui->labelMaxMemIcon->fontInfo().pixelSize(); - QIcon icon = APPLICATION->getThemedIcon(iconName); - QPixmap pix = icon.pixmap(height, height); - ui->labelMaxMemIcon->setPixmap(pix); - } -} diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h index 8b78dcb7f..fa1dce3dc 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.h +++ b/launcher/ui/pages/instance/InstanceSettingsPage.h @@ -35,59 +35,29 @@ #pragma once -#include - -#include -#include #include "Application.h" #include "BaseInstance.h" -#include "JavaCommon.h" -#include "java/JavaChecker.h" #include "ui/pages/BasePage.h" +#include "ui/widgets/MinecraftSettingsWidget.h" +#include -class JavaChecker; -namespace Ui { -class InstanceSettingsPage; -} - -class InstanceSettingsPage : public QWidget, public BasePage { +class InstanceSettingsPage : public MinecraftSettingsWidget, public BasePage { Q_OBJECT public: - explicit InstanceSettingsPage(BaseInstance* inst, QWidget* parent = 0); - virtual ~InstanceSettingsPage(); - virtual QString displayName() const override { return tr("Settings"); } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("instance-settings"); } - virtual QString id() const override { return "settings"; } - virtual bool apply() override; - virtual QString helpPage() const override { return "Instance-settings"; } - void retranslate() override; - - void updateThresholds(); - - private slots: - void on_javaDetectBtn_clicked(); - void on_javaTestBtn_clicked(); - void on_javaBrowseBtn_clicked(); - void on_maxMemSpinBox_valueChanged(int i); - - void onUseNativeGLFWChanged(bool checked); - void onUseNativeOpenALChanged(bool checked); - - void applySettings(); - void loadSettings(); - - void checkerFinished(); - - void globalSettingsButtonClicked(bool checked); - - void updateAccountsMenu(); - QIcon getFaceForAccount(MinecraftAccountPtr account); - void changeInstanceAccount(int index); - - private: - Ui::InstanceSettingsPage* ui; - BaseInstance* m_instance; - SettingsObjectPtr m_settings; - unique_qobject_ptr checker; + explicit InstanceSettingsPage(MinecraftInstancePtr instance, QWidget* parent = nullptr) : MinecraftSettingsWidget(std::move(instance), parent) + { + connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::saveSettings); + connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings); + } + ~InstanceSettingsPage() override {} + QString displayName() const override { return tr("Settings"); } + QIcon icon() const override { return APPLICATION->getThemedIcon("instance-settings"); } + QString id() const override { return "settings"; } + bool apply() override + { + saveSettings(); + return true; + } + QString helpPage() const override { return "Instance-settings"; } }; diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui deleted file mode 100644 index 3b06ac865..000000000 --- a/launcher/ui/pages/instance/InstanceSettingsPage.ui +++ /dev/null @@ -1,779 +0,0 @@ - - - InstanceSettingsPage - - - - 0 - 0 - 691 - 581 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Open Global Settings - - - The settings here are overrides for global settings. - - - - - - - 0 - - - - Java - - - - - - true - - - Java insta&llation - - - true - - - false - - - - - - If enabled, the launcher will not check if an instance is compatible with the selected Java version. - - - Skip Java compatibility checks - - - - - - - - - - - - Browse - - - - - - - - - - - Auto-detect... - - - - - - - Test - - - - - - - - - - - - true - - - Memor&y - - - true - - - false - - - - - - PermGen: - - - - - - - Minimum memory allocation: - - - - - - - Maximum memory allocation: - - - - - - - Note: Permgen is set automatically by Java 8 and later - - - - - - - The amount of memory Minecraft is started with. - - - MiB - - - 8 - - - 1048576 - - - 128 - - - 256 - - - - - - - The maximum amount of memory Minecraft is allowed to use. - - - MiB - - - 8 - - - 1048576 - - - 128 - - - 1024 - - - - - - - The amount of memory available to store loaded Java classes. - - - MiB - - - 4 - - - 999999999 - - - 8 - - - 64 - - - - - - - - - - Qt::AlignCenter - - - maxMemSpinBox - - - - - - - - - - true - - - Java argumen&ts - - - true - - - false - - - - - - - - - - - - - Game windows - - - - - - true - - - Game Window - - - true - - - false - - - - - - Start Minecraft maximized - - - - - - - - - Window height: - - - - - - - Window width: - - - - - - - 1 - - - 65536 - - - 1 - - - 854 - - - - - - - 1 - - - 65536 - - - 480 - - - - - - - - - - - - true - - - Conso&le Settings - - - true - - - false - - - - - - Show console while the game is running - - - - - - - Automatically close console when the game quits - - - - - - - Show console when the game crashes - - - - - - - - - - Miscellaneous - - - true - - - false - - - - - - Close the launcher after game window opens - - - - - - - Quit the launcher after game window closes - - - - - - - - - - Qt::Vertical - - - - 88 - 125 - - - - - - - - - Custom commands - - - - - - - - - - Environment variables - - - - - - - - - - Workarounds - - - - - - true - - - Native libraries - - - true - - - false - - - - - - Use system installation of OpenAL - - - - - - - &GLFW library path - - - lineEditGLFWPath - - - - - - - Use system installation of GLFW - - - - - - - false - - - - - - - &OpenAL library path - - - lineEditOpenALPath - - - - - - - false - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - Performance - - - - - - true - - - Performance - - - true - - - false - - - - - - <html><head/><body><p>Enable Feral Interactive's GameMode, to potentially improve gaming performance.</p></body></html> - - - Enable Feral GameMode - - - - - - - <html><head/><body><p>Enable MangoHud's advanced performance overlay.</p></body></html> - - - Enable MangoHud - - - - - - - <html><head/><body><p>Use the discrete GPU instead of the primary GPU.</p></body></html> - - - Use discrete GPU - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - Miscellaneous - - - - - - Legacy settings - - - true - - - false - - - - - - <html><head/><body><p>Emulates usages of old online services which are no longer operating.</p><p>This currently allows modern skins to be used.</p></body></html> - - - Enable online fixes (experimental) - - - - - - - - - - true - - - Override global game time settings - - - true - - - false - - - - - - Show time spent playing this instance - - - - - - - Record time spent playing this instance - - - - - - - - - - Set a server to join on launch - - - true - - - false - - - - - - - - - 0 - 0 - - - - Server address: - - - - - - - - - - - - - - - Override default account - - - true - - - false - - - - - - - - - 0 - 0 - - - - Account: - - - - - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - - CustomCommands - QWidget -
    ui/widgets/CustomCommands.h
    - 1 -
    - - EnvironmentVariables - QWidget -
    ui/widgets/EnvironmentVariables.h
    - 1 -
    -
    - - openGlobalJavaSettingsButton - settingsTabs - javaSettingsGroupBox - memoryGroupBox - minMemSpinBox - maxMemSpinBox - permGenSpinBox - javaArgumentsGroupBox - jvmArgsTextBox - windowSizeGroupBox - maximizedCheckBox - windowWidthSpinBox - windowHeightSpinBox - consoleSettingsBox - showConsoleCheck - autoCloseConsoleCheck - showConsoleErrorCheck - nativeWorkaroundsGroupBox - useNativeGLFWCheck - useNativeOpenALCheck - showGameTime - recordGameTime - - - -
    diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index 8e1e53762..4962f90ce 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,30 +57,40 @@ 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); - auto compare = [&](int r) -> QModelIndex { + auto compare = [this, start, parentIndex, value](int r) -> QModelIndex { QModelIndex idx = index(r, start.column(), parentIndex); if (!idx.isValid() || idx == start) { return QModelIndex(); @@ -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 { @@ -231,7 +234,7 @@ bool LogPage::apply() bool LogPage::shouldDisplay() const { - return m_instance->isRunning() || m_proxy->rowCount() > 0; + return true; } void LogPage::on_btnPaste_clicked() diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp index 26a8302db..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" @@ -131,6 +132,22 @@ ManagedPackPage::~ManagedPackPage() void ManagedPackPage::openedImpl() { + if (m_inst->getManagedPackID().isEmpty()) { + ui->packVersion->hide(); + ui->packVersionLabel->hide(); + ui->packOrigin->hide(); + ui->packOriginLabel->hide(); + ui->versionsComboBox->hide(); + ui->updateButton->hide(); + ui->updateToVersionLabel->hide(); + ui->updateFromFileButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + + ui->packName->setText(m_inst->name()); + ui->changelogTextBrowser->setText(tr("This is a local modpack.\n" + "This can be updated only using a file in %1 format\n") + .arg(displayName())); + return; + } ui->packName->setText(m_inst->getManagedPackName()); ui->packVersion->setText(m_inst->getManagedPackVersionName()); ui->packOrigin->setText(tr("Website: %2 | Pack ID: %3 | Version ID: %4") @@ -316,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(); } @@ -354,7 +371,9 @@ 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; extra_info.insert("pack_id", m_inst->getManagedPackID()); extra_info.insert("pack_version_id", QString()); @@ -402,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; } @@ -472,7 +491,7 @@ void FlameManagedPackPage::parseManagedPack() QString FlameManagedPackPage::url() const { // FIXME: We should display the websiteUrl field, but this requires doing the API request first :( - return {}; + return "https://www.curseforge.com/projects/" + m_inst->getManagedPackID(); } void FlameManagedPackPage::suggestVersion() @@ -484,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(); } @@ -518,7 +538,9 @@ 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; QMap extra_info; extra_info.insert("pack_id", m_inst->getManagedPackID()); 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 ce1cc3a90..95507ac22 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 @@ -49,107 +51,60 @@ #include "Application.h" -#include "ui/GuiUtil.h" #include "ui/dialogs/CustomMessageBox.h" -#include "ui/dialogs/ModUpdateDialog.h" #include "ui/dialogs/ResourceDownloadDialog.h" - -#include "DesktopServices.h" +#include "ui/dialogs/ResourceUpdateDialog.h" #include "minecraft/PackProfile.h" #include "minecraft/VersionFilterData.h" #include "minecraft/mod/Mod.h" #include "minecraft/mod/ModFolderModel.h" -#include "modplatform/ModIndex.h" -#include "modplatform/ResourceAPI.h" - -#include "Version.h" #include "tasks/ConcurrentTask.h" +#include "tasks/Task.h" #include "ui/dialogs/ProgressDialog.h" -ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent) - : ExternalResourcesPage(inst, mods, parent), m_model(mods) +ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr model, QWidget* parent) + : ExternalResourcesPage(inst, model, parent), m_model(model) { - // This is structured like that so that these changes - // do not affect the Resource pack and Shader pack tabs - { - ui->actionDownloadItem->setText(tr("Download mods")); - ui->actionDownloadItem->setToolTip(tr("Download mods from online mod platforms")); - ui->actionDownloadItem->setEnabled(true); - ui->actionAddItem->setText(tr("Add file")); - ui->actionAddItem->setToolTip(tr("Add a locally downloaded file")); + ui->actionDownloadItem->setText(tr("Download Mods")); + ui->actionDownloadItem->setToolTip(tr("Download mods from online mod platforms")); + ui->actionDownloadItem->setEnabled(true); + ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); - ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); + connect(ui->actionDownloadItem, &QAction::triggered, this, &ModFolderPage::downloadMods); - connect(ui->actionDownloadItem, &QAction::triggered, this, &ModFolderPage::installMods); + ui->actionUpdateItem->setToolTip(tr("Try to check or update all selected mods (all mods if none are selected)")); + connect(ui->actionUpdateItem, &QAction::triggered, this, &ModFolderPage::updateMods); + ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionUpdateItem); - // update menu - auto updateMenu = ui->actionUpdateItem->menu(); - if (updateMenu) { - updateMenu->clear(); - } else { - updateMenu = new QMenu(this); - } + auto updateMenu = new QMenu(this); - auto update = updateMenu->addAction(tr("Check for Updates")); - update->setToolTip(tr("Try to check or update all selected mods (all mods if none are selected)")); - connect(update, &QAction::triggered, this, &ModFolderPage::updateMods); + auto update = updateMenu->addAction(tr("Check for Updates")); + connect(update, &QAction::triggered, this, &ModFolderPage::updateMods); - auto updateWithDeps = updateMenu->addAction(tr("Verify Dependencies")); - updateWithDeps->setToolTip( - tr("Try to update and check for missing dependencies all selected mods (all mods if none are selected)")); - connect(updateWithDeps, &QAction::triggered, this, [this] { updateMods(true); }); + updateMenu->addAction(ui->actionVerifyItemDependencies); + connect(ui->actionVerifyItemDependencies, &QAction::triggered, this, [this] { updateMods(true); }); - auto depsDisabled = APPLICATION->settings()->getSetting("ModDependenciesDisabled"); - updateWithDeps->setVisible(!depsDisabled->get().toBool()); - connect(depsDisabled.get(), &Setting::SettingChanged, this, - [updateWithDeps](const Setting& setting, QVariant value) { updateWithDeps->setVisible(!value.toBool()); }); + auto depsDisabled = APPLICATION->settings()->getSetting("ModDependenciesDisabled"); + ui->actionVerifyItemDependencies->setVisible(!depsDisabled->get().toBool()); + connect(depsDisabled.get(), &Setting::SettingChanged, this, + [this](const Setting& setting, const QVariant& value) { ui->actionVerifyItemDependencies->setVisible(!value.toBool()); }); - auto actionRemoveItemMetadata = updateMenu->addAction(tr("Reset update metadata")); - actionRemoveItemMetadata->setToolTip(tr("Remove mod's metadata")); - connect(actionRemoveItemMetadata, &QAction::triggered, this, &ModFolderPage::deleteModMetadata); - actionRemoveItemMetadata->setEnabled(false); + updateMenu->addAction(ui->actionResetItemMetadata); + connect(ui->actionResetItemMetadata, &QAction::triggered, this, &ModFolderPage::deleteModMetadata); - ui->actionUpdateItem->setMenu(updateMenu); + ui->actionUpdateItem->setMenu(updateMenu); - ui->actionUpdateItem->setToolTip(tr("Try to check or update all selected mods (all mods if none are selected)")); - connect(ui->actionUpdateItem, &QAction::triggered, this, &ModFolderPage::updateMods); - ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionUpdateItem); + 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->actionVisitItemPage->setToolTip(tr("Go to mod's home page")); - ui->actionsToolbar->addAction(ui->actionVisitItemPage); - connect(ui->actionVisitItemPage, &QAction::triggered, this, &ModFolderPage::visitModPages); + ui->actionViewHomepage->setToolTip(tr("View the homepages of all selected mods.")); - auto check_allow_update = [this] { return ui->treeView->selectionModel()->hasSelection() || !m_model->empty(); }; - - connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, - [this, check_allow_update, actionRemoveItemMetadata] { - ui->actionUpdateItem->setEnabled(check_allow_update()); - - auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); - auto mods_list = m_model->selectedMods(selection); - auto selected = std::count_if(mods_list.cbegin(), mods_list.cend(), - [](Mod* v) { return v->metadata() != nullptr || v->homeurl().size() != 0; }); - if (selected <= 1) { - ui->actionVisitItemPage->setText(tr("Visit mod's page")); - ui->actionVisitItemPage->setToolTip(tr("Go to mod's home page")); - - } else { - ui->actionVisitItemPage->setText(tr("Visit mods' pages")); - ui->actionVisitItemPage->setToolTip(tr("Go to the pages of the selected mods")); - } - ui->actionVisitItemPage->setEnabled(selected != 0); - actionRemoveItemMetadata->setEnabled(selected != 0); - }); - - auto updateButtons = [this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); }; - connect(mods.get(), &ModFolderModel::rowsInserted, this, updateButtons); - - connect(mods.get(), &ModFolderModel::rowsRemoved, this, updateButtons); - - connect(mods.get(), &ModFolderModel::updateFinished, this, updateButtons); - } + ui->actionExportMetadata->setToolTip(tr("Export mod's metadata to text.")); + connect(ui->actionExportMetadata, &QAction::triggered, this, &ModFolderPage::exportModMetadata); + ui->actionsToolbar->insertActionAfter(ui->actionViewHomepage, ui->actionExportMetadata); } bool ModFolderPage::shouldDisplay() const @@ -157,15 +112,12 @@ bool ModFolderPage::shouldDisplay() const return true; } -bool ModFolderPage::onSelectionChanged(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) +void ModFolderPage::updateFrame(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); - Mod const* m = m_model->at(row); - if (m) - ui->frame->updateWithMod(*m); - - return true; + const Mod& mod = m_model->at(row); + ui->frame->updateWithMod(mod); } void ModFolderPage::removeItems(const QItemSelection& selection) @@ -180,10 +132,10 @@ void ModFolderPage::removeItems(const QItemSelection& selection) if (response != QMessageBox::Yes) return; } - m_model->deleteMods(selection.indexes()); + m_model->deleteResources(selection.indexes()); } -void ModFolderPage::installMods() +void ModFolderPage::downloadMods() { if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance @@ -196,7 +148,7 @@ void ModFolderPage::installMods() ResourceDownload::ModDownloadDialog mdownload(this, m_model, m_instance); if (mdownload.exec()) { - auto tasks = new ConcurrentTask(this, "Download Mods", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + auto tasks = new ConcurrentTask(tr("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(); @@ -242,7 +194,7 @@ void ModFolderPage::updateMods(bool includeDeps) if (m_instance != nullptr && m_instance->isRunning()) { auto response = CustomMessageBox::selectable(this, tr("Confirm Update"), - tr("If you update mods while the game is running may cause mod duplication and game crashes.\n" + tr("Updating mods while the game is running may cause mod duplication and game crashes.\n" "The old files may not be deleted as they are in use.\n" "Are you sure you want to do this?"), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) @@ -253,12 +205,12 @@ void ModFolderPage::updateMods(bool includeDeps) } auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); - auto mods_list = m_model->selectedMods(selection); + auto mods_list = m_model->selectedResources(selection); bool use_all = mods_list.empty(); if (use_all) - mods_list = m_model->allMods(); + mods_list = m_model->allResources(); - ModUpdateDialog update_dialog(this, m_instance, m_model, mods_list, includeDeps); + ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, includeDeps, true); update_dialog.checkCandidates(); if (update_dialog.aborted()) { @@ -279,7 +231,7 @@ void ModFolderPage::updateMods(bool includeDeps) } if (update_dialog.exec()) { - auto tasks = new ConcurrentTask(this, "Download Mods", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + auto tasks = new ConcurrentTask("Download Mods", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); @@ -308,50 +260,6 @@ void ModFolderPage::updateMods(bool includeDeps) } } -CoreModFolderPage::CoreModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent) - : ModFolderPage(inst, mods, parent) -{} - -bool CoreModFolderPage::shouldDisplay() const -{ - if (ModFolderPage::shouldDisplay()) { - auto inst = dynamic_cast(m_instance); - if (!inst) - return true; - - auto version = inst->getPackProfile(); - - if (!version) - return true; - if (!version->getComponent("net.minecraftforge")) - return false; - if (!version->getComponent("net.minecraft")) - return false; - if (version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate) - return true; - } - return false; -} - -NilModFolderPage::NilModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent) - : ModFolderPage(inst, mods, parent) -{} - -bool NilModFolderPage::shouldDisplay() const -{ - return m_model->dir().exists(); -} - -void ModFolderPage::visitModPages() -{ - auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); - for (auto mod : m_model->selectedMods(selection)) { - auto url = mod->metaurl(); - if (!url.isEmpty()) - DesktopServices::openUrl(url); - } -} - void ModFolderPage::deleteModMetadata() { auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); @@ -370,5 +278,117 @@ void ModFolderPage::deleteModMetadata() return; } - m_model->deleteModsMetadata(selection); + 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.setResourceMetadata((*mods_list.begin())->metadata()); + if (mdownload.exec()) { + auto tasks = new ConcurrentTask("Download Mods", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + 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 +{ + if (ModFolderPage::shouldDisplay()) { + auto inst = dynamic_cast(m_instance); + if (!inst) + return true; + + auto version = inst->getPackProfile(); + if (!version || !version->getComponent("net.minecraftforge") || !version->getComponent("net.minecraft")) + return false; + auto minecraftCmp = version->getComponent("net.minecraft"); + return minecraftCmp->m_loaded && minecraftCmp->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate; + } + return false; +} + +NilModFolderPage::NilModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent) + : ModFolderPage(inst, mods, parent) +{} + +bool NilModFolderPage::shouldDisplay() const +{ + return m_model->dir().exists(); } diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index 4672350c6..a7d078f50 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -44,7 +44,7 @@ class ModFolderPage : public ExternalResourcesPage { Q_OBJECT public: - explicit ModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent = nullptr); + explicit ModFolderPage(BaseInstance* inst, std::shared_ptr model, QWidget* parent = nullptr); virtual ~ModFolderPage() = default; void setFilter(const QString& filter) { m_fileSelectionFilter = filter; } @@ -57,21 +57,23 @@ class ModFolderPage : public ExternalResourcesPage { virtual bool shouldDisplay() const override; public slots: - bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override; + void updateFrame(const QModelIndex& current, const QModelIndex& previous) override; private slots: void removeItems(const QItemSelection& selection) override; - void deleteModMetadata(); - void installMods(); + void downloadMods(); void updateMods(bool includeDeps = false); - void visitModPages(); + void deleteModMetadata(); + void exportModMetadata(); + void changeModVersion(); protected: std::shared_ptr m_model; }; class CoreModFolderPage : public ModFolderPage { + Q_OBJECT public: explicit CoreModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent = 0); virtual ~CoreModFolderPage() = default; @@ -85,6 +87,7 @@ class CoreModFolderPage : public ModFolderPage { }; class NilModFolderPage : public ModFolderPage { + Q_OBJECT public: explicit NilModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent = 0); virtual ~NilModFolderPage() = default; diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index ab5d98289..ed8ef68d9 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -138,7 +138,7 @@ void OtherLogsPage::on_btnReload_clicked() m_currentFile = QString(); QMessageBox::critical(this, tr("Error"), tr("Unable to open %1 for reading: %2").arg(m_currentFile, file.errorString())); } else { - auto setPlainText = [&](const QString& text) { + auto setPlainText = [this](const QString& text) { QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString(); bool conversionOk = false; int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk); @@ -149,7 +149,7 @@ void OtherLogsPage::on_btnReload_clicked() doc->setDefaultFont(QFont(fontFamily, fontSize)); ui->text->setPlainText(text); }; - auto showTooBig = [&]() { + auto showTooBig = [setPlainText, &file]() { setPlainText(tr("The file (%1) is too big. You may want to open it in a viewer optimized " "for large files.") .arg(file.fileName())); 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/ResourcePackPage.cpp b/launcher/ui/pages/instance/ResourcePackPage.cpp index 85be64256..79e677765 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.cpp +++ b/launcher/ui/pages/instance/ResourcePackPage.cpp @@ -37,43 +37,56 @@ #include "ResourcePackPage.h" -#include "ResourceDownloadTask.h" - #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ResourceDownloadDialog.h" +#include "ui/dialogs/ResourceUpdateDialog.h" ResourcePackPage::ResourcePackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent) - : ExternalResourcesPage(instance, model, parent) + : ExternalResourcesPage(instance, model, parent), m_model(model) { - ui->actionDownloadItem->setText(tr("Download packs")); - ui->actionDownloadItem->setToolTip(tr("Download resource packs from online platforms")); + ui->actionDownloadItem->setText(tr("Download Packs")); + ui->actionDownloadItem->setToolTip(tr("Download resource packs from online mod platforms")); ui->actionDownloadItem->setEnabled(true); - connect(ui->actionDownloadItem, &QAction::triggered, this, &ResourcePackPage::downloadRPs); ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); - ui->actionViewConfigs->setVisible(false); + connect(ui->actionDownloadItem, &QAction::triggered, this, &ResourcePackPage::downloadResourcePacks); + + ui->actionUpdateItem->setToolTip(tr("Try to check or update all selected resource packs (all resource packs if none are selected)")); + connect(ui->actionUpdateItem, &QAction::triggered, this, &ResourcePackPage::updateResourcePacks); + ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionUpdateItem); + + auto updateMenu = new QMenu(this); + + auto update = updateMenu->addAction(ui->actionUpdateItem->text()); + connect(update, &QAction::triggered, this, &ResourcePackPage::updateResourcePacks); + + updateMenu->addAction(ui->actionResetItemMetadata); + connect(ui->actionResetItemMetadata, &QAction::triggered, this, &ResourcePackPage::deleteResourcePackMetadata); + + ui->actionUpdateItem->setMenu(updateMenu); + + ui->actionChangeVersion->setToolTip(tr("Change a mod's version.")); + connect(ui->actionChangeVersion, &QAction::triggered, this, &ResourcePackPage::changeResourcePackVersion); + ui->actionsToolbar->insertActionAfter(ui->actionUpdateItem, ui->actionChangeVersion); } -bool ResourcePackPage::onSelectionChanged(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) +void ResourcePackPage::updateFrame(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); auto& rp = static_cast(m_model->at(row)); ui->frame->updateWithResourcePack(rp); - - return true; } -void ResourcePackPage::downloadRPs() +void ResourcePackPage::downloadResourcePacks() { if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance - ResourceDownload::ResourcePackDownloadDialog mdownload(this, std::static_pointer_cast(m_model), m_instance); + ResourceDownload::ResourcePackDownloadDialog mdownload(this, m_model, m_instance); if (mdownload.exec()) { - auto tasks = - new ConcurrentTask(this, "Download Resource Pack", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + auto tasks = new ConcurrentTask("Download Resource Pack", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); @@ -101,3 +114,155 @@ void ResourcePackPage::downloadRPs() m_model->update(); } } + +void ResourcePackPage::updateResourcePacks() +{ + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + auto profile = static_cast(m_instance)->getPackProfile(); + if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { + QMessageBox::critical(this, tr("Error"), tr("Resource pack updates are unavailable when metadata is disabled!")); + return; + } + if (m_instance != nullptr && m_instance->isRunning()) { + auto response = CustomMessageBox::selectable( + this, tr("Confirm Update"), + tr("Updating resource packs while the game is running may cause pack duplication and game crashes.\n" + "The old files may not be deleted as they are in use.\n" + "Are you sure you want to do this?"), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + + auto mods_list = m_model->selectedResources(selection); + bool use_all = mods_list.empty(); + if (use_all) + mods_list = m_model->allResources(); + + ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false, false); + update_dialog.checkCandidates(); + + if (update_dialog.aborted()) { + CustomMessageBox::selectable(this, tr("Aborted"), tr("The resource pack updater was aborted!"), QMessageBox::Warning)->show(); + return; + } + if (update_dialog.noUpdates()) { + QString message{ tr("'%1' is up-to-date! :)").arg(mods_list.front()->name()) }; + if (mods_list.size() > 1) { + if (use_all) { + message = tr("All resource packs are up-to-date! :)"); + } else { + message = tr("All selected resource packs are up-to-date! :)"); + } + } + CustomMessageBox::selectable(this, tr("Update checker"), message)->exec(); + return; + } + + if (update_dialog.exec()) { + auto tasks = new ConcurrentTask("Download Resource Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + 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 : update_dialog.getTasks()) { + tasks->addTask(task); + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } +} + +void ResourcePackPage::deleteResourcePackMetadata() +{ + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + auto selectionCount = m_model->selectedResourcePacks(selection).length(); + if (selectionCount == 0) + return; + if (selectionCount > 1) { + auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), + tr("You are about to remove the metadata for %1 resource packs.\n" + "Are you sure?") + .arg(selectionCount), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + + m_model->deleteMetadata(selection); +} + +void ResourcePackPage::changeResourcePackVersion() +{ + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { + QMessageBox::critical(this, tr("Error"), tr("Resource pack updates are unavailable when metadata is disabled!")); + return; + } + + const QModelIndexList rows = ui->treeView->selectionModel()->selectedRows(); + + if (rows.count() != 1) + return; + + Resource& resource = m_model->at(m_filterModel->mapToSource(rows[0]).row()); + + if (resource.metadata() == nullptr) + return; + + ResourceDownload::ResourcePackDownloadDialog mdownload(this, m_model, m_instance); + mdownload.setResourceMetadata(resource.metadata()); + if (mdownload.exec()) { + auto tasks = new ConcurrentTask("Download Resource Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + 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(); + } +} \ No newline at end of file diff --git a/launcher/ui/pages/instance/ResourcePackPage.h b/launcher/ui/pages/instance/ResourcePackPage.h index cb84ca96d..55abe007c 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.h +++ b/launcher/ui/pages/instance/ResourcePackPage.h @@ -58,6 +58,14 @@ class ResourcePackPage : public ExternalResourcesPage { } public slots: - bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override; - void downloadRPs(); + void updateFrame(const QModelIndex& current, const QModelIndex& previous) override; + + private slots: + void downloadResourcePacks(); + void updateResourcePacks(); + void deleteResourcePackMetadata(); + void changeResourcePackVersion(); + + protected: + std::shared_ptr m_model; }; diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index 83fb0d5ff..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); @@ -324,8 +329,7 @@ void ScreenshotsPage::onItemActivated(QModelIndex index) if (!index.isValid()) return; auto info = m_model->fileInfo(index); - QString fileName = info.absoluteFilePath(); - DesktopServices::openFile(info.absoluteFilePath()); + DesktopServices::openPath(info); } void ScreenshotsPage::onCurrentSelectionChanged(const QItemSelection& selected) @@ -352,7 +356,7 @@ void ScreenshotsPage::onCurrentSelectionChanged(const QItemSelection& selected) void ScreenshotsPage::on_actionView_Folder_triggered() { - DesktopServices::openDirectory(m_folder, true); + DesktopServices::openPath(m_folder, true); } void ScreenshotsPage::on_actionUpload_triggered() 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.cpp b/launcher/ui/pages/instance/ShaderPackPage.cpp index 40366a1be..a287d3edf 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.cpp +++ b/launcher/ui/pages/instance/ShaderPackPage.cpp @@ -45,27 +45,197 @@ #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ResourceDownloadDialog.h" +#include "ui/dialogs/ResourceUpdateDialog.h" ShaderPackPage::ShaderPackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent) - : ExternalResourcesPage(instance, model, parent) + : ExternalResourcesPage(instance, model, parent), m_model(model) { - ui->actionDownloadItem->setText(tr("Download shaders")); - ui->actionDownloadItem->setToolTip(tr("Download shaders from online platforms")); + ui->actionDownloadItem->setText(tr("Download Packs")); + ui->actionDownloadItem->setToolTip(tr("Download shader packs from online mod platforms")); ui->actionDownloadItem->setEnabled(true); - connect(ui->actionDownloadItem, &QAction::triggered, this, &ShaderPackPage::downloadShaders); ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); - ui->actionViewConfigs->setVisible(false); + connect(ui->actionDownloadItem, &QAction::triggered, this, &ShaderPackPage::downloadShaderPack); + + ui->actionUpdateItem->setToolTip(tr("Try to check or update all selected shader packs (all shader packs if none are selected)")); + connect(ui->actionUpdateItem, &QAction::triggered, this, &ShaderPackPage::updateShaderPacks); + ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionUpdateItem); + + auto updateMenu = new QMenu(this); + + auto update = updateMenu->addAction(ui->actionUpdateItem->text()); + connect(update, &QAction::triggered, this, &ShaderPackPage::updateShaderPacks); + + updateMenu->addAction(ui->actionResetItemMetadata); + connect(ui->actionResetItemMetadata, &QAction::triggered, this, &ShaderPackPage::deleteShaderPackMetadata); + + ui->actionUpdateItem->setMenu(updateMenu); + + ui->actionChangeVersion->setToolTip(tr("Change a shader pack's version.")); + connect(ui->actionChangeVersion, &QAction::triggered, this, &ShaderPackPage::changeShaderPackVersion); + ui->actionsToolbar->insertActionAfter(ui->actionUpdateItem, ui->actionChangeVersion); } -void ShaderPackPage::downloadShaders() +void ShaderPackPage::downloadShaderPack() { if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance - ResourceDownload::ShaderPackDownloadDialog mdownload(this, std::static_pointer_cast(m_model), m_instance); + ResourceDownload::ShaderPackDownloadDialog mdownload(this, m_model, m_instance); if (mdownload.exec()) { - auto tasks = new ConcurrentTask(this, "Download Shaders", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + auto tasks = new ConcurrentTask("Download Shader Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + 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 ShaderPackPage::updateShaderPacks() +{ + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + auto profile = static_cast(m_instance)->getPackProfile(); + if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { + QMessageBox::critical(this, tr("Error"), tr("Shader pack updates are unavailable when metadata is disabled!")); + return; + } + if (m_instance != nullptr && m_instance->isRunning()) { + auto response = + CustomMessageBox::selectable(this, tr("Confirm Update"), + tr("Updating shader packs while the game is running may pack duplication and game crashes.\n" + "The old files may not be deleted as they are in use.\n" + "Are you sure you want to do this?"), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + + auto mods_list = m_model->selectedResources(selection); + bool use_all = mods_list.empty(); + if (use_all) + mods_list = m_model->allResources(); + + ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false, false); + update_dialog.checkCandidates(); + + if (update_dialog.aborted()) { + CustomMessageBox::selectable(this, tr("Aborted"), tr("The shader pack updater was aborted!"), QMessageBox::Warning)->show(); + return; + } + if (update_dialog.noUpdates()) { + QString message{ tr("'%1' is up-to-date! :)").arg(mods_list.front()->name()) }; + if (mods_list.size() > 1) { + if (use_all) { + message = tr("All shader packs are up-to-date! :)"); + } else { + message = tr("All selected shader packs are up-to-date! :)"); + } + } + CustomMessageBox::selectable(this, tr("Update checker"), message)->exec(); + return; + } + + if (update_dialog.exec()) { + auto tasks = new ConcurrentTask("Download Shader Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + 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 : update_dialog.getTasks()) { + tasks->addTask(task); + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } +} + +void ShaderPackPage::deleteShaderPackMetadata() +{ + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + auto selectionCount = m_model->selectedShaderPacks(selection).length(); + if (selectionCount == 0) + return; + if (selectionCount > 1) { + auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), + tr("You are about to remove the metadata for %1 shader packs.\n" + "Are you sure?") + .arg(selectionCount), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + + m_model->deleteMetadata(selection); +} + +void ShaderPackPage::changeShaderPackVersion() +{ + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { + QMessageBox::critical(this, tr("Error"), tr("Shader pack updates are unavailable when metadata is disabled!")); + return; + } + + const QModelIndexList rows = ui->treeView->selectionModel()->selectedRows(); + + if (rows.count() != 1) + return; + + Resource& resource = m_model->at(m_filterModel->mapToSource(rows[0]).row()); + + if (resource.metadata() == nullptr) + return; + + ResourceDownload::ShaderPackDownloadDialog mdownload(this, m_model, m_instance); + mdownload.setResourceMetadata(resource.metadata()); + if (mdownload.exec()) { + auto tasks = new ConcurrentTask("Download Shader Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); diff --git a/launcher/ui/pages/instance/ShaderPackPage.h b/launcher/ui/pages/instance/ShaderPackPage.h index 7c43a3756..ebf7f1d58 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.h +++ b/launcher/ui/pages/instance/ShaderPackPage.h @@ -48,10 +48,16 @@ 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; } public slots: - void downloadShaders(); + void downloadShaderPack(); + void updateShaderPacks(); + void deleteShaderPackMetadata(); + void changeShaderPackVersion(); + + private: + std::shared_ptr m_model; }; diff --git a/launcher/ui/pages/instance/TexturePackPage.cpp b/launcher/ui/pages/instance/TexturePackPage.cpp index 7c8d7e061..fd1e0a2fc 100644 --- a/launcher/ui/pages/instance/TexturePackPage.cpp +++ b/launcher/ui/pages/instance/TexturePackPage.cpp @@ -44,38 +44,207 @@ #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ResourceDownloadDialog.h" +#include "ui/dialogs/ResourceUpdateDialog.h" TexturePackPage::TexturePackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent) - : ExternalResourcesPage(instance, model, parent) + : ExternalResourcesPage(instance, model, parent), m_model(model) { - ui->actionDownloadItem->setText(tr("Download packs")); - ui->actionDownloadItem->setToolTip(tr("Download texture packs from online platforms")); + ui->actionDownloadItem->setText(tr("Download Packs")); + ui->actionDownloadItem->setToolTip(tr("Download texture packs from online mod platforms")); ui->actionDownloadItem->setEnabled(true); - connect(ui->actionDownloadItem, &QAction::triggered, this, &TexturePackPage::downloadTPs); ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); - ui->actionViewConfigs->setVisible(false); + connect(ui->actionDownloadItem, &QAction::triggered, this, &TexturePackPage::downloadTexturePacks); + + ui->actionUpdateItem->setToolTip(tr("Try to check or update all selected texture packs (all texture packs if none are selected)")); + connect(ui->actionUpdateItem, &QAction::triggered, this, &TexturePackPage::updateTexturePacks); + ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionUpdateItem); + + auto updateMenu = new QMenu(this); + + auto update = updateMenu->addAction(ui->actionUpdateItem->text()); + connect(update, &QAction::triggered, this, &TexturePackPage::updateTexturePacks); + + updateMenu->addAction(ui->actionResetItemMetadata); + connect(ui->actionResetItemMetadata, &QAction::triggered, this, &TexturePackPage::deleteTexturePackMetadata); + + ui->actionUpdateItem->setMenu(updateMenu); + + ui->actionChangeVersion->setToolTip(tr("Change a texture pack's version.")); + connect(ui->actionChangeVersion, &QAction::triggered, this, &TexturePackPage::changeTexturePackVersion); + ui->actionsToolbar->insertActionAfter(ui->actionUpdateItem, ui->actionChangeVersion); + + ui->actionViewHomepage->setToolTip(tr("View the homepages of all selected texture packs.")); } -bool TexturePackPage::onSelectionChanged(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) +void TexturePackPage::updateFrame(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); auto& rp = static_cast(m_model->at(row)); ui->frame->updateWithTexturePack(rp); - - return true; } -void TexturePackPage::downloadTPs() +void TexturePackPage::downloadTexturePacks() { if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance - ResourceDownload::TexturePackDownloadDialog mdownload(this, std::static_pointer_cast(m_model), m_instance); + ResourceDownload::TexturePackDownloadDialog mdownload(this, m_model, m_instance); if (mdownload.exec()) { - auto tasks = - new ConcurrentTask(this, "Download Texture Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + auto tasks = new ConcurrentTask("Download Texture Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + 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 TexturePackPage::updateTexturePacks() +{ + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + auto profile = static_cast(m_instance)->getPackProfile(); + if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { + QMessageBox::critical(this, tr("Error"), tr("Texture pack updates are unavailable when metadata is disabled!")); + return; + } + if (m_instance != nullptr && m_instance->isRunning()) { + auto response = CustomMessageBox::selectable( + this, tr("Confirm Update"), + tr("Updating texture packs while the game is running may cause pack duplication and game crashes.\n" + "The old files may not be deleted as they are in use.\n" + "Are you sure you want to do this?"), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + + auto mods_list = m_model->selectedResources(selection); + bool use_all = mods_list.empty(); + if (use_all) + mods_list = m_model->allResources(); + + ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false, false); + update_dialog.checkCandidates(); + + if (update_dialog.aborted()) { + CustomMessageBox::selectable(this, tr("Aborted"), tr("The texture pack updater was aborted!"), QMessageBox::Warning)->show(); + return; + } + if (update_dialog.noUpdates()) { + QString message{ tr("'%1' is up-to-date! :)").arg(mods_list.front()->name()) }; + if (mods_list.size() > 1) { + if (use_all) { + message = tr("All texture packs are up-to-date! :)"); + } else { + message = tr("All selected texture packs are up-to-date! :)"); + } + } + CustomMessageBox::selectable(this, tr("Update checker"), message)->exec(); + return; + } + + if (update_dialog.exec()) { + auto tasks = new ConcurrentTask("Download Texture Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + 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 : update_dialog.getTasks()) { + tasks->addTask(task); + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } +} + +void TexturePackPage::deleteTexturePackMetadata() +{ + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + auto selectionCount = m_model->selectedTexturePacks(selection).length(); + if (selectionCount == 0) + return; + if (selectionCount > 1) { + auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), + tr("You are about to remove the metadata for %1 texture packs.\n" + "Are you sure?") + .arg(selectionCount), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + + m_model->deleteMetadata(selection); +} + +void TexturePackPage::changeTexturePackVersion() +{ + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { + QMessageBox::critical(this, tr("Error"), tr("Texture pack updates are unavailable when metadata is disabled!")); + return; + } + + const QModelIndexList rows = ui->treeView->selectionModel()->selectedRows(); + + if (rows.count() != 1) + return; + + Resource& resource = m_model->at(m_filterModel->mapToSource(rows[0]).row()); + + if (resource.metadata() == nullptr) + return; + + ResourceDownload::TexturePackDownloadDialog mdownload(this, m_model, m_instance); + mdownload.setResourceMetadata(resource.metadata()); + if (mdownload.exec()) { + auto tasks = new ConcurrentTask("Download Texture Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); diff --git a/launcher/ui/pages/instance/TexturePackPage.h b/launcher/ui/pages/instance/TexturePackPage.h index 9c4f24b70..28d7ba209 100644 --- a/launcher/ui/pages/instance/TexturePackPage.h +++ b/launcher/ui/pages/instance/TexturePackPage.h @@ -55,6 +55,12 @@ class TexturePackPage : public ExternalResourcesPage { virtual bool shouldDisplay() const override { return m_instance->traits().contains("texturepacks"); } public slots: - bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override; - void downloadTPs(); + void updateFrame(const QModelIndex& current, const QModelIndex& previous) override; + void downloadTexturePacks(); + void updateTexturePacks(); + void deleteTexturePackMetadata(); + void changeTexturePackVersion(); + + private: + std::shared_ptr m_model; }; diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 2918261d2..975c44de2 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -49,8 +49,12 @@ #include #include #include +#include +#include "QObjectPtr.h" #include "VersionPage.h" +#include "meta/JsonFormat.h" +#include "tasks/SequentialTask.h" #include "ui/dialogs/InstallLoaderDialog.h" #include "ui_VersionPage.h" @@ -63,11 +67,9 @@ #include "DesktopServices.h" #include "Exception.h" -#include "Version.h" #include "icons/IconList.h" #include "minecraft/PackProfile.h" #include "minecraft/auth/AccountList.h" -#include "minecraft/mod/Mod.h" #include "meta/Index.h" #include "meta/VersionList.h" @@ -241,7 +243,7 @@ void VersionPage::updateButtons(int row) ui->actionRemove->setEnabled(patch && patch->isRemovable()); ui->actionMove_down->setEnabled(patch && patch->isMoveable()); ui->actionMove_up->setEnabled(patch && patch->isMoveable()); - ui->actionChange_version->setEnabled(patch && patch->isVersionChangeable()); + ui->actionChange_version->setEnabled(patch && patch->isVersionChangeable(false)); ui->actionEdit->setEnabled(patch && patch->isCustom()); ui->actionCustomize->setEnabled(patch && patch->isCustomizable()); ui->actionRevert->setEnabled(patch && patch->isRevertible()); @@ -250,8 +252,11 @@ void VersionPage::updateButtons(int row) bool VersionPage::reloadPackProfile() { try { - m_profile->reload(Net::Mode::Online); - return true; + auto result = m_profile->reload(Net::Mode::Online); + if (!result) { + QMessageBox::critical(this, tr("Error"), result.error); + } + return result; } catch (const Exception& e) { QMessageBox::critical(this, tr("Error"), e.cause()); return false; @@ -295,16 +300,9 @@ void VersionPage::on_actionRemove_triggered() m_container->refreshContainer(); } -void VersionPage::on_actionInstall_mods_triggered() -{ - if (m_container) { - m_container->selectPage("mods"); - } -} - void VersionPage::on_actionAdd_to_Minecraft_jar_triggered() { - auto list = GuiUtil::BrowseForFiles("jarmod", tr("Select jar mods"), tr("Minecraft.jar mods (*.zip *.jar)"), + 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); @@ -314,7 +312,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); @@ -324,7 +322,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()) { @@ -339,7 +337,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()) @@ -377,11 +375,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.")); @@ -400,6 +412,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); @@ -417,14 +434,18 @@ void VersionPage::on_actionDownload_All_triggered() return; } - auto updateTask = m_inst->createUpdateTask(Net::Mode::Online); - if (!updateTask) { + auto updateTasks = m_inst->createUpdateTask(); + if (updateTasks.isEmpty()) { return; } + auto task = makeShared(); + for (auto t : updateTasks) { + task->addTask(t); + } ProgressDialog tDialog(this); - connect(updateTask.get(), &Task::failed, this, &VersionPage::onGameUpdateError); + connect(task.get(), &Task::failed, this, &VersionPage::onGameUpdateError); // FIXME: unused return value - tDialog.execWithTask(updateTask.get()); + tDialog.execWithTask(task.get()); updateButtons(); m_container->refreshContainer(); } @@ -454,12 +475,12 @@ void VersionPage::on_actionAdd_Empty_triggered() void VersionPage::on_actionLibrariesFolder_triggered() { - DesktopServices::openDirectory(m_inst->getLocalLibraryPath(), true); + DesktopServices::openPath(m_inst->getLocalLibraryPath(), true); } void VersionPage::on_actionMinecraftFolder_triggered() { - DesktopServices::openDirectory(m_inst->gameRoot(), true); + DesktopServices::openPath(m_inst->gameRoot(), true); } void VersionPage::versionCurrent(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) diff --git a/launcher/ui/pages/instance/VersionPage.h b/launcher/ui/pages/instance/VersionPage.h index 6915be883..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" @@ -80,7 +81,6 @@ class VersionPage : public QMainWindow, public BasePage { void on_actionAdd_Agents_triggered(); void on_actionRevert_triggered(); void on_actionEdit_triggered(); - void on_actionInstall_mods_triggered(); void on_actionCustomize_triggered(); void on_actionDownload_All_triggered(); diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 587bb6ce6..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); @@ -207,7 +212,7 @@ void WorldListPage::on_actionRemove_triggered() void WorldListPage::on_actionView_Folder_triggered() { - DesktopServices::openDirectory(m_worlds->dir().absolutePath(), true); + DesktopServices::openPath(m_worlds->dir().absolutePath(), true); } void WorldListPage::on_actionDatapacks_triggered() @@ -223,7 +228,7 @@ void WorldListPage::on_actionDatapacks_triggered() auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString(); - DesktopServices::openDirectory(FS::PathCombine(fullPath, "datapacks"), true); + DesktopServices::openPath(FS::PathCombine(fullPath, "datapacks"), true); } void WorldListPage::on_actionReset_Icon_triggered() @@ -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 3e3c36b7b..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 @@ -123,6 +125,10 @@ void ImportPage::updateState() // need to find the download link for the modpack // format of url curseforge://install?addonId=IDHERE&fileId=IDHERE QUrlQuery query(url); + if (query.allQueryItemValues("addonId").isEmpty() || query.allQueryItemValues("fileId").isEmpty()) { + qDebug() << "Invalid curseforge link:" << url; + return; + } auto addonId = query.allQueryItemValues("addonId")[0]; auto fileId = query.allQueryItemValues("fileId")[0]; auto array = std::make_shared(); @@ -200,7 +206,9 @@ void ImportPage::setExtraInfo(const QMap& extra_info) void ImportPage::on_modpackBtn_clicked() { - auto filter = QMimeDatabase().mimeTypeForName("application/zip").filterString(); + const QMimeType zip = QMimeDatabase().mimeTypeForName("application/zip"); + auto filter = tr("Supported files") + QString(" (%1 *.mrpack)").arg(zip.globPatterns().join(" ")); + filter += ";;" + zip.filterString(); //: Option for filtering for *.mrpack files when importing filter += ";;" + tr("Modrinth pack") + " (*.mrpack)"; const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), filter); diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index c628f74ac..cfc262b62 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -25,15 +25,23 @@ 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, m_filter->openSource + }; } ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& entry) @@ -45,10 +53,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 +90,42 @@ 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 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()) && + m_filter->checkMcVersions(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 d6cc1fdcc..f0cc2df54 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,14 +90,16 @@ 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(); m_ui->packDescription->clear(); m_ui->versionSelectionBox->clear(); updateSelectionButton(); - static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), m_filter_widget->changed()); - m_fetch_progress.watch(m_model->activeSearchJob().get()); + static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), changed); + m_fetchProgress.watch(m_model->activeSearchJob().get()); } QMap ModPage::urlHandlers() const @@ -110,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/ModpackProviderBasePage.h b/launcher/ui/pages/modplatform/ModpackProviderBasePage.h new file mode 100644 index 000000000..a3daa9a81 --- /dev/null +++ b/launcher/ui/pages/modplatform/ModpackProviderBasePage.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "ui/pages/BasePage.h" + +class ModpackProviderBasePage : public BasePage { + public: + /** Programatically set the term in the search bar. */ + virtual void setSearchTerm(QString) = 0; + /** Get the current term in the search bar. */ + [[nodiscard]] virtual QString getSerachTerm() const = 0; +}; \ No newline at end of file diff --git a/launcher/ui/pages/modplatform/OptionalModDialog.cpp b/launcher/ui/pages/modplatform/OptionalModDialog.cpp index fc1c8b3cb..5dc53d9dc 100644 --- a/launcher/ui/pages/modplatform/OptionalModDialog.cpp +++ b/launcher/ui/pages/modplatform/OptionalModDialog.cpp @@ -43,6 +43,9 @@ OptionalModDialog::OptionalModDialog(QWidget* parent, const QStringList& mods) : else item->setCheckState(Qt::Checked); }); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } OptionalModDialog::~OptionalModDialog() diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index 48e66efca..6b8309fb7 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -31,9 +31,9 @@ QHash ResourceModel::s_running_models; ResourceModel::ResourceModel(ResourceAPI* api) : QAbstractListModel(), m_api(api) { s_running_models.insert(this, true); -#ifndef LAUNCHER_TEST - m_current_info_job.setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); -#endif + if (APPLICATION_DYN) { + m_current_info_job.setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + } } ResourceModel::~ResourceModel() @@ -60,11 +60,15 @@ auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant return pack->description; } case Qt::DecorationRole: { - if (auto icon_or_none = const_cast(this)->getIcon(const_cast(index), pack->logoUrl); - icon_or_none.has_value()) - return icon_or_none.value(); + if (APPLICATION_DYN) { + if (auto icon_or_none = const_cast(this)->getIcon(const_cast(index), pack->logoUrl); + icon_or_none.has_value()) + return icon_or_none.value(); - return APPLICATION->getThemedIcon("screenshot-placeholder"); + return APPLICATION->getThemedIcon("screenshot-placeholder"); + } else { + return {}; + } } case Qt::SizeHintRole: return QSize(0, 58); @@ -207,6 +211,11 @@ void ResourceModel::loadEntry(QModelIndex& entry) return; versionRequestSucceeded(doc, pack, entry); }; + if (!callbacks.on_fail) + callbacks.on_fail = [](QString reason, int) { + QMessageBox::critical(nullptr, tr("Error"), + tr("A network error occurred. Could not load project versions: %1").arg(reason)); + }; if (auto job = m_api->getProjectVersions(std::move(args), std::move(callbacks)); job) runInfoJob(job); @@ -228,7 +237,13 @@ void ResourceModel::loadEntry(QModelIndex& entry) callbacks.on_fail = [this](QString reason) { if (!s_running_models.constFind(this).value()) return; - QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load project info:%1").arg(reason)); + QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load project info: %1").arg(reason)); + }; + if (!callbacks.on_abort) + callbacks.on_abort = [this] { + if (!s_running_models.constFind(this).value()) + return; + qCritical() << tr("The request was aborted for an unknown reason"); }; if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job) @@ -306,8 +321,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 {}; @@ -320,7 +337,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, [this, url, full_file_path, index] { auto icon = QIcon(full_file_path); QPixmapCache::insert(url.toString(), icon.pixmap(icon.actualSize({ 64, 64 }))); @@ -328,7 +345,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, [this, url] { m_currently_running_icon_actions.remove(url); m_failed_icon_actions.insert(url); }); @@ -399,12 +416,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(); } @@ -547,4 +569,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 2a4d02815..99039476e 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); } @@ -23,6 +22,7 @@ ResourcePackResourcePage::ResourcePackResourcePage(ResourceDownloadDialog* dialo void ResourcePackResourcePage::triggerSearch() { + m_ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect); m_ui->packView->clearSelection(); m_ui->packDescription->clear(); m_ui->versionSelectionBox->clear(); @@ -30,7 +30,7 @@ void ResourcePackResourcePage::triggerSearch() updateSelectionButton(); static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt()); - m_fetch_progress.watch(m_model->activeSearchJob().get()); + m_fetchProgress.watch(m_model->activeSearchJob().get()); } QMap ResourcePackResourcePage::urlHandlers() const diff --git a/launcher/ui/pages/modplatform/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 44a91003d..2dd5ccf0f 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 @@ -38,13 +39,16 @@ #include "ResourcePage.h" #include "modplatform/ModIndex.h" +#include "ui/dialogs/CustomMessageBox.h" #include "ui_ResourcePage.h" +#include #include #include #include "Markdown.h" +#include "Application.h" #include "ui/dialogs/ResourceDownloadDialog.h" #include "ui/pages/modplatform/ResourceModel.h" #include "ui/widgets/ProjectItem.h" @@ -52,7 +56,7 @@ namespace ResourceDownload { ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_instance) - : QWidget(parent), m_base_instance(base_instance), m_ui(new Ui::ResourcePage), m_parent_dialog(parent), m_fetch_progress(this, false) + : QWidget(parent), m_baseInstance(base_instance), m_ui(new Ui::ResourcePage), m_parentDialog(parent), m_fetchProgress(this, false) { m_ui->setupUi(this); @@ -61,16 +65,18 @@ ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_in m_ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); m_ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); - m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); - m_search_timer.setSingleShot(true); + m_searchTimer.setTimerType(Qt::TimerType::CoarseTimer); + m_searchTimer.setSingleShot(true); - connect(&m_search_timer, &QTimer::timeout, this, &ResourcePage::triggerSearch); + connect(&m_searchTimer, &QTimer::timeout, this, &ResourcePage::triggerSearch); - m_fetch_progress.hideIfInactive(true); - m_fetch_progress.setFixedHeight(24); - m_fetch_progress.progressFormat(""); + // hide progress bar to prevent weird artifact + m_fetchProgress.hide(); + m_fetchProgress.hideIfInactive(true); + m_fetchProgress.setFixedHeight(24); + m_fetchProgress.progressFormat(""); - m_ui->gridLayout_3->addWidget(&m_fetch_progress, 0, 0, 1, m_ui->gridLayout_3->columnCount()); + m_ui->verticalLayout->insertWidget(1, &m_fetchProgress); m_ui->packView->setItemDelegate(new ProjectItemDelegate(this)); m_ui->packView->installEventFilter(this); @@ -92,8 +98,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())); @@ -114,10 +122,10 @@ auto ResourcePage::eventFilter(QObject* watched, QEvent* event) -> bool keyEvent->accept(); return true; } else { - if (m_search_timer.isActive()) - m_search_timer.stop(); + if (m_searchTimer.isActive()) + m_searchTimer.stop(); - m_search_timer.start(350); + m_searchTimer.start(350); } } else if (watched == m_ui->packView) { if (keyEvent->key() == Qt::Key_Return) { @@ -200,6 +208,11 @@ void ResourcePage::updateUi() } if (current_pack->extraDataLoaded) { + if (current_pack->extraData.status == "archived") { + text += "

    " + tr("This project has been archived. It will not receive any further updates unless the author decides " + "to unarchive the project."); + } + if (!current_pack->extraData.donate.isEmpty()) { text += "

    " + tr("Donate information: "); auto donateToStr = [](ModPlatform::DonationData& donate) -> QString { @@ -229,21 +242,24 @@ 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(); } void ResourcePage::updateSelectionButton() { - if (!isOpened || m_selected_version_index < 0) { + if (!isOpened || m_selectedVersionIndex < 0) { m_ui->resourceSelectionButton->setEnabled(false); return; } m_ui->resourceSelectionButton->setEnabled(true); if (auto current_pack = getCurrentPack(); current_pack) { - if (!current_pack->isVersionSelected(m_selected_version_index)) + if (current_pack->versionsLoaded && current_pack->versions.empty()) { + m_ui->resourceSelectionButton->setEnabled(false); + qWarning() << tr("No version available for the selected pack"); + } else if (!current_pack->isVersionSelected(m_selectedVersionIndex)) m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString())); else m_ui->resourceSelectionButton->setText(tr("Deselect %1 for download").arg(resourceString())); @@ -260,18 +276,25 @@ 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)); - } + auto versionText = version.version; + if (version.version_type.isValid()) { + versionText += QString(" [%1]").arg(version.version_type.toString()); + } + if (version.fileId == installedVersion) { + versionText += tr(" [installed]", "Mod version select"); + } + m_ui->versionSelectionBox->addItem(versionText, 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 :(")); @@ -307,32 +330,28 @@ 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_selectedVersionIndex = m_ui->versionSelectionBox->itemData(index).toInt(); updateSelectionButton(); } void ResourcePage::addResourceToDialog(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& version) { - m_parent_dialog->addResource(pack, version); + m_parentDialog->addResource(pack, version); } void ResourcePage::removeResourceFromDialog(const QString& pack_name) { - m_parent_dialog->removeResource(pack_name); + m_parentDialog->removeResource(pack_name); } void ResourcePage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& ver, const std::shared_ptr base_model) { - m_model->addPack(pack, ver, base_model); + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_model->addPack(pack, ver, base_model, is_indexed); } void ResourcePage::removeResourceFromPage(const QString& name) @@ -342,14 +361,15 @@ void ResourcePage::removeResourceFromPage(const QString& name) void ResourcePage::onResourceSelected() { - if (m_selected_version_index < 0) + if (m_selectedVersionIndex < 0) return; auto current_pack = getCurrentPack(); - if (!current_pack || !current_pack->versionsLoaded) + if (!current_pack || !current_pack->versionsLoaded || current_pack->versions.size() < m_selectedVersionIndex) return; - auto& version = current_pack->versions[m_selected_version_index]; + auto& version = current_pack->versions[m_selectedVersionIndex]; + Q_ASSERT(!version.downloadUrl.isNull()); if (version.is_currently_selected) removeResourceFromDialog(current_pack->name); else @@ -388,14 +408,14 @@ void ResourcePage::openUrl(const QUrl& url) } } - if (!page.isNull()) { + if (!page.isNull() && !m_doNotJumpToMod) { const QString slug = match.captured(1); // ensure the user isn't opening the same mod if (auto current_pack = getCurrentPack(); current_pack && slug != current_pack->slug) { - m_parent_dialog->selectPage(page); + m_parentDialog->selectPage(page); - auto newPage = m_parent_dialog->selectedPage(); + auto newPage = m_parentDialog->selectedPage(); QLineEdit* searchEdit = newPage->m_ui->searchEdit; auto model = newPage->m_model; @@ -404,9 +424,9 @@ void ResourcePage::openUrl(const QUrl& url) auto jump = [url, slug, model, view] { for (int row = 0; row < model->rowCount({}); row++) { const QModelIndex index = model->index(row); - const auto pack = model->data(index, Qt::UserRole).value(); + const auto pack = model->data(index, Qt::UserRole).value(); - if (pack.slug == slug) { + if (pack->slug == slug) { view->setCurrentIndex(index); return; } @@ -432,4 +452,55 @@ 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_doNotJumpToMod = 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); + cancelBtn->setText(tr("Cancel")); + + connect(okBtn, &QPushButton::clicked, this, [this] { + onResourceSelected(); + m_parentDialog->accept(); + }); + + connect(cancelBtn, &QPushButton::clicked, m_parentDialog, &ResourceDownloadDialog::reject); + m_ui->gridLayout_4->addWidget(buttonBox, 1, 2); + + connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, + [this, okBtn](int index) { okBtn->setEnabled(m_ui->versionSelectionBox->itemData(index).toInt() >= 0); }); + + auto jump = [this] { + for (int row = 0; row < m_model->rowCount({}); row++) { + const QModelIndex index = m_model->index(row); + m_ui->packView->setCurrentIndex(index); + 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..09c512df4 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.h +++ b/launcher/ui/pages/modplatform/ResourcePage.h @@ -62,7 +62,7 @@ class ResourcePage : public QWidget, public BasePage { [[nodiscard]] bool setCurrentPack(ModPlatform::IndexedPack::Ptr); [[nodiscard]] auto getCurrentPack() const -> ModPlatform::IndexedPack::Ptr; - [[nodiscard]] auto getDialog() const -> const ResourceDownloadDialog* { return m_parent_dialog; } + [[nodiscard]] auto getDialog() const -> const ResourceDownloadDialog* { return m_parentDialog; } [[nodiscard]] auto getModel() const -> ResourceModel* { return m_model; } protected: @@ -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,28 +98,23 @@ 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; + BaseInstance& m_baseInstance; protected: Ui::ResourcePage* m_ui; - ResourceDownloadDialog* m_parent_dialog = nullptr; + ResourceDownloadDialog* m_parentDialog = nullptr; ResourceModel* m_model = nullptr; - int m_selected_version_index = -1; + int m_selectedVersionIndex = -1; - ProgressWidget m_fetch_progress; + ProgressWidget m_fetchProgress; // Used to do instant searching with a delay to cache quick changes - QTimer m_search_timer; + QTimer m_searchTimer; + + bool m_doNotJumpToMod = 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 586dffc55..08acf361a 100644 --- a/launcher/ui/pages/modplatform/ShaderPackPage.cpp +++ b/launcher/ui/pages/modplatform/ShaderPackPage.cpp @@ -8,6 +8,7 @@ #include "ShaderPackModel.h" +#include "Application.h" #include "ui/dialogs/ResourceDownloadDialog.h" #include @@ -16,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); } @@ -24,6 +24,7 @@ ShaderPackResourcePage::ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, void ShaderPackResourcePage::triggerSearch() { + m_ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect); m_ui->packView->clearSelection(); m_ui->packDescription->clear(); m_ui->versionSelectionBox->clear(); @@ -31,7 +32,7 @@ void ShaderPackResourcePage::triggerSearch() updateSelectionButton(); static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt()); - m_fetch_progress.watch(m_model->activeSearchJob().get()); + m_fetchProgress.watch(m_model->activeSearchJob().get()); } QMap ShaderPackResourcePage::urlHandlers() const @@ -48,10 +49,11 @@ void ShaderPackResourcePage::addResourceToPage(ModPlatform::IndexedPack::Ptr pac ModPlatform::IndexedVersion& version, const std::shared_ptr base_model) { + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); QString custom_target_folder; if (version.loaders & ModPlatform::Cauldron) custom_target_folder = QStringLiteral("resourcepacks"); - m_model->addPack(pack, version, base_model, false, custom_target_folder); + m_model->addPack(pack, version, base_model, is_indexed, custom_target_folder); } } // namespace ResourceDownload 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..7c69b315e 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); @@ -163,3 +164,13 @@ void AtlPage::onVersionSelectionChanged(QString version) selectedVersion = version; suggestCurrent(); } + +void AtlPage::setSearchTerm(QString term) +{ + ui->searchEdit->setText(term); +} + +QString AtlPage::getSerachTerm() const +{ + return ui->searchEdit->text(); +} diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h index 6bc449649..73460232b 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h @@ -42,8 +42,7 @@ #include #include "Application.h" -#include "tasks/Task.h" -#include "ui/pages/BasePage.h" +#include "ui/pages/modplatform/ModpackProviderBasePage.h" namespace Ui { class AtlPage; @@ -51,7 +50,7 @@ class AtlPage; class NewInstanceDialog; -class AtlPage : public QWidget, public BasePage { +class AtlPage : public QWidget, public ModpackProviderBasePage { Q_OBJECT public: @@ -66,6 +65,11 @@ class AtlPage : public QWidget, public BasePage { void openedImpl() override; + /** Programatically set the term in the search bar. */ + virtual void setSearchTerm(QString) override; + /** Get the current term in the search bar. */ + [[nodiscard]] virtual QString getSerachTerm() const override; + private: void suggestCurrent(); diff --git a/launcher/ui/pages/modplatform/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 8875a9452..18a2adc49 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -1,6 +1,7 @@ #include "FlameModel.h" #include #include "Application.h" +#include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" #include "modplatform/flame/FlameAPI.h" #include "ui/widgets/ProjectItem.h" @@ -110,6 +111,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(); @@ -170,6 +172,10 @@ void ListModel::performPaginatedSearch() callbacks.on_fail = [this](QString reason) { searchRequestFailed(reason); }; callbacks.on_succeed = [this](auto& doc, auto& pack) { searchRequestForOneSucceeded(doc); }; + callbacks.on_abort = [this] { + qCritical() << "Search task aborted by an unknown reason!"; + searchRequestFailed("Aborted"); + }; static const FlameAPI api; if (auto job = api.getProjectInfo({ projectId }, std::move(callbacks)); job) { jobPtr = job; @@ -178,34 +184,28 @@ void ListModel::performPaginatedSearch() return; } } - auto netJob = makeShared("Flame::Search", APPLICATION->network()); - auto searchUrl = QString( - "https://api.curseforge.com/v1/mods/search?" - "gameId=432&" - "classId=4471&" - "index=%1&" - "pageSize=25&" - "searchFilter=%2&" - "sortField=%3&" - "sortOrder=desc") - .arg(nextSearchOffset) - .arg(currentSearchTerm) - .arg(currentSort + 1); + ResourceAPI::SortingMethod sort{}; + sort.index = currentSort + 1; - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response)); + auto netJob = makeShared("Flame::Search", APPLICATION->network()); + auto searchUrl = FlameAPI().getSearchURL({ ModPlatform::ResourceType::MODPACK, nextSearchOffset, currentSearchTerm, sort, + m_filter->loaders, m_filter->versions, "", m_filter->categoryIds, m_filter->openSource }); + + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl.value()), response)); jobPtr = netJob; jobPtr->start(); QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished); QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::searchRequestFailed); } -void ListModel::searchWithTerm(const QString& term, int sort) +void ListModel::searchWithTerm(const QString& term, int sort, std::shared_ptr filter, bool filterChanged) { - if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { + if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort && !filterChanged) { return; } currentSearchTerm = term; currentSort = sort; + m_filter = filter; if (hasActiveSearchJob()) { jobPtr->abort(); searchState = ResetRequested; diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.h b/launcher/ui/pages/modplatform/flame/FlameModel.h index 9b6d70fec..026f6d1ee 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameModel.h @@ -14,6 +14,7 @@ #include #include +#include "ui/widgets/ModFilterWidget.h" #include @@ -38,7 +39,7 @@ class ListModel : public QAbstractListModel { void fetchMore(const QModelIndex& parent) override; void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); - void searchWithTerm(const QString& term, int sort); + void searchWithTerm(const QString& term, int sort, std::shared_ptr filter, bool filterChanged); [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } @@ -65,6 +66,7 @@ class ListModel : public QAbstractListModel { QString currentSearchTerm; int currentSort = 0; + std::shared_ptr m_filter; int nextSearchOffset = 0; enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; Task::Ptr jobPtr; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index 16055c4bb..de6b3d633 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -34,14 +34,20 @@ */ #include "FlamePage.h" +#include "Version.h" +#include "modplatform/flame/FlamePackIndex.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/widgets/ModFilterWidget.h" #include "ui_FlamePage.h" #include +#include #include "Application.h" #include "FlameModel.h" #include "InstanceImportTask.h" #include "Json.h" +#include "StringUtils.h" #include "modplatform/flame/FlameAPI.h" #include "ui/dialogs/NewInstanceDialog.h" #include "ui/widgets/ProjectItem.h" @@ -54,7 +60,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); @@ -71,7 +76,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")); @@ -83,10 +88,11 @@ 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"); + createFilterWidget(); } FlamePage::~FlamePage() @@ -130,10 +136,25 @@ void FlamePage::openedImpl() void FlamePage::triggerSearch() { - listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); + ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect); + ui->packView->clearSelection(); + ui->packDescription->clear(); + ui->versionSelectionBox->clear(); + listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), + m_filterWidget->changed()); m_fetch_progress.watch(listModel->activeSearchJob().get()); } +bool checkVersionFilters(const Flame::IndexedVersion& v, std::shared_ptr filter) +{ + if (!filter) + return true; + return ((!filter->loaders || !v.loaders || filter->loaders & v.loaders) && // loaders + (filter->releases.empty() || // releases + std::find(filter->releases.cbegin(), filter->releases.cend(), v.version_type) != filter->releases.cend()) && + filter->checkMcVersions({ v.mcVersion })); // mcVersions} +} + void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelIndex prev) { ui->versionSelectionBox->clear(); @@ -147,7 +168,7 @@ void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelInde current = listModel->data(curr, Qt::UserRole).value(); - if (current.versionsLoaded == false) { + if (!current.versionsLoaded || m_filterWidget->changed()) { qDebug() << "Loading flame modpack versions"; auto netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name), APPLICATION->network()); auto response = std::make_shared(); @@ -175,9 +196,23 @@ void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelInde qWarning() << "Error while reading flame modpack version: " << e.cause(); } + auto pred = [this](const Flame::IndexedVersion& v) { return !checkVersionFilters(v, m_filterWidget->getFilter()); }; +#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) + current.versions.removeIf(pred); +#else + for (auto it = current.versions.begin(); it != current.versions.end();) + if (pred(*it)) + it = current.versions.erase(it); + else + ++it; +#endif for (auto version : current.versions) { auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; - 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; @@ -193,6 +228,8 @@ void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelInde suggestCurrent(); }); QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); }); + connect(netJob, &NetJob::failed, + [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); netJob->start(); } else { for (auto version : current.versions) { @@ -233,17 +270,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); + ui->versionSelectionBox->itemData(index).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()); @@ -289,6 +326,37 @@ 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(); } +QString FlamePage::getSerachTerm() const +{ + return ui->searchEdit->text(); +} + +void FlamePage::setSearchTerm(QString term) +{ + ui->searchEdit->setText(term); +} + +void FlamePage::createFilterWidget() +{ + auto widget = ModFilterWidget::create(nullptr, false, this); + m_filterWidget.swap(widget); + auto old = ui->splitter->replaceWidget(0, m_filterWidget.get()); + // because we replaced the widget we also need to delete it + if (old) { + delete old; + } + + connect(ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); + + connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &FlamePage::triggerSearch); + auto response = std::make_shared(); + m_categoriesTask = FlameAPI::getCategories(response, ModPlatform::ResourceType::MODPACK); + QObject::connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { + auto categories = FlameAPI::loadModCategories(response); + m_filterWidget->setCategories(categories); + }); + m_categoriesTask->start(); +} diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index d35858fbc..27c96d2f1 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -40,7 +40,8 @@ #include #include #include -#include "ui/pages/BasePage.h" +#include "ui/pages/modplatform/ModpackProviderBasePage.h" +#include "ui/widgets/ModFilterWidget.h" #include "ui/widgets/ProgressWidget.h" namespace Ui { @@ -53,7 +54,7 @@ namespace Flame { class ListModel; } -class FlamePage : public QWidget, public BasePage { +class FlamePage : public QWidget, public ModpackProviderBasePage { Q_OBJECT public: @@ -72,13 +73,19 @@ class FlamePage : public QWidget, public BasePage { bool eventFilter(QObject* watched, QEvent* event) override; + /** Programatically set the term in the search bar. */ + virtual void setSearchTerm(QString) override; + /** Get the current term in the search bar. */ + [[nodiscard]] virtual QString getSerachTerm() const override; + private: void suggestCurrent(); private slots: void triggerSearch(); void onSelectionChanged(QModelIndex first, QModelIndex second); - void onVersionSelectionChanged(QString data); + void onVersionSelectionChanged(int index); + void createFilterWidget(); private: Ui::FlamePage* ui = nullptr; @@ -92,4 +99,7 @@ class FlamePage : public QWidget, public BasePage { // Used to do instant searching with a delay to cache quick changes QTimer m_search_timer; + + unique_qobject_ptr m_filterWidget; + Task::Ptr m_categoriesTask; }; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.ui b/launcher/ui/pages/modplatform/flame/FlamePage.ui index f9e1fe67f..cf882ef1c 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.ui +++ b/launcher/ui/pages/modplatform/flame/FlamePage.ui @@ -10,8 +10,8 @@ 600 - - + + @@ -29,8 +29,15 @@ - - + + + + + + Filter options + + + @@ -38,46 +45,45 @@ - - - - Search - - - - - - - - - Qt::ScrollBarAlwaysOff - - - true - - - - 48 - 48 - - - - - - - - true - - - true - - - - + + + + + 0 + 0 + + + + Qt::Horizontal + + + + + Qt::ScrollBarAlwaysOff + + + true + + + + 48 + 48 + + + + + + true + + + true + + + - + 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..4e01f3a65 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_baseInstance), false, this); +} + +void FlameModPage::prepareProviderCategories() +{ + auto response = std::make_shared(); + m_categoriesTask = FlameAPI::getModCategories(response); + QObject::connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { + auto categories = FlameAPI::loadModCategories(response); + m_filter_widget->setCategories(categories); + }); + m_categoriesTask->start(); +}; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h index f2f5cecad..052706549 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,14 @@ 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; + + private: + Task::Ptr m_categoriesTask; }; class FlameResourcePackPage : public ResourcePackResourcePage { @@ -124,8 +127,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 +152,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 +177,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 d46f16b4d..15303bb22 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp @@ -20,6 +20,8 @@ #include "ui/widgets/ProjectItem.h" #include "ui_ImportFTBPage.h" +#include +#include #include #include "FileSystem.h" #include "ListModel.h" @@ -56,6 +58,13 @@ ImportFTBPage::ImportFTBPage(NewInstanceDialog* dialog, QWidget* parent) : QWidg connect(ui->searchEdit, &QLineEdit::textChanged, this, &ImportFTBPage::triggerSearch); + connect(ui->browseButton, &QPushButton::clicked, this, [this] { + QString dir = QFileDialog::getExistingDirectory(this, tr("Select FTBApp instances directory"), listModel->getUserPath(), + QFileDialog::ShowDirsOnly); + if (!dir.isEmpty()) + listModel->setPath(dir); + }); + ui->modpackList->setItemDelegate(new ProjectItemDelegate(this)); ui->modpackList->selectionModel()->reset(); } @@ -126,4 +135,13 @@ void ImportFTBPage::triggerSearch() currentModel->setSearchTerm(ui->searchEdit->text()); } +void ImportFTBPage::setSearchTerm(QString term) +{ + ui->searchEdit->setText(term); +} + +QString ImportFTBPage::getSerachTerm() const +{ + return ui->searchEdit->text(); +} } // namespace FTBImportAPP diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h index 8e9661272..7afff5a9d 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h @@ -25,7 +25,7 @@ #include #include "modplatform/import_ftb/PackHelpers.h" -#include "ui/pages/BasePage.h" +#include "ui/pages/modplatform/ModpackProviderBasePage.h" #include "ui/pages/modplatform/import_ftb/ListModel.h" class NewInstanceDialog; @@ -35,7 +35,7 @@ namespace Ui { class ImportFTBPage; } -class ImportFTBPage : public QWidget, public BasePage { +class ImportFTBPage : public QWidget, public ModpackProviderBasePage { Q_OBJECT public: @@ -44,11 +44,16 @@ 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; + /** Programatically set the term in the search bar. */ + virtual void setSearchTerm(QString) override; + /** Get the current term in the search bar. */ + [[nodiscard]] virtual QString getSerachTerm() const override; + private: void suggestCurrent(); void onPackSelectionChanged(Modpack* pack = nullptr); diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui index 5e09fb6d1..337c3e474 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui @@ -10,18 +10,23 @@ 1011 - - - - - - 16777215 - 16777215 - + + + + + + true + + + + Note: If your FTB instances are not in the default location, select it using the button next to search. + + + Qt::AlignCenter - + @@ -34,15 +39,35 @@ - + + + Select FTBApp instances directory + - Search + + + + + .. + + + true - + + + + + 16777215 + 16777215 + + + + + diff --git a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp index 134bdc0c3..f3c737977 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp @@ -17,50 +17,83 @@ */ #include "ListModel.h" +#include #include #include #include #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 getPath() +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"); } -const QString ListModel::FTB_APP_PATH = getPath(); +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 = FS::PathCombine(FTB_APP_PATH, "instances"); - 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(); } @@ -68,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) { } @@ -108,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 @@ -118,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; } @@ -134,38 +167,51 @@ 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) +{ + APPLICATION->settings()->set("FTBAppInstancesPath", path); + update(); +} + +QString ListModel::getUserPath() +{ + auto path = APPLICATION->settings()->get("FTBAppInstancesPath").toString(); + 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 111928276..a842ac8ff 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ListModel.h +++ b/launcher/ui/pages/modplatform/import_ftb/ListModel.h @@ -42,27 +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(); - static const QString FTB_APP_PATH; + 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..226a30ee3 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++) { @@ -367,4 +369,13 @@ void Page::triggerSearch() currentModel->setSearchTerm(ui->searchEdit->text()); } +void Page::setSearchTerm(QString term) +{ + ui->searchEdit->setText(term); +} + +QString Page::getSerachTerm() const +{ + return ui->searchEdit->text(); +} } // namespace LegacyFTB diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.h b/launcher/ui/pages/modplatform/legacy_ftb/Page.h index 4d317b7c0..a2dee24e9 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.h +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.h @@ -43,7 +43,7 @@ #include "QObjectPtr.h" #include "modplatform/legacy_ftb/PackFetchTask.h" #include "modplatform/legacy_ftb/PackHelpers.h" -#include "ui/pages/BasePage.h" +#include "ui/pages/modplatform/ModpackProviderBasePage.h" class NewInstanceDialog; @@ -57,7 +57,7 @@ class ListModel; class FilterModel; class PrivatePackManager; -class Page : public QWidget, public BasePage { +class Page : public QWidget, public ModpackProviderBasePage { Q_OBJECT public: @@ -66,11 +66,16 @@ 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; + /** Programatically set the term in the search bar. */ + virtual void setSearchTerm(QString) override; + /** Get the current term in the search bar. */ + [[nodiscard]] virtual QString getSerachTerm() const override; + private: void suggestCurrent(); void onPackSelectionChanged(Modpack* pack = nullptr); diff --git a/launcher/ui/pages/modplatform/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 f691a185d..416c69d28 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -140,6 +140,10 @@ void ModpackListModel::performPaginatedSearch() callbacks.on_fail = [this](QString reason) { searchRequestFailed(reason); }; callbacks.on_succeed = [this](auto& doc, auto& pack) { searchRequestForOneSucceeded(doc); }; + callbacks.on_abort = [this] { + qCritical() << "Search task aborted by an unknown reason!"; + searchRequestFailed("Aborted"); + }; static const ModrinthAPI api; if (auto job = api.getProjectInfo({ projectId }, std::move(callbacks)); job) { jobPtr = job; @@ -148,33 +152,26 @@ void ModpackListModel::performPaginatedSearch() return; } } // TODO: Move to standalone API - auto netJob = makeShared("Modrinth::SearchModpack", APPLICATION->network()); - auto searchAllUrl = QString(BuildConfig.MODRINTH_PROD_URL + - "/search?" - "offset=%1&" - "limit=%2&" - "query=%3&" - "index=%4&" - "facets=[[\"project_type:modpack\"]]") - .arg(nextSearchOffset) - .arg(m_modpacks_per_page) - .arg(currentSearchTerm) - .arg(currentSort); + ResourceAPI::SortingMethod sort{}; + sort.name = currentSort; + auto searchUrl = ModrinthAPI().getSearchURL({ ModPlatform::ResourceType::MODPACK, nextSearchOffset, currentSearchTerm, sort, + m_filter->loaders, m_filter->versions, "", m_filter->categoryIds, m_filter->openSource }); - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchAllUrl), m_all_response)); + auto netJob = makeShared("Modrinth::SearchModpack", APPLICATION->network()); + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl.value()), m_allResponse)); QObject::connect(netJob.get(), &NetJob::succeeded, this, [this] { - QJsonParseError parse_error_all{}; + QJsonParseError parseError{}; - QJsonDocument doc_all = QJsonDocument::fromJson(*m_all_response, &parse_error_all); - if (parse_error_all.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from " << debugName() << " at " << parse_error_all.offset - << " reason: " << parse_error_all.errorString(); - qWarning() << *m_all_response; + QJsonDocument doc = QJsonDocument::fromJson(*m_allResponse, &parseError); + if (parseError.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from " << debugName() << " at " << parseError.offset + << " reason: " << parseError.errorString(); + qWarning() << *m_allResponse; return; } - searchRequestFinished(doc_all); + searchRequestFinished(doc); }); QObject::connect(netJob.get(), &NetJob::failed, this, &ModpackListModel::searchRequestFailed); @@ -216,19 +213,23 @@ static auto sortFromIndex(int index) -> QString } } -void ModpackListModel::searchWithTerm(const QString& term, const int sort) +void ModpackListModel::searchWithTerm(const QString& term, + const int sort, + std::shared_ptr filter, + bool filterChanged) { if (sort > 5 || sort < 0) return; auto sort_str = sortFromIndex(sort); - if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort_str) { + if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort_str && !filterChanged) { return; } currentSearchTerm = term; currentSort = sort_str; + m_filter = filter; refresh(); } @@ -250,6 +251,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(); @@ -348,10 +350,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/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 514ee4484..640ddf688 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -71,7 +71,7 @@ class ModpackListModel : public QAbstractListModel { /* Ask the API for more information */ void fetchMore(const QModelIndex& parent) override; void refresh(); - void searchWithTerm(const QString& term, int sort); + void searchWithTerm(const QString& term, int sort, std::shared_ptr filter, bool filterChanged); [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } @@ -112,12 +112,13 @@ class ModpackListModel : public QAbstractListModel { QString currentSearchTerm; QString currentSort; + std::shared_ptr m_filter; int nextSearchOffset = 0; enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; Task::Ptr jobPtr; - std::shared_ptr m_all_response = std::make_shared(); + std::shared_ptr m_allResponse = std::make_shared(); QByteArray m_specific_response; int m_modpacks_per_page = 20; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 8e2b9a902..7d70abec4 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -35,6 +35,9 @@ */ #include "ModrinthPage.h" +#include "Version.h" +#include "modplatform/modrinth/ModrinthAPI.h" +#include "ui/dialogs/CustomMessageBox.h" #include "ui_ModrinthPage.h" #include "ModrinthModel.h" @@ -43,6 +46,7 @@ #include "InstanceImportTask.h" #include "Json.h" #include "Markdown.h" +#include "StringUtils.h" #include "ui/widgets/ProjectItem.h" @@ -56,8 +60,8 @@ ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog), m_fetch_progress(this, false) { ui->setupUi(this); + createFilterWidget(); - connect(ui->searchButton, &QPushButton::clicked, this, &ModrinthPage::triggerSearch); ui->searchEdit->installEventFilter(this); m_model = new Modrinth::ModpackListModel(this); ui->packView->setModel(m_model); @@ -74,7 +78,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")); @@ -84,7 +88,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()); @@ -103,6 +107,7 @@ void ModrinthPage::retranslate() void ModrinthPage::openedImpl() { BasePage::openedImpl(); + suggestCurrent(); triggerSearch(); } @@ -124,6 +129,16 @@ bool ModrinthPage::eventFilter(QObject* watched, QEvent* event) return QObject::eventFilter(watched, event); } +bool checkVersionFilters(const Modrinth::ModpackVersion& v, std::shared_ptr filter) +{ + if (!filter) + return true; + return ((!filter->loaders || !v.loaders || filter->loaders & v.loaders) && // loaders + (filter->releases.empty() || // releases + std::find(filter->releases.cbegin(), filter->releases.cend(), v.version_type) != filter->releases.cend()) && + filter->checkMcVersions({ v.gameVersion })); // gameVersion} +} + void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelIndex prev) { ui->versionSelectionBox->clear(); @@ -182,11 +197,13 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI suggestCurrent(); }); QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); }); + connect(netJob, &NetJob::failed, + [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); netJob->start(); } else updateUI(); - if (!current.versionsLoaded) { + if (!current.versionsLoaded || m_filterWidget->changed()) { qDebug() << "Loading modrinth modpack versions"; auto netJob = new NetJob(QString("Modrinth::PackVersions(%1)").arg(current.name), APPLICATION->network()); @@ -217,13 +234,24 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI qDebug() << *response; qWarning() << "Error while reading modrinth modpack version: " << e.cause(); } + auto pred = [this](const Modrinth::ModpackVersion& v) { return !checkVersionFilters(v, m_filterWidget->getFilter()); }; +#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) + current.versions.removeIf(pred); +#else + for (auto it = current.versions.begin(); it != current.versions.end();) + if (pred(*it)) + it = current.versions.erase(it); + else + ++it; +#endif for (auto version : current.versions) { auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; - 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; @@ -235,6 +263,8 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI suggestCurrent(); }); QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); }); + connect(netJob, &NetJob::failed, + [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); netJob->start(); } else { @@ -262,6 +292,11 @@ void ModrinthPage::updateUI() text += "
    " + tr(" by ") + QString("%2").arg(std::get<1>(current.author).toString(), std::get<0>(current.author)); if (current.extraInfoLoaded) { + if (current.extra.status == "archived") { + text += "

    " + tr("This project has been archived. It will not receive any further updates unless the author decides " + "to unarchive the project."); + } + if (!current.extra.donate.isEmpty()) { text += "

    " + tr("Donate information: "); auto donateToStr = [](Modrinth::DonationData& donate) -> QString { @@ -293,7 +328,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(); } @@ -326,16 +361,52 @@ void ModrinthPage::suggestCurrent() void ModrinthPage::triggerSearch() { - m_model->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); + ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect); + ui->packView->clearSelection(); + ui->packDescription->clear(); + ui->versionSelectionBox->clear(); + m_model->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), m_filterWidget->changed()); m_fetch_progress.watch(m_model->activeSearchJob().get()); } -void ModrinthPage::onVersionSelectionChanged(QString version) +void ModrinthPage::onVersionSelectionChanged(int index) { - if (version.isNull() || version.isEmpty()) { + if (index == -1) { selectedVersion = ""; return; } - selectedVersion = ui->versionSelectionBox->currentData().toString(); + selectedVersion = ui->versionSelectionBox->itemData(index).toString(); suggestCurrent(); } + +void ModrinthPage::setSearchTerm(QString term) +{ + ui->searchEdit->setText(term); +} + +QString ModrinthPage::getSerachTerm() const +{ + return ui->searchEdit->text(); +} + +void ModrinthPage::createFilterWidget() +{ + auto widget = ModFilterWidget::create(nullptr, true, this); + m_filterWidget.swap(widget); + auto old = ui->splitter->replaceWidget(0, m_filterWidget.get()); + // because we replaced the widget we also need to delete it + if (old) { + delete old; + } + + connect(ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); + + connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &ModrinthPage::triggerSearch); + auto response = std::make_shared(); + m_categoriesTask = ModrinthAPI::getModCategories(response); + QObject::connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { + auto categories = ModrinthAPI::loadCategories(response, "modpack"); + m_filterWidget->setCategories(categories); + }); + m_categoriesTask->start(); +} \ No newline at end of file diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 4240dcafb..7f504cdbd 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -38,9 +38,10 @@ #include "Application.h" #include "ui/dialogs/NewInstanceDialog.h" -#include "ui/pages/BasePage.h" #include "modplatform/modrinth/ModrinthPackManifest.h" +#include "ui/pages/modplatform/ModpackProviderBasePage.h" +#include "ui/widgets/ModFilterWidget.h" #include "ui/widgets/ProgressWidget.h" #include @@ -54,7 +55,7 @@ namespace Modrinth { class ModpackListModel; } -class ModrinthPage : public QWidget, public BasePage { +class ModrinthPage : public QWidget, public ModpackProviderBasePage { Q_OBJECT public: @@ -78,10 +79,16 @@ class ModrinthPage : public QWidget, public BasePage { void openedImpl() override; bool eventFilter(QObject* watched, QEvent* event) override; + /** Programatically set the term in the search bar. */ + virtual void setSearchTerm(QString) override; + /** Get the current term in the search bar. */ + [[nodiscard]] virtual QString getSerachTerm() const override; + private slots: void onSelectionChanged(QModelIndex first, QModelIndex second); - void onVersionSelectionChanged(QString data); + void onVersionSelectionChanged(int index); void triggerSearch(); + void createFilterWidget(); private: Ui::ModrinthPage* ui; @@ -95,4 +102,7 @@ class ModrinthPage : public QWidget, public BasePage { // Used to do instant searching with a delay to cache quick changes QTimer m_search_timer; + + unique_qobject_ptr m_filterWidget; + Task::Ptr m_categoriesTask; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui index 78a25feae..d6e983929 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -10,74 +10,62 @@ 600 - - - - - - true - - - - Note: Modrinth modpacks are still in alpha phase. Some things may be rough on the edges, or not working at all! Use it with caution. - - - Qt::AlignCenter - - - true - - - - - + + + + + + + Filter options + + + - Search and filter ... - - - - - - - Search + Search and filter... - - - - - - Qt::ScrollBarAlwaysOff - - - true - - - - 48 - 48 - - - - - - - - true - - - true - - - - + + + + + 0 + 0 + + + + Qt::Horizontal + + + + + Qt::ScrollBarAlwaysOff + + + true + + + + 48 + 48 + + + + + + true + + + true + + + - + @@ -107,8 +95,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..4ee620677 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_baseInstance), 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..f7e7f4433 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -154,6 +154,10 @@ void Technic::ListModel::performSearch() QString("%1search?build=%2&q=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD, currentSearchTerm); searchMode = List; } + auto clientId = APPLICATION->settings()->get("TechnicClientID").toString(); + if (!clientId.isEmpty()) { + searchUrl += "?cid=" + clientId; + } netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response)); jobPtr = netJob; jobPtr->start(); @@ -292,6 +296,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 190b7c68f..50d267b1f 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -34,6 +34,7 @@ */ #include "TechnicPage.h" +#include "ui/dialogs/CustomMessageBox.h" #include "ui/widgets/ProjectItem.h" #include "ui_TechnicPage.h" @@ -43,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" @@ -56,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); @@ -70,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); @@ -208,6 +209,8 @@ void TechnicPage::suggestCurrent() metadataLoaded(); }); + connect(jobPtr.get(), &NetJob::failed, + [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); jobPtr = netJob; jobPtr->start(); @@ -230,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) { @@ -258,6 +261,8 @@ void TechnicPage::metadataLoaded() netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(url), response)); QObject::connect(netJob.get(), &NetJob::succeeded, this, &TechnicPage::onSolderLoaded); + connect(jobPtr.get(), &NetJob::failed, + [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); jobPtr = netJob; jobPtr->start(); @@ -337,3 +342,13 @@ void TechnicPage::onVersionSelectionChanged(QString version) selectedVersion = version; selectVersion(); } + +void TechnicPage::setSearchTerm(QString term) +{ + ui->searchEdit->setText(term); +} + +QString TechnicPage::getSerachTerm() const +{ + return ui->searchEdit->text(); +} diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.h b/launcher/ui/pages/modplatform/technic/TechnicPage.h index 01439337d..d1f691b22 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.h +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.h @@ -41,7 +41,7 @@ #include #include "TechnicData.h" #include "net/NetJob.h" -#include "ui/pages/BasePage.h" +#include "ui/pages/modplatform/ModpackProviderBasePage.h" #include "ui/widgets/ProgressWidget.h" namespace Ui { @@ -54,7 +54,7 @@ namespace Technic { class ListModel; } -class TechnicPage : public QWidget, public BasePage { +class TechnicPage : public QWidget, public ModpackProviderBasePage { Q_OBJECT public: @@ -71,6 +71,11 @@ class TechnicPage : public QWidget, public BasePage { bool eventFilter(QObject* watched, QEvent* event) override; + /** Programatically set the term in the search bar. */ + virtual void setSearchTerm(QString) override; + /** Get the current term in the search bar. */ + [[nodiscard]] virtual QString getSerachTerm() const override; + private: void suggestCurrent(); void metadataLoaded(); diff --git a/launcher/ui/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..a862524b0 --- /dev/null +++ b/launcher/ui/setupwizard/AutoJavaWizardPage.ui @@ -0,0 +1,93 @@ + + + AutoJavaWizardPage + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + <html><head/><body><p><span style=" font-size:14pt; font-weight:600;">New Feature Alert!</span></p></body></html> + + + Qt::RichText + + + true + + + + + + + We've added a feature to automatically download the correct Java version for each version of Minecraft (this can be changed in the Java Settings). Would you like to enable or disable this feature? + + + true + + + + + + + Qt::Horizontal + + + + + + + Enable Auto-Download + + + true + + + buttonGroup + + + + + + + Disable Auto-Download + + + false + + + buttonGroup + + + + + + + Qt::Vertical + + + + 20 + 156 + + + + + + + + + + + + diff --git a/launcher/ui/setupwizard/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..6b8ece9f7 100644 --- a/launcher/ui/setupwizard/JavaWizardPage.cpp +++ b/launcher/ui/setupwizard/JavaWizardPage.cpp @@ -12,13 +12,9 @@ #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/JavaWizardWidget.h" #include "ui/widgets/VersionSelectWidget.h" JavaWizardPage::JavaWizardPage(QWidget* parent) : BaseWizardPage(parent) @@ -31,7 +27,7 @@ void JavaWizardPage::setupUi() setObjectName(QStringLiteral("javaPage")); QVBoxLayout* layout = new QVBoxLayout(this); - m_java_widget = new JavaSettingsWidget(this); + m_java_widget = new JavaWizardWidget(this); layout->addWidget(m_java_widget); setLayout(layout); @@ -57,15 +53,18 @@ 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: { + case JavaWizardWidget::ValidationStatus::Bad: { return false; } - case JavaSettingsWidget::ValidationStatus::AllOK: { + case JavaWizardWidget::ValidationStatus::AllOK: { settings->set("JavaPath", m_java_widget->javaPath()); } /* fallthrough */ - case JavaSettingsWidget::ValidationStatus::JavaBad: { + case JavaWizardWidget::ValidationStatus::JavaBad: { // Memory auto s = APPLICATION->settings(); s->set("MinMemAlloc", m_java_widget->minHeapSize()); @@ -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..914630d0b 100644 --- a/launcher/ui/setupwizard/JavaWizardPage.h +++ b/launcher/ui/setupwizard/JavaWizardPage.h @@ -2,14 +2,14 @@ #include "BaseWizardPage.h" -class JavaSettingsWidget; +class JavaWizardWidget; class JavaWizardPage : public BaseWizardPage { Q_OBJECT public: explicit JavaWizardPage(QWidget* parent = Q_NULLPTR); - virtual ~JavaWizardPage(){}; + virtual ~JavaWizardPage() = default; bool wantsRefreshButton() override; void refresh() override; @@ -21,5 +21,5 @@ class JavaWizardPage : public BaseWizardPage { void retranslate() override; private: /* data */ - JavaSettingsWidget* m_java_widget = nullptr; + JavaWizardWidget* m_java_widget = nullptr; }; 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/setupwizard/SetupWizard.cpp b/launcher/ui/setupwizard/SetupWizard.cpp index 4e5bd1dca..f2e51ee41 100644 --- a/launcher/ui/setupwizard/SetupWizard.cpp +++ b/launcher/ui/setupwizard/SetupWizard.cpp @@ -57,7 +57,7 @@ void SetupWizard::pageChanged(int id) if (basePagePtr->wantsRefreshButton()) { setButtonLayout({ QWizard::CustomButton1, QWizard::Stretch, QWizard::BackButton, QWizard::NextButton, QWizard::FinishButton }); auto customButton = button(QWizard::CustomButton1); - connect(customButton, &QAbstractButton::clicked, [&]() { + connect(customButton, &QAbstractButton::clicked, [this]() { auto basePagePtr = getCurrentBasePage(); if (basePagePtr) { basePagePtr->refresh(); 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/CatPack.cpp b/launcher/ui/themes/CatPack.cpp index 50c10189e..85eb85a18 100644 --- a/launcher/ui/themes/CatPack.cpp +++ b/launcher/ui/themes/CatPack.cpp @@ -36,7 +36,10 @@ #include "ui/themes/CatPack.h" #include #include +#include #include +#include +#include #include "FileSystem.h" #include "Json.h" @@ -79,7 +82,7 @@ JsonCatPack::JsonCatPack(QFileInfo& manifestInfo) : BasicCatPack(manifestInfo.di auto doc = Json::requireDocument(manifestInfo.absoluteFilePath(), "CatPack JSON file"); const auto root = doc.object(); m_name = Json::requireString(root, "name", "Catpack name"); - m_defaultPath = FS::PathCombine(path, Json::requireString(root, "default", "Default Cat")); + m_default_path = FS::PathCombine(path, Json::requireString(root, "default", "Default Cat")); auto variants = Json::ensureArray(root, "variants", QJsonArray(), "Catpack Variants"); for (auto v : variants) { auto variant = Json::ensureObject(v, QJsonObject(), "Cat variant"); @@ -117,5 +120,21 @@ QString JsonCatPack::path(QDate now) if (startDate <= now && now <= endDate) return var.path; } - return m_defaultPath; + auto dInfo = QFileInfo(m_default_path); + if (!dInfo.isDir()) + return m_default_path; + + QStringList supportedImageFormats; + for (auto format : QImageReader::supportedImageFormats()) { + supportedImageFormats.append("*." + format); + } + + auto files = QDir(m_default_path).entryInfoList(supportedImageFormats, QDir::Files, QDir::Name); + if (files.length() == 0) + return ""; + auto idx = (now.dayOfYear() - 1) % files.length(); + auto isRandom = dInfo.fileName().compare("random", Qt::CaseInsensitive) == 0; + if (isRandom) + idx = QRandomGenerator::global()->bounded(0, files.length()); + return files[idx].absoluteFilePath(); } diff --git a/launcher/ui/themes/CatPack.h b/launcher/ui/themes/CatPack.h index 1d310e796..5a13d0cef 100644 --- a/launcher/ui/themes/CatPack.h +++ b/launcher/ui/themes/CatPack.h @@ -87,6 +87,6 @@ class JsonCatPack : public BasicCatPack { QString path(QDate now); private: - QString m_defaultPath; + QString m_default_path; QList m_variants; }; diff --git a/launcher/ui/themes/CustomTheme.cpp b/launcher/ui/themes/CustomTheme.cpp index 4859983c6..b8c5738b7 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 = [this, readColor, colorsRoot](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 = [this, readColor, logColorsRoot](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 0bcac100c..30a1fe7be 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,16 @@ ThemeManager::ThemeManager() { + QIcon::setFallbackThemeName(QIcon::themeName()); + QIcon::setThemeSearchPaths(QIcon::themeSearchPaths() << m_iconThemeFolder.path()); + + themeDebugLog() << "Determining System Widget Theme..."; + const auto& style = QApplication::style(); + m_defaultStyle = style->objectName(); + themeDebugLog() << "System theme seems to be:" << m_defaultStyle; + + m_defaultPalette = QApplication::palette(); + initializeThemes(); initializeCatPacks(); } @@ -84,10 +96,6 @@ void ThemeManager::initializeIcons() // set icon theme search path! themeDebugLog() << "<> Initializing Icon Themes"; - auto searchPaths = QIcon::themeSearchPaths(); - searchPaths.append(m_iconThemeFolder.path()); - QIcon::setThemeSearchPaths(searchPaths); - for (const QString& id : builtinIcons) { IconTheme theme(id, QString(":/icons/%1").arg(id)); if (!theme.load()) { @@ -120,13 +128,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"; @@ -228,6 +247,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,7 +259,11 @@ 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."; } @@ -313,3 +338,13 @@ void ThemeManager::initializeCatPacks() } } } + +void ThemeManager::refresh() +{ + m_themes.clear(); + m_icons.clear(); + m_catPacks.clear(); + + initializeThemes(); + initializeCatPacks(); +} diff --git a/launcher/ui/themes/ThemeManager.h b/launcher/ui/themes/ThemeManager.h index b5c66677b..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,6 +57,10 @@ 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; @@ -62,6 +68,9 @@ class ThemeManager { QDir m_applicationThemeFolder{ "themes" }; QDir m_catPacksFolder{ "catpacks" }; 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..02b629162 --- /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 m_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 (m_checked.contains(txt)) { + m_checked.removeOne(txt); + } else { + m_checked.push_back(txt); + } + emit dataChanged(index, index); + emit checkStateChanged(); + return true; + } + return QIdentityProxyModel::setData(index, value, role); + } + QStringList getChecked() { return m_checked; } + + signals: + void checkStateChanged(); + + private: + QStringList m_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 (!m_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); + m_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..469587762 --- /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 m_containerMousePress = false; +}; \ No newline at end of file diff --git a/launcher/ui/widgets/CustomCommands.ui b/launcher/ui/widgets/CustomCommands.ui index 4a39ff7f7..b485c293e 100644 --- a/launcher/ui/widgets/CustomCommands.ui +++ b/launcher/ui/widgets/CustomCommands.ui @@ -38,19 +38,6 @@ false - - - - P&ost-exit command: - - - postExitCmdTextBox - - - - - - @@ -61,8 +48,8 @@ - - + + @@ -77,6 +64,19 @@ + + + + P&ost-exit command: + + + postExitCmdTextBox + + + + + +
    diff --git a/launcher/ui/widgets/DropLabel.cpp b/launcher/ui/widgets/DropLabel.cpp deleted file mode 100644 index b1473b358..000000000 --- a/launcher/ui/widgets/DropLabel.cpp +++ /dev/null @@ -1,40 +0,0 @@ -#include "DropLabel.h" - -#include -#include - -DropLabel::DropLabel(QWidget* parent) : QLabel(parent) -{ - setAcceptDrops(true); -} - -void DropLabel::dragEnterEvent(QDragEnterEvent* event) -{ - event->acceptProposedAction(); -} - -void DropLabel::dragMoveEvent(QDragMoveEvent* event) -{ - event->acceptProposedAction(); -} - -void DropLabel::dragLeaveEvent(QDragLeaveEvent* event) -{ - event->accept(); -} - -void DropLabel::dropEvent(QDropEvent* event) -{ - const QMimeData* mimeData = event->mimeData(); - - if (!mimeData) { - return; - } - - if (mimeData->hasUrls()) { - auto urls = mimeData->urls(); - emit droppedURLs(urls); - } - - event->acceptProposedAction(); -} diff --git a/launcher/ui/widgets/DropLabel.h b/launcher/ui/widgets/DropLabel.h deleted file mode 100644 index 0027f48b1..000000000 --- a/launcher/ui/widgets/DropLabel.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include - -class DropLabel : public QLabel { - Q_OBJECT - - public: - explicit DropLabel(QWidget* parent = nullptr); - - signals: - void droppedURLs(QList urls); - - protected: - void dropEvent(QDropEvent* event) override; - void dragEnterEvent(QDragEnterEvent* event) override; - void dragMoveEvent(QDragMoveEvent* event) override; - void dragLeaveEvent(QDragLeaveEvent* event) override; -}; diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp index 69f72fea2..3ef5dcb88 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" @@ -82,7 +84,7 @@ void InfoFrame::updateWithMod(Mod const& m) QString text = ""; QString name = ""; - QString link = m.metaurl(); + QString link = m.homepage(); if (m.name().isEmpty()) name = m.internal_id(); else @@ -91,7 +93,7 @@ void InfoFrame::updateWithMod(Mod const& m) if (link.isEmpty()) text = name; else { - text = "" + name + ""; + text = "" + name + ""; } if (!m.authors().isEmpty()) text += " by " + m.authors().join(", "); @@ -143,7 +145,13 @@ void InfoFrame::updateWithMod(Mod const& m) void InfoFrame::updateWithResource(const Resource& resource) { - setName(resource.name()); + const QString homepage = resource.homepage(); + + if (!homepage.isEmpty()) + setName("" + resource.name() + ""); + else + setName(resource.name()); + setImage(); } @@ -207,14 +215,28 @@ QString InfoFrame::renderColorCodes(QString input) void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack) { - setName(renderColorCodes(resource_pack.name())); + QString name = renderColorCodes(resource_pack.name()); + + const QString homepage = resource_pack.homepage(); + if (!homepage.isEmpty()) { + name = "" + name + ""; + } + + setName(name); setDescription(renderColorCodes(resource_pack.description())); setImage(resource_pack.image({ 64, 64 })); } void InfoFrame::updateWithTexturePack(TexturePack& texture_pack) { - setName(renderColorCodes(texture_pack.name())); + QString name = renderColorCodes(texture_pack.name()); + + const QString homepage = texture_pack.homepage(); + if (!homepage.isEmpty()) { + name = "" + name + ""; + } + + setName(name); setDescription(renderColorCodes(texture_pack.description())); setImage(texture_pack.image({ 64, 64 })); } @@ -274,12 +296,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 +353,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..a255168e9 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.cpp +++ b/launcher/ui/widgets/JavaSettingsWidget.cpp @@ -1,432 +1,314 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2022 Sefa Eyeoglu + * 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 "JavaSettingsWidget.h" #include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "FileSystem.h" -#include "JavaCommon.h" -#include "java/JavaInstall.h" -#include "java/JavaUtils.h" - -#include "ui/dialogs/CustomMessageBox.h" -#include "ui/widgets/VersionSelectWidget.h" - +#include #include "Application.h" #include "BuildConfig.h" +#include "FileSystem.h" +#include "JavaCommon.h" +#include "java/JavaInstallList.h" +#include "java/JavaUtils.h" +#include "settings/Setting.h" +#include "sys.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/VersionSelectDialog.h" +#include "ui/java/InstallJavaDialog.h" -JavaSettingsWidget::JavaSettingsWidget(QWidget* parent) : QWidget(parent) +#include "ui_JavaSettingsWidget.h" + +JavaSettingsWidget::JavaSettingsWidget(InstancePtr instance, QWidget* parent) + : QWidget(parent), m_instance(std::move(instance)), m_ui(new Ui::JavaSettingsWidget) { - m_availableMemory = Sys::getSystemRam() / Sys::mebibyte; + m_ui->setupUi(this); - goodIcon = APPLICATION->getThemedIcon("status-good"); - yellowIcon = APPLICATION->getThemedIcon("status-yellow"); - badIcon = APPLICATION->getThemedIcon("status-bad"); - setupUi(); + if (m_instance == nullptr) { + m_ui->javaDownloadBtn->hide(); + if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { + connect(m_ui->autodetectJavaCheckBox, &QCheckBox::stateChanged, this, [this](bool state) { + m_ui->autodownloadJavaCheckBox->setEnabled(state); + if (!state) + m_ui->autodownloadJavaCheckBox->setChecked(false); + }); + } else { + m_ui->autodownloadJavaCheckBox->hide(); + } + } else { + m_ui->javaDownloadBtn->setVisible(BuildConfig.JAVA_DOWNLOADER_ENABLED); + m_ui->skipWizardCheckBox->hide(); + m_ui->autodetectJavaCheckBox->hide(); + m_ui->autodownloadJavaCheckBox->hide(); - 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_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); -} + m_ui->javaInstallationGroupBox->setCheckable(true); + m_ui->memoryGroupBox->setCheckable(true); + m_ui->javaArgumentsGroupBox->setCheckable(true); -void JavaSettingsWidget::setupUi() -{ - setObjectName(QStringLiteral("javaSettingsWidget")); - m_verticalLayout = new QVBoxLayout(this); - m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); + SettingsObjectPtr settings = m_instance->settings(); - m_versionWidget = new VersionSelectWidget(this); - m_verticalLayout->addWidget(m_versionWidget); + connect(settings->getSetting("OverrideJavaLocation").get(), &Setting::SettingChanged, m_ui->javaInstallationGroupBox, + [this, settings] { m_ui->javaInstallationGroupBox->setChecked(settings->get("OverrideJavaLocation").toBool()); }); + connect(settings->getSetting("JavaPath").get(), &Setting::SettingChanged, m_ui->javaInstallationGroupBox, + [this, settings] { m_ui->javaPathTextBox->setText(settings->get("JavaPath").toString()); }); - m_horizontalLayout = new QHBoxLayout(); - m_horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); - m_javaPathTextBox = new QLineEdit(this); - m_javaPathTextBox->setObjectName(QStringLiteral("javaPathTextBox")); + connect(m_ui->javaDownloadBtn, &QPushButton::clicked, this, [this] { + auto javaDialog = new Java::InstallDialog({}, m_instance.get(), this); + javaDialog->exec(); + }); + connect(m_ui->javaPathTextBox, &QLineEdit::textChanged, [this](QString newValue) { + if (m_instance->settings()->get("JavaPath").toString() != newValue) { + m_instance->settings()->set("AutomaticJava", false); + } + }); + } - m_horizontalLayout->addWidget(m_javaPathTextBox); + connect(m_ui->javaTestBtn, &QPushButton::clicked, this, &JavaSettingsWidget::onJavaTest); + connect(m_ui->javaDetectBtn, &QPushButton::clicked, this, &JavaSettingsWidget::onJavaAutodetect); + connect(m_ui->javaBrowseBtn, &QPushButton::clicked, this, &JavaSettingsWidget::onJavaBrowse); - m_javaBrowseBtn = new QPushButton(this); - m_javaBrowseBtn->setObjectName(QStringLiteral("javaBrowseBtn")); + connect(m_ui->maxMemSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &JavaSettingsWidget::updateThresholds); + connect(m_ui->minMemSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &JavaSettingsWidget::updateThresholds); - m_horizontalLayout->addWidget(m_javaBrowseBtn); - - m_javaStatusBtn = new QToolButton(this); - 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); - m_gridLayout_2->setObjectName(QStringLiteral("gridLayout_2")); - m_gridLayout_2->setColumnStretch(0, 1); - - m_labelMinMem = new QLabel(m_memoryGroupBox); - m_labelMinMem->setObjectName(QStringLiteral("labelMinMem")); - m_gridLayout_2->addWidget(m_labelMinMem, 0, 0, 1, 1); - - m_minMemSpinBox = new QSpinBox(m_memoryGroupBox); - m_minMemSpinBox->setObjectName(QStringLiteral("minMemSpinBox")); - m_minMemSpinBox->setSuffix(QStringLiteral(" MiB")); - m_minMemSpinBox->setMinimum(8); - m_minMemSpinBox->setMaximum(1048576); - m_minMemSpinBox->setSingleStep(128); - m_labelMinMem->setBuddy(m_minMemSpinBox); - m_gridLayout_2->addWidget(m_minMemSpinBox, 0, 1, 1, 1); - - m_labelMaxMem = new QLabel(m_memoryGroupBox); - m_labelMaxMem->setObjectName(QStringLiteral("labelMaxMem")); - m_gridLayout_2->addWidget(m_labelMaxMem, 1, 0, 1, 1); - - m_maxMemSpinBox = new QSpinBox(m_memoryGroupBox); - m_maxMemSpinBox->setObjectName(QStringLiteral("maxMemSpinBox")); - m_maxMemSpinBox->setSuffix(QStringLiteral(" MiB")); - m_maxMemSpinBox->setMinimum(8); - m_maxMemSpinBox->setMaximum(1048576); - m_maxMemSpinBox->setSingleStep(128); - m_labelMaxMem->setBuddy(m_maxMemSpinBox); - m_gridLayout_2->addWidget(m_maxMemSpinBox, 1, 1, 1, 1); - - m_labelMaxMemIcon = new QLabel(m_memoryGroupBox); - m_labelMaxMemIcon->setObjectName(QStringLiteral("labelMaxMemIcon")); - m_gridLayout_2->addWidget(m_labelMaxMemIcon, 1, 2, 1, 1); - - m_labelPermGen = new QLabel(m_memoryGroupBox); - m_labelPermGen->setObjectName(QStringLiteral("labelPermGen")); - m_labelPermGen->setText(QStringLiteral("PermGen:")); - m_gridLayout_2->addWidget(m_labelPermGen, 2, 0, 1, 1); - m_labelPermGen->setVisible(false); - - m_permGenSpinBox = new QSpinBox(m_memoryGroupBox); - m_permGenSpinBox->setObjectName(QStringLiteral("permGenSpinBox")); - m_permGenSpinBox->setSuffix(QStringLiteral(" MiB")); - m_permGenSpinBox->setMinimum(4); - m_permGenSpinBox->setMaximum(1048576); - m_permGenSpinBox->setSingleStep(8); - m_gridLayout_2->addWidget(m_permGenSpinBox, 2, 1, 1, 1); - m_permGenSpinBox->setVisible(false); - - m_verticalLayout->addWidget(m_memoryGroupBox); - - retranslate(); -} - -void JavaSettingsWidget::initialize() -{ - m_versionWidget->initialize(APPLICATION->javalist().get()); - m_versionWidget->selectSearch(); - m_versionWidget->setResizeOn(2); - auto s = APPLICATION->settings(); - // Memory - observedMinMemory = s->get("MinMemAlloc").toInt(); - observedMaxMemory = s->get("MaxMemAlloc").toInt(); - observedPermGenMemory = s->get("PermGen").toInt(); - m_minMemSpinBox->setValue(observedMinMemory); - m_maxMemSpinBox->setValue(observedMaxMemory); - m_permGenSpinBox->setValue(observedPermGenMemory); + loadSettings(); updateThresholds(); } -void JavaSettingsWidget::refresh() +JavaSettingsWidget::~JavaSettingsWidget() +{ + delete m_ui; +} + +void JavaSettingsWidget::loadSettings() +{ + SettingsObjectPtr settings; + + if (m_instance != nullptr) + settings = m_instance->settings(); + else + settings = APPLICATION->settings(); + + // Java Settings + m_ui->javaInstallationGroupBox->setChecked(settings->get("OverrideJavaLocation").toBool()); + m_ui->javaPathTextBox->setText(settings->get("JavaPath").toString()); + + m_ui->skipCompatibilityCheckBox->setChecked(settings->get("IgnoreJavaCompatibility").toBool()); + + m_ui->javaArgumentsGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideJavaArgs").toBool()); + m_ui->jvmArgsTextBox->setPlainText(settings->get("JvmArgs").toString()); + + if (m_instance == nullptr) { + m_ui->skipWizardCheckBox->setChecked(settings->get("IgnoreJavaWizard").toBool()); + m_ui->autodetectJavaCheckBox->setChecked(settings->get("AutomaticJavaSwitch").toBool()); + m_ui->autodetectJavaCheckBox->stateChanged(m_ui->autodetectJavaCheckBox->isChecked()); + m_ui->autodownloadJavaCheckBox->setChecked(settings->get("AutomaticJavaDownload").toBool()); + } + + // Memory + m_ui->memoryGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideMemory").toBool()); + int min = settings->get("MinMemAlloc").toInt(); + int max = settings->get("MaxMemAlloc").toInt(); + if (min < max) { + m_ui->minMemSpinBox->setValue(min); + m_ui->maxMemSpinBox->setValue(max); + } else { + m_ui->minMemSpinBox->setValue(max); + m_ui->maxMemSpinBox->setValue(min); + } + m_ui->permGenSpinBox->setValue(settings->get("PermGen").toInt()); + + // Java arguments + m_ui->javaArgumentsGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideJavaArgs").toBool()); + m_ui->jvmArgsTextBox->setPlainText(settings->get("JvmArgs").toString()); +} + +void JavaSettingsWidget::saveSettings() +{ + SettingsObjectPtr settings; + + if (m_instance != nullptr) + settings = m_instance->settings(); + else + settings = APPLICATION->settings(); + + SettingsObject::Lock lock(settings); + + // Java Install Settings + bool javaInstall = m_instance == nullptr || m_ui->javaInstallationGroupBox->isChecked(); + + if (m_instance != nullptr) + settings->set("OverrideJavaLocation", javaInstall); + + if (javaInstall) { + settings->set("JavaPath", m_ui->javaPathTextBox->text()); + settings->set("IgnoreJavaCompatibility", m_ui->skipCompatibilityCheckBox->isChecked()); + } else { + settings->reset("JavaPath"); + settings->reset("IgnoreJavaCompatibility"); + } + + if (m_instance == nullptr) { + settings->set("IgnoreJavaWizard", m_ui->skipWizardCheckBox->isChecked()); + settings->set("AutomaticJavaSwitch", m_ui->autodetectJavaCheckBox->isChecked()); + settings->set("AutomaticJavaDownload", m_ui->autodownloadJavaCheckBox->isChecked()); + } + + // Memory + bool memory = m_instance == nullptr || m_ui->memoryGroupBox->isChecked(); + + if (m_instance != nullptr) + settings->set("OverrideMemory", memory); + + if (memory) { + int min = m_ui->minMemSpinBox->value(); + int max = m_ui->maxMemSpinBox->value(); + if (min < max) { + settings->set("MinMemAlloc", min); + settings->set("MaxMemAlloc", max); + } else { + settings->set("MinMemAlloc", max); + settings->set("MaxMemAlloc", min); + } + settings->set("PermGen", m_ui->permGenSpinBox->value()); + } else { + settings->reset("MinMemAlloc"); + settings->reset("MaxMemAlloc"); + settings->reset("PermGen"); + } + + // Java arguments + bool javaArgs = m_instance == nullptr || m_ui->javaArgumentsGroupBox->isChecked(); + + if (m_instance != nullptr) + settings->set("OverrideJavaArgs", javaArgs); + + if (javaArgs) { + settings->set("JvmArgs", m_ui->jvmArgsTextBox->toPlainText().replace("\n", " ")); + } else { + settings->reset("JvmArgs"); + } +} + +void JavaSettingsWidget::onJavaBrowse() +{ + QString rawPath = QFileDialog::getOpenFileName(this, tr("Find Java executable")); + + // do not allow current dir - it's dirty. Do not allow dirs that don't exist + if (rawPath.isEmpty()) { + return; + } + + QString cookedPath = FS::NormalizePath(rawPath); + QFileInfo javaInfo(cookedPath); + if (!javaInfo.exists() || !javaInfo.isExecutable()) { + return; + } + m_ui->javaPathTextBox->setText(cookedPath); +} + +void JavaSettingsWidget::onJavaTest() +{ + if (m_checker != nullptr) + return; + + QString jvmArgs; + + if (m_instance == nullptr || m_ui->javaArgumentsGroupBox->isChecked()) + jvmArgs = m_ui->jvmArgsTextBox->toPlainText().replace("\n", " "); + else + jvmArgs = APPLICATION->settings()->get("JvmArgs").toString(); + + m_checker.reset(new JavaCommon::TestCheck(this, m_ui->javaPathTextBox->text(), jvmArgs, m_ui->minMemSpinBox->value(), + m_ui->maxMemSpinBox->value(), m_ui->permGenSpinBox->value())); + connect(m_checker.get(), &JavaCommon::TestCheck::finished, this, [this] { m_checker.reset(); }); + m_checker->run(); +} + +void JavaSettingsWidget::onJavaAutodetect() { if (JavaUtils::getJavaCheckPath().isEmpty()) { JavaCommon::javaCheckNotFound(this); return; } - m_versionWidget->loadList(); -} -JavaSettingsWidget::ValidationStatus JavaSettingsWidget::validate() -{ - switch (javaStatus) { - default: - case JavaStatus::NotSet: - case JavaStatus::DoesNotExist: - case JavaStatus::DoesNotStart: - 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; - } - return ValidationStatus::JavaBad; - } break; - case JavaStatus::Pending: { - return ValidationStatus::Bad; - } - case JavaStatus::Good: { - return ValidationStatus::AllOK; + VersionSelectDialog versionDialog(APPLICATION->javalist().get(), tr("Select a Java version"), this, true); + versionDialog.setResizeOn(2); + versionDialog.exec(); + + if (versionDialog.result() == QDialog::Accepted && versionDialog.selectedVersion()) { + JavaInstallPtr java = std::dynamic_pointer_cast(versionDialog.selectedVersion()); + m_ui->javaPathTextBox->setText(java->path); + + if (!java->is_64bit && m_ui->maxMemSpinBox->value() > 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(); } } } - -QString JavaSettingsWidget::javaPath() const -{ - return m_javaPathTextBox->text(); -} - -int JavaSettingsWidget::maxHeapSize() const -{ - auto min = m_minMemSpinBox->value(); - auto max = m_maxMemSpinBox->value(); - if (max < min) - max = min; - return max; -} - -int JavaSettingsWidget::minHeapSize() const -{ - auto min = m_minMemSpinBox->value(); - auto max = m_maxMemSpinBox->value(); - if (min > max) - min = max; - return min; -} - -bool JavaSettingsWidget::permGenEnabled() const -{ - return m_permGenSpinBox->isVisible(); -} - -int JavaSettingsWidget::permGenSize() const -{ - return m_permGenSpinBox->value(); -} - -void JavaSettingsWidget::memoryValueChanged(int) -{ - 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) { - observedMinMemory = min; - actuallyChanged = true; - } else if (obj == m_maxMemSpinBox && max != observedMaxMemory) { - observedMaxMemory = max; - actuallyChanged = true; - } else if (obj == m_permGenSpinBox && permgen != observedPermGenMemory) { - observedPermGenMemory = permgen; - actuallyChanged = true; - } - if (actuallyChanged) { - checkJavaPathOnEdit(m_javaPathTextBox->text()); - updateThresholds(); - } -} - -void JavaSettingsWidget::javaVersionSelected(BaseVersion::Ptr version) -{ - auto java = std::dynamic_pointer_cast(version); - if (!java) { - return; - } - auto visible = java->id.requiresPermGen(); - m_labelPermGen->setVisible(visible); - m_permGenSpinBox->setVisible(visible); - m_javaPathTextBox->setText(java->path); - checkJavaPath(java->path); -} - -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); - if (raw_path.isEmpty()) { - return; - } - QString cooked_path = FS::NormalizePath(raw_path); - m_javaPathTextBox->setText(cooked_path); - checkJavaPath(cooked_path); -} - -void JavaSettingsWidget::on_javaStatusBtn_clicked() -{ - QString text; - bool failed = false; - switch (javaStatus) { - case JavaStatus::NotSet: - checkJavaPath(m_javaPathTextBox->text()); - return; - case JavaStatus::DoesNotExist: - text += QObject::tr("The specified file either doesn't exist or is not a proper executable."); - failed = true; - break; - case JavaStatus::DoesNotStart: { - text += QObject::tr("The specified Java binary didn't start properly.
    "); - auto htmlError = m_result.errorLog; - if (!htmlError.isEmpty()) { - htmlError.replace('\n', "
    "); - text += QString("%1").arg(htmlError); - } - failed = true; - break; - } - case JavaStatus::ReturnedInvalidData: { - text += QObject::tr("The specified Java binary returned unexpected results:
    "); - auto htmlOut = m_result.outLog; - if (!htmlOut.isEmpty()) { - htmlOut.replace('\n', "
    "); - text += QString("%1").arg(htmlOut); - } - failed = true; - break; - } - case JavaStatus::Good: - text += QObject::tr( - "Java test succeeded!
    Platform reported: %1
    Java version " - "reported: %2
    ") - .arg(m_result.realPlatform, m_result.javaVersion.toString()); - break; - case JavaStatus::Pending: - // TODO: abort here? - return; - } - CustomMessageBox::selectable(this, failed ? QObject::tr("Java test failure") : QObject::tr("Java test success"), text, - failed ? QMessageBox::Critical : QMessageBox::Information) - ->show(); -} - -void JavaSettingsWidget::setJavaStatus(JavaSettingsWidget::JavaStatus status) -{ - javaStatus = status; - switch (javaStatus) { - case JavaStatus::Good: - m_javaStatusBtn->setIcon(goodIcon); - break; - case JavaStatus::NotSet: - case JavaStatus::Pending: - m_javaStatusBtn->setIcon(yellowIcon); - break; - default: - m_javaStatusBtn->setIcon(badIcon); - break; - } -} - -void JavaSettingsWidget::javaPathEdited(const QString& path) -{ - checkJavaPathOnEdit(path); -} - -void JavaSettingsWidget::checkJavaPathOnEdit(const QString& path) -{ - auto realPath = FS::ResolveExecutable(path); - QFileInfo pathInfo(realPath); - if (pathInfo.baseName().toLower().contains("java")) { - checkJavaPath(path); - } else { - if (!m_checker) { - setJavaStatus(JavaStatus::NotSet); - } - } -} - -void JavaSettingsWidget::checkJavaPath(const QString& path) -{ - if (m_checker) { - queuedCheck = path; - return; - } - auto realPath = FS::ResolveExecutable(path); - if (realPath.isNull()) { - setJavaStatus(JavaStatus::DoesNotExist); - 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(); - } - connect(m_checker.get(), &JavaChecker::checkFinished, this, &JavaSettingsWidget::checkFinished); - m_checker->performCheck(); -} - -void JavaSettingsWidget::checkFinished(JavaCheckResult result) -{ - m_result = result; - switch (result.validity) { - case JavaCheckResult::Validity::Valid: { - setJavaStatus(JavaStatus::Good); - break; - } - case JavaCheckResult::Validity::ReturnedInvalidData: { - setJavaStatus(JavaStatus::ReturnedInvalidData); - break; - } - case JavaCheckResult::Validity::Errored: { - setJavaStatus(JavaStatus::DoesNotStart); - break; - } - } - m_checker.reset(); - if (!queuedCheck.isNull()) { - checkJavaPath(queuedCheck); - queuedCheck.clear(); - } -} - -void JavaSettingsWidget::retranslate() -{ - m_memoryGroupBox->setTitle(tr("Memory")); - m_maxMemSpinBox->setToolTip(tr("The maximum amount of memory Minecraft is allowed to use.")); - m_labelMinMem->setText(tr("Minimum memory allocation:")); - m_labelMaxMem->setText(tr("Maximum memory allocation:")); - 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")); -} - void JavaSettingsWidget::updateThresholds() { + auto sysMiB = Sys::getSystemRam() / Sys::mebibyte; + unsigned int maxMem = m_ui->maxMemSpinBox->value(); + unsigned int minMem = m_ui->minMemSpinBox->value(); + QString iconName; - if (observedMaxMemory >= m_availableMemory) { + if (maxMem >= sysMiB) { iconName = "status-bad"; - m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation exceeds your system memory capacity.")); - } else if (observedMaxMemory > (m_availableMemory * 0.9)) { + m_ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation exceeds your system memory capacity.")); + } else if (maxMem > (sysMiB * 0.9)) { iconName = "status-yellow"; - m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity.")); - } else if (observedMaxMemory < observedMinMemory) { + m_ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity.")); + } else if (maxMem < minMem) { iconName = "status-yellow"; - m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation is smaller than the minimum value")); + m_ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation is smaller than the minimum value")); } else { iconName = "status-good"; - m_labelMaxMemIcon->setToolTip(""); + m_ui->labelMaxMemIcon->setToolTip(""); } { - auto height = m_labelMaxMemIcon->fontInfo().pixelSize(); + auto height = m_ui->labelMaxMemIcon->fontInfo().pixelSize(); QIcon icon = APPLICATION->getThemedIcon(iconName); QPixmap pix = icon.pixmap(height, height); - m_labelMaxMemIcon->setPixmap(pix); + m_ui->labelMaxMemIcon->setPixmap(pix); } } diff --git a/launcher/ui/widgets/JavaSettingsWidget.h b/launcher/ui/widgets/JavaSettingsWidget.h index 6ea73da60..21a71fb8b 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.h +++ b/launcher/ui/widgets/JavaSettingsWidget.h @@ -1,90 +1,68 @@ -#pragma once -#include - -#include -#include -#include -#include - -class QLineEdit; -class VersionSelectWidget; -class QSpinBox; -class QPushButton; -class QVBoxLayout; -class QHBoxLayout; -class QGroupBox; -class QGridLayout; -class QLabel; -class QToolButton; - -/** - * This is a widget for all the Java settings dialogs and pages. +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * 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. */ + +#pragma once + +#include +#include "BaseInstance.h" +#include "JavaCommon.h" + +namespace Ui { +class JavaSettingsWidget; +} + class JavaSettingsWidget : public QWidget { Q_OBJECT public: - explicit JavaSettingsWidget(QWidget* parent); - virtual ~JavaSettingsWidget(){}; + explicit JavaSettingsWidget(QWidget* parent = nullptr) : JavaSettingsWidget(nullptr, nullptr) {} + explicit JavaSettingsWidget(InstancePtr instance, QWidget* parent = nullptr); + ~JavaSettingsWidget() override; - enum class JavaStatus { NotSet, Pending, Good, DoesNotExist, DoesNotStart, ReturnedInvalidData } javaStatus = JavaStatus::NotSet; - - enum class ValidationStatus { Bad, JavaBad, AllOK }; - - void refresh(); - void initialize(); - ValidationStatus validate(); - void retranslate(); - - bool permGenEnabled() const; - int permGenSize() const; - int minHeapSize() const; - int maxHeapSize() const; - QString javaPath() const; + void loadSettings(); + void saveSettings(); + private slots: + void onJavaBrowse(); + void onJavaAutodetect(); + void onJavaTest(); void updateThresholds(); - protected slots: - void memoryValueChanged(int); - void javaPathEdited(const QString& path); - void javaVersionSelected(BaseVersion::Ptr version); - void on_javaBrowseBtn_clicked(); - void on_javaStatusBtn_clicked(); - void checkFinished(JavaCheckResult result); - - protected: /* methods */ - void checkJavaPathOnEdit(const QString& path); - void checkJavaPath(const QString& path); - void setJavaStatus(JavaStatus status); - void setupUi(); - - private: /* data */ - VersionSelectWidget* m_versionWidget = nullptr; - QVBoxLayout* m_verticalLayout = nullptr; - - QLineEdit* m_javaPathTextBox = nullptr; - QPushButton* m_javaBrowseBtn = nullptr; - QToolButton* m_javaStatusBtn = nullptr; - QHBoxLayout* m_horizontalLayout = nullptr; - - QGroupBox* m_memoryGroupBox = nullptr; - QGridLayout* m_gridLayout_2 = nullptr; - QSpinBox* m_maxMemSpinBox = nullptr; - QLabel* m_labelMinMem = nullptr; - QLabel* m_labelMaxMem = nullptr; - QLabel* m_labelMaxMemIcon = nullptr; - QSpinBox* m_minMemSpinBox = nullptr; - QLabel* m_labelPermGen = nullptr; - QSpinBox* m_permGenSpinBox = nullptr; - QIcon goodIcon; - QIcon yellowIcon; - QIcon badIcon; - - 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; + private: + InstancePtr m_instance; + Ui::JavaSettingsWidget* m_ui; + unique_qobject_ptr m_checker; }; diff --git a/launcher/ui/widgets/JavaSettingsWidget.ui b/launcher/ui/widgets/JavaSettingsWidget.ui new file mode 100644 index 000000000..15ce88f0c --- /dev/null +++ b/launcher/ui/widgets/JavaSettingsWidget.ui @@ -0,0 +1,269 @@ + + + JavaSettingsWidget + + + + 0 + 0 + 500 + 600 + + + + Form + + + + + + true + + + Java Insta&llation + + + false + + + false + + + + + + Auto-&detect Java version + + + + + + + + + + + + Browse + + + + + + + + + + + Download Java + + + + + + + Auto-detect... + + + + + + + Test + + + + + + + + + Automatically downloads and selects the Java build recommended by Mojang. + + + Auto-download &Mojang Java + + + + + + + If enabled, the launcher won't prompt you to choose a Java version if one is not found on startup. + + + Skip Java setup prompt on startup + + + + + + + If enabled, the launcher will not check if an instance is compatible with the selected Java version. + + + Skip Java compatibility checks + + + + + + + + + + true + + + Memor&y + + + false + + + false + + + + + + PermGen (Java 7 and earlier): + + + + + + + Minimum memory allocation: + + + + + + + The amount of memory available to store loaded Java classes. + + + MiB + + + 4 + + + 999999999 + + + 8 + + + 64 + + + + + + + Maximum memory allocation: + + + + + + + + + + Qt::AlignCenter + + + maxMemSpinBox + + + + + + + The maximum amount of memory Minecraft is allowed to use. + + + MiB + + + 8 + + + 1048576 + + + 128 + + + 1024 + + + + + + + The amount of memory Minecraft is started with. + + + MiB + + + 8 + + + 1048576 + + + 128 + + + 256 + + + + + + + + + + true + + + Java Argumen&ts + + + false + + + false + + + + + + + + + + + + javaPathTextBox + javaBrowseBtn + javaDownloadBtn + javaDetectBtn + javaTestBtn + skipCompatibilityCheckBox + skipWizardCheckBox + autodetectJavaCheckBox + autodownloadJavaCheckBox + minMemSpinBox + maxMemSpinBox + permGenSpinBox + jvmArgsTextBox + + + + diff --git a/launcher/ui/widgets/JavaWizardWidget.cpp b/launcher/ui/widgets/JavaWizardWidget.cpp new file mode 100644 index 000000000..02bb57474 --- /dev/null +++ b/launcher/ui/widgets/JavaWizardWidget.cpp @@ -0,0 +1,559 @@ +#include "JavaWizardWidget.h" + +#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" +#include "BuildConfig.h" + +JavaWizardWidget::JavaWizardWidget(QWidget* parent) : QWidget(parent) +{ + m_availableMemory = Sys::getSystemRam() / Sys::mebibyte; + + 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(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, &JavaWizardWidget::memoryValueChanged); + connect(m_versionWidget, &VersionSelectWidget::selectedVersionChanged, this, &JavaWizardWidget::javaVersionSelected); + connect(m_javaBrowseBtn, &QPushButton::clicked, this, &JavaWizardWidget::on_javaBrowseBtn_clicked); + connect(m_javaPathTextBox, &QLineEdit::textEdited, this, &JavaWizardWidget::javaPathEdited); + connect(m_javaStatusBtn, &QToolButton::clicked, this, &JavaWizardWidget::on_javaStatusBtn_clicked); + if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { + connect(m_javaDownloadBtn, &QPushButton::clicked, this, &JavaWizardWidget::javaDownloadBtn_clicked); + } +} + +void JavaWizardWidget::setupUi() +{ + setObjectName(QStringLiteral("javaSettingsWidget")); + m_verticalLayout = new QVBoxLayout(this); + m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); + + m_versionWidget = new VersionSelectWidget(this); + + m_horizontalLayout = new QHBoxLayout(); + m_horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); + m_javaPathTextBox = new QLineEdit(this); + m_javaPathTextBox->setObjectName(QStringLiteral("javaPathTextBox")); + + m_horizontalLayout->addWidget(m_javaPathTextBox); + + m_javaBrowseBtn = new QPushButton(this); + m_javaBrowseBtn->setObjectName(QStringLiteral("javaBrowseBtn")); + + m_horizontalLayout->addWidget(m_javaBrowseBtn); + + m_javaStatusBtn = new QToolButton(this); + m_javaStatusBtn->setIcon(yellowIcon); + m_horizontalLayout->addWidget(m_javaStatusBtn); + + m_memoryGroupBox = new QGroupBox(this); + m_memoryGroupBox->setObjectName(QStringLiteral("memoryGroupBox")); + m_gridLayout_2 = new QGridLayout(m_memoryGroupBox); + m_gridLayout_2->setObjectName(QStringLiteral("gridLayout_2")); + m_gridLayout_2->setColumnStretch(0, 1); + + m_labelMinMem = new QLabel(m_memoryGroupBox); + m_labelMinMem->setObjectName(QStringLiteral("labelMinMem")); + m_gridLayout_2->addWidget(m_labelMinMem, 0, 0, 1, 1); + + m_minMemSpinBox = new QSpinBox(m_memoryGroupBox); + m_minMemSpinBox->setObjectName(QStringLiteral("minMemSpinBox")); + m_minMemSpinBox->setSuffix(QStringLiteral(" MiB")); + m_minMemSpinBox->setMinimum(8); + m_minMemSpinBox->setMaximum(1048576); + m_minMemSpinBox->setSingleStep(128); + m_labelMinMem->setBuddy(m_minMemSpinBox); + m_gridLayout_2->addWidget(m_minMemSpinBox, 0, 1, 1, 1); + + m_labelMaxMem = new QLabel(m_memoryGroupBox); + m_labelMaxMem->setObjectName(QStringLiteral("labelMaxMem")); + m_gridLayout_2->addWidget(m_labelMaxMem, 1, 0, 1, 1); + + m_maxMemSpinBox = new QSpinBox(m_memoryGroupBox); + m_maxMemSpinBox->setObjectName(QStringLiteral("maxMemSpinBox")); + m_maxMemSpinBox->setSuffix(QStringLiteral(" MiB")); + m_maxMemSpinBox->setMinimum(8); + m_maxMemSpinBox->setMaximum(1048576); + m_maxMemSpinBox->setSingleStep(128); + m_labelMaxMem->setBuddy(m_maxMemSpinBox); + m_gridLayout_2->addWidget(m_maxMemSpinBox, 1, 1, 1, 1); + + m_labelMaxMemIcon = new QLabel(m_memoryGroupBox); + m_labelMaxMemIcon->setObjectName(QStringLiteral("labelMaxMemIcon")); + m_gridLayout_2->addWidget(m_labelMaxMemIcon, 1, 2, 1, 1); + + m_labelPermGen = new QLabel(m_memoryGroupBox); + m_labelPermGen->setObjectName(QStringLiteral("labelPermGen")); + m_labelPermGen->setText(QStringLiteral("PermGen:")); + m_gridLayout_2->addWidget(m_labelPermGen, 2, 0, 1, 1); + m_labelPermGen->setVisible(false); + + m_permGenSpinBox = new QSpinBox(m_memoryGroupBox); + m_permGenSpinBox->setObjectName(QStringLiteral("permGenSpinBox")); + m_permGenSpinBox->setSuffix(QStringLiteral(" MiB")); + m_permGenSpinBox->setMinimum(4); + m_permGenSpinBox->setMaximum(1048576); + m_permGenSpinBox->setSingleStep(8); + m_gridLayout_2->addWidget(m_permGenSpinBox, 2, 1, 1, 1); + m_permGenSpinBox->setVisible(false); + + 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(); +} + +void JavaWizardWidget::initialize() +{ + m_versionWidget->initialize(APPLICATION->javalist().get()); + m_versionWidget->selectSearch(); + m_versionWidget->setResizeOn(2); + auto s = APPLICATION->settings(); + // Memory + observedMinMemory = s->get("MinMemAlloc").toInt(); + observedMaxMemory = s->get("MaxMemAlloc").toInt(); + observedPermGenMemory = s->get("PermGen").toInt(); + m_minMemSpinBox->setValue(observedMinMemory); + m_maxMemSpinBox->setValue(observedMaxMemory); + m_permGenSpinBox->setValue(observedPermGenMemory); + updateThresholds(); + if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { + m_autodownloadCheckBox->setChecked(true); + } +} + +void JavaWizardWidget::refresh() +{ + if (BuildConfig.JAVA_DOWNLOADER_ENABLED && m_autodownloadCheckBox->isChecked()) { + return; + } + if (JavaUtils::getJavaCheckPath().isEmpty()) { + JavaCommon::javaCheckNotFound(this); + return; + } + m_versionWidget->loadList(); +} + +JavaWizardWidget::ValidationStatus JavaWizardWidget::validate() +{ + switch (javaStatus) { + default: + case JavaStatus::NotSet: + /* fallthrough */ + case JavaStatus::DoesNotExist: + /* fallthrough */ + case JavaStatus::DoesNotStart: + /* fallthrough */ + case JavaStatus::ReturnedInvalidData: { + 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; + case JavaStatus::Pending: { + return ValidationStatus::Bad; + } + case JavaStatus::Good: { + return ValidationStatus::AllOK; + } + } +} + +QString JavaWizardWidget::javaPath() const +{ + return m_javaPathTextBox->text(); +} + +int JavaWizardWidget::maxHeapSize() const +{ + auto min = m_minMemSpinBox->value(); + auto max = m_maxMemSpinBox->value(); + if (max < min) + max = min; + return max; +} + +int JavaWizardWidget::minHeapSize() const +{ + auto min = m_minMemSpinBox->value(); + auto max = m_maxMemSpinBox->value(); + if (min > max) + min = max; + return min; +} + +bool JavaWizardWidget::permGenEnabled() const +{ + return m_permGenSpinBox->isVisible(); +} + +int JavaWizardWidget::permGenSize() const +{ + return m_permGenSpinBox->value(); +} + +void JavaWizardWidget::memoryValueChanged() +{ + bool actuallyChanged = false; + unsigned int min = m_minMemSpinBox->value(); + unsigned int max = m_maxMemSpinBox->value(); + unsigned int permgen = m_permGenSpinBox->value(); + if (min != observedMinMemory) { + observedMinMemory = min; + actuallyChanged = true; + } + if (max != observedMaxMemory) { + observedMaxMemory = max; + actuallyChanged = true; + } + if (permgen != observedPermGenMemory) { + observedPermGenMemory = permgen; + actuallyChanged = true; + } + if (actuallyChanged) { + checkJavaPathOnEdit(m_javaPathTextBox->text()); + updateThresholds(); + } +} + +void JavaWizardWidget::javaVersionSelected(BaseVersion::Ptr version) +{ + auto java = std::dynamic_pointer_cast(version); + if (!java) { + return; + } + auto visible = java->id.requiresPermGen(); + m_labelPermGen->setVisible(visible); + m_permGenSpinBox->setVisible(visible); + m_javaPathTextBox->setText(java->path); + checkJavaPath(java->path); +} + +void JavaWizardWidget::on_javaBrowseBtn_clicked() +{ + 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; + } + auto cooked_path = FS::NormalizePath(raw_path); + m_javaPathTextBox->setText(cooked_path); + checkJavaPath(cooked_path); +} + +void JavaWizardWidget::javaDownloadBtn_clicked() +{ + auto jdialog = new Java::InstallDialog({}, nullptr, this); + jdialog->exec(); +} + +void JavaWizardWidget::on_javaStatusBtn_clicked() +{ + QString text; + bool failed = false; + switch (javaStatus) { + case JavaStatus::NotSet: + checkJavaPath(m_javaPathTextBox->text()); + return; + case JavaStatus::DoesNotExist: + text += QObject::tr("The specified file either doesn't exist or is not a proper executable."); + failed = true; + break; + case JavaStatus::DoesNotStart: { + text += QObject::tr("The specified Java binary didn't start properly.
    "); + auto htmlError = m_result.errorLog; + if (!htmlError.isEmpty()) { + htmlError.replace('\n', "
    "); + text += QString("%1").arg(htmlError); + } + failed = true; + break; + } + case JavaStatus::ReturnedInvalidData: { + text += QObject::tr("The specified Java binary returned unexpected results:
    "); + auto htmlOut = m_result.outLog; + if (!htmlOut.isEmpty()) { + htmlOut.replace('\n', "
    "); + text += QString("%1").arg(htmlOut); + } + failed = true; + break; + } + case JavaStatus::Good: + text += QObject::tr( + "Java test succeeded!
    Platform reported: %1
    Java version " + "reported: %2
    ") + .arg(m_result.realPlatform, m_result.javaVersion.toString()); + break; + case JavaStatus::Pending: + // TODO: abort here? + return; + } + CustomMessageBox::selectable(this, failed ? QObject::tr("Java test failure") : QObject::tr("Java test success"), text, + failed ? QMessageBox::Critical : QMessageBox::Information) + ->show(); +} + +void JavaWizardWidget::setJavaStatus(JavaWizardWidget::JavaStatus status) +{ + javaStatus = status; + switch (javaStatus) { + case JavaStatus::Good: + m_javaStatusBtn->setIcon(goodIcon); + break; + case JavaStatus::NotSet: + case JavaStatus::Pending: + m_javaStatusBtn->setIcon(yellowIcon); + break; + default: + m_javaStatusBtn->setIcon(badIcon); + break; + } +} + +void JavaWizardWidget::javaPathEdited(const QString& path) +{ + checkJavaPathOnEdit(path); +} + +void JavaWizardWidget::checkJavaPathOnEdit(const QString& path) +{ + auto realPath = FS::ResolveExecutable(path); + QFileInfo pathInfo(realPath); + if (pathInfo.baseName().toLower().contains("java")) { + checkJavaPath(path); + } else { + if (!m_checker) { + setJavaStatus(JavaStatus::NotSet); + } + } +} + +void JavaWizardWidget::checkJavaPath(const QString& path) +{ + if (m_checker) { + queuedCheck = path; + return; + } + auto realPath = FS::ResolveExecutable(path); + if (realPath.isNull()) { + setJavaStatus(JavaStatus::DoesNotExist); + return; + } + setJavaStatus(JavaStatus::Pending); + m_checker.reset( + new JavaChecker(path, "", minHeapSize(), maxHeapSize(), m_permGenSpinBox->isVisible() ? m_permGenSpinBox->value() : 0, 0)); + connect(m_checker.get(), &JavaChecker::checkFinished, this, &JavaWizardWidget::checkFinished); + m_checker->start(); +} + +void JavaWizardWidget::checkFinished(const JavaChecker::Result& result) +{ + m_result = result; + switch (result.validity) { + case JavaChecker::Result::Validity::Valid: { + setJavaStatus(JavaStatus::Good); + break; + } + case JavaChecker::Result::Validity::ReturnedInvalidData: { + setJavaStatus(JavaStatus::ReturnedInvalidData); + break; + } + case JavaChecker::Result::Validity::Errored: { + setJavaStatus(JavaStatus::DoesNotStart); + break; + } + } + updateThresholds(); + m_checker.reset(); + if (!queuedCheck.isNull()) { + checkJavaPath(queuedCheck); + queuedCheck.clear(); + } +} + +void JavaWizardWidget::retranslate() +{ + m_memoryGroupBox->setTitle(tr("Memory")); + m_maxMemSpinBox->setToolTip(tr("The maximum amount of memory Minecraft is allowed to use.")); + m_labelMinMem->setText(tr("Minimum memory allocation:")); + m_labelMaxMem->setText(tr("Maximum memory allocation:")); + 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("Auto-detect Java version")); + m_autoJavaGroupBox->setTitle(tr("Autodetect Java")); +} + +void JavaWizardWidget::updateThresholds() +{ + QString iconName; + + if (observedMaxMemory >= m_availableMemory) { + iconName = "status-bad"; + m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation exceeds your system memory capacity.")); + } else if (observedMaxMemory > (m_availableMemory * 0.9)) { + iconName = "status-yellow"; + m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity.")); + } 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(""); + } + + { + auto height = m_labelMaxMemIcon->fontInfo().pixelSize(); + QIcon icon = APPLICATION->getThemedIcon(iconName); + QPixmap pix = icon.pixmap(height, height); + m_labelMaxMemIcon->setPixmap(pix); + } +} + +bool JavaWizardWidget::autoDownloadJava() const +{ + return m_autodownloadCheckBox && m_autodownloadCheckBox->isChecked(); +} + +bool JavaWizardWidget::autoDetectJava() const +{ + return m_autodetectJavaCheckBox->isChecked(); +} + +void JavaWizardWidget::onSpinBoxValueChanged(int) +{ + m_memoryTimer->start(500); +} + +JavaWizardWidget::~JavaWizardWidget() +{ + delete m_verticalSpacer; +}; \ No newline at end of file diff --git a/launcher/ui/widgets/JavaWizardWidget.h b/launcher/ui/widgets/JavaWizardWidget.h new file mode 100644 index 000000000..69f093000 --- /dev/null +++ b/launcher/ui/widgets/JavaWizardWidget.h @@ -0,0 +1,103 @@ +#pragma once +#include + +#include +#include +#include +#include +#include + +class QLineEdit; +class VersionSelectWidget; +class QSpinBox; +class QPushButton; +class QVBoxLayout; +class QHBoxLayout; +class QGroupBox; +class QGridLayout; +class QLabel; +class QToolButton; +class QSpacerItem; + +class JavaWizardWidget : public QWidget { + Q_OBJECT + + public: + explicit JavaWizardWidget(QWidget* parent); + virtual ~JavaWizardWidget(); + + enum class JavaStatus { NotSet, Pending, Good, DoesNotExist, DoesNotStart, ReturnedInvalidData } javaStatus = JavaStatus::NotSet; + + enum class ValidationStatus { Bad, JavaBad, AllOK }; + + void refresh(); + void initialize(); + ValidationStatus validate(); + void retranslate(); + + bool permGenEnabled() const; + int permGenSize() const; + int minHeapSize() const; + int maxHeapSize() const; + QString javaPath() const; + bool autoDetectJava() const; + bool autoDownloadJava() const; + + void updateThresholds(); + + protected slots: + void onSpinBoxValueChanged(int); + void memoryValueChanged(); + void javaPathEdited(const QString& path); + void javaVersionSelected(BaseVersion::Ptr version); + void on_javaBrowseBtn_clicked(); + void on_javaStatusBtn_clicked(); + void javaDownloadBtn_clicked(); + void checkFinished(const JavaChecker::Result& result); + + protected: /* methods */ + void checkJavaPathOnEdit(const QString& path); + void checkJavaPath(const QString& path); + void setJavaStatus(JavaStatus status); + void setupUi(); + + private: /* data */ + VersionSelectWidget* m_versionWidget = nullptr; + QVBoxLayout* m_verticalLayout = nullptr; + QSpacerItem* m_verticalSpacer = nullptr; + + QLineEdit* m_javaPathTextBox = nullptr; + QPushButton* m_javaBrowseBtn = nullptr; + QToolButton* m_javaStatusBtn = nullptr; + QHBoxLayout* m_horizontalLayout = nullptr; + + QGroupBox* m_memoryGroupBox = nullptr; + QGridLayout* m_gridLayout_2 = nullptr; + QSpinBox* m_maxMemSpinBox = nullptr; + QLabel* m_labelMinMem = nullptr; + QLabel* m_labelMaxMem = nullptr; + QLabel* m_labelMaxMemIcon = nullptr; + 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; + 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/LogView.cpp b/launcher/ui/widgets/LogView.cpp index 4096889d3..6578b1f12 100644 --- a/launcher/ui/widgets/LogView.cpp +++ b/launcher/ui/widgets/LogView.cpp @@ -36,6 +36,7 @@ #include "LogView.h" #include #include +#include LogView::LogView(QWidget* parent) : QPlainTextEdit(parent) { @@ -117,6 +118,9 @@ void LogView::rowsAboutToBeInserted(const QModelIndex& parent, int first, int la void LogView::rowsInserted(const QModelIndex& parent, int first, int last) { + QTextDocument document; + QTextCursor cursor(&document); + for (int i = first; i <= last; i++) { auto idx = m_model->index(i, 0, parent); auto text = m_model->data(idx, Qt::DisplayRole).toString(); @@ -133,11 +137,16 @@ void LogView::rowsInserted(const QModelIndex& parent, int first, int last) if (bg.isValid()) { format.setBackground(bg.value()); } - auto workCursor = textCursor(); - workCursor.movePosition(QTextCursor::End); - workCursor.insertText(text, format); - workCursor.insertBlock(); + cursor.movePosition(QTextCursor::End); + cursor.insertText(text, format); + cursor.insertBlock(); } + + QTextDocumentFragment fragment(&document); + QTextCursor workCursor = textCursor(); + workCursor.movePosition(QTextCursor::End); + workCursor.insertFragment(fragment); + if (m_scroll && !m_scrolling) { m_scrolling = true; QMetaObject::invokeMethod(this, "scrollToBottom", Qt::QueuedConnection); diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.cpp b/launcher/ui/widgets/MinecraftSettingsWidget.cpp new file mode 100644 index 000000000..cec7f267f --- /dev/null +++ b/launcher/ui/widgets/MinecraftSettingsWidget.cpp @@ -0,0 +1,460 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2022 Sefa Eyeoglu + * 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 "MinecraftSettingsWidget.h" + +#include "Application.h" +#include "BuildConfig.h" +#include "minecraft/WorldList.h" +#include "minecraft/auth/AccountList.h" +#include "settings/Setting.h" +#include "ui_MinecraftSettingsWidget.h" + +MinecraftSettingsWidget::MinecraftSettingsWidget(MinecraftInstancePtr instance, QWidget* parent) + : QWidget(parent), m_instance(std::move(instance)), m_ui(new Ui::MinecraftSettingsWidget) +{ + m_ui->setupUi(this); + + if (m_instance == nullptr) { + for (int i = m_ui->settingsTabs->count() - 1; i >= 0; --i) { + const QString name = m_ui->settingsTabs->widget(i)->objectName(); + + if (name == "javaPage" || name == "launchPage") + m_ui->settingsTabs->removeTab(i); + } + + m_ui->openGlobalSettingsButton->setVisible(false); + } else { + m_javaSettings = new JavaSettingsWidget(m_instance, this); + m_ui->javaScrollArea->setWidget(m_javaSettings); + + m_ui->showGameTime->setText(tr("Show time &playing this instance")); + m_ui->recordGameTime->setText(tr("&Record time playing this instance")); + m_ui->showGlobalGameTime->hide(); + m_ui->showGameTimeWithoutDays->hide(); + + m_ui->maximizedWarning->setText( + tr("Warning: The maximized option is " + "not fully supported on this Minecraft version.")); + + m_ui->miscellaneousSettingsBox->setCheckable(true); + m_ui->consoleSettingsBox->setCheckable(true); + m_ui->windowSizeGroupBox->setCheckable(true); + m_ui->nativeWorkaroundsGroupBox->setCheckable(true); + m_ui->perfomanceGroupBox->setCheckable(true); + m_ui->gameTimeGroupBox->setCheckable(true); + m_ui->legacySettingsGroupBox->setCheckable(true); + + m_quickPlaySingleplayer = m_instance->traits().contains("feature:is_quick_play_singleplayer"); + if (m_quickPlaySingleplayer) { + auto worlds = m_instance->worldList(); + worlds->update(); + for (const auto& world : worlds->allWorlds()) { + m_ui->worldsCb->addItem(world.folderName()); + } + } else { + m_ui->worldsCb->hide(); + m_ui->worldJoinButton->hide(); + m_ui->serverJoinAddressButton->setChecked(true); + m_ui->serverJoinAddress->setEnabled(true); + m_ui->serverJoinAddressButton->setStyleSheet("QRadioButton::indicator { width: 0px; height: 0px; }"); + } + + connect(m_ui->openGlobalSettingsButton, &QCommandLinkButton::clicked, this, &MinecraftSettingsWidget::openGlobalSettings); + connect(m_ui->serverJoinAddressButton, &QAbstractButton::toggled, m_ui->serverJoinAddress, &QWidget::setEnabled); + connect(m_ui->worldJoinButton, &QAbstractButton::toggled, m_ui->worldsCb, &QWidget::setEnabled); + } + + m_ui->maximizedWarning->hide(); + + connect(m_ui->maximizedCheckBox, &QCheckBox::toggled, this, + [this](const bool value) { m_ui->maximizedWarning->setVisible(value && (m_instance == nullptr || !m_instance->isLegacy())); }); + +#if !defined(Q_OS_LINUX) + m_ui->perfomanceGroupBox->hide(); +#endif + + if (!(APPLICATION->capabilities() & Application::SupportsGameMode)) { + m_ui->enableFeralGamemodeCheck->setDisabled(true); + m_ui->enableFeralGamemodeCheck->setToolTip(tr("Feral Interactive's GameMode could not be found on your system.")); + } + + if (!(APPLICATION->capabilities() & Application::SupportsMangoHud)) { + m_ui->enableMangoHud->setEnabled(false); + m_ui->enableMangoHud->setToolTip(tr("MangoHud could not be found on your system.")); + } + + connect(m_ui->useNativeOpenALCheck, &QAbstractButton::toggled, m_ui->lineEditOpenALPath, &QWidget::setEnabled); + connect(m_ui->useNativeGLFWCheck, &QAbstractButton::toggled, m_ui->lineEditGLFWPath, &QWidget::setEnabled); + + loadSettings(); +} + +MinecraftSettingsWidget::~MinecraftSettingsWidget() +{ + delete m_ui; +} + +void MinecraftSettingsWidget::loadSettings() +{ + SettingsObjectPtr settings; + + if (m_instance != nullptr) + settings = m_instance->settings(); + else + settings = APPLICATION->settings(); + + // Game Window + m_ui->windowSizeGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideWindow").toBool()); + m_ui->windowSizeGroupBox->setChecked(settings->get("OverrideWindow").toBool()); + m_ui->maximizedCheckBox->setChecked(settings->get("LaunchMaximized").toBool()); + m_ui->windowWidthSpinBox->setValue(settings->get("MinecraftWinWidth").toInt()); + m_ui->windowHeightSpinBox->setValue(settings->get("MinecraftWinHeight").toInt()); + + // Game Time + m_ui->gameTimeGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideGameTime").toBool()); + m_ui->showGameTime->setChecked(settings->get("ShowGameTime").toBool()); + m_ui->recordGameTime->setChecked(settings->get("RecordGameTime").toBool()); + m_ui->showGlobalGameTime->setChecked(m_instance == nullptr && settings->get("ShowGlobalGameTime").toBool()); + m_ui->showGameTimeWithoutDays->setChecked(m_instance == nullptr && settings->get("ShowGameTimeWithoutDays").toBool()); + + // Console + m_ui->consoleSettingsBox->setChecked(m_instance == nullptr || settings->get("OverrideConsole").toBool()); + m_ui->showConsoleCheck->setChecked(settings->get("ShowConsole").toBool()); + m_ui->autoCloseConsoleCheck->setChecked(settings->get("AutoCloseConsole").toBool()); + m_ui->showConsoleErrorCheck->setChecked(settings->get("ShowConsoleOnError").toBool()); + + // Miscellaneous + m_ui->miscellaneousSettingsBox->setChecked(settings->get("OverrideMiscellaneous").toBool()); + m_ui->closeAfterLaunchCheck->setChecked(settings->get("CloseAfterLaunch").toBool()); + m_ui->quitAfterGameStopCheck->setChecked(settings->get("QuitAfterGameStop").toBool()); + + if (m_javaSettings != nullptr) + m_javaSettings->loadSettings(); + + // Custom commands + m_ui->customCommands->initialize(m_instance != nullptr, m_instance == nullptr || settings->get("OverrideCommands").toBool(), + settings->get("PreLaunchCommand").toString(), settings->get("WrapperCommand").toString(), + settings->get("PostExitCommand").toString()); + + // Environment variables + m_ui->environmentVariables->initialize(m_instance != nullptr, m_instance == nullptr || settings->get("OverrideEnv").toBool(), + settings->get("Env").toMap()); + + // Legacy Tweaks + m_ui->legacySettingsGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideLegacySettings").toBool()); + m_ui->onlineFixes->setChecked(settings->get("OnlineFixes").toBool()); + + // Native Libraries + m_ui->nativeWorkaroundsGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideNativeWorkarounds").toBool()); + m_ui->useNativeGLFWCheck->setChecked(settings->get("UseNativeGLFW").toBool()); + m_ui->lineEditGLFWPath->setText(settings->get("CustomGLFWPath").toString()); +#ifdef Q_OS_LINUX + m_ui->lineEditGLFWPath->setPlaceholderText(APPLICATION->m_detectedGLFWPath); +#else + m_ui->lineEditGLFWPath->setPlaceholderText(tr("Path to %1 library file").arg(BuildConfig.GLFW_LIBRARY_NAME)); +#endif + m_ui->useNativeOpenALCheck->setChecked(settings->get("UseNativeOpenAL").toBool()); + m_ui->lineEditOpenALPath->setText(settings->get("CustomOpenALPath").toString()); +#ifdef Q_OS_LINUX + m_ui->lineEditOpenALPath->setPlaceholderText(APPLICATION->m_detectedOpenALPath); +#else + m_ui->lineEditOpenALPath->setPlaceholderText(tr("Path to %1 library file").arg(BuildConfig.OPENAL_LIBRARY_NAME)); +#endif + + // Performance + m_ui->perfomanceGroupBox->setChecked(m_instance == nullptr || settings->get("OverridePerformance").toBool()); + m_ui->enableFeralGamemodeCheck->setChecked(settings->get("EnableFeralGamemode").toBool()); + m_ui->enableMangoHud->setChecked(settings->get("EnableMangoHud").toBool()); + m_ui->useDiscreteGpuCheck->setChecked(settings->get("UseDiscreteGpu").toBool()); + m_ui->useZink->setChecked(settings->get("UseZink").toBool()); + + m_ui->serverJoinGroupBox->setChecked(settings->get("JoinServerOnLaunch").toBool()); + + if (m_instance != nullptr) { + if (auto server = settings->get("JoinServerOnLaunchAddress").toString(); !server.isEmpty()) { + m_ui->serverJoinAddress->setText(server); + m_ui->serverJoinAddressButton->setChecked(true); + m_ui->worldJoinButton->setChecked(false); + m_ui->serverJoinAddress->setEnabled(true); + m_ui->worldsCb->setEnabled(false); + } else if (auto world = settings->get("JoinWorldOnLaunch").toString(); !world.isEmpty() && m_quickPlaySingleplayer) { + m_ui->worldsCb->setCurrentText(world); + m_ui->serverJoinAddressButton->setChecked(false); + m_ui->worldJoinButton->setChecked(true); + m_ui->serverJoinAddress->setEnabled(false); + m_ui->worldsCb->setEnabled(true); + } else { + m_ui->serverJoinAddressButton->setChecked(true); + m_ui->worldJoinButton->setChecked(false); + m_ui->serverJoinAddress->setEnabled(true); + m_ui->worldsCb->setEnabled(false); + } + + m_ui->instanceAccountGroupBox->setChecked(settings->get("UseAccountForInstance").toBool()); + updateAccountsMenu(*settings); + } + + m_ui->legacySettingsGroupBox->setChecked(settings->get("OverrideLegacySettings").toBool()); + m_ui->onlineFixes->setChecked(settings->get("OnlineFixes").toBool()); +} + +void MinecraftSettingsWidget::saveSettings() +{ + SettingsObjectPtr settings; + + if (m_instance != nullptr) + settings = m_instance->settings(); + else + settings = APPLICATION->settings(); + + { + SettingsObject::Lock lock(settings); + + // Miscellaneous + bool miscellaneous = m_instance == nullptr || m_ui->miscellaneousSettingsBox->isChecked(); + + if (m_instance != nullptr) + settings->set("OverrideMiscellaneous", miscellaneous); + + if (miscellaneous) { + settings->set("CloseAfterLaunch", m_ui->closeAfterLaunchCheck->isChecked()); + settings->set("QuitAfterGameStop", m_ui->quitAfterGameStopCheck->isChecked()); + } else { + settings->reset("CloseAfterLaunch"); + settings->reset("QuitAfterGameStop"); + } + + // Console + bool console = m_instance == nullptr || m_ui->consoleSettingsBox->isChecked(); + + if (m_instance != nullptr) + settings->set("OverrideConsole", console); + + if (console) { + settings->set("ShowConsole", m_ui->showConsoleCheck->isChecked()); + settings->set("AutoCloseConsole", m_ui->autoCloseConsoleCheck->isChecked()); + settings->set("ShowConsoleOnError", m_ui->showConsoleErrorCheck->isChecked()); + } else { + settings->reset("ShowConsole"); + settings->reset("AutoCloseConsole"); + settings->reset("ShowConsoleOnError"); + } + + // Window Size + bool window = m_instance == nullptr || m_ui->windowSizeGroupBox->isChecked(); + + if (m_instance != nullptr) + settings->set("OverrideWindow", window); + + if (window) { + settings->set("LaunchMaximized", m_ui->maximizedCheckBox->isChecked()); + settings->set("MinecraftWinWidth", m_ui->windowWidthSpinBox->value()); + settings->set("MinecraftWinHeight", m_ui->windowHeightSpinBox->value()); + } else { + settings->reset("LaunchMaximized"); + settings->reset("MinecraftWinWidth"); + settings->reset("MinecraftWinHeight"); + } + + // Custom Commands + bool custcmd = m_instance == nullptr || m_ui->customCommands->checked(); + + if (m_instance != nullptr) + settings->set("OverrideCommands", custcmd); + + if (custcmd) { + settings->set("PreLaunchCommand", m_ui->customCommands->prelaunchCommand()); + settings->set("WrapperCommand", m_ui->customCommands->wrapperCommand()); + settings->set("PostExitCommand", m_ui->customCommands->postexitCommand()); + } else { + settings->reset("PreLaunchCommand"); + settings->reset("WrapperCommand"); + settings->reset("PostExitCommand"); + } + + // Environment Variables + auto env = m_instance == nullptr || m_ui->environmentVariables->override(); + + if (m_instance != nullptr) + settings->set("OverrideEnv", env); + + if (env) + settings->set("Env", m_ui->environmentVariables->value()); + else + settings->reset("Env"); + + // Workarounds + bool workarounds = m_instance == nullptr || m_ui->nativeWorkaroundsGroupBox->isChecked(); + + if (m_instance != nullptr) + settings->set("OverrideNativeWorkarounds", workarounds); + + if (workarounds) { + settings->set("UseNativeGLFW", m_ui->useNativeGLFWCheck->isChecked()); + settings->set("CustomGLFWPath", m_ui->lineEditGLFWPath->text()); + settings->set("UseNativeOpenAL", m_ui->useNativeOpenALCheck->isChecked()); + settings->set("CustomOpenALPath", m_ui->lineEditOpenALPath->text()); + } else { + settings->reset("UseNativeGLFW"); + settings->reset("CustomGLFWPath"); + settings->reset("UseNativeOpenAL"); + settings->reset("CustomOpenALPath"); + } + + // Performance + bool performance = m_instance == nullptr || m_ui->perfomanceGroupBox->isChecked(); + + if (m_instance != nullptr) + settings->set("OverridePerformance", performance); + + if (performance) { + settings->set("EnableFeralGamemode", m_ui->enableFeralGamemodeCheck->isChecked()); + settings->set("EnableMangoHud", m_ui->enableMangoHud->isChecked()); + settings->set("UseDiscreteGpu", m_ui->useDiscreteGpuCheck->isChecked()); + settings->set("UseZink", m_ui->useZink->isChecked()); + } else { + settings->reset("EnableFeralGamemode"); + settings->reset("EnableMangoHud"); + settings->reset("UseDiscreteGpu"); + settings->reset("UseZink"); + } + + // Game time + bool gameTime = m_instance == nullptr || m_ui->gameTimeGroupBox->isChecked(); + + if (m_instance != nullptr) + settings->set("OverrideGameTime", gameTime); + + if (gameTime) { + settings->set("ShowGameTime", m_ui->showGameTime->isChecked()); + settings->set("RecordGameTime", m_ui->recordGameTime->isChecked()); + } else { + settings->reset("ShowGameTime"); + settings->reset("RecordGameTime"); + } + + if (m_instance == nullptr) { + settings->set("ShowGlobalGameTime", m_ui->showGlobalGameTime->isChecked()); + settings->set("ShowGameTimeWithoutDays", m_ui->showGameTimeWithoutDays->isChecked()); + } + + if (m_instance != nullptr) { + // Join server on launch + bool joinServerOnLaunch = m_ui->serverJoinGroupBox->isChecked(); + settings->set("JoinServerOnLaunch", joinServerOnLaunch); + if (joinServerOnLaunch) { + if (m_ui->serverJoinAddressButton->isChecked() || !m_quickPlaySingleplayer) { + settings->set("JoinServerOnLaunchAddress", m_ui->serverJoinAddress->text()); + settings->reset("JoinWorldOnLaunch"); + } else { + settings->set("JoinWorldOnLaunch", m_ui->worldsCb->currentText()); + settings->reset("JoinServerOnLaunchAddress"); + } + } else { + settings->reset("JoinServerOnLaunchAddress"); + settings->reset("JoinWorldOnLaunch"); + } + + // Use an account for this instance + bool useAccountForInstance = m_ui->instanceAccountGroupBox->isChecked(); + settings->set("UseAccountForInstance", useAccountForInstance); + if (useAccountForInstance) { + int accountIndex = m_ui->instanceAccountSelector->currentIndex(); + + if (accountIndex != -1) { + const MinecraftAccountPtr account = APPLICATION->accounts()->at(accountIndex); + if (account != nullptr) + settings->set("InstanceAccountId", account->profileId()); + } + } else { + settings->reset("InstanceAccountId"); + } + } + + bool overrideLegacySettings = m_instance == nullptr || m_ui->legacySettingsGroupBox->isChecked(); + + if (m_instance != nullptr) + settings->set("OverrideLegacySettings", overrideLegacySettings); + + if (overrideLegacySettings) { + settings->set("OnlineFixes", m_ui->onlineFixes->isChecked()); + } else { + settings->reset("OnlineFixes"); + } + } + + if (m_javaSettings != nullptr) + m_javaSettings->saveSettings(); +} + +void MinecraftSettingsWidget::openGlobalSettings() +{ + const QString id = m_ui->settingsTabs->currentWidget()->objectName(); + + qDebug() << id; + + if (id == "javaPage") + APPLICATION->ShowGlobalSettings(this, "java-settings"); + else // TODO select tab + APPLICATION->ShowGlobalSettings(this, "minecraft-settings"); +} + +void MinecraftSettingsWidget::updateAccountsMenu(const SettingsObject& settings) +{ + m_ui->instanceAccountSelector->clear(); + auto accounts = APPLICATION->accounts(); + int accountIndex = accounts->findAccountByProfileId(settings.get("InstanceAccountId").toString()); + + for (int i = 0; i < accounts->count(); i++) { + MinecraftAccountPtr account = accounts->at(i); + + QIcon face = account->getFace(); + + if (face.isNull()) + face = APPLICATION->getThemedIcon("noaccount"); + + m_ui->instanceAccountSelector->addItem(face, account->profileName(), i); + if (i == accountIndex) + m_ui->instanceAccountSelector->setCurrentIndex(i); + } +} + +bool MinecraftSettingsWidget::isQuickPlaySupported() +{ + return m_instance->traits().contains("feature:is_quick_play_singleplayer"); +} diff --git a/launcher/ui/pages/global/CustomCommandsPage.h b/launcher/ui/widgets/MinecraftSettingsWidget.h similarity index 69% rename from launcher/ui/pages/global/CustomCommandsPage.h rename to launcher/ui/widgets/MinecraftSettingsWidget.h index ec1204ffe..86effb337 100644 --- a/launcher/ui/pages/global/CustomCommandsPage.h +++ b/launcher/ui/widgets/MinecraftSettingsWidget.h @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield + * 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 @@ -35,29 +36,29 @@ #pragma once -#include -#include +#include +#include "JavaSettingsWidget.h" +#include "minecraft/MinecraftInstance.h" -#include -#include "ui/pages/BasePage.h" -#include "ui/widgets/CustomCommands.h" - -class CustomCommandsPage : public QWidget, public BasePage { - Q_OBJECT +namespace Ui { +class MinecraftSettingsWidget; +} +class MinecraftSettingsWidget : public QWidget { public: - explicit CustomCommandsPage(QWidget* parent = 0); - ~CustomCommandsPage(); + MinecraftSettingsWidget(MinecraftInstancePtr instance, QWidget* parent = nullptr); + ~MinecraftSettingsWidget() override; - QString displayName() const override { return tr("Custom Commands"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("custom-commands"); } - QString id() const override { return "custom-commands"; } - QString helpPage() const override { return "Custom-commands"; } - bool apply() override; - void retranslate() override; + void loadSettings(); + void saveSettings(); private: - void applySettings(); - void loadSettings(); - CustomCommands* commands; + void openGlobalSettings(); + void updateAccountsMenu(const SettingsObject& settings); + bool isQuickPlaySupported(); + + MinecraftInstancePtr m_instance; + Ui::MinecraftSettingsWidget* m_ui; + JavaSettingsWidget* m_javaSettings = nullptr; + bool m_quickPlaySingleplayer = false; }; diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.ui b/launcher/ui/widgets/MinecraftSettingsWidget.ui new file mode 100644 index 000000000..daa065ac8 --- /dev/null +++ b/launcher/ui/widgets/MinecraftSettingsWidget.ui @@ -0,0 +1,686 @@ + + + MinecraftSettingsWidget + + + + 0 + 0 + 648 + 400 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Open &Global Settings + + + The settings here are overrides for global settings. + + + + + + + 0 + + + + General + + + + + + + 0 + 0 + + + + Qt::ScrollBarAlwaysOff + + + true + + + + + 0 + -253 + 610 + 550 + + + + + + + true + + + Game &Window + + + false + + + false + + + + + + Start Minecraft maximized + + + + + + + The base game only supports resolution. In order to simulate the maximized behaviour the current implementation approximates the maximum display size. + + + <html><head/><body><p><span style=" font-weight:600; color:#f5c211;">Warning</span><span style=" color:#f5c211;">: The maximized option may not be fully supported on all Minecraft versions.</span></p></body></html> + + + + + + + + + Window height: + + + + + + + Window width: + + + + + + + 1 + + + 65536 + + + 1 + + + 854 + + + + + + + 1 + + + 65536 + + + 480 + + + + + + + + + + + + true + + + Game &Time + + + false + + + false + + + + + + Show time spent &playing instances + + + + + + + &Record time spent playing instances + + + + + + + Show the &total time played across instances + + + + + + + Always show durations in &hours + + + + + + + + + + true + + + &Console + + + false + + + false + + + + + + Show console while the game is running + + + + + + + Automatically close console when the game quits + + + + + + + Show console when the game crashes + + + + + + + + + + &Miscellaneous + + + false + + + false + + + + + + Close the launcher after game window opens + + + + + + + Quit the launcher after game window closes + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + Java + + + + + + true + + + + + 0 + 0 + 624 + 297 + + + + + + + + + + Tweaks + + + + + + Qt::ScrollBarAlwaysOff + + + true + + + + + 0 + -101 + 610 + 398 + + + + + + + &Legacy Tweaks + + + false + + + false + + + + + + <html><head/><body><p>Emulates usages of old online services which are no longer operating.</p><p>Current fixes include: skin and online mode support.</p></body></html> + + + Enable online fixes (experimental) + + + + + + + + + + true + + + &Native Libraries + + + false + + + false + + + + + + Use system installation of OpenAL + + + + + + + &GLFW library path + + + lineEditGLFWPath + + + + + + + Use system installation of GLFW + + + + + + + false + + + + + + + &OpenAL library path + + + lineEditOpenALPath + + + + + + + false + + + + + + + + + + true + + + &Performance + + + false + + + false + + + + + + <html><head/><body><p>Enable Feral Interactive's GameMode, to potentially improve gaming performance.</p></body></html> + + + Enable Feral GameMode + + + + + + + <html><head/><body><p>Enable MangoHud's advanced performance overlay.</p></body></html> + + + Enable MangoHud + + + + + + + <html><head/><body><p>Use the discrete GPU instead of the primary GPU.</p></body></html> + + + Use discrete GPU + + + + + + + Use Zink, a Mesa OpenGL driver that implements OpenGL on top of Vulkan. Performance may vary depending on the situation. Note: If no suitable Vulkan driver is found, software rendering will be used. + + + Use Zink + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + Launch + + + + + + true + + + + + 0 + 0 + 624 + 297 + + + + + + + Override default &account + + + true + + + false + + + + + + + + + 0 + 0 + + + + Account: + + + + + + + + + + + + + + + Set a &target to join on launch + + + true + + + false + + + + + + Server address: + + + + + + + + + + Singleplayer world + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + Custom Commands + + + + + + + + + + Environment Variables + + + + + + + + + + + + + + CustomCommands + QWidget +
    ui/widgets/CustomCommands.h
    + 1 +
    + + EnvironmentVariables + QWidget +
    ui/widgets/EnvironmentVariables.h
    + 1 +
    +
    + + openGlobalSettingsButton + settingsTabs + scrollArea + maximizedCheckBox + windowWidthSpinBox + windowHeightSpinBox + showGameTime + recordGameTime + showGlobalGameTime + showGameTimeWithoutDays + showConsoleCheck + autoCloseConsoleCheck + showConsoleErrorCheck + closeAfterLaunchCheck + quitAfterGameStopCheck + javaScrollArea + scrollArea_2 + onlineFixes + useNativeGLFWCheck + lineEditGLFWPath + useNativeOpenALCheck + lineEditOpenALPath + perfomanceGroupBox + enableFeralGamemodeCheck + enableMangoHud + useDiscreteGpuCheck + useZink + scrollArea_3 + instanceAccountGroupBox + instanceAccountSelector + serverJoinGroupBox + serverJoinAddressButton + serverJoinAddress + worldJoinButton + worldsCb + + + +
    diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp index c2c099eeb..37211693f 100644 --- a/launcher/ui/widgets/ModFilterWidget.cpp +++ b/launcher/ui/widgets/ModFilterWidget.cpp @@ -1,13 +1,192 @@ +// 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); + if (role == Qt::UserRole) + return QIdentityProxyModel::data(index, BaseVersionList::VersionIdRole); + return {}; + } +}; + +class AllVersionProxyModel : public QSortFilterProxyModel { + Q_OBJECT + + public: + AllVersionProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {} + + int rowCount(const QModelIndex& parent = QModelIndex()) const override { return QSortFilterProxyModel::rowCount(parent) + 1; } + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override + { + if (!index.isValid()) { + return {}; + } + + if (index.row() == 0) { + if (role == Qt::DisplayRole) { + return tr("All Versions"); + } + if (role == Qt::UserRole) { + return "all"; + } + return {}; + } + + QModelIndex newIndex = QSortFilterProxyModel::index(index.row() - 1, index.column()); + return QSortFilterProxyModel::data(newIndex, role); + } + + Qt::ItemFlags flags(const QModelIndex& index) const override + { + if (index.row() == 0) { + return Qt::ItemIsSelectable | Qt::ItemIsEnabled; + } + return QSortFilterProxyModel::flags(index); + } +}; + +ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended, QWidget* parent) + : QTabWidget(parent), ui(new Ui::ModFilterWidget), m_instance(instance), m_filter(new Filter()) +{ + ui->setupUi(this); + + m_versions_proxy = new VersionProxyModel(this); + m_versions_proxy->setFilter(BaseVersionList::TypeRole, new ExactFilter("release")); + + QAbstractProxyModel* proxy = new VersionBasicModel(this); + proxy->setSourceModel(m_versions_proxy); + + if (extended) { + if (!m_instance) { + ui->environmentGroup->hide(); + } + ui->versions->setSourceModel(proxy); + ui->versions->setSeparator(", "); + ui->versions->setDefaultText(tr("All Versions")); + ui->version->hide(); + } else { + auto allVersions = new AllVersionProxyModel(this); + allVersions->setSourceModel(proxy); + proxy = allVersions; + ui->version->setModel(proxy); + ui->versions->hide(); + ui->showAllVersions->hide(); + ui->environmentGroup->hide(); + ui->openSource->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); + connect(ui->openSource, &QCheckBox::stateChanged, this, &ModFilterWidget::onOpenSourceFilterChanged); + + connect(ui->releaseCb, &QCheckBox::stateChanged, this, &ModFilterWidget::onReleaseFilterChanged); + connect(ui->betaCb, &QCheckBox::stateChanged, this, &ModFilterWidget::onReleaseFilterChanged); + connect(ui->alphaCb, &QCheckBox::stateChanged, this, &ModFilterWidget::onReleaseFilterChanged); + connect(ui->unknownCb, &QCheckBox::stateChanged, this, &ModFilterWidget::onReleaseFilterChanged); + + 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 +195,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 +210,166 @@ 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); -} - -void ModFilterWidget::setInstance(MinecraftInstance* instance) -{ - 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])); + m_filter->openSource = false; + if (m_instance) { + m_filter->hideInstalled = false; + m_filter->side = ""; // or "both" + auto loaders = m_instance->getPackProfile()->getSupportedModLoaders().value(); + ui->neoForge->setChecked(loaders & ModPlatform::NeoForge); + ui->forge->setChecked(loaders & ModPlatform::Forge); + ui->fabric->setChecked(loaders & ModPlatform::Fabric); + ui->quilt->setChecked(loaders & ModPlatform::Quilt); + m_filter->loaders = loaders; + auto def = m_instance->getPackProfile()->getComponentVersion("net.minecraft"); + m_filter->versions.emplace_front(def); + ui->versions->setCheckedItems({ def }); + ui->version->setCurrentIndex(ui->version->findText(def)); } else { - ui->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)); + ui->hideInstalled->hide(); } } -void ModFilterWidget::onVersionFilterChanged(int id) +void ModFilterWidget::onShowAllVersionsChanged() { - // 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()) { + side = "client"; + } else if (!ui->clientSide->isChecked() && ui->serverSide->isChecked()) { + side = "server"; + } else if (ui->clientSide->isChecked() && ui->serverSide->isChecked()) { + side = "both"; + } else { + 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(); + if (ui->version->currentData(Qt::UserRole) != "all") { + 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(); + }); + } +} + +void ModFilterWidget::onOpenSourceFilterChanged() +{ + auto open = ui->openSource->isChecked(); + m_filter_changed = open != m_filter->openSource; + m_filter->openSource = open; + if (m_filter_changed) + emit filterChanged(); +} + +void ModFilterWidget::onReleaseFilterChanged() +{ + std::list releases; + if (ui->releaseCb->isChecked()) + releases.push_back(ModPlatform::IndexedVersionType(ModPlatform::IndexedVersionType::VersionType::Release)); + if (ui->betaCb->isChecked()) + releases.push_back(ModPlatform::IndexedVersionType(ModPlatform::IndexedVersionType::VersionType::Beta)); + if (ui->alphaCb->isChecked()) + releases.push_back(ModPlatform::IndexedVersionType(ModPlatform::IndexedVersionType::VersionType::Alpha)); + if (ui->unknownCb->isChecked()) + releases.push_back(ModPlatform::IndexedVersionType(ModPlatform::IndexedVersionType::VersionType::Unknown)); + m_filter_changed = releases != m_filter->releases; + m_filter->releases = releases; + if (m_filter_changed) + emit filterChanged(); +} + +#include "ModFilterWidget.moc" diff --git a/launcher/ui/widgets/ModFilterWidget.h b/launcher/ui/widgets/ModFilterWidget.h index ed6cd0ea7..41a2f1bbd 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,69 @@ 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 openSource; - 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 && openSource == other.openSource; + } bool operator!=(const Filter& other) const { return !(*this == other); } + + bool checkMcVersions(QStringList value) + { + for (auto mcVersion : versions) + if (value.contains(mcVersion.toString())) + return true; + + return versions.empty(); + } }; - 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(); + void onOpenSourceFilterChanged(); + void onReleaseFilterChanged(); 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..807a0019a 100644 --- a/launcher/ui/widgets/ModFilterWidget.ui +++ b/launcher/ui/widgets/ModFilterWidget.ui @@ -1,54 +1,263 @@ 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 + 294 + 781 + + + + + + + Categories + + + false + + + false + + + + + + + Loaders + + + false + + + false + + + + + + NeoForge + + + + + + + Forge + + + + + + + Fabric + + + + + + + Quilt + + + + + + + + + + Versions + + + false + + + false + + + + + + Show all versions + + + + + + + + + + + + + + + + Environments + + + false + + + false + + + + + + Client + + + + + + + Server + + + + + + + + + + Hide installed items + + + + + + + Open source only + + + + + + + Release type + + + + + + Release + + + + + + + Beta + + + + + + + Alpha + + + + + + + Unknown + + + + + + + + + + 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/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp index 60b92b28b..6946df41f 100644 --- a/launcher/ui/widgets/ProjectItem.cpp +++ b/launcher/ui/widgets/ProjectItem.cpp @@ -88,7 +88,7 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o } { // Description painting - auto description = index.data(UserDataTypes::DESCRIPTION).toString(); + auto description = index.data(UserDataTypes::DESCRIPTION).toString().simplified(); QTextLayout text_layout(description, opt.font); diff --git a/launcher/ui/widgets/SubTaskProgressBar.ui b/launcher/ui/widgets/SubTaskProgressBar.ui index 5431eab60..aabb68329 100644 --- a/launcher/ui/widgets/SubTaskProgressBar.ui +++ b/launcher/ui/widgets/SubTaskProgressBar.ui @@ -47,6 +47,9 @@ true + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse +
    @@ -68,6 +71,9 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse +
    diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.cpp b/launcher/ui/widgets/ThemeCustomizationWidget.cpp index 0de97441f..097678b8d 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, @@ -34,11 +35,13 @@ ThemeCustomizationWidget::ThemeCustomizationWidget(QWidget* parent) : QWidget(pa connect(ui->backgroundCatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyCatTheme); connect(ui->iconsFolder, &QPushButton::clicked, this, - [] { DesktopServices::openDirectory(APPLICATION->themeManager()->getIconThemesFolder().path()); }); + [] { DesktopServices::openPath(APPLICATION->themeManager()->getIconThemesFolder().path()); }); connect(ui->widgetStyleFolder, &QPushButton::clicked, this, - [] { DesktopServices::openDirectory(APPLICATION->themeManager()->getApplicationThemesFolder().path()); }); + [] { DesktopServices::openPath(APPLICATION->themeManager()->getApplicationThemesFolder().path()); }); connect(ui->catPackFolder, &QPushButton::clicked, this, - [] { DesktopServices::openDirectory(APPLICATION->themeManager()->getCatPacksFolder().path()); }); + [] { DesktopServices::openPath(APPLICATION->themeManager()->getCatPacksFolder().path()); }); + + connect(ui->refreshButton, &QPushButton::clicked, this, &ThemeCustomizationWidget::refresh); } ThemeCustomizationWidget::~ThemeCustomizationWidget() @@ -84,7 +87,7 @@ void ThemeCustomizationWidget::applyIconTheme(int index) { auto settings = APPLICATION->settings(); auto originalIconTheme = settings->get("IconTheme").toString(); - auto newIconTheme = ui->iconsComboBox->currentData().toString(); + auto newIconTheme = ui->iconsComboBox->itemData(index).toString(); if (originalIconTheme != newIconTheme) { settings->set("IconTheme", newIconTheme); APPLICATION->themeManager()->applyCurrentlySelectedTheme(); @@ -97,7 +100,7 @@ void ThemeCustomizationWidget::applyWidgetTheme(int index) { auto settings = APPLICATION->settings(); auto originalAppTheme = settings->get("ApplicationTheme").toString(); - auto newAppTheme = ui->widgetStyleComboBox->currentData().toString(); + auto newAppTheme = ui->widgetStyleComboBox->itemData(index).toString(); if (originalAppTheme != newAppTheme) { settings->set("ApplicationTheme", newAppTheme); APPLICATION->themeManager()->applyCurrentlySelectedTheme(); @@ -110,7 +113,7 @@ void ThemeCustomizationWidget::applyCatTheme(int index) { auto settings = APPLICATION->settings(); auto originalCat = settings->get("BackgroundCat").toString(); - auto newCat = ui->backgroundCatComboBox->currentData().toString(); + auto newCat = ui->backgroundCatComboBox->itemData(index).toString(); if (originalCat != newCat) { settings->set("BackgroundCat", newCat); } @@ -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 f655fc38d..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,30 +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; - connect(job, &NetJob::succeeded, this, [this, doc, full_entry_path, source_url, posInDocument] { - qDebug() << "Loaded resource at" << full_entry_path; - - // If we flushed, don't proceed. - if (!m_fetching_images.contains(source_url)) - return; - - QImage image(full_entry_path); + 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). @@ -121,6 +158,23 @@ void VariableSizedImageObject::loadImage(QTextDocument* doc, const QUrl& source, doc->setPageSize(size); m_fetching_images.remove(source_url); + }; + connect(job, &NetJob::succeeded, this, [this, full_entry_path, source_url, loadImage] { + qDebug() << "Loaded resource at:" << full_entry_path; + // If we flushed, don't proceed. + if (!m_fetching_images.contains(source_url)) + return; + + QImage image(full_entry_path); + loadImage(image); + }); + connect(job, &NetJob::failed, this, [this, full_entry_path, source_url, loadImage](QString reason) { + qWarning() << "Failed resource at:" << full_entry_path << " because:" << reason; + // If we flushed, don't proceed. + if (!m_fetching_images.contains(source_url)) + return; + + loadImage(QImage()); }); connect(job, &NetJob::finished, job, &NetJob::deleteLater); 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 1c5aaf1e8..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))); @@ -436,8 +449,8 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar } { // log debug program info - qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME) << "Updater" - << ", (c) 2022-2023 " << qPrintable(QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", ")); + qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + " Updater, " + + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", ")); qDebug() << "Version : " << BuildConfig.printableVersionString(); qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT; qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC; @@ -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..eab3e6bbb 100644 --- a/launcher/updater/prismupdater/UpdaterDialogs.cpp +++ b/launcher/updater/prismupdater/UpdaterDialogs.cpp @@ -24,8 +24,10 @@ #include "ui_SelectReleaseDialog.h" +#include #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) @@ -54,6 +56,9 @@ SelectReleaseDialog::SelectReleaseDialog(const Version& current_version, const Q connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &SelectReleaseDialog::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &SelectReleaseDialog::reject); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } SelectReleaseDialog::~SelectReleaseDialog() @@ -96,7 +101,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/cmark b/libraries/cmark index 5ba25ff40..3460cd809 160000 --- a/libraries/cmark +++ b/libraries/cmark @@ -1 +1 @@ -Subproject commit 5ba25ff40eba44c811f79ab6a792baf945b8307c +Subproject commit 3460cd809b6dd311b58e92733ece2fc956224fd2 diff --git a/libraries/extra-cmake-modules b/libraries/extra-cmake-modules index bbcbaff78..a3d9394ab 160000 --- a/libraries/extra-cmake-modules +++ b/libraries/extra-cmake-modules @@ -1 +1 @@ -Subproject commit bbcbaff78283270c2beee69afd8d5b91da854af8 +Subproject commit a3d9394aba4b35789293378e04fb7473d65edf97 diff --git a/libraries/filesystem b/libraries/filesystem index 8a2edd6d9..076592ce6 160000 --- a/libraries/filesystem +++ b/libraries/filesystem @@ -1 +1 @@ -Subproject commit 8a2edd6d92ed820521d42c94d179462bf06b5ed3 +Subproject commit 076592ce6e64568521b88a11881aa36b3d3f7048 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/CMakeLists.txt b/libraries/launcher/CMakeLists.txt index 7bf160760..4cd1ba58b 100644 --- a/libraries/launcher/CMakeLists.txt +++ b/libraries/launcher/CMakeLists.txt @@ -25,13 +25,14 @@ set(LEGACY_SRC legacy/org/prismlauncher/legacy/LegacyLauncher.java legacy/org/prismlauncher/legacy/fix/online/Handler.java legacy/org/prismlauncher/legacy/fix/online/OnlineFixes.java + legacy/org/prismlauncher/legacy/fix/online/OnlineModeFix.java legacy/org/prismlauncher/legacy/fix/online/SkinFix.java legacy/org/prismlauncher/legacy/utils/Base64.java legacy/org/prismlauncher/legacy/utils/api/MojangApi.java legacy/org/prismlauncher/legacy/utils/api/Texture.java legacy/org/prismlauncher/legacy/utils/json/JsonParseException.java legacy/org/prismlauncher/legacy/utils/json/JsonParser.java - legacy/org/prismlauncher/legacy/utils/url/CustomUrlConnection.java + legacy/org/prismlauncher/legacy/utils/url/ByteArrayUrlConnection.java legacy/org/prismlauncher/legacy/utils/url/UrlUtils.java legacy/net/minecraft/Launcher.java legacy/org/prismlauncher/legacy/LegacyProxy.java diff --git a/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/Handler.java b/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/Handler.java index f85a8bc79..5ef3d7ac2 100644 --- a/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/Handler.java +++ b/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/Handler.java @@ -53,11 +53,16 @@ final class Handler extends URLStreamHandler { protected URLConnection openConnection(URL address, Proxy proxy) throws IOException { URLConnection result; - // try skin fix + // try various fixes... result = SkinFix.openConnection(address, proxy); if (result != null) return result; + result = OnlineModeFix.openConnection(address, proxy); + if (result != null) + return result; + + // ...then give up and make the request directly return UrlUtils.openConnection(address, proxy); } } diff --git a/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/OnlineFixes.java b/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/OnlineFixes.java index 88facff69..9ba57ff71 100644 --- a/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/OnlineFixes.java +++ b/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/OnlineFixes.java @@ -46,6 +46,8 @@ import java.net.URLStreamHandlerFactory; /** * Fixes skins by redirecting to other URLs. + * Thanks to MineOnline for the implementation from which this was inspired! + * See https://github.com/ahnewark/MineOnline/tree/main/src/main/java/gg/codie/mineonline/protocol. * * @see {@link Handler} * @see {@link UrlUtils} diff --git a/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/OnlineModeFix.java b/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/OnlineModeFix.java new file mode 100644 index 000000000..1bab76d53 --- /dev/null +++ b/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/OnlineModeFix.java @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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. + * + * Linking this library statically or dynamically with other modules is + * making a combined work based on this library. Thus, the terms and + * conditions of the GNU General Public License cover the whole + * combination. + * + * As a special exception, the copyright holders of this library give + * you permission to link this library with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also meet, + * for each linked independent module, the terms and conditions of the + * license of that module. An independent module is a module which is + * not derived from or based on this library. If you modify this + * library, you may extend this exception to your version of the + * library, but you are not obliged to do so. If you do not wish to do + * so, delete this exception statement from your version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.prismlauncher.legacy.fix.online; + +import org.prismlauncher.legacy.utils.url.UrlUtils; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; + +public final class OnlineModeFix { + public static URLConnection openConnection(URL address, Proxy proxy) throws IOException { + // we start with "http://www.minecraft.net/game/joinserver.jsp?user=..." + if (!(address.getHost().equals("www.minecraft.net") && address.getPath().equals("/game/joinserver.jsp"))) + return null; + + // change it to "https://session.minecraft.net/game/joinserver.jsp?user=..." + // this seems to be the modern version of the same endpoint... + // maybe Mojang planned to patch old versions of the game to use it + // if it ever disappears this should be changed to use sessionserver.mojang.com/session/minecraft/join + // which of course has a different usage requiring JSON serialisation... + URL url; + try { + url = new URL("https", "session.minecraft.net", address.getPort(), address.getFile()); + } catch (MalformedURLException e) { + throw new AssertionError("url should be valid", e); + } + + return UrlUtils.openConnection(url, proxy); + } +} diff --git a/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/SkinFix.java b/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/SkinFix.java index e734bdbc7..d5b185450 100644 --- a/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/SkinFix.java +++ b/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/SkinFix.java @@ -54,7 +54,7 @@ package org.prismlauncher.legacy.fix.online; import org.prismlauncher.legacy.utils.api.MojangApi; import org.prismlauncher.legacy.utils.api.Texture; -import org.prismlauncher.legacy.utils.url.CustomUrlConnection; +import org.prismlauncher.legacy.utils.url.ByteArrayUrlConnection; import org.prismlauncher.legacy.utils.url.UrlUtils; import java.awt.AlphaComposite; @@ -97,9 +97,9 @@ final class SkinFix { URLConnection connection = UrlUtils.openConnection(texture.getUrl(), proxy); try (InputStream in = connection.getInputStream()) { - // thank you craftycodie! + // thank you ahnewark! // this is heavily based on - // https://github.com/craftycodie/MineOnline/blob/4f4f86f9d051e0a6fd7ff0b95b2a05f7437683d7/src/main/java/gg/codie/mineonline/gui/textures/TextureHelper.java#L17 + // https://github.com/ahnewark/MineOnline/blob/4f4f86f9d051e0a6fd7ff0b95b2a05f7437683d7/src/main/java/gg/codie/mineonline/gui/textures/TextureHelper.java#L17 BufferedImage image = ImageIO.read(in); Graphics2D graphics = image.createGraphics(); graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER)); @@ -131,7 +131,7 @@ final class SkinFix { image = image.getSubimage(0, 0, 64, 32); ImageIO.write(image, "png", out); - return new CustomUrlConnection(out.toByteArray()); + return new ByteArrayUrlConnection(out.toByteArray()); } } diff --git a/libraries/launcher/legacy/org/prismlauncher/legacy/utils/url/CustomUrlConnection.java b/libraries/launcher/legacy/org/prismlauncher/legacy/utils/url/ByteArrayUrlConnection.java similarity index 86% rename from libraries/launcher/legacy/org/prismlauncher/legacy/utils/url/CustomUrlConnection.java rename to libraries/launcher/legacy/org/prismlauncher/legacy/utils/url/ByteArrayUrlConnection.java index 71b0e68f2..bc9cf2cc2 100644 --- a/libraries/launcher/legacy/org/prismlauncher/legacy/utils/url/CustomUrlConnection.java +++ b/libraries/launcher/legacy/org/prismlauncher/legacy/utils/url/ByteArrayUrlConnection.java @@ -40,16 +40,12 @@ import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; -public final class CustomUrlConnection extends HttpURLConnection { +public final class ByteArrayUrlConnection extends HttpURLConnection { private final InputStream in; - public CustomUrlConnection(byte[] data) { - this(new ByteArrayInputStream(data)); - } - - public CustomUrlConnection(InputStream in) { + public ByteArrayUrlConnection(byte[] data) { super(null); - this.in = in; + this.in = new ByteArrayInputStream(data); } @Override @@ -58,12 +54,7 @@ public final class CustomUrlConnection extends HttpURLConnection { } @Override - public void disconnect() { - try { - in.close(); - } catch (IOException e) { - } - } + public void disconnect() {} @Override public InputStream getInputStream() throws IOException { diff --git a/libraries/launcher/legacy/org/prismlauncher/legacy/utils/url/UrlUtils.java b/libraries/launcher/legacy/org/prismlauncher/legacy/utils/url/UrlUtils.java index b0072485e..ae91b683c 100644 --- a/libraries/launcher/legacy/org/prismlauncher/legacy/utils/url/UrlUtils.java +++ b/libraries/launcher/legacy/org/prismlauncher/legacy/utils/url/UrlUtils.java @@ -101,7 +101,7 @@ public final class UrlUtils { } catch (IOException | Error | RuntimeException e) { throw e; // rethrow if possible } catch (Throwable e) { - throw new AssertionError(e); // oh dear! this isn't meant to happen + throw new AssertionError("openConnection should not throw", e); // oh dear! this isn't meant to happen } } } diff --git a/libraries/launcher/org/prismlauncher/EntryPoint.java b/libraries/launcher/org/prismlauncher/EntryPoint.java index 699adfe30..8b9046f0c 100644 --- a/libraries/launcher/org/prismlauncher/EntryPoint.java +++ b/libraries/launcher/org/prismlauncher/EntryPoint.java @@ -81,10 +81,9 @@ public final class EntryPoint { PreLaunchAction action = PreLaunchAction.PROCEED; try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8))) { - String line; - while (action == PreLaunchAction.PROCEED) { - if ((line = reader.readLine()) != null) + String line = reader.readLine(); + if (line != null) action = parseLine(line, params); else action = PreLaunchAction.ABORT; @@ -105,7 +104,7 @@ public final class EntryPoint { return ExitCode.ABORT; } - setProperties(params); + SystemProperties.apply(params); String launcherType = params.getString("launcher"); @@ -141,43 +140,10 @@ public final class EntryPoint { } } - private static void setProperties(Parameters params) { - String launcherBrand = params.getString("launcherBrand", null); - String launcherVersion = params.getString("launcherVersion", null); - String name = params.getString("instanceName", null); - String iconId = params.getString("instanceIconKey", null); - String iconPath = params.getString("instanceIconPath", null); - String windowTitle = params.getString("windowTitle", null); - String windowDimensions = params.getString("windowParams", null); - - if (launcherBrand != null) - System.setProperty("minecraft.launcher.brand", launcherBrand); - if (launcherVersion != null) - System.setProperty("minecraft.launcher.version", launcherVersion); - - // set useful properties for mods - if (name != null) - System.setProperty("org.prismlauncher.instance.name", name); - if (iconId != null) - System.setProperty("org.prismlauncher.instance.icon.id", iconId); - if (iconPath != null) - System.setProperty("org.prismlauncher.instance.icon.path", iconPath); - if (windowTitle != null) - System.setProperty("org.prismlauncher.window.title", windowTitle); - if (windowDimensions != null) - System.setProperty("org.prismlauncher.window.dimensions", windowDimensions); - - // set multimc properties for compatibility - if (name != null) - System.setProperty("multimc.instance.title", name); - if (iconId != null) - System.setProperty("multimc.instance.icon", iconId); - } - private static PreLaunchAction parseLine(String input, Parameters params) throws ParseException { switch (input) { case "": - break; + return PreLaunchAction.PROCEED; case "launch": return PreLaunchAction.LAUNCH; @@ -192,9 +158,9 @@ public final class EntryPoint { throw new ParseException(input, "[key] [value]"); params.add(pair[0], pair[1]); - } - return PreLaunchAction.PROCEED; + return PreLaunchAction.PROCEED; + } } private enum PreLaunchAction { PROCEED, LAUNCH, ABORT } diff --git a/libraries/launcher/org/prismlauncher/SystemProperties.java b/libraries/launcher/org/prismlauncher/SystemProperties.java new file mode 100644 index 000000000..5120e930d --- /dev/null +++ b/libraries/launcher/org/prismlauncher/SystemProperties.java @@ -0,0 +1,38 @@ +package org.prismlauncher; + +import org.prismlauncher.utils.Parameters; + +public final class SystemProperties { + public static void apply(Parameters params) { + String launcherBrand = params.getString("launcherBrand", null); + String launcherVersion = params.getString("launcherVersion", null); + String name = params.getString("instanceName", null); + String iconId = params.getString("instanceIconKey", null); + String iconPath = params.getString("instanceIconPath", null); + String windowTitle = params.getString("windowTitle", null); + String windowDimensions = params.getString("windowParams", null); + + if (launcherBrand != null) + System.setProperty("minecraft.launcher.brand", launcherBrand); + if (launcherVersion != null) + System.setProperty("minecraft.launcher.version", launcherVersion); + + // set useful properties for mods + if (name != null) + System.setProperty("org.prismlauncher.instance.name", name); + if (iconId != null) + System.setProperty("org.prismlauncher.instance.icon.id", iconId); + if (iconPath != null) + System.setProperty("org.prismlauncher.instance.icon.path", iconPath); + if (windowTitle != null) + System.setProperty("org.prismlauncher.window.title", windowTitle); + if (windowDimensions != null) + System.setProperty("org.prismlauncher.window.dimensions", windowDimensions); + + // set multimc properties for compatibility + if (name != null) + System.setProperty("multimc.instance.title", name); + if (iconId != null) + System.setProperty("multimc.instance.icon", iconId); + } +} 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 186909123..084fbc849 100644 --- a/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java +++ b/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java @@ -58,28 +58,43 @@ import org.prismlauncher.utils.Parameters; import org.prismlauncher.utils.ReflectionUtils; import java.lang.invoke.MethodHandle; +import java.util.Collections; +import java.util.List; public final class StandardLauncher extends AbstractLauncher { + private final boolean quickPlayMultiplayerSupported; + private final boolean quickPlaySingleplayerSupported; + public StandardLauncher(Parameters params) { super(params); + + List traits = params.getList("traits", Collections.emptyList()); + quickPlayMultiplayerSupported = traits.contains("feature:is_quick_play_multiplayer"); + quickPlaySingleplayerSupported = traits.contains("feature:is_quick_play_singleplayer"); } @Override public void launch() throws Throwable { // window size, title and state - // FIXME doesn't support maximisation - if (!maximize) { - gameArgs.add("--width"); - gameArgs.add(Integer.toString(width)); - gameArgs.add("--height"); - gameArgs.add(Integer.toString(height)); - } + gameArgs.add("--width"); + gameArgs.add(Integer.toString(width)); + gameArgs.add("--height"); + gameArgs.add(Integer.toString(height)); if (serverAddress != null) { - gameArgs.add("--server"); - gameArgs.add(serverAddress); - gameArgs.add("--port"); - gameArgs.add(serverPort); + if (quickPlayMultiplayerSupported) { + // as of 23w14a + gameArgs.add("--quickPlayMultiplayer"); + gameArgs.add(serverAddress + ':' + serverPort); + } else { + gameArgs.add("--server"); + gameArgs.add(serverAddress); + 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/launcher/org/prismlauncher/utils/ReflectionUtils.java b/libraries/launcher/org/prismlauncher/utils/ReflectionUtils.java index 3b2bfd9bb..9d03a90a6 100644 --- a/libraries/launcher/org/prismlauncher/utils/ReflectionUtils.java +++ b/libraries/launcher/org/prismlauncher/utils/ReflectionUtils.java @@ -54,15 +54,9 @@ package org.prismlauncher.utils; -import org.prismlauncher.utils.logging.Log; - -import java.applet.Applet; -import java.io.File; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; public final class ReflectionUtils { private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); diff --git a/libraries/libnbtplusplus b/libraries/libnbtplusplus index a5e8fd52b..23b955121 160000 --- a/libraries/libnbtplusplus +++ b/libraries/libnbtplusplus @@ -1 +1 @@ -Subproject commit a5e8fd52b8bf4ab5d5bcc042b2a247867589985f +Subproject commit 23b955121b8217c1c348a9ed2483167a6f3ff4ad 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/libraries/quazip b/libraries/quazip index 6117161af..8aeb3f7d8 160000 --- a/libraries/quazip +++ b/libraries/quazip @@ -1 +1 @@ -Subproject commit 6117161af08e366c37499895b00ef62f93adc345 +Subproject commit 8aeb3f7d8254f4bf1f7c6cf2a8f59c2ca141a552 diff --git a/libraries/tomlplusplus b/libraries/tomlplusplus index 7eb2ffcc0..c4369ae1d 160000 --- a/libraries/tomlplusplus +++ b/libraries/tomlplusplus @@ -1 +1 @@ -Subproject commit 7eb2ffcc09f8e9890dc0b77ff8ab00fc53b1f2b8 +Subproject commit c4369ae1d8955cae20c4ab40b9813ef4b60e48be diff --git a/libraries/zlib b/libraries/zlib index 04f42ceca..51b7f2abd 160000 --- a/libraries/zlib +++ b/libraries/zlib @@ -1 +1 @@ -Subproject commit 04f42ceca40f73e2978b50e93806c2a18c1281fc +Subproject commit 51b7f2abdade71cd9bb0e7a373ef2610ec6f9daf diff --git a/nix/README.md b/nix/README.md index 7a422fc5b..7c43658f9 100644 --- a/nix/README.md +++ b/nix/README.md @@ -4,68 +4,31 @@ Prism Launcher is packaged in [nixpkgs](https://github.com/NixOS/nixpkgs/) since 22.11. -See [Package variants](#package-variants) for a list of available packages. +Check the [NixOS Wiki](https://wiki.nixos.org/wiki/Prism_Launcher) for up-to-date instructions. ## Installing a development release (flake) -We use [garnix](https://garnix.io/) to build and cache our development builds. -If you want to avoid rebuilds you may add the garnix cache to your substitutors. +We use [cachix](https://cachix.org/) to cache our development and release builds. +If you want to avoid rebuilds you may add the Cachix bucket to your substitutors, or use `--accept-flake-config` +to temporarily enable it when using `nix` commands. Example (NixOS): ```nix -{...}: { nix.settings = { - trusted-substituters = [ - "https://cache.garnix.io" - ]; + trusted-substituters = [ "https://prismlauncher.cachix.org" ]; trusted-public-keys = [ - "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" + "prismlauncher.cachix.org-1:9/n/FGyABA2jLUVfY+DEp4hKds/rwO+SCOtbOkDzd+c=" ]; }; } ``` -### Using the overlay - -After adding `github:PrismLauncher/PrismLauncher` to your flake inputs, you can add the `default` overlay to your nixpkgs instance. - -Example: - -```nix -{ - 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 - # inputs.nixpkgs.follows = "nixpkgs"; - }; - }; - - outputs = {nixpkgs, prismlauncher}: { - nixosConfigurations.foo = nixpkgs.lib.nixosSystem { - system = "x86_64-linux"; - - modules = [ - ({pkgs, ...}: { - nixpkgs.overlays = [prismlauncher.overlays.default]; - - environment.systemPackages = [pkgs.prismlauncher]; - }) - ]; - }; - } -} -``` - ### Installing the package directly -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. +After adding `github:PrismLauncher/PrismLauncher` to your flake inputs, you can access the flake's `packages` output. Example: @@ -73,25 +36,86 @@ 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, ... }: + { + environment.systemPackages = [ prismlauncher.packages.${pkgs.system}.prismlauncher ]; + } + ) + ]; + }; + }; +} +``` + +### Using the overlay + +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: + +```nix +{ + 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 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 { + modules = [ + ./configuration.nix + + ( + { pkgs, ... }: + { + nixpkgs.overlays = [ prismlauncher.overlays.default ]; + + environment.systemPackages = [ pkgs.prismlauncher ]; + } + ) + ]; + }; }; - } } ``` @@ -111,50 +135,57 @@ nix profile install github:PrismLauncher/PrismLauncher ## Installing a development release (without flakes) -We use [garnix](https://garnix.io/) to build and cache our development builds. -If you want to avoid rebuilds you may add the garnix cache to your substitutors. +We use [Cachix](https://cachix.org/) to cache our development and release builds. +If you want to avoid rebuilds you may add the Cachix bucket to your substitutors. Example (NixOS): ```nix -{...}: { nix.settings = { - trusted-substituters = [ - "https://cache.garnix.io" - ]; + trusted-substituters = [ "https://prismlauncher.cachix.org" ]; trusted-public-keys = [ - "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" + "prismlauncher.cachix.org-1:9/n/FGyABA2jLUVfY+DEp4hKds/rwO+SCOtbOkDzd+c=" ]; }; } ``` -### 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 ]; } ``` @@ -176,18 +207,19 @@ 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) diff --git a/nix/checks.nix b/nix/checks.nix new file mode 100644 index 000000000..ec219d6f8 --- /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 --dry-run --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 ca9999dcb..000000000 --- a/nix/distribution.nix +++ /dev/null @@ -1,65 +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"; - - filteredSelf = inputs.nix-filter.lib.filter { - root = ../.; - include = [ - "buildconfig" - "cmake" - "launcher" - "libraries" - "program_info" - "tests" - ../COPYING.md - ../CMakeLists.txt - ]; - }; - - # common args for prismlauncher evaluations - unwrappedArgs = { - self = filteredSelf; - - 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 fd19a0b3d..000000000 --- a/nix/pkg/default.nix +++ /dev/null @@ -1,74 +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, - self, - version, - libnbtplusplus, -}: -assert lib.assertMsg (stdenv.isLinux || !gamemodeSupport) "gamemodeSupport is only available on Linux"; - stdenv.mkDerivation rec { - pname = "prismlauncher-unwrapped"; - inherit version; - - src = lib.cleanSource self; - - 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 cd356c8d7..000000000 --- a/nix/pkg/wrapper.nix +++ /dev/null @@ -1,95 +0,0 @@ -{ - lib, - stdenv, - symlinkJoin, - prismlauncher-unwrapped, - wrapQtAppsHook, - addOpenGLRunpath, - qtbase, # needed for wrapQtAppsHook - qtsvg, - qtwayland, - xorg, - libpulseaudio, - libGL, - glfw, - openal, - jdk8, - jdk17, - gamemode, - flite, - mesa-demos, - udev, - libusb1, - msaClientID ? null, - gamemodeSupport ? stdenv.isLinux, - textToSpeechSupport ? stdenv.isLinux, - controllerSupport ? stdenv.isLinux, - jdks ? [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..7ba20b68b --- /dev/null +++ b/nix/unwrapped.nix @@ -0,0 +1,113 @@ +{ + lib, + stdenv, + cmake, + cmark, + apple-sdk_11, + extra-cmake-modules, + gamemode, + ghc_filesystem, + jdk17, + kdePackages, + libnbtplusplus, + ninja, + nix-filter, + self, + stripJavaArchivesHook, + tomlplusplus, + zlib, + + msaClientID ? null, + gamemodeSupport ? stdenv.hostPlatform.isLinux, +}: + +assert lib.assertMsg ( + gamemodeSupport -> stdenv.hostPlatform.isLinux +) "gamemodeSupport is only available on Linux."; + +stdenv.mkDerivation { + pname = "prismlauncher-unwrapped"; + version = self.shortRev or self.dirtyShortRev or "unknown"; + + 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.hostPlatform.isDarwin [ apple-sdk_11 ] + ++ lib.optional gamemodeSupport gamemode; + + hardeningEnable = lib.optionals stdenv.hostPlatform.isLinux [ "pie" ]; + + cmakeFlags = + [ + # downstream branding + (lib.cmakeFeature "Launcher_BUILD_PLATFORM" "nixpkgs") + ] + ++ lib.optionals (msaClientID != null) [ + (lib.cmakeFeature "Launcher_MSA_CLIENT_ID" (toString msaClientID)) + ] + ++ lib.optionals (lib.versionOlder kdePackages.qtbase.version "6") [ + (lib.cmakeFeature "Launcher_QT_VERSION_MAJOR" "5") + ] + ++ lib.optionals stdenv.hostPlatform.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/") + ]; + + doCheck = true; + + 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..03c1f0421 --- /dev/null +++ b/nix/wrapper.nix @@ -0,0 +1,134 @@ +{ + addDriverRunpath, + alsa-lib, + flite, + gamemode, + glfw3-minecraft, + jdk17, + jdk21, + jdk8, + kdePackages, + lib, + libGL, + libX11, + libXcursor, + libXext, + libXrandr, + libXxf86vm, + libjack2, + libpulseaudio, + libusb1, + mesa-demos, + openal, + pciutils, + pipewire, + prismlauncher-unwrapped, + stdenv, + symlinkJoin, + udev, + vulkan-loader, + xrandr, + + additionalLibs ? [ ], + additionalPrograms ? [ ], + controllerSupport ? stdenv.hostPlatform.isLinux, + gamemodeSupport ? stdenv.hostPlatform.isLinux, + jdks ? [ + jdk21 + jdk17 + jdk8 + ], + msaClientID ? null, + textToSpeechSupport ? stdenv.hostPlatform.isLinux, +}: + +assert lib.assertMsg ( + controllerSupport -> stdenv.hostPlatform.isLinux +) "controllerSupport only has an effect on Linux."; + +assert lib.assertMsg ( + textToSpeechSupport -> stdenv.hostPlatform.isLinux +) "textToSpeechSupport only has an effect on Linux."; + +let + prismlauncher' = prismlauncher-unwrapped.override { inherit msaClientID gamemodeSupport; }; +in + +symlinkJoin { + name = "prismlauncher-${prismlauncher'.version}"; + + paths = [ prismlauncher' ]; + + nativeBuildInputs = [ kdePackages.wrapQtAppsHook ]; + + buildInputs = + [ + kdePackages.qtbase + kdePackages.qtsvg + ] + ++ lib.optional ( + lib.versionAtLeast kdePackages.qtbase.version "6" && stdenv.hostPlatform.isLinux + ) kdePackages.qtwayland; + + postBuild = '' + wrapQtAppsHook + ''; + + qtWrapperArgs = + let + runtimeLibs = + [ + (lib.getLib stdenv.cc.cc) + ## native versions + glfw3-minecraft + openal + + ## openal + alsa-lib + libjack2 + libpulseaudio + pipewire + + ## glfw + libGL + libX11 + libXcursor + libXext + libXrandr + libXxf86vm + + udev # oshi + + vulkan-loader # VulkanMod's lwjgl + ] + ++ lib.optional textToSpeechSupport flite + ++ lib.optional gamemodeSupport gamemode.lib + ++ lib.optional controllerSupport libusb1 + ++ additionalLibs; + + runtimePrograms = [ + mesa-demos + pciutils # need lspci + 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.hostPlatform.isLinux [ + "--set LD_LIBRARY_PATH ${addDriverRunpath.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/AdhocSignedApp.entitlements b/program_info/AdhocSignedApp.entitlements new file mode 100644 index 000000000..032308a18 --- /dev/null +++ b/program_info/AdhocSignedApp.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.disable-library-validation + + com.apple.security.device.audio-input + + com.apple.security.device.camera + + + diff --git a/program_info/App.entitlements b/program_info/App.entitlements index b46e8ff2a..73bf832c7 100644 --- a/program_info/App.entitlements +++ b/program_info/App.entitlements @@ -2,10 +2,6 @@ - com.apple.security.cs.disable-library-validation - - com.apple.security.cs.allow-dyld-environment-variables - com.apple.security.device.audio-input com.apple.security.device.camera diff --git a/program_info/CMakeLists.txt b/program_info/CMakeLists.txt index 8c6827119..e693d757a 100644 --- a/program_info/CMakeLists.txt +++ b/program_info/CMakeLists.txt @@ -14,8 +14,8 @@ set(Launcher_DisplayName "Prism Launcher") set(Launcher_Name "${Launcher_CommonName}" PARENT_SCOPE) set(Launcher_DisplayName "${Launcher_DisplayName}" PARENT_SCOPE) -set(Launcher_Copyright "Prism Launcher Contributors\\n© 2021-2022 PolyMC Contributors \\n© 2012-2021 MultiMC Contributors") -set(Launcher_Copyright_Mac "Prism Launcher Contributors, © 2021-2022 PolyMC Contributors and © 2012-2021 MultiMC Contributors" PARENT_SCOPE) +set(Launcher_Copyright "© 2022-2024 Prism Launcher Contributors\\n© 2021-2022 PolyMC Contributors\\n© 2012-2021 MultiMC Contributors") +set(Launcher_Copyright_Mac "© 2022-2024 Prism Launcher Contributors, © 2021-2022 PolyMC Contributors and © 2012-2021 MultiMC Contributors" PARENT_SCOPE) set(Launcher_Copyright "${Launcher_Copyright}" PARENT_SCOPE) set(Launcher_Domain "prismlauncher.org" PARENT_SCOPE) set(Launcher_UserAgent "${Launcher_CommonName}/${Launcher_VERSION_NAME}" PARENT_SCOPE) @@ -41,6 +41,31 @@ configure_file(org.prismlauncher.PrismLauncher.metainfo.xml.in org.prismlauncher configure_file(prismlauncher.rc.in prismlauncher.rc @ONLY) configure_file(prismlauncher.manifest.in prismlauncher.manifest @ONLY) configure_file(prismlauncher.ico prismlauncher.ico COPYONLY) + +if(MSVC) + set(Launcher_MSVC_Redist_NSIS_Section [=[ +!ifdef haveNScurl +Section "Visual Studio Runtime" + Var /GLOBAL vc_redist_exe + ${If} ${IsNativeARM64} + StrCpy $vc_redist_exe "vc_redist.arm64.exe" + ${Else} + StrCpy $vc_redist_exe "vc_redist.x64.exe" + ${EndIf} + DetailPrint 'Downloading Microsoft Visual C++ Redistributable...' + NScurl::http GET "https://aka.ms/vs/17/release/$vc_redist_exe" "$INSTDIR\vc_redist\$vc_redist_exe" /INSIST /CANCEL /Zone.Identifier /END + Pop $0 + ${If} $0 == "OK" + DetailPrint "Download successful" + ExecWait "$INSTDIR\vc_redist\$vc_redist_exe /install /passive /norestart" + ${Else} + DetailPrint "Download failed with error $0" + ${EndIf} +SectionEnd +!endif +]=]) +endif() + configure_file(win_install.nsi.in win_install.nsi @ONLY) if(SCDOC_FOUND) diff --git a/program_info/genicons.sh b/program_info/genicons.sh index fe8d2e35e..cad811822 100755 --- a/program_info/genicons.sh +++ b/program_info/genicons.sh @@ -9,16 +9,7 @@ svg2png() { inkscape -w "$width" -h "$height" -o "$output_file" "$input_file" } -sipsresize() { - input_file="$1" - output_file="$2" - width="$3" - height="$4" - - sips -z "$width" "$height" "$input_file" --out "$output_file" -} - -if command -v "inkscape" && command -v "icotool"; then +if command -v "inkscape" && command -v "icotool" && command -v "oxipng"; then # Windows ICO d=$(mktemp -d) @@ -30,6 +21,8 @@ if command -v "inkscape" && command -v "icotool"; then svg2png org.prismlauncher.PrismLauncher.svg "$d/prismlauncher_128.png" 128 128 svg2png org.prismlauncher.PrismLauncher.svg "$d/prismlauncher_256.png" 256 256 + oxipng --opt max --strip all --alpha --interlace 0 "$d/prismlauncher_"*".png" + rm prismlauncher.ico && icotool -o prismlauncher.ico -c \ "$d/prismlauncher_256.png" \ "$d/prismlauncher_128.png" \ @@ -40,10 +33,10 @@ if command -v "inkscape" && command -v "icotool"; then "$d/prismlauncher_16.png" else echo "ERROR: Windows icons were NOT generated!" >&2 - echo "ERROR: requires inkscape and icotool in PATH" + echo "ERROR: requires inkscape, icotool and oxipng in PATH" fi -if command -v "inkscape" && command -v "sips" && command -v "iconutil"; then +if command -v "inkscape" && command -v "iconutil" && command -v "oxipng"; then # macOS ICNS d=$(mktemp -d) @@ -51,19 +44,22 @@ if command -v "inkscape" && command -v "sips" && command -v "iconutil"; then mkdir -p "$d" + svg2png org.prismlauncher.PrismLauncher.bigsur.svg "$d/icon_16x16.png" 16 16 + svg2png org.prismlauncher.PrismLauncher.bigsur.svg "$d/icon_16x16@2.png" 32 32 + svg2png org.prismlauncher.PrismLauncher.bigsur.svg "$d/icon_32x32.png" 32 32 + svg2png org.prismlauncher.PrismLauncher.bigsur.svg "$d/icon_32x32@2.png" 64 64 + svg2png org.prismlauncher.PrismLauncher.bigsur.svg "$d/icon_128x128.png" 128 128 + svg2png org.prismlauncher.PrismLauncher.bigsur.svg "$d/icon_128x128@2.png" 256 256 + svg2png org.prismlauncher.PrismLauncher.bigsur.svg "$d/icon_256x256.png" 256 256 + svg2png org.prismlauncher.PrismLauncher.bigsur.svg "$d/icon_256x256@2.png" 512 512 svg2png org.prismlauncher.PrismLauncher.bigsur.svg "$d/icon_512x512@2x.png" 1024 1024 - sipsresize "$d/icon_512x512@2.png" "$d/icon_16x16.png" 16 16 - sipsresize "$d/icon_512x512@2.png" "$d/icon_16x16@2.png" 32 32 - sipsresize "$d/icon_512x512@2.png" "$d/icon_32x32.png" 32 32 - sipsresize "$d/icon_512x512@2.png" "$d/icon_32x32@2.png" 64 64 - sipsresize "$d/icon_512x512@2.png" "$d/icon_128x128.png" 128 128 - sipsresize "$d/icon_512x512@2.png" "$d/icon_128x128@2.png" 256 256 - sipsresize "$d/icon_512x512@2.png" "$d/icon_256x256.png" 256 256 - sipsresize "$d/icon_512x512@2.png" "$d/icon_256x256@2.png" 512 512 + + oxipng --opt max --strip all --alpha --interlace 0 "$d/icon_"*".png" + iconutil -c icns "$d" else echo "ERROR: macOS icons were NOT generated!" >&2 - echo "ERROR: requires inkscape, sips and iconutil in PATH" + echo "ERROR: requires inkscape, iconutil and oxipng in PATH" fi # replace icon in themes diff --git a/program_info/org.prismlauncher.PrismLauncher.desktop.in b/program_info/org.prismlauncher.PrismLauncher.desktop.in index 76f4b19c0..182d02b1d 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 -Categories=Game;ActionGame;AdventureGame;Simulation; +Icon=org.@Launcher_APP_BINARY_NAME@.@Launcher_CommonName@ +Categories=Game;ActionGame;AdventureGame;Simulation;PackageManager; 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/org.prismlauncher.PrismLauncher.metainfo.xml.in b/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in index a482f0e38..3c542c060 100644 --- a/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in +++ b/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in @@ -4,7 +4,7 @@ org.prismlauncher.PrismLauncher.desktop Prism Launcher Prism Launcher Contributors - A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once + Custom Minecraft Launcher to easily manage multiple Minecraft installations at once CC0-1.0 GPL-3.0-only https://prismlauncher.org/ diff --git a/program_info/prismlauncher.icns b/program_info/prismlauncher.icns index a4c0f7ea4..a5e6a8c3a 100644 Binary files a/program_info/prismlauncher.icns and b/program_info/prismlauncher.icns differ diff --git a/program_info/prismlauncher.manifest.in b/program_info/prismlauncher.manifest.in index fb28afc17..71378134c 100644 --- a/program_info/prismlauncher.manifest.in +++ b/program_info/prismlauncher.manifest.in @@ -1,5 +1,10 @@ + + + true + + diff --git a/program_info/win_install.nsi.in b/program_info/win_install.nsi.in index eda85821b..24f6ee4e8 100644 --- a/program_info/win_install.nsi.in +++ b/program_info/win_install.nsi.in @@ -2,6 +2,8 @@ !include "LogicLib.nsh" !include "MUI2.nsh" +!include "x64.nsh" + Unicode true Name "@Launcher_DisplayName@" @@ -112,6 +114,16 @@ VIAddVersionKey /LANG=${LANG_ENGLISH} "LegalCopyright" "@Launcher_Copyright@" VIAddVersionKey /LANG=${LANG_ENGLISH} "FileVersion" "@Launcher_VERSION_NAME4@" VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductVersion" "@Launcher_VERSION_NAME4@" +;-------------------------------- +; Conditional comp with file exist + +!macro CompileTimeIfFileExist path define +!tempfile tmpinc +!system 'IF EXIST "${path}" echo !define ${define} > "${tmpinc}"' +!include "${tmpinc}" +!delfile "${tmpinc}" +!undef tmpinc +!macroend ;-------------------------------- ; Shell Associate Macros @@ -175,7 +187,7 @@ VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductVersion" "@Launcher_VERSION_NAME4@ !macroend -!macro APP_UNASSOCIATE EXT APP_ID +!macro APP_UNASSOCIATE EXT APP_ID APP_EXE # Unregister file type ClearErrors @@ -336,6 +348,19 @@ Section "" UninstallPrevious SectionEnd +;------------------------------------ +; include nice plugins + +; NScurl - curl in NSIS +; used for MSVS redist download +; extract to ../NSISPlugins/NScurl +; https://github.com/negrutiu/nsis-nscurl/releases/latest/download/NScurl.zip +!insertmacro CompileTimeIfFileExist "../NSISPlugins/NScurl/Plugins/" haveNScurl +!ifdef haveNScurl +!AddPluginDir /x86-unicode "../NSISPlugins/NScurl/Plugins/x86-unicode" +!AddPluginDir /x86-ansi "../NSISPlugins/NScurl/Plugins/x86-ansi" +!AddPluginDir /amd64-unicode "../NSISPlugins/NScurl/Plugins/amd64-unicode" +!endif ;------------------------------------ @@ -368,6 +393,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 @@ -392,6 +421,8 @@ Section "@Launcher_DisplayName@" SectionEnd +@Launcher_MSVC_Redist_NSIS_Section@ + Section "Start Menu Shortcut" SM_SHORTCUTS CreateShortcut "$SMPROGRAMS\@Launcher_DisplayName@.lnk" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" 0 @@ -460,8 +491,8 @@ Section -un.ShellAssoc !insertmacro APP_TEARDOWN_DEFAULT `${APPID}` `${APPNAME}` `${APPEXE}` - !insertmacro APP_UNASSOCIATE ".zip" `${APPID}` - !insertmacro APP_UNASSOCIATE ".mrpack" `${APPID}` + !insertmacro APP_UNASSOCIATE ".zip" `${APPID}` `${APPEXE}` + !insertmacro APP_UNASSOCIATE ".mrpack" `${APPID}` `${APPEXE}` !insertmacro NotifyShell_AssocChanged SectionEnd diff --git a/scripts/compress_images.sh b/scripts/compress_images.sh new file mode 100755 index 000000000..1eef9f1c4 --- /dev/null +++ b/scripts/compress_images.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +## If current working dirctory is ./scripts, ask to invoke from one directory up +if [ ! -d "scripts" ]; then + echo "Please run this script from the root directory of the project" + exit 1 +fi + +find . -type f -name '*.png' -not -path '*/libraries/*' -exec oxipng --opt max --strip all --alpha --interlace 0 {} \; 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/DummyResourceAPI.h b/tests/DummyResourceAPI.h index 35de95151..f8ab71e59 100644 --- a/tests/DummyResourceAPI.h +++ b/tests/DummyResourceAPI.h @@ -37,7 +37,7 @@ class DummyResourceAPI : public ResourceAPI { [[nodiscard]] Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&& callbacks) const override { auto task = makeShared(); - QObject::connect(task.get(), &Task::succeeded, [=] { + QObject::connect(task.get(), &Task::succeeded, [callbacks] { auto json = searchRequestResult(); callbacks.on_succeed(json); }); diff --git a/tests/FileSystem_test.cpp b/tests/FileSystem_test.cpp index 1d3cee85f..ca0313bb4 100644 --- a/tests/FileSystem_test.cpp +++ b/tests/FileSystem_test.cpp @@ -63,7 +63,7 @@ class LinkTask : public Task { qDebug() << "EXPECTED: Link failure, Windows requires permissions for symlinks"; qDebug() << "atempting to run with privelage"; - connect(m_lnk, &FS::create_link::finishedPrivileged, this, [&](bool gotResults) { + connect(m_lnk, &FS::create_link::finishedPrivileged, this, [this](bool gotResults) { if (gotResults) { emitSucceeded(); } else { @@ -113,22 +113,12 @@ class FileSystemTest : public QObject { QTest::addColumn("path1"); QTest::addColumn("path2"); - QTest::newRow("qt 1") << "/abc/def/ghi/jkl" - << "/abc/def" - << "ghi/jkl"; - QTest::newRow("qt 2") << "/abc/def/ghi/jkl" - << "/abc/def/" - << "ghi/jkl"; + QTest::newRow("qt 1") << "/abc/def/ghi/jkl" << "/abc/def" << "ghi/jkl"; + QTest::newRow("qt 2") << "/abc/def/ghi/jkl" << "/abc/def/" << "ghi/jkl"; #if defined(Q_OS_WIN) - QTest::newRow("win native, from C:") << "C:/abc" - << "C:" - << "abc"; - QTest::newRow("win native 1") << "C:/abc/def/ghi/jkl" - << "C:\\abc\\def" - << "ghi\\jkl"; - QTest::newRow("win native 2") << "C:/abc/def/ghi/jkl" - << "C:\\abc\\def\\" - << "ghi\\jkl"; + QTest::newRow("win native, from C:") << "C:/abc" << "C:" << "abc"; + QTest::newRow("win native 1") << "C:/abc/def/ghi/jkl" << "C:\\abc\\def" << "ghi\\jkl"; + QTest::newRow("win native 2") << "C:/abc/def/ghi/jkl" << "C:\\abc\\def\\" << "ghi\\jkl"; #endif } @@ -148,39 +138,15 @@ class FileSystemTest : public QObject { QTest::addColumn("path2"); QTest::addColumn("path3"); - QTest::newRow("qt 1") << "/abc/def/ghi/jkl" - << "/abc" - << "def" - << "ghi/jkl"; - QTest::newRow("qt 2") << "/abc/def/ghi/jkl" - << "/abc/" - << "def" - << "ghi/jkl"; - QTest::newRow("qt 3") << "/abc/def/ghi/jkl" - << "/abc" - << "def/" - << "ghi/jkl"; - QTest::newRow("qt 4") << "/abc/def/ghi/jkl" - << "/abc/" - << "def/" - << "ghi/jkl"; + QTest::newRow("qt 1") << "/abc/def/ghi/jkl" << "/abc" << "def" << "ghi/jkl"; + QTest::newRow("qt 2") << "/abc/def/ghi/jkl" << "/abc/" << "def" << "ghi/jkl"; + QTest::newRow("qt 3") << "/abc/def/ghi/jkl" << "/abc" << "def/" << "ghi/jkl"; + QTest::newRow("qt 4") << "/abc/def/ghi/jkl" << "/abc/" << "def/" << "ghi/jkl"; #if defined(Q_OS_WIN) - QTest::newRow("win 1") << "C:/abc/def/ghi/jkl" - << "C:\\abc" - << "def" - << "ghi\\jkl"; - QTest::newRow("win 2") << "C:/abc/def/ghi/jkl" - << "C:\\abc\\" - << "def" - << "ghi\\jkl"; - QTest::newRow("win 3") << "C:/abc/def/ghi/jkl" - << "C:\\abc" - << "def\\" - << "ghi\\jkl"; - QTest::newRow("win 4") << "C:/abc/def/ghi/jkl" - << "C:\\abc\\" - << "def" - << "ghi\\jkl"; + QTest::newRow("win 1") << "C:/abc/def/ghi/jkl" << "C:\\abc" << "def" << "ghi\\jkl"; + QTest::newRow("win 2") << "C:/abc/def/ghi/jkl" << "C:\\abc\\" << "def" << "ghi\\jkl"; + QTest::newRow("win 3") << "C:/abc/def/ghi/jkl" << "C:\\abc" << "def\\" << "ghi\\jkl"; + QTest::newRow("win 4") << "C:/abc/def/ghi/jkl" << "C:\\abc\\" << "def" << "ghi\\jkl"; #endif } @@ -369,11 +335,12 @@ class FileSystemTest : public QObject { LinkTask lnk_tsk(folder, target_dir.path()); lnk_tsk.linkRecursively(false); - QObject::connect(&lnk_tsk, &Task::finished, - [&] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); + QObject::connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { + QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); + }); lnk_tsk.start(); - QVERIFY2(QTest::qWaitFor([&]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); for (auto entry : target_dir.entryList()) { qDebug() << entry; @@ -465,11 +432,12 @@ class FileSystemTest : public QObject { RegexpMatcher re("[.]?mcmeta"); lnk_tsk.matcher(&re); lnk_tsk.linkRecursively(true); - QObject::connect(&lnk_tsk, &Task::finished, - [&] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); + QObject::connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { + QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); + }); lnk_tsk.start(); - QVERIFY2(QTest::qWaitFor([&]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); for (auto entry : target_dir.entryList()) { qDebug() << entry; @@ -512,11 +480,12 @@ class FileSystemTest : public QObject { lnk_tsk.matcher(&re); lnk_tsk.linkRecursively(true); lnk_tsk.whitelist(true); - QObject::connect(&lnk_tsk, &Task::finished, - [&] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); + QObject::connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { + QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); + }); lnk_tsk.start(); - QVERIFY2(QTest::qWaitFor([&]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); for (auto entry : target_dir.entryList()) { qDebug() << entry; @@ -556,11 +525,12 @@ class FileSystemTest : public QObject { LinkTask lnk_tsk(folder, target_dir.path()); lnk_tsk.linkRecursively(true); - QObject::connect(&lnk_tsk, &Task::finished, - [&] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); + QObject::connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { + QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); + }); lnk_tsk.start(); - QVERIFY2(QTest::qWaitFor([&]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); auto filter = QDir::Filter::Files | QDir::Filter::Dirs | QDir::Filter::Hidden; @@ -604,11 +574,12 @@ class FileSystemTest : public QObject { qDebug() << target_dir.path(); LinkTask lnk_tsk(file, target_dir.filePath("pack.mcmeta")); - QObject::connect(&lnk_tsk, &Task::finished, - [&] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); + QObject::connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { + QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); + }); lnk_tsk.start(); - QVERIFY2(QTest::qWaitFor([&]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); auto filter = QDir::Filter::Files; @@ -639,11 +610,12 @@ class FileSystemTest : public QObject { LinkTask lnk_tsk(folder, target_dir.path()); lnk_tsk.linkRecursively(true); lnk_tsk.setMaxDepth(0); - QObject::connect(&lnk_tsk, &Task::finished, - [&] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); + QObject::connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { + QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); + }); lnk_tsk.start(); - QVERIFY2(QTest::qWaitFor([&]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); QVERIFY(!QFileInfo(target_dir.path()).isSymLink()); @@ -689,13 +661,14 @@ class FileSystemTest : public QObject { LinkTask lnk_tsk(folder, target_dir.path()); lnk_tsk.linkRecursively(true); lnk_tsk.setMaxDepth(-1); - QObject::connect(&lnk_tsk, &Task::finished, - [&] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); + QObject::connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { + QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); + }); lnk_tsk.start(); - QVERIFY2(QTest::qWaitFor([&]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); - std::function verify_check = [&](QString check_path) { + std::function verify_check = [&verify_check](QString check_path) { QDir check_dir(check_path); auto filter = QDir::Filter::Files | QDir::Filter::Dirs | QDir::Filter::Hidden; for (auto entry : check_dir.entryList(filter)) { 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..f2201a5e9 100644 --- a/tests/ResourceFolderModel_test.cpp +++ b/tests/ResourceFolderModel_test.cpp @@ -87,7 +87,7 @@ class ResourceFolderModelTest : public QObject { QEventLoop loop; - ModFolderModel m(tempDir.path(), nullptr, true); + ModFolderModel m(tempDir.path(), nullptr, true, true); connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit); @@ -96,7 +96,7 @@ class ResourceFolderModelTest : public QObject { expire_timer.setSingleShot(true); expire_timer.start(4000); - m.installMod(folder); + m.installResource(folder); loop.exec(); @@ -111,7 +111,7 @@ class ResourceFolderModelTest : public QObject { QString folder = source + '/'; QTemporaryDir tempDir; QEventLoop loop; - ModFolderModel m(tempDir.path(), nullptr, true); + ModFolderModel m(tempDir.path(), nullptr, true, true); connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit); @@ -120,7 +120,7 @@ class ResourceFolderModelTest : public QObject { expire_timer.setSingleShot(true); expire_timer.start(4000); - m.installMod(folder); + m.installResource(folder); loop.exec(); @@ -134,7 +134,7 @@ class ResourceFolderModelTest : public QObject { void test_addFromWatch() { QString source = QFINDTESTDATA("testdata/ResourceFolderModel"); - ModFolderModel model(source, nullptr); + ModFolderModel model(source, nullptr, false, true); QCOMPARE(model.size(), 0); @@ -154,7 +154,7 @@ class ResourceFolderModelTest : public QObject { QString file_mod = QFINDTESTDATA("testdata/ResourceFolderModel/supercoolmod.jar"); QTemporaryDir tmp; - ResourceFolderModel model(QDir(tmp.path()), nullptr); + ResourceFolderModel model(QDir(tmp.path()), nullptr, false, false); QCOMPARE(model.size(), 0); @@ -199,15 +199,18 @@ class ResourceFolderModelTest : public QObject { QString file_mod = QFINDTESTDATA("testdata/ResourceFolderModel/supercoolmod.jar"); QTemporaryDir tmp; - ResourceFolderModel model(tmp.path(), nullptr); + ResourceFolderModel model(tmp.path(), nullptr, false, false); 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) } - for (auto res : model.all()) + for (auto res : model.allResources()) qDebug() << res->name(); QCOMPARE(model.size(), 2); diff --git a/tests/Task_test.cpp b/tests/Task_test.cpp index abc9be905..8333840c1 100644 --- a/tests/Task_test.cpp +++ b/tests/Task_test.cpp @@ -16,7 +16,7 @@ class BasicTask : public Task { friend class TaskTest; public: - BasicTask(bool show_debug_log = true) : Task(nullptr, show_debug_log) {} + BasicTask(bool show_debug_log = true) : Task(show_debug_log) {} private: void executeTask() override { emitSucceeded(); } @@ -37,13 +37,13 @@ class BasicTask_MultiStep : public Task { class BigConcurrentTask : public ConcurrentTask { Q_OBJECT - void startNext() override + void executeNextSubTask() override { // This is here only to help fill the stack a bit more quickly (if there's an issue, of course :^)) // Each tasks thus adds 1024 * 4 bytes to the stack, at the very least. [[maybe_unused]] volatile std::array some_data_on_the_stack{}; - ConcurrentTask::startNext(); + ConcurrentTask::executeNextSubTask(); } }; @@ -66,16 +66,19 @@ class BigConcurrentTaskThread : public QThread { } connect(&big_task, &Task::finished, this, &QThread::quit); - connect(&m_deadline, &QTimer::timeout, this, [&] { + connect(&m_deadline, &QTimer::timeout, this, [this] { passed_the_deadline = true; quit(); }); - m_deadline.start(); + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, &BigConcurrentTaskThread::start_timer, Qt::QueuedConnection); + } big_task.run(); exec(); } + void start_timer() { m_deadline.start(); } public: bool passed_the_deadline = false; @@ -125,10 +128,10 @@ class TaskTest : public QObject { { BasicTask t; QObject::connect(&t, &Task::finished, - [&] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); }); + [&t] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); }); t.start(); - QVERIFY2(QTest::qWaitFor([&]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&t]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } void test_basicConcurrentRun() @@ -151,7 +154,7 @@ class TaskTest : public QObject { }); t.start(); - QVERIFY2(QTest::qWaitFor([&]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&t]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } // Tests if starting new tasks after the 6 initial ones is working @@ -193,7 +196,7 @@ class TaskTest : public QObject { }); t.start(); - QVERIFY2(QTest::qWaitFor([&]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&t]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } void test_basicSequentialRun() @@ -216,7 +219,7 @@ class TaskTest : public QObject { }); t.start(); - QVERIFY2(QTest::qWaitFor([&]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&t]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } void test_basicMultipleOptionsRun() @@ -239,7 +242,7 @@ class TaskTest : public QObject { }); t.start(); - QVERIFY2(QTest::qWaitFor([&]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&t]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } void test_stackOverflowInConcurrentTask() 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!" +}